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