import {
assertNoUserErrors,
clampInt,
nodesFromConnection,
pageInfoFromConnection,
shopifyGraphql,
trimString,
} from './lib/client.ts'
import { CUSTOMER_FIELDS, PAGE_INFO_FIELDS } from './lib/fragments.ts'
import { toGid } from './lib/shop.ts'
import type { Money, ShopifyClientOptions, ShopifyPageInfo } from './types.ts'
export type CustomerSummary = {
id: string
displayName: string | null
firstName: string | null
lastName: string | null
email: string | null
phone: string | null
createdAt: string | null
updatedAt: string | null
numberOfOrders: string | number | null
amountSpent: Money | null
defaultAddress: {
id: string | null
formatted: Array<string>
city: string | null
province: string | null
country: string | null
zip: string | null
} | null
}
export type ListCustomersParams = ShopifyClientOptions & {
query?: string
first?: number
after?: string
}
function mapMoney(value: unknown): Money | null {
if (!value || typeof value !== 'object') return null
const amount = trimString((value as { amount?: unknown }).amount)
const currencyCode = trimString(
(value as { currencyCode?: unknown }).currencyCode,
)
if (!amount || !currencyCode) return null
return { amount, currencyCode }
}
function mapCustomer(node: Record<string, unknown>): CustomerSummary {
const address =
node.defaultAddress && typeof node.defaultAddress === 'object'
? (node.defaultAddress as Record<string, unknown>)
: null
return {
id: String(node.id || ''),
displayName: typeof node.displayName === 'string' ? node.displayName : null,
firstName: typeof node.firstName === 'string' ? node.firstName : null,
lastName: typeof node.lastName === 'string' ? node.lastName : null,
email: typeof node.email === 'string' ? node.email : null,
phone: typeof node.phone === 'string' ? node.phone : null,
createdAt: typeof node.createdAt === 'string' ? node.createdAt : null,
updatedAt: typeof node.updatedAt === 'string' ? node.updatedAt : null,
numberOfOrders:
typeof node.numberOfOrders === 'number' ||
typeof node.numberOfOrders === 'string'
? node.numberOfOrders
: null,
amountSpent: mapMoney(node.amountSpent),
defaultAddress: address
? {
id: typeof address.id === 'string' ? address.id : null,
formatted: Array.isArray(address.formatted)
? address.formatted.map(String)
: [],
city: typeof address.city === 'string' ? address.city : null,
province: typeof address.province === 'string' ? address.province : null,
country: typeof address.country === 'string' ? address.country : null,
zip: typeof address.zip === 'string' ? address.zip : null,
}
: null,
}
}
/**
* List customers with optional Shopify search syntax.
*
* @example
* import { listCustomers } from 'kody:@kody/shopify/customers'
* const { items } = await listCustomers({ shop: 'acme', query: 'email:ada@example.com' })
*/
export async function listCustomers(params: ListCustomersParams = {}) {
const first = clampInt(params.first, 1, 50, 20)
const data = await shopifyGraphql<{ customers: unknown }>({
shop: params.shop,
apiVersion: params.apiVersion,
query: `query ListCustomers($first: Int!, $after: String, $query: String) {
customers(first: $first, after: $after, query: $query) {
${PAGE_INFO_FIELDS}
nodes { ${CUSTOMER_FIELDS} }
}
}`,
variables: {
first,
after: trimString(params.after) || null,
query: trimString(params.query) || null,
},
})
return {
items: nodesFromConnection<Record<string, unknown>>(data.customers).map(
mapCustomer,
),
pageInfo: pageInfoFromConnection(data.customers) as ShopifyPageInfo,
}
}
/**
* Get one customer by GID or numeric id.
*/
export async function getCustomer(
params: ShopifyClientOptions & { id: string },
): Promise<CustomerSummary> {
const id = toGid('Customer', params.id)
const data = await shopifyGraphql<{
customer: Record<string, unknown> | null
}>({
shop: params.shop,
apiVersion: params.apiVersion,
query: `query GetCustomer($id: ID!) {
customer(id: $id) { ${CUSTOMER_FIELDS} }
}`,
variables: { id },
})
if (!data.customer) throw new Error(`Customer not found: ${id}`)
return mapCustomer(data.customer)
}
export type CreateCustomerParams = ShopifyClientOptions & {
email?: string
firstName?: string
lastName?: string
phone?: string
note?: string
dryRun?: boolean
}
/**
* Create a customer. Defaults to dry-run; pass `dryRun: false` to write.
*/
export async function createCustomer(params: CreateCustomerParams) {
const input = {
email: trimString(params.email) || undefined,
firstName: trimString(params.firstName) || undefined,
lastName: trimString(params.lastName) || undefined,
phone: trimString(params.phone) || undefined,
note: trimString(params.note) || undefined,
}
if (!input.email && !input.phone) {
throw new Error('createCustomer requires email or phone.')
}
if (params.dryRun !== false) {
return {
dryRun: true as const,
wouldCall: { mutation: 'customerCreate', input },
}
}
const data = await shopifyGraphql<{ customerCreate: unknown }>({
shop: params.shop,
apiVersion: params.apiVersion,
query: `mutation CreateCustomer($input: CustomerInput!) {
customerCreate(input: $input) {
customer { ${CUSTOMER_FIELDS} }
userErrors { field message }
}
}`,
variables: { input },
})
assertNoUserErrors(data.customerCreate, 'customerCreate')
const created = (data.customerCreate as { customer?: Record<string, unknown> })
.customer
if (!created) throw new Error('customerCreate returned no customer.')
return mapCustomer(created)
}
/** Default export: list customers. */
export default listCustomers