Skip to content

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

Package listing

@kody/shopify

src/webhooks.ts

151 lines · 4.3 KB · TypeScript
import {
	assertNoUserErrors,
	clampInt,
	nodesFromConnection,
	pageInfoFromConnection,
	shopifyGraphql,
	trimString,
} from './lib/client.ts'
import { PAGE_INFO_FIELDS } from './lib/fragments.ts'
import { toGid } from './lib/shop.ts'
import type { ShopifyClientOptions, ShopifyPageInfo } from './types.ts'

export type WebhookSummary = {
	id: string
	topic: string | null
	uri: string | null
	format: string | null
	createdAt: string | null
}

function mapWebhook(node: Record<string, unknown>): WebhookSummary {
	return {
		id: String(node.id || ''),
		topic: typeof node.topic === 'string' ? node.topic : null,
		uri: typeof node.uri === 'string' ? node.uri : null,
		format: typeof node.format === 'string' ? node.format : null,
		createdAt: typeof node.createdAt === 'string' ? node.createdAt : null,
	}
}

/**
 * List webhook subscriptions for the current app.
 *
 * @example
 * import { listWebhooks } from 'kody:@kody/shopify/webhooks'
 * const { items } = await listWebhooks({ shop: 'acme' })
 */
export async function listWebhooks(
	params: ShopifyClientOptions & { first?: number; after?: string } = {},
) {
	const first = clampInt(params.first, 1, 50, 20)
	const data = await shopifyGraphql<{ webhookSubscriptions: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query ListWebhooks($first: Int!, $after: String) {
			webhookSubscriptions(first: $first, after: $after) {
				${PAGE_INFO_FIELDS}
				nodes { id topic uri format createdAt }
			}
		}`,
		variables: {
			first,
			after: trimString(params.after) || null,
		},
	})
	return {
		items: nodesFromConnection<Record<string, unknown>>(
			data.webhookSubscriptions,
		).map(mapWebhook),
		pageInfo: pageInfoFromConnection(
			data.webhookSubscriptions,
		) as ShopifyPageInfo,
	}
}

export type CreateWebhookParams = ShopifyClientOptions & {
	/** GraphQL enum, e.g. `ORDERS_CREATE` or `PRODUCTS_UPDATE`. */
	topic: string
	uri: string
	format?: 'JSON' | 'XML'
	dryRun?: boolean
}

/**
 * Create a webhook subscription. Defaults to dry-run.
 *
 * Shopify must be able to POST to `uri`. After you fork, pair this with a
 * Kody package webhook if you want inbound events in your account.
 */
export async function createWebhook(params: CreateWebhookParams) {
	const topic = trimString(params.topic).toUpperCase()
	const uri = trimString(params.uri)
	if (!topic) throw new Error('topic is required (e.g. ORDERS_CREATE).')
	if (!uri) throw new Error('uri is required.')
	const webhookSubscription = {
		callbackUrl: uri,
		format: params.format || 'JSON',
	}
	if (params.dryRun !== false) {
		return {
			dryRun: true as const,
			wouldCall: {
				mutation: 'webhookSubscriptionCreate',
				topic,
				webhookSubscription,
			},
		}
	}
	const data = await shopifyGraphql<{ webhookSubscriptionCreate: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `mutation CreateWebhook($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
			webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
				webhookSubscription { id topic uri format createdAt }
				userErrors { field message }
			}
		}`,
		variables: { topic, webhookSubscription },
	})
	assertNoUserErrors(data.webhookSubscriptionCreate, 'webhookSubscriptionCreate')
	const created = (
		data.webhookSubscriptionCreate as {
			webhookSubscription?: Record<string, unknown>
		}
	).webhookSubscription
	if (!created) {
		throw new Error('webhookSubscriptionCreate returned no subscription.')
	}
	return mapWebhook(created)
}

/**
 * Delete a webhook subscription. Defaults to dry-run.
 */
export async function deleteWebhook(
	params: ShopifyClientOptions & { id: string; dryRun?: boolean },
) {
	const id = toGid('WebhookSubscription', params.id)
	if (params.dryRun !== false) {
		return {
			dryRun: true as const,
			wouldCall: { mutation: 'webhookSubscriptionDelete', id },
		}
	}
	const data = await shopifyGraphql<{ webhookSubscriptionDelete: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `mutation DeleteWebhook($id: ID!) {
			webhookSubscriptionDelete(id: $id) {
				deletedWebhookSubscriptionId
				userErrors { field message }
			}
		}`,
		variables: { id },
	})
	assertNoUserErrors(data.webhookSubscriptionDelete, 'webhookSubscriptionDelete')
	return data.webhookSubscriptionDelete
}

/** Default export: list webhook subscriptions. */
export default listWebhooks