С помощью методов findAll, findBy, findOneBy и countBy репозитория heap-таблицы можно фильтровать целевой набор записей.
Ниже представлено описание мини-"языка", с помощью которого можно описать правила фильтрации от очень простых до довольно сложных.
Все примеры в данном руководстве работают с простой таблицей задач со следующей структурой:
const Tasks = Heap.Table('tasks', {
title: Heap.String(),
priority: Heap.Integer(),
info: Heap.Object({
contact: Heap.Nullable(Heap.String()),
customer: Heap.Optional(Heap.RefLink('customers')),
area: Heap.Tuple([
Heap.Number(),
Heap.Union([Heap.Literal('sqm'), Heap.Literal('sqf')])
]),
}),
labels: Heap.Array(Heap.NonEmptyString(), { uniqueItems: true }),
customParams: Heap.Record(Heap.String(), Heap.String()),
})
Правила фильтрации задаются с помощью простого объекта. В простейшем варианте единственным ключом этого объекта является название одного из полей верхнего уровня heap-таблицы, а значением - значение этого поля, по которому мы хотим отфильтровать набор записей (используя точное соответствие - равенство). Пример: получить все задачи с приоритетом 5:
const tasks = await Tasks.findBy(ctx, { priority: 5 })
Если нужно отфильтровать по вложенному полю, то необходимо путь к нему отобразить в структуре объекта фильтра:
const tasks = await Tasks.findBy(ctx, { info: { contact: null } })
Если нужно отфильтровать сразу по нескольким полям, то в большинстве случаев достаточно просто скомбинировать
несколько фильтров в одном объекте. Все фильтры будут объединены через правило AND
(для сложных условий может
понадобится напрямую использовать специальный оператор $and
, о нём ниже):
const tasks = await Tasks.findBy(ctx, {
priority: 5,
info: { contact: null },
})
Обратите внимание, что фильтрация по точному соответствию (равенству) работает для простых типов, но для вложенных объектов происходит "частичная" фильтрация только по тем вложенным полям, которые указаны в фильтре.
Так, в примере выше, вложенный объект
info
фильтруется только по полюcontact
, при этом значение поляinfo.customer
не учитывается.
Почти все типы полей, значения которых хранятся в виде простых скаляров, поддерживают фильтрацию сразу по списку
из нескольких значений, которая работает аналогично оператору IN
в SQL:
const tasks = await Tasks.findBy(ctx, { priority: [1, 2, 3] })
Такая фильтрация возможна только по простым скалярным значениям, или значениям, которые приводятся к скалярам (например, heap-объект приводится к своему идентификатору для фильтрации поля типа RefLink):
const tasks = await Tasks.findBy(ctx, {
info: { customer: [user1, user2] },
})
Фильтрация по списку значений НЕ поддерживается для следующих типов полей:
Для многих естественно-ранжируемых типов полей поддерживается фильтрация не только по точному соответствию, но и с помощью операторов сравнения больше/меньше:
$gt
- больше (greater than).$lt
- меньше (less than).$gte
- больше либо равно (greater than or equals).$lte
- меньше либо равно (less than or equals)Для использования этих операторов, вместо значения поля с правой стороны необходимо подставить объект с единственным ключом, соответствующим нужному оператору, и значением, с которым необходимо сравнить для фильтрации:
const tasks = await Tasks.findBy(ctx, {
priority: { $gt: 5 },
})
Список типов полей, поддерживающих операторы сравнения:
Для формирования более сложных фильтров поддерживаются логические операторы $not
, $and
и $or
, имеющие
интуитивно-понятное предназначение. Все они представляют собой объект с единственным ключом, соответствующим
оператору, и значением-фильтром, над которым требуется произвести логическую операцию.
В качестве значения может быть как конечное конкретное значение(я) фильтра (для точного сравнения), так и вложенный фильтр(ы) с допольнительными операторами и фильтрацией по вложенным полям.
Инвертирует значение переданного условия.
Выборка всех задач, приоритет которых НЕ
равен 5:
const tasks = await Tasks.findBy(ctx, {
priority: { $not: 5 },
})
Выборка всех задач, приоритет которых НЕ
больше 5:
const tasks = await Tasks.findBy(ctx, {
priority: { $not: { $gt: 5 } },
})
Выборка всех задач, приоритет которых НЕ
входит в заданный список значений:
const tasks = await Tasks.findBy(ctx, {
priority: { $not: [1, 2, 3] },
})
Пересекает переданный список условий, аналогично оператору AND
в SQL.
Выборка всех задач, приоритет которых больше 3 И
не равен 5:
const tasks = await Tasks.findBy(ctx, {
priority: { $and: [
{ $gt: 3 },
{ $not: 5 },
]},
})
Суммирует переданный список условий, аналогично оператору OR
в SQL.
Выборка всех задач, приоритет которых больше 5 ИЛИ
меньше 2:
const tasks = await Tasks.findBy(ctx, {
priority: { $or: [
{ $gt: 5 },
{ $lt: 2 },
]},
})
Выборка всех задач, приоритет которых больше 5 ИЛИ
задан контакт:
const tasks = await Tasks.findBy(ctx, {
$or: [
{ info: { contact: { $not: null } } },
],
})
Логические операторы могут применяться на любом уровне вложенности фильтра. При этом значение вложенного фильтра
должно соответствовать уровню вложенности. То есть, если мы применяем логический оператор на верхнем уровне, то
вложенное условие должно быть сформулировано, как будто оно формулируется для всей записи целиком, а если,
например, на уровне вложенного объекта info
, то и вложенное условие должно соответствовать структуре объекта info
.
Ниже для наглядности приведены несколько примеров однинаковых фильтров, использующих логические операторы на разных уровнях вложенности:
await Tasks.findBy(ctx, { priority: { $not: 5 } })
await Tasks.findBy(ctx, { $not: { priority: 5 } })
await Tasks.findBy(ctx, {
priority: { $and: [
{ $gt: 3 },
{ $not: 5 },
]},
})
await Tasks.findBy(ctx, {
$and: [
{ priority: { $gt: 3 } },
{ priority: { $not: 5 } },
],
})
await Tasks.findBy(ctx, {
$or: [
{ info: { contact: { $not: null } } },
{ info: { customer: { $not: null } } },
],
})
await Tasks.findBy(ctx, {
info: {
$or: [
{ contact: { $not: null } },
{ customer: { $not: null } },
],
},
})
Достаточно часто возникает ситуация, когда условие фильтрации формируется динамически и часть условия фильтрации
должна вставляться не всегда. Для удобства формулирования подобных ситуаций с помощью тернарного оператора
поддерживается оператор { $noop: true }
, который означает буквально "нет условия".
let onlyHighPriority = req.query?.onlyHighPriority ?? false
const tasks = await Tasks.findBy(ctx, {
priority: onlyHighPriority ? { $gt: 5 } : { $noop: true },
info: { contact: { $not: null } } },
})
Важно понимать, что пустое условие - это НЕ то же самое, что всегда TRUE или всегда FALSE.
Сравните
{ $and: [{ field: null }, { $noop: true }] }
и
{ $and: [{ field: null }, falseCondition] }
- первое условие аналогично{ field: null }
, а второе будет всегда выдавать на выходе пустое множество.
Значение null
, переданное в фильтр НЕ означает буквально "вернуть все записи со значением этого поля равным null
",
как может показаться. Такой фильтр вернёт как записи с явным значением null
у поля, так и записи с отсутствующим
(или undefined
) значением.
С помощью языка фильтраций отличить null
-значения от отсутствующих невозможно. Если такое понадобилось,
то есть 2 выбора:
В дополнение к полям, объявленным в heap-таблице, поддерживается фильтрация по системным полям, которые неявно присутствуют у любого heap-объекта:
id
- уникальный идентификатор записи. Может быть полезно в сочетании с фильтрацией по
списку значений.createdAt/updatedAt
- время создания/последнего изменения записи. Работают аналогично
полям типа DateTime.createdBy/updatedBy
- пользователь создавший/последний изменивший запись. Работают
аналогично полям типа RefLink.Почти все типы полей, которое имеют строковое представление в хранилище, поддерживают регистронезависимую
фильтрацию по шаблону с помощью оператора $ilike
, который работает аналогично оператору
ILIKE в PostgreSQL.
Если шаблон не содержит знаков процента и подчёркиваний, тогда шаблон представляет в точности строку и
$ilike
работает как оператор регистронезависимого сравнения. Подчёркивание (_) в шаблоне
подменяет (вместо него подходит) любой символ, а знак процента (%) подменяет любую (в том числе и пустую)
последовательность символов.
Пример: получить все задачи, в заголовке которых встречается important:
const tasks = await Tasks.findBy(ctx, {
title: { $ilike: '%important%' },
})
Оператор $ilike
поддерживается для следующих типов полей: Any,
Enum, KeyOf, String,
Unknown.
Для полей типа Array поддерживается специальный оператор $includes
, который позволяет
проверить наличие заданного значения в массиве. Он работает только для массивов, элементы которых хранятся
в виде простых скалярных значений.
Пример: получить все задачи, для которых проставлена метка vip:
const tasks = await Tasks.findBy(ctx, {
labels: { $includes: 'vip' },
})
Для полей динамических объектов (Record и Intersect),
у которых набор ключей не прописан жёстко, поддерживается специальный оператор $has
,
который позволяет проверить наличие заданного ключа у хранящегося объекта.
Пример: получить все задачи, для которых задан параметр budget:
const tasks = await Tasks.findBy(ctx, {
customParams: { $has: 'budget' },
})
Обратите внимание, что оператор
$has
проверяет только наличие заданного ключа, безотносительно значения, соответствующего этому ключу - значение может быть пустым.
Некоторые типы полей поддерживают дополнительные вложенные псевдо-поля для более умной фильтрации:
.type
для фильтрации по
типу таблицы, на которую указывает конкретная ссылка. Подробнее об этом читайте в разделе
"Работа со ссылками" > "Фильтрация")..amount
и .currency
для фильтрации отдельно
по сумме и валюте. Подробнее об этом читайте в разделе
"Работа с денежными суммами" > "Фильтрация".Поля типа Tuple позволяют "проваливаться" вглубь своих элементов при фильтрации, подобно тому, как это делается для элементов Object по названию ключа, только в случае с Tuple в качестве ключа следует использовать числовой индекс позиции элемента, отсчитываемый от нуля.
Пример: получить все задачи, для которых площадь задана в квадратных футах:
const tasks = await Tasks.findBy(ctx, {
info: { area: { 1: 'sqf' } },
})
Однако, если в качестве значения фильтра передать массив, то это интерпретируется как "отфильтровать по всем элементам кортежа в соответствии с переданным массивом фильтров попозиционно". Например, получить все задачи с площадью больше 10 квадратных метров, заданной в квадратных метрах:
const tasks = await Tasks.findBy(ctx, {
info: { area: [{ $gt: 10 }, 'sqm'] },
})
Поля с типом Union из-за своей потенциальной сложности накладывают ограничения на возможности фильтрации по ним.
Когда значение, по которому фильтруем - это постой скалярный тип (string
, number
или boolean
), либо null
,
фильтрация работает как обычно, поскольку не нужно "понимать", какая из альтернатив, перечисленных в Union имеется
в виду.
Однако, когда входящее значение более сложное - например, фильтр по вложенному объекту, то работают далеко не все варианты фильтрации. Ниже перечислены правила, по которым работает фильтрация по Union-полям для сложных входящих значений фильтра:
.id
..id
. Другими словами - произойдёт фильтрация по идентификатору из GenericLink..hash
.Тип Nullable является частным случаем Union, но достаточно простым, чтобы обойти вышеперечисленные ограничения фильтрации по Union.
Если фильтр по Nullable-полю отличается от null
, то поле фильтруется по правилам, соответствующим "значимому" типу.
Другими словами, поле, объявленное как, Heap.Nullable(Heap.Object({ ... }))
может быть отфильтровано
по вложенным полям как обычно.
Это исключение касается не только Nullable, но и любого Union-типа с двумя альтернативами, одной из которых является Null.