4 minute read

У нас есть следующий запрос, вот как мы можем реструктурировать его в переменные

const promises = metafieldsChunks.map((chunk) =>
  client.request(
    `
  mutation ($metafields: [MetafieldIdentifierInput!]!) {
    metafieldsDelete(metafields: $metafields) {
      deletedMetafields {
        key
        namespace
        ownerId
      }
      userErrors {
        field
        message
      }
    }
  }
`,
    { variables: { metafields: chunk } }
  )
);

Вы можете определить поля как объект и преобразовать их в строку запроса GraphQL. Вот как:

const deletedMetafieldFields = { key, namespace, ownerId };
const userErrorFields = { field, message };

const DELETE_METAFIELDS_MUTATION = `
  mutation DeleteMetafields($metafields: [MetafieldIdentifierInput!]!) {
    metafieldsDelete(metafields: $metafields) {
      deletedMetafields {
        ${Object.keys(deletedMetafieldFields).join("\n        ")}
      }
      userErrors {
        ${Object.keys(userErrorFields).join("\n        ")}
      }
    }
  }
`;

const promises = metafieldsChunks.map((chunk) =>
  client.request(DELETE_METAFIELDS_MUTATION, {
    metafields: chunk,
  })
);

Этот подход:

  • Определяет поля как структурированный объект
  • Динамически преобразует объект в строку запроса GraphQL
  • Делает управление полями более удобным для поддержки
  • Позволяет легко добавлять/удалять поля

Этот код динамически собирает GraphQL-мутацию для удаления метафилдов в Shopify и отправляет её несколько раз батчами (chunks), параллельно.

Теперь разберём по слоям — от простого к более глубокому.

1️⃣ Эти объекты — не данные, а шаблоны полей

const deletedMetafieldFields = { key, namespace, ownerId };
const userErrorFields = { field, message };

⚠️ Важный момент:

key, namespace, ownerId, field, messageне строки и не значения, а переменные, которые должны существовать в скоупе (обычно как строки).

На практике это чаще пишут так:

const deletedMetafieldFields = {
  key: true,
  namespace: true,
  ownerId: true,
};

Но здесь используется трюк:

Object.keys(deletedMetafieldFields);
// → ["key", "namespace", "ownerId"]

👉 Эти объекты нужны только для генерации списка полей в GraphQL, не для логики.

2️⃣ Сборка GraphQL-мутации строкой

const DELETE_METAFIELDS_MUTATION = `
  mutation DeleteMetafields($metafields: [MetafieldIdentifierInput!]!) {
    metafieldsDelete(metafields: $metafields) {
      deletedMetafields {
        ${Object.keys(deletedMetafieldFields).join("\n        ")}
      }
      userErrors {
        ${Object.keys(userErrorFields).join("\n        ")}
      }
    }
  }
`;

Что получится в итоге

После подстановки Object.keys(...) мутация станет примерно такой:

mutation DeleteMetafields($metafields: [MetafieldIdentifierInput!]!) {
  metafieldsDelete(metafields: $metafields) {
    deletedMetafields {
      key
      namespace
      ownerId
    }
    userErrors {
      field
      message
    }
  }
}

Зачем так делать?

✅ Удобно менять поля в одном месте

✅ Не дублировать строки

❌ Немного ухудшает читаемость

❌ Нет автокомплита / типов

3️⃣ Что такое MetafieldIdentifierInput

В Shopify GraphQL это объект вида:

{
  ownerId: ID!
  namespace: String!
  key: String!
}

То есть минимальный набор, чтобы Shopify понял, какой именно метафилд удалить.

4️⃣ Удаление идёт батчами (chunks)

const promises = metafieldsChunks.map((chunk) =>
  client.request(DELETE_METAFIELDS_MUTATION, {
    metafields: chunk,
  })
);

Предположим:

metafieldsChunks = [
  [ { ownerId, namespace, key }, { ... } ],
  [ { ownerId, namespace, key } ],
  ...
];

👉 Каждая итерация:

  • отправляет один GraphQL-запрос
  • удаляет несколько метафилдов за раз

Это важно, потому что:

  • Shopify ограничивает размер запроса
  • и rate limits

5️⃣ Что вернёт каждый request

Каждый промис вернёт:

