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<string>).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<string, unknown>): 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<Record<string, unknown>> })?.nodes,
)
? (
data.node.metafields as { nodes: Array<Record<string, unknown>> }
).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<Record<string, unknown>> })
.metafields
return { items: (created || []).map(mapMetafield) }
}
/** Default export: list metafields. */
export default listMetafields