import { assertNoUserErrors, shopifyGraphql, trimString, } from './lib/client.ts' import { toGid } from './lib/shop.ts' import type { ShopifyClientOptions } from './types.ts' const OWNER_TYPES = [ 'Product', 'ProductVariant', 'Customer', 'Order', 'Collection', 'Shop', 'DraftOrder', 'Location', ] as const export type MetafieldOwnerType = (typeof OWNER_TYPES)[number] export type MetafieldSummary = { id: string namespace: string | null key: string | null type: string | null value: string | null } function assertOwnerType(type: string): MetafieldOwnerType { if ((OWNER_TYPES as ReadonlyArray).includes(type)) { return type as MetafieldOwnerType } throw new Error( `Unsupported metafield owner type ${JSON.stringify(type)}. Use one of: ${OWNER_TYPES.join(', ')}.`, ) } function mapMetafield(node: Record): MetafieldSummary { return { id: String(node.id || ''), namespace: typeof node.namespace === 'string' ? node.namespace : null, key: typeof node.key === 'string' ? node.key : null, type: typeof node.type === 'string' ? node.type : null, value: typeof node.value === 'string' ? node.value : null, } } /** * List metafields on a resource. * * @example * import { listMetafields } from 'kody:@kody/shopify/metafields' * const { items } = await listMetafields({ * shop: 'acme', * ownerType: 'Product', * ownerId: '123', * namespace: 'custom', * }) */ export async function listMetafields( params: ShopifyClientOptions & { ownerType: MetafieldOwnerType ownerId: string namespace?: string first?: number }, ) { const ownerType = assertOwnerType(params.ownerType) const id = toGid(ownerType, params.ownerId) const first = params.first && params.first > 0 ? Math.min(params.first, 50) : 20 const namespace = trimString(params.namespace) || null const data = await shopifyGraphql<{ node: { metafields?: unknown } | null }>({ shop: params.shop, apiVersion: params.apiVersion, query: `query ListMetafields($id: ID!, $first: Int!, $namespace: String) { node(id: $id) { ... on HasMetafields { metafields(first: $first, namespace: $namespace) { nodes { id namespace key type value } } } } }`, variables: { id, first, namespace }, }) if (!data.node) throw new Error(`Owner not found: ${id}`) return { ownerId: id, items: Array.isArray( (data.node.metafields as { nodes?: Array> })?.nodes, ) ? ( data.node.metafields as { nodes: Array> } ).nodes.map(mapMetafield) : [], } } export type SetMetafieldParams = ShopifyClientOptions & { ownerType: MetafieldOwnerType ownerId: string namespace: string key: string type: string value: string dryRun?: boolean } /** * Set one metafield. Defaults to dry-run. */ export async function setMetafield(params: SetMetafieldParams) { const ownerType = assertOwnerType(params.ownerType) const metafields = [ { ownerId: toGid(ownerType, params.ownerId), namespace: trimString(params.namespace), key: trimString(params.key), type: trimString(params.type), value: trimString(params.value), }, ] if (!metafields[0].namespace || !metafields[0].key || !metafields[0].type) { throw new Error('namespace, key, and type are required.') } if (params.dryRun !== false) { return { dryRun: true as const, wouldCall: { mutation: 'metafieldsSet', metafields }, } } const data = await shopifyGraphql<{ metafieldsSet: unknown }>({ shop: params.shop, apiVersion: params.apiVersion, query: `mutation SetMetafields($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { id namespace key type value } userErrors { field message } } }`, variables: { metafields }, }) assertNoUserErrors(data.metafieldsSet, 'metafieldsSet') const created = (data.metafieldsSet as { metafields?: Array> }) .metafields return { items: (created || []).map(mapMetafield) } } /** Default export: list metafields. */ export default listMetafields