Как собирать и возвращать предупреждения от сервисов при обработке запроса GraphQL?

Как лучше всего собрать какое-то конкретное свойство со всех листьев графа 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:

  1. Для сбора предупреждений от сервиса, т.е. в общем случае, функция вызывает стек произвольной глубины. На самом деле это больше похоже на общую проблему программирования, чем на NestJS.
  2. Но даже при реализации фичи из первого абзаца NestJS будет сам ходить по графу GraphQL и во вложенных полях могут быть дополнительные логи.

Решение в его полной общей форме, вероятно, будет слишком сложным, но, по крайней мере, может ли кто-нибудь предложить хороший дизайн для случая, представленного в примере кода?

У меня есть пара мыслей:

  1. Должна ли каждая функция в службе возвращать свои предупреждения вместе со своим основным ответом (например, в кортеже), чтобы мы могли постепенно «сворачивать» массив предупреждений при «разворачивании» стека вызовов?
  2. Может лучше реализовать с помощью какого-нибудь декоратора, которым мы будем помечать наши сервисные методы?
  3. Может быть, RxJS — любимый NestJS — может предложить нам какое-то решение? (Я мало знаю об этой библиотеке/их философии)
  4. На самом деле форма вывода NestJS по умолчанию уже похожа на то, что я хочу, это JSON с двумя корневыми свойствами: «ошибки» и «данные». И они могут быть автоматически отправлены вам одновременно, если ошибка не настолько фатальная, чтобы продолжить. Можем ли мы как-то перезаписать схему объекта ответа по умолчанию и разместить там предупреждения?

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

ошибки в graphql возвращаются в отдельном error[response] sibling, а не внутри data... extensions может быть?

xadm 24.12.2020 21:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
0
1
399
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Поэтому я реализовал собственную фабрику контекстов, которая автоматически выполняется при каждом запросе 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 05.02.2021 10:54

@xadm да. В настоящее время я вижу свое новое прикрепленное свойство (по крайней мере, на игровой площадке GraphQL). Что ж, тогда мой единственный оставшийся вариант - реализовать оболочку GraphQL для тела ответа... Для подхода, основанного на схеме, это будет очень многословно...

Hello Human 06.02.2021 21:47

прочитайте еще раз spec.graphql.org/June2018/#sec-Data ... если ошибка, то «данные» ДОЛЖНЫ быть null ... пригодными для использования на игровой площадке, НЕИСПОЛЬЗУЕМЫМИ в apollo, самом популярном клиенте graphql, правда? ... не может использоваться в коде уровня приложения ... предупреждения (такие как журналы и другие данные отладки / профилирования) должны быть прикреплены к error ... и, конечно же, должны быть настраиваемыми (отключены в производстве) ... TS вне этого академическое обсуждение, предупреждения или ошибки не являются частью данных [формы], по крайней мере, в graphql

xadm 07.02.2021 01:28

Хорошо, я отменил свой ответ и добавил примечание, спасибо

Hello Human 08.02.2021 11:03

@xadm Я полностью переписал свой ответ на основе ваших предложений.

Hello Human 11.02.2021 11:09

Служба IMHO не требует инъекции контекста, она может просто возвращать предупреждения в ответе (законно здесь, недопустимо для всего ответа graphql)... его можно/нужно собирать в распознавателе в контекст... в распознавателе: 1: res=await someService(), 2. предупреждения о копировании context.warnings[]=res.warnings, 3. return res ... и ничего более (не требует удаления), так как дополнительные возвращаемые распознавателем элементы (не соответствующие запрошенным реквизитам/дереву) отфильтровываются из ответа (дерева) перед возвратом - это возможность иногда используется для передачи произвольных данных между родительским и дочерним распознавателем

xadm 11.02.2021 12:04

@xadm Хорошо, понял, может быть, это «более чистое» решение, чтобы не предоставлять сервису контекстные вещи. Только один вопрос, функция formatResponse в этом случае остается прежней, верно?

Hello Human 11.02.2021 16:23

@xadm о, понятно, но моя жалоба была на подпись распознавателя, а не на службу. Например, Nest генерирует следующее: export interface IMutation { update(id: string, data: UpdateInput) ..., и я хотел бы использовать его в определении распознавателя: export class Resolver implements Partial<gqlSchema.IQuery>, Partial<gqlSchema.IMutation> { ..., но теперь моя реализация update() также содержит аргумент ctx, поэтому она путается с IMutation

Hello Human 11.02.2021 17:21

IDK nest.js, но я не думаю, что это правильный способ создания промежуточного программного обеспечения, вам, вероятно, следует прикрепить массив к GqlExecutionContext, использовать @Injectable... что-то вроде этого

xadm 13.02.2021 14:49

@xadm похоже, что вы тоже правы, сэр :) В обычной конфигурации по умолчанию мы не можем получить доступ к контексту, если мы явно не внедрим его в метод распознавателя. Но мы можем переключиться на режим на основе запросов, когда экземпляр класса обслуживания будет создаваться индивидуально для каждого нового запроса: docs.nestjs.com/fundamentals/injection-scopes. И тогда мы можем более легко получить доступ к контексту. Но это сопровождается повышенными задержками и, возможно, потреблением памяти.

Hello Human 18.02.2021 23:56

И то же самое относится и к распознавателю, мы также можем переключить его на базу для каждого запроса, так как ранее мы решили не предоставлять сервису вещи, связанные с запросом.

Hello Human 19.02.2021 00:07

за запрос [обычно] требуется для аутентификации

xadm 19.02.2021 00:18

Другие вопросы по теме