From 8d591bb769ee9c67807fbb9d49f855c9b72492bf Mon Sep 17 00:00:00 2001 From: Mathieu Date: Tue, 9 Jun 2026 10:46:34 +0200 Subject: [PATCH] fix: fetchMetafieldDefinitions + ensureMetafieldDefinition via GraphQL (REST n'expose pas cet endpoint) --- src/lib/shopifyMetafields.ts | 103 +++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/src/lib/shopifyMetafields.ts b/src/lib/shopifyMetafields.ts index d6d47de..175fcac 100644 --- a/src/lib/shopifyMetafields.ts +++ b/src/lib/shopifyMetafields.ts @@ -43,43 +43,98 @@ export function matchMetafields(columnHeaders: string[], definitions: MetafieldD return { matched, unmatched } } +/** + * Récupère toutes les définitions de métachamps produit via l'API GraphQL Admin. + * L'API REST ne dispose pas de cet endpoint — seul GraphQL l'expose. + */ export async function fetchMetafieldDefinitions(): Promise { - const { baseUrl, headers } = getConfig() - const res = await fetch(`${baseUrl}/metafield_definitions.json?owner_type=PRODUCT&limit=250`, { headers }) - if (!res.ok) throw new Error(`Shopify metafield definitions error: ${res.status}`) + const domain = process.env.SHOPIFY_STORE_DOMAIN + const token = process.env.SHOPIFY_ADMIN_API_TOKEN + if (!domain || !token) throw new Error('SHOPIFY_STORE_DOMAIN et SHOPIFY_ADMIN_API_TOKEN requis') + + const query = `{ + metafieldDefinitions(ownerType: PRODUCT, first: 250) { + edges { + node { + id + namespace + key + name + type { name } + } + } + } + }` + + const res = await fetch(`https://${domain}/admin/api/2024-01/graphql.json`, { + method: 'POST', + headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + }) + + if (!res.ok) throw new Error(`Shopify GraphQL error: ${res.status}`) const data = await res.json() - return (data.metafield_definitions ?? []).map((d: { - id: string | number; namespace: string; key: string; name: string; type: { name: string } | string + + if (data.errors) { + // Si les définitions ne sont pas accessibles, on retourne une liste vide + // plutôt que de bloquer — l'utilisateur verra toutes les colonnes comme "non matchées" + console.warn('[shopifyMetafields] GraphQL errors:', data.errors) + return [] + } + + return (data.data?.metafieldDefinitions?.edges ?? []).map((edge: { + node: { id: string; namespace: string; key: string; name: string; type: { name: string } } }) => ({ - id: String(d.id), - namespace: d.namespace, - key: d.key, - name: d.name, - type: typeof d.type === 'string' ? d.type : d.type?.name ?? 'single_line_text_field', + id: edge.node.id, + namespace: edge.node.namespace, + key: edge.node.key, + name: edge.node.name, + type: edge.node.type?.name ?? 'single_line_text_field', })) } +/** + * Crée une définition de métachamp produit via GraphQL si elle n'existe pas encore. + */ export async function ensureMetafieldDefinition(resolution: MetafieldResolution): Promise { if (resolution.action !== 'create') return - const { baseUrl, headers } = getConfig() - const res = await fetch(`${baseUrl}/metafield_definitions.json`, { + + const domain = process.env.SHOPIFY_STORE_DOMAIN + const token = process.env.SHOPIFY_ADMIN_API_TOKEN + if (!domain || !token) throw new Error('SHOPIFY_STORE_DOMAIN et SHOPIFY_ADMIN_API_TOKEN requis') + + const mutation = `mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) { + metafieldDefinitionCreate(definition: $definition) { + createdDefinition { id key namespace } + userErrors { field message } + } + }` + + const res = await fetch(`https://${domain}/admin/api/2024-01/graphql.json`, { method: 'POST', - headers, + headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ - metafield_definition: { - namespace: resolution.namespace, - key: resolution.key, - name: resolution.columnHeader, - type: 'single_line_text_field', - owner_type: 'PRODUCT', + query: mutation, + variables: { + definition: { + namespace: resolution.namespace, + key: resolution.key, + name: resolution.columnHeader, + type: 'single_line_text_field', + ownerType: 'PRODUCT', + }, }, }), }) - if (!res.ok) { - const err = await res.text() - if (!err.includes('taken') && !err.includes('already')) { - throw new Error(`Shopify create metafield definition error: ${res.status} — ${err}`) - } + + if (!res.ok) throw new Error(`Shopify GraphQL metafield create error: ${res.status}`) + const data = await res.json() + const errors = data.data?.metafieldDefinitionCreate?.userErrors ?? [] + const isAlreadyTaken = errors.some((e: { message: string }) => + e.message.toLowerCase().includes('taken') || e.message.toLowerCase().includes('already'), + ) + if (errors.length > 0 && !isAlreadyTaken) { + throw new Error(`Shopify metafield definition error: ${errors.map((e: { message: string }) => e.message).join(', ')}`) } }