/**
* Shopify Admin API 客戶端
*/
import { getAppSecrets } from '../../utils/secrets.js'
export interface ShopifyProduct {
id: string
title: string
handle: string
status: string
variants: {
edges: Array<{
node: {
id: string
title: string
sku: string
price: string
inventoryQuantity: number
}
}>
}
}
export interface ShopifySearchParams {
query?: string
limit?: number
cursor?: string
}
export interface ShopifyUpdateProductParams {
id: string
title?: string
bodyHtml?: string
status?: 'ACTIVE' | 'DRAFT' | 'ARCHIVED'
}
export class ShopifyClient {
private store: string
private accessToken: string
private apiVersion = '2024-01'
constructor(store: string, accessToken: string) {
this.store = store
this.accessToken = accessToken
}
private async graphql<T = any>(query: string, variables?: Record<string, any>): Promise<T> {
const url = `https://${this.store}.myshopify.com/admin/api/${this.apiVersion}/graphql.json`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': this.accessToken,
},
body: JSON.stringify({ query, variables }),
})
const data = await response.json() as any
if (data.errors) {
throw new Error(`Shopify GraphQL Error: ${JSON.stringify(data.errors)}`)
}
return data.data
}
async searchProducts(params: ShopifySearchParams): Promise<{
products: ShopifyProduct[]
hasNextPage: boolean
endCursor?: string
}> {
const query = `
query SearchProducts($query: String, $first: Int!, $after: String) {
products(query: $query, first: $first, after: $after) {
edges {
node {
id
title
handle
status
variants(first: 10) {
edges {
node {
id
title
sku
price
inventoryQuantity
}
}
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`
const result = await this.graphql<{
products: {
edges: Array<{ node: ShopifyProduct; cursor: string }>
pageInfo: { hasNextPage: boolean; endCursor?: string }
}
}>(query, {
query: params.query || '',
first: params.limit || 20,
after: params.cursor,
})
return {
products: result.products.edges.map(e => e.node),
hasNextPage: result.products.pageInfo.hasNextPage,
endCursor: result.products.pageInfo.endCursor,
}
}
async getProduct(id: string): Promise<ShopifyProduct | null> {
const query = `
query GetProduct($id: ID!) {
product(id: $id) {
id
title
handle
status
variants(first: 100) {
edges {
node {
id
title
sku
price
inventoryQuantity
}
}
}
}
}
`
const result = await this.graphql<{ product: ShopifyProduct | null }>(query, { id })
return result.product
}
async updateProduct(params: ShopifyUpdateProductParams): Promise<ShopifyProduct> {
const mutation = `
mutation UpdateProduct($input: ProductInput!) {
productUpdate(input: $input) {
product {
id
title
handle
status
variants(first: 10) {
edges {
node {
id
title
sku
price
inventoryQuantity
}
}
}
}
userErrors {
field
message
}
}
}
`
const input: any = { id: params.id }
if (params.title) input.title = params.title
if (params.bodyHtml) input.bodyHtml = params.bodyHtml
if (params.status) input.status = params.status
const result = await this.graphql<{
productUpdate: {
product: ShopifyProduct
userErrors: Array<{ field: string; message: string }>
}
}>(mutation, { input })
if (result.productUpdate.userErrors.length > 0) {
throw new Error(`Shopify Update Error: ${JSON.stringify(result.productUpdate.userErrors)}`)
}
return result.productUpdate.product
}
async updateVariantPrice(variantId: string, price: string): Promise<void> {
const mutation = `
mutation UpdateVariant($input: ProductVariantInput!) {
productVariantUpdate(input: $input) {
productVariant {
id
price
}
userErrors {
field
message
}
}
}
`
const result = await this.graphql<{
productVariantUpdate: {
userErrors: Array<{ field: string; message: string }>
}
}>(mutation, { input: { id: variantId, price } })
if (result.productVariantUpdate.userErrors.length > 0) {
throw new Error(`Shopify Update Error: ${JSON.stringify(result.productVariantUpdate.userErrors)}`)
}
}
async getLocations(): Promise<Array<{ id: string; name: string }>> {
const query = `
query GetLocations {
locations(first: 10) {
edges {
node {
id
name
}
}
}
}
`
const result = await this.graphql<{
locations: { edges: Array<{ node: { id: string; name: string } }> }
}>(query)
return result.locations.edges.map(e => e.node)
}
}
let shopifyClient: ShopifyClient | null = null
export async function getShopifyClient(): Promise<ShopifyClient> {
if (!shopifyClient) {
const secrets = await getAppSecrets()
shopifyClient = new ShopifyClient(secrets.shopifyStore, secrets.shopifyAccessToken)
}
return shopifyClient
}