Разбиение запроса на части
У нас есть следующий запрос, вот как мы можем реструктурировать его в переменные
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️⃣ Что происходит в целом (алгоритм)
- Есть список метафилдов для удаления
- Он разбивается на чанки
- Для каждого чанка:
- отправляется
metafieldsDelete
- отправляется
- Shopify:
- удаляет существующие
- возвращает ошибки для проблемных
- Код получает массив промисов (обычно дальше
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, логирование)