{
  metafieldsDelete: {
    deletedMetafields: [
      { key, namespace, ownerId }
    ],
    userErrors: [
      { field, message }
    ]
  }
}

Типичные ошибки:

  • метафилд не найден
  • нет прав
  • неправильный ownerId

6️⃣ Зачем userErrors, если запрос успешный?

GraphQL в Shopify работает так:

HTTP 200 ≠ операция успешна

Ошибки бизнес-логики всегда здесь:

userErrors {
  field
  message
}

Поэтому проверять их обязательно.


7️⃣ Что происходит в целом (алгоритм)

  1. Есть список метафилдов для удаления
  2. Он разбивается на чанки
  3. Для каждого чанка:
    • отправляется metafieldsDelete
  4. Shopify:
    • удаляет существующие
    • возвращает ошибки для проблемных
  5. Код получает массив промисов (обычно дальше await Promise.all(promises))

8️⃣ Что здесь можно улучшить

✔️ Явно писать поля (читабельнее)

deletedMetafields {
  key
  namespace
  ownerId
}

✔️ Типизировать (если TS)

type MetafieldIdentifier = {
  ownerId: string;
  namespace: string;
  key: string;
};

✔️ Обрабатывать ошибки

const results = await Promise.all(promises);

results.forEach((r) => {
  if (r.metafieldsDelete.userErrors.length) {
    console.error(r.metafieldsDelete.userErrors);
  }
});

Итог

Этот код:

  • динамически собирает GraphQL-мутацию
  • удаляет метафилды пачками
  • корректно запрашивает результат и ошибки
  • оптимизирован под лимиты Shopify API

Более читаемая TS-версия

Ниже — TypeScript-версия, ориентированная на Shopify Admin GraphQL.

1️⃣ Типы данных

// Минимальный идентификатор метафилда для удаления
export interface MetafieldIdentifier {
  ownerId: string;
  namespace: string;
  key: string;
}

// Ошибка, которую возвращает Shopify GraphQL
export interface UserError {
  field: string[] | null;
  message: string;
}

// Ответ одной мутации
export interface DeleteMetafieldsResponse {
  metafieldsDelete: {
    deletedMetafields: MetafieldIdentifier[];
    userErrors: UserError[];
  };
}

2️⃣ Явная GraphQL-мутация (без динамики)

const DELETE_METAFIELDS_MUTATION = `
  mutation DeleteMetafields($metafields: [MetafieldIdentifierInput!]!) {
    metafieldsDelete(metafields: $metafields) {
      deletedMetafields {
        key
        namespace
        ownerId
      }
      userErrors {
        field
        message
      }
    }
  }
`;

📌 Почему так лучше:

  • сразу видно, какие поля запрашиваются
  • автокомплит в GraphQL IDE
  • проще поддерживать
  • меньше “магии” в рантайме

3️⃣ Функция удаления метафилдов батчами

export async function deleteMetafieldsInChunks(
  client: {
    request<T>(query: string, variables: unknown): Promise<T>;
  },
  metafieldsChunks: MetafieldIdentifier[][]
): Promise<DeleteMetafieldsResponse[]> {
  const requests = metafieldsChunks.map((chunk) =>
    client.request<DeleteMetafieldsResponse>(DELETE_METAFIELDS_MUTATION, { metafields: chunk })
  );

  return Promise.all(requests);
}

4️⃣ Использование + обработка ошибок

const results = await deleteMetafieldsInChunks(client, metafieldsChunks);

for (const result of results) {
  const { deletedMetafields, userErrors } = result.metafieldsDelete;

  if (userErrors.length > 0) {
    console.error("Metafield delete errors:", userErrors);
  }

  console.log(
    "Deleted metafields:",
    deletedMetafields.map((m) => `${m.namespace}.${m.key}`)
  );
}

5️⃣ (Опционально) Безопасный helper для логирования

function logUserErrors(errors: UserError[]) {
  for (const error of errors) {
    console.error(`❌ ${error.message}`, error.field ? `(${error.field.join(".")})` : "");
  }
}

6️⃣ Что мы выиграли по сравнению с исходным кодом

❌ Было

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

✅ Стало

  • читаемый GraphQL
  • строгие типы
  • IDE помогает
  • проще дебажить Shopify userErrors
  • код легко расширяется (retry, rate limit, логирование)