GraphQL resolver inputs

В GraphQL входные данные резолверов представляют собой строго структурированный набор аргументов, которые приходят из клиентского запроса и проходят через слой схемы до выполнения бизнес-логики. Каждый резолвер получает четыре базовых параметра: parent, args, context, info. Основной интерес для валидации представляет объект args, так как именно он содержит пользовательские входные данные.

Типичная проблема при работе с GraphQL заключается в том, что система типизации схемы не всегда достаточна для сложной доменной логики. GraphQL обеспечивает базовую проверку типов (scalar, enum, non-null, list), однако не покрывает:

  • сложные взаимозависимости полей
  • условные ограничения
  • бизнес-валидацию
  • нормализацию входных данных
  • проверку вложенных структур с динамическими правилами

Эта зона ответственности часто переносится в резолвер или сервисный слой, где использование JSON Schema и Ajv становится практичным решением.


Роль Ajv в валидации входных аргументов

Ajv представляет собой высокопроизводительный валидатор JSON Schema, ориентированный на строгую проверку структур данных. Его использование в GraphQL-резолверах позволяет формализовать проверку входных аргументов в виде декларативных схем.

Ключевая особенность заключается в том, что схема становится единым источником правил:

  • типы данных
  • обязательные поля
  • диапазоны значений
  • форматы строк
  • вложенные структуры
  • условные ограничения

В отличие от встроенной системы GraphQL, Ajv позволяет описывать гораздо более сложные правила без разрастания логики внутри резолверов.


Интеграция Ajv в слой резолверов

Типичная архитектура интеграции строится вокруг промежуточной функции-валидатора, которая вызывается перед основной логикой резолвера.

import Ajv fr om "ajv";

const ajv = new Ajv({ allErrors: true });

const schema = {
  type: "object",
  required: ["id", "limit"],
  properties: {
    id: { type: "string", minLength: 1 },
    lim it: { type: "integer", minimum: 1, maximum: 100 }
  },
  additionalProperties: false
};

const validate = ajv.compile(schema);

function validateArgs(args) {
  const valid = validate(args);
  if (!valid) {
    throw new Error(JSON.stringify(validate.errors));
  }
}

Использование в резолвере:

const resolvers = {
  Query: {
    users: (_, args, context) => {
      validateArgs(args);
      return context.db.users.findMany(args);
    }
  }
};

Такая схема позволяет отделить валидацию от бизнес-логики и избежать разрастания проверок внутри каждого резолвера.


Структурирование схем для GraphQL аргументов

Входные аргументы GraphQL часто имеют вложенную структуру:

type Query {
  searchUsers(filter: UserFilter!, pagination: Pagination): [User]
}

Соответствующая схема Ajv:

const schema = {
  type: "object",
  required: ["filter"],
  properties: {
    filter: {
      type: "object",
      required: ["status"],
      properties: {
        status: { type: "string", enum: ["active", "blocked"] },
        age: {
          type: "object",
          properties: {
            min: { type: "integer", minimum: 0 },
            max: { type: "integer", minimum: 0 }
          },
          additionalProperties: false
        }
      },
      additionalProperties: false
    },
    pagination: {
      type: "object",
      properties: {
        limit: { type: "integer", minimum: 1, maximum: 50 },
        offset: { type: "integer", minimum: 0 }
      },
      additionalProperties: false
    }
  },
  additionalProperties: false
};

Такое описание отражает реальную структуру GraphQL-API, но добавляет более строгие ограничения, чем сама схема GraphQL.


Проблема несоответствия типов GraphQL и JSON Schema

GraphQL и JSON Schema используют разные модели типизации:

  • GraphQL ориентирован на контракт API
  • JSON Schema ориентирован на валидацию JSON-структур

Несовпадения проявляются в нескольких аспектах:

  1. Nullability

    • GraphQL: String!
    • JSON Schema: type: ["string"] + отсутствие null
  2. Массивы

    • GraphQL: [Int!]
    • JSON Schema: type: "array", items: { type: "integer" }
  3. Optional fields

    • GraphQL: отсутствие аргумента
    • JSON Schema: отсутствие required

Ajv требует явного описания всех ограничений, что делает поведение более предсказуемым, но требует дисциплины при синхронизации схем.


Валидация вложенных аргументов и резолверных цепочек

