Всем привет!
Сегодня я хотел бы рассмотреть один кейс, который хоть и не очень распространенный, но все же встречается, а его реализация требует поднапрячь мозги. Вот и сейчас я опять с ним столкнулся и решил более ответственно подойти к реализации, а заодно и задокументировать это.
Задача: добавить на уровне GraphQL фильтр по нескольким полям, к примеру, по юзернейму, имени и емейлу. В штатном режиме запрос у нас бы выглядел примерно вот так:
query users{
users(
first: 10
where: {
OR: [
{ username_contains: "search_string" }
{ fullname_contains: "search_string" }
{ email_contains: "search_string" }
]
}
orderBy: createdAt_DESC
) {
id
username
fullname
email
}
}
Уточню, что при выполнении этого запроса мы должны получить пользователей, у которых имя, юзернейм или емейл содержат заданную строку.
Иногда такое перечисления полей в интерфейсах не очень удобно, тем более для простых сотрудников, которые не хотят заполнять несколько отдельных полей (в нашем случае username, fullname, email), а хотят заполнять одно поисковое поле и чтобы по нему "все искалось". При чем логика бывает более сложная (к примеру, заказы по номеру или владельцу, у которого имя или емейл совпадает С). Но GraphQL просто так ничего не принимает лишнего в запросах. Если вы хотите передавать какое-то новое поле в запросе, вы его должны описать в схеме и заставить сервер обрабатывать этот запрос. Вот решение для подобных случаев я и хочу описать.
1. Добавляем свое поле в API-схему
input UserWhereInput {
search: String
}
Напоминаю, что при сборке API-схемы командой yarn build-api выполняется суммирование всех объектов схемы, таким образом на выходе мы получим UserWhereInput не только с полем search, но и с другими полями, описанными ранее, то есть фактически мы именно добавляем поле, а не просто объявляем новый объект.
2. Расширяем резолвер
Как я говорил ранее, API состоят из двух частей: 1. Низкоуровневое, генерируемое самой призмой (и которое обрабатывается в docker-сервере). 2. Внешнее общедоступное API.
Так вот, сейчас мы добавили новое поле search в наше фронтовое API и оно в запросе это поле примет. Но далее запрос улетает на низкоуровневое API, а там об этом поле ничего не известно, и в случае отправки такого запроса туда, мы получим ошибку от сервера, что такое поле в схеме не описано и запрос выполнить нельзя. Но мы сейчас и не будем пытаться заставить низкоуровневое API понимать поле search, наша задача в другом: приняв search на входе, отправить на низкоуровневое API измененный запрос, а именно с условием:
where: {
OR: [
{ username_contains: "search_string" }
{ fullname_contains: "search_string" }
{ email_contains: "search_string" }
]
}
в то время как на вход (во внешнем API) мы будем принимать запрос попроще:
where: {
search: "search_string"
}
То есть в нашем резолвере мы должны, получив условие с полем search, удалить его из запроса, вместо него подставить измененное условие и отправить запрос далее.
Вот здесь я переопределяю два резолвера (users и usersConnection). Прежде чем отправить запрос далее, я вызываю метод addQueryConditions и передаю в него текущие условия параметры запроса. Вот этот метод:
addQueryConditions(args, ctx, info) {
const {
modifyArgs,
} = ctx;
const {
where,
} = args;
modifyArgs(where, this.injectWhere, info);
}
Он в свою очередь берет из контекста метод modifyArgs и передает в него модификатор this.injectWhere. Вот его код для наглядности:
injectWhere(where) {
let {
search,
...other
} = where || {};
let condition;
if (search !== undefined) {
delete where.search;
if (search) {
condition = {
OR: [
{
fullname_contains: search,
},
{
username_contains: search,
},
{
email_contains: search,
},
],
}
}
}
if (condition) {
/**
* Если объект условия пустой, то во избежание лишней вложенности
* присваиваем ему полученное условие
*/
if (!Object.keys(where).length) {
Object.assign(where, condition);
}
/**
* Иначе нам надо добавить полученное условие в массив AND,
* чтобы объединить с другими условиями
*/
else {
if (!where.AND) {
where.AND = [];
}
where.AND.push(condition);
}
}
return where;
}
В принципе, хоть и несколько запутанно написано, но на самом деле здесь всего понимать не надо и достаточно только сосредоточиться на написании своего метода injectWhere, а он не особо сложный.
А вот результат для примера: https://prisma-cms.com/people?filters=%7B%22search%22%3A%22test%22%7D
Что хорошо в данной реализации, так это то, что она работает на любом уровне вложенности запроса и даже в массивах условий (AND и OR). Вот пример: https://prisma-cms.com/people?filters=%7B%22Resources_some%22%3A%7B%22CreatedBy%22%3A%7B%22search%22%3A%22test%22%7D%7D%7D
А еще этот метод за раз можно несколько раз вызывать (если вы написали несколько отдельных модификаторов, а не прописали все условия в одном).
Но если и минус, который скорее всего не будет решен. Дело в том, что мы добавили кастомное поле search в объект UserWhereInput. А этот объект используется не только в выборках пользователей, но и в запросах других объектов, связанных с пользователем, к примеру, в ресурсах. То есть если мы во фронте пропишем вот такой запрос:
query resources {
resources (
first: 10
where:{
CreatedBy:{
search: "Fi1osof"
}
}
){
id
name
}
}
то синтаксических ошибок мы не получим, технически здесь все ОК. Но при выполнении запроса мы получим ошибку
{
"data": null,
"errors": [
{
"message": "Variable \"$_v0_where\" got invalid value { CreatedBy: { search: \"Fi1osof\" } }; Field \"search\" is not defined by type UserWhereInput at value.CreatedBy.",
"locations": [
{
"line": 15,
"column": 3
}
],
"path": [
"resources"
]
}
]
}
Потому что в бэк-API такое поле неизвестно. Для решения этой проблемы придется еще и модификатор писать для ресурс-запросов.
Но в целом эта проблема не особо критична, о ней просто надо знать (чтобы не выполнять). Но для самостоятельных запросов метод очень полезный.
очень интересно