Skip to content

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

Package listing

@kody/shopify

src/collections.ts

133 lines · 3.5 KB · TypeScript
import {
	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 CollectionSummary = {
	id: string
	title: string
	handle: string | null
	updatedAt: string | null
	sortOrder: string | null
	productsCount: number | null
}

export type ListCollectionsParams = ShopifyClientOptions & {
	query?: string
	first?: number
	after?: string
}

function mapCollection(node: Record<string, unknown>): CollectionSummary {
	const count =
		node.productsCount && typeof node.productsCount === 'object'
			? (node.productsCount as { count?: unknown }).count
			: node.productsCount
	return {
		id: String(node.id || ''),
		title: String(node.title || ''),
		handle: typeof node.handle === 'string' ? node.handle : null,
		updatedAt: typeof node.updatedAt === 'string' ? node.updatedAt : null,
		sortOrder: typeof node.sortOrder === 'string' ? node.sortOrder : null,
		productsCount: typeof count === 'number' ? count : null,
	}
}

/**
 * List collections (custom and smart).
 *
 * @example
 * import { listCollections } from 'kody:@kody/shopify/collections'
 * const { items } = await listCollections({ shop: 'acme', first: 20 })
 */
export async function listCollections(params: ListCollectionsParams = {}) {
	const first = clampInt(params.first, 1, 50, 20)
	const data = await shopifyGraphql<{ collections: unknown }>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query ListCollections($first: Int!, $after: String, $query: String) {
			collections(first: $first, after: $after, query: $query) {
				${PAGE_INFO_FIELDS}
				nodes {
					id
					title
					handle
					updatedAt
					sortOrder
					productsCount { count }
				}
			}
		}`,
		variables: {
			first,
			after: trimString(params.after) || null,
			query: trimString(params.query) || null,
		},
	})
	return {
		items: nodesFromConnection<Record<string, unknown>>(data.collections).map(
			mapCollection,
		),
		pageInfo: pageInfoFromConnection(data.collections) as ShopifyPageInfo,
	}
}

/**
 * Get one collection and a page of its products.
 */
export async function getCollection(
	params: ShopifyClientOptions & {
		id: string
		productFirst?: number
	},
) {
	const id = toGid('Collection', params.id)
	const productFirst = clampInt(params.productFirst, 1, 50, 10)
	const data = await shopifyGraphql<{
		collection: (Record<string, unknown> & { products?: unknown }) | null
	}>({
		shop: params.shop,
		apiVersion: params.apiVersion,
		query: `query GetCollection($id: ID!, $productFirst: Int!) {
			collection(id: $id) {
				id
				title
				handle
				updatedAt
				sortOrder
				description
				productsCount { count }
				products(first: $productFirst) {
					nodes { id title handle status }
				}
			}
		}`,
		variables: { id, productFirst },
	})
	if (!data.collection) throw new Error(`Collection not found: ${id}`)
	const collection = mapCollection(data.collection)
	return {
		...collection,
		description:
			typeof data.collection.description === 'string'
				? data.collection.description
				: null,
		products: nodesFromConnection<Record<string, unknown>>(
			data.collection.products,
		).map((node) => ({
			id: String(node.id || ''),
			title: String(node.title || ''),
			handle: typeof node.handle === 'string' ? node.handle : null,
			status: typeof node.status === 'string' ? node.status : null,
		})),
	}
}

/** Default export: list collections. */
export default listCollections