В сложных GraphQL API резолверы часто вызывают друг друга через сервисный слой. В таких случаях входные данные могут проходить несколько уровней трансформации.

Пример цепочки:

  1. GraphQL resolver
  2. service layer
  3. repository layer

Ajv может использоваться только на первом уровне, но при необходимости может применяться и на промежуточных слоях для защиты доменной модели.

function serviceSearchUsers(input) {
  validateArgs(input);
  return repository.search(input.filter, input.pagination);
}

Нормализация данных перед валидацией

Во многих случаях входные данные требуют приведения к нормализованному виду до проверки:

  • преобразование строк в числа
  • удаление лишних полей
  • установка значений по умолчанию

Ajv поддерживает подобное поведение через опции:

const ajv = new Ajv({
  coerceTypes: true,
  useDefaults: true,
  removeAdditional: true
});

Это позволяет автоматически:

  • превращать "10" в 10
  • подставлять значения по умолчанию
  • удалять неизвестные поля

Пользовательские ключевые слова Ajv в контексте GraphQL

GraphQL часто требует бизнес-ограничений, которые невозможно выразить стандартной схемой.

Пример: проверка зависимости полей

ajv.addKeyword({
  keyword: "mutuallyExclusive",
  type: "object",
  validate: function (schema, data) {
    if (schema.includes("email") && schema.includes("phone")) {
      return !(data.email && data.phone);
    }
    return true;
  }
});

Использование:

const schema = {
  type: "object",
  properties: {
    email: { type: "string" },
    phone: { type: "string" }
  },
  mutuallyExclusive: ["email", "phone"]
};

Такие конструкции позволяют переносить сложную бизнес-логику из резолверов в слой схем.


Ошибки валидации и их обработка в GraphQL

В GraphQL важно сохранять предсказуемую структуру ошибок. Ajv возвращает массив ошибок, который необходимо адаптировать под формат GraphQL.

Пример преобразования:

function formatAjvErrors(errors) {
  return errors.map(err => ({
    message: `${err.instancePath} ${err.message}`,
    path: err.instancePath,
    keyword: err.keyword
  }));
}

Далее ошибки могут быть возвращены через стандартный GraphQL error mechanism.


Производительность валидации входных данных

Ajv оптимизирован через компиляцию схем в функции JavaScript. Это критически важно для GraphQL, где резолверы могут вызываться тысячи раз в секунду.

Особенности производительности:

  • схема компилируется один раз
  • повторное использование функции валидации без пересборки
  • минимальные накладные расходы на выполнение

Рекомендованная практика — избегать компиляции схем внутри резолвера:

// плохо
function resolver(args) {
  const validate = ajv.compile(schema);
  validate(args);
}

// правильно
const validate = ajv.compile(schema);

function resolver(args) {
  validate(args);
}

Работа с динамическими схемами GraphQL

В некоторых API структура аргументов зависит от входных параметров запроса. Это приводит к необходимости динамической генерации схем Ajv.

Пример:

function getSchema(type) {
  if (type === "admin") {
    return {
      type: "object",
      required: ["id", "role"],
      properties: {
        id: { type: "string" },
        role: { type: "string", enum: ["admin"] }
      }
    };
  }

  return {
    type: "object",
    required: ["id"],
    properties: {
      id: { type: "string" }
    }
  };
}

Такой подход увеличивает гибкость API, но требует аккуратного кеширования скомпилированных схем.


Кеширование скомпилированных схем

При большом количестве резолверов и схем возникает необходимость кеширования:

const schemaCache = new Map();

function getValidator(schemaKey, schema) {
  if (!schemaCache.has(schemaKey)) {
    schemaCache.set(schemaKey, ajv.compile(schema));
  }
  return schemaCache.get(schemaKey);
}

Это снижает нагрузку на CPU и ускоряет обработку GraphQL-запросов.


Связь между схемой GraphQL и схемой Ajv

Несмотря на пересечение ответственности, схемы GraphQL и Ajv выполняют разные функции:

  • GraphQL schema: контракт API и типизация
  • Ajv schema: строгая валидация входных данных

В зрелых системах они используются совместно:

  1. GraphQL гарантирует структуру запроса
  2. Ajv проверяет бизнес-правила и ограничения
  3. резолвер выполняет бизнес-логику

Такая многослойная проверка снижает вероятность неконсистентных данных в системе и упрощает сопровождение сложных API.