Николай Ланец
1 июня 2019 г., 12:31

Кастомные условия поиска в @prisma-cms

Всем привет!

Сегодня я хотел бы рассмотреть один кейс, который хоть и не очень распространенный, но все же встречается, а его реализация требует поднапрячь мозги. Вот и сейчас я опять с ним столкнулся и решил более ответственно подойти к реализации, а заодно и задокументировать это.

Задача: добавить на уровне 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-схему

В файл схемы user.graphql дописываем
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 такое поле неизвестно. Для решения этой проблемы придется еще и модификатор писать для ресурс-запросов.

Но в целом эта проблема не особо критична, о ней просто надо знать (чтобы не выполнять). Но для самостоятельных запросов метод очень полезный.

Добавить комментарий