Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/shopify

src/metafields.ts

153 lines · 4.0 KB · TypeScript
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