Как лучше всего собрать какое-то конкретное свойство со всех листьев графа GraphQL, сведя его к какому-то единому массиву? Например, мои сервисные функции могут «выбрасывать» какие-то произвольные строковые предупреждения, которые я хочу собирать и предоставлять клиенту помимо основных данных, например. ожидаемый результат:
type EntityOutput {
entity: Entity
warnings: [String!]
}
Резолвер:
@Mutation()
async updateEntity(
@Args('id', ParseUUIDPipe) id: string,
@Args('data') input: UpdateDto
): Promise<EntityOutputDto>
{
return {
entity: await this.service.update(id, input),
warnings: [] // ???
};
}
Метод обслуживания:
async update(id: string, input: UpdateDto): Promise<Entity> {
const entity = await this.repository.findOneOrFail(id, { relations: ['type'] }); // check existence
if (Object.values(input).some(v => v !== undefined)) {
const updateData: Partial<Entity & UpdateDto> = Object.assign({ id }, input);
if (input.isCurrentEntityOfItsType === true) {
await this.typesService.update(entity.type.id, { currentEntityId: id }); // <-- this also can create its own warnings
} else if (input.isCurrentEntityOfItsType === false) {
await this.typesService.update(entity.type.id, { currentEntityId: null as any });
}
await this.repository.save(updateData);
} else {
console.warn(`No properties to change were been provided`); // <-- this is a warning I want to save
}
return this.findOne(id);
}
Я думаю, что мой вопрос можно разделить на 2:
Решение в его полной общей форме, вероятно, будет слишком сложным, но, по крайней мере, может ли кто-нибудь предложить хороший дизайн для случая, представленного в примере кода?
У меня есть пара мыслей:
Весь вопрос в значительной степени вдохновлен этим обсуждением SO, но, к сожалению, ничего не говорит о фактической возможной реализации.


Поэтому я реализовал собственную фабрику контекстов, которая автоматически выполняется при каждом запросе GraphQL и создает объект нужного формата:
app.module.ts:
export interface AppContext {
warnings: string[];
}
const contextFactory: ContextFunction<any, AppContext> = () => ({
warnings: []
});
Теперь мы можем извлечь выгоду из нашего недавно созданного интерфейса, чтобы добавлять строгие типизации всякий раз, когда мы ссылаемся на контекст, например:
некоторые.resolver.ts
@Mutation()
async remove(
@Args('id', ParseUUIDPipe) id: string,
@Context() ctx: AppContext
): Promise<FindOneDto>
{
return new FindOneDto(await this.service.remove(id, ctx.warnings));
}
Здесь сервис может добавлять в контекст собственные предупреждения.
Чтобы собрать их все и вернуться к вызывающей стороне API, я переопределяю функцию formatResponse и добавляю предупреждения к расширениям (это специальное метаполе GraphQL, служащее целям разработки):
app.module.ts:
const graphqlConfig: GqlModuleOptions = {
context: contextFactory,
formatResponse: (
response: GraphQLResponse | null,
context: GraphQLRequestContext<AppContext>,
): GraphQLResponse =>
{
const warnings = context.context.warnings;
if (warnings.length) {
if (response) {
const extensions = response.extensions || (response.extensions = {});
extensions.warnings = warnings;
} else {
return { extensions: { warnings } };
}
}
return response || {};
},
...
}
Аналогичный подход используется в официальном примере расширения Apollo: https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts .
Единственный недостаток, который я вижу сейчас, заключается в том, что внедрение контекста в аргументы преобразователя нарушает соответствие с автоматически сгенерированными интерфейсами TypeScript (я использую подход, основанный на схеме). В таком случае мы можем переключиться в режим, основанный на запросе, чтобы наш экземпляр класса распознавателя/сервиса создавался индивидуально для каждого нового запроса: https://docs.nestjs.com/fundamentals/injection-scopes . Теперь мы можем обращаться к контексту прямо в методах, не вводя никаких дополнительных параметров. Но это сопровождается повышенными задержками и, возможно, потреблением памяти. Другой подход будет заключаться в создании автономного перехватчика Nest.
СНОВА! не прикрепляйте [ничего] к data!!! ... клиент apollo записывает ответ (данные) в [нормализованный] кеш, не запрошенные части / реквизиты отфильтровываются ... вы не получите warnings, например. useQuerydata [результат] опора
@xadm да. В настоящее время я вижу свое новое прикрепленное свойство (по крайней мере, на игровой площадке GraphQL). Что ж, тогда мой единственный оставшийся вариант - реализовать оболочку GraphQL для тела ответа... Для подхода, основанного на схеме, это будет очень многословно...
прочитайте еще раз spec.graphql.org/June2018/#sec-Data ... если ошибка, то «данные» ДОЛЖНЫ быть null ... пригодными для использования на игровой площадке, НЕИСПОЛЬЗУЕМЫМИ в apollo, самом популярном клиенте graphql, правда? ... не может использоваться в коде уровня приложения ... предупреждения (такие как журналы и другие данные отладки / профилирования) должны быть прикреплены к error ... и, конечно же, должны быть настраиваемыми (отключены в производстве) ... TS вне этого академическое обсуждение, предупреждения или ошибки не являются частью данных [формы], по крайней мере, в graphql
Хорошо, я отменил свой ответ и добавил примечание, спасибо
@xadm Я полностью переписал свой ответ на основе ваших предложений.
Служба IMHO не требует инъекции контекста, она может просто возвращать предупреждения в ответе (законно здесь, недопустимо для всего ответа graphql)... его можно/нужно собирать в распознавателе в контекст... в распознавателе: 1: res=await someService(), 2. предупреждения о копировании context.warnings[]=res.warnings, 3. return res ... и ничего более (не требует удаления), так как дополнительные возвращаемые распознавателем элементы (не соответствующие запрошенным реквизитам/дереву) отфильтровываются из ответа (дерева) перед возвратом - это возможность иногда используется для передачи произвольных данных между родительским и дочерним распознавателем
@xadm Хорошо, понял, может быть, это «более чистое» решение, чтобы не предоставлять сервису контекстные вещи. Только один вопрос, функция formatResponse в этом случае остается прежней, верно?
@xadm о, понятно, но моя жалоба была на подпись распознавателя, а не на службу. Например, Nest генерирует следующее: export interface IMutation { update(id: string, data: UpdateInput) ..., и я хотел бы использовать его в определении распознавателя: export class Resolver implements Partial<gqlSchema.IQuery>, Partial<gqlSchema.IMutation> { ..., но теперь моя реализация update() также содержит аргумент ctx, поэтому она путается с IMutation
IDK nest.js, но я не думаю, что это правильный способ создания промежуточного программного обеспечения, вам, вероятно, следует прикрепить массив к GqlExecutionContext, использовать @Injectable... что-то вроде этого
@xadm похоже, что вы тоже правы, сэр :) В обычной конфигурации по умолчанию мы не можем получить доступ к контексту, если мы явно не внедрим его в метод распознавателя. Но мы можем переключиться на режим на основе запросов, когда экземпляр класса обслуживания будет создаваться индивидуально для каждого нового запроса: docs.nestjs.com/fundamentals/injection-scopes. И тогда мы можем более легко получить доступ к контексту. Но это сопровождается повышенными задержками и, возможно, потреблением памяти.
И то же самое относится и к распознавателю, мы также можем переключить его на базу для каждого запроса, так как ранее мы решили не предоставлять сервису вещи, связанные с запросом.
за запрос [обычно] требуется для аутентификации
ошибки в graphql возвращаются в отдельном
error[response] sibling, а не внутриdata...extensionsможет быть?