mcpField
S
spree-typescript-sdk

spree-typescript-sdk

github.com/spree開発10222 回インストール4 週間前に更新
リポジトリ

spree-typescript-sdk について

spree-typescript-sdk は、Spree 6 の Store、Admin、Seller API 向け TypeScript SDK 群です。ストアフロントや連携機能の開発者向けに、共通のリクエスト形式、リトライ、冪等性対応、任意の Zod スキーマを提供します。

主な機能

  1. 01Store、Admin、Seller API に対応
  2. 02リソース CRUD とドメイン操作
  3. 03トークン認証とリクエスト単位のオプション
  4. 04リトライ、エラー正規化、冪等性
  5. 05生成型と任意の Zod スキーマ

活用シーン

  1. 01Spree のストアフロントと決済フローの構築
  2. 02管理画面やセラー画面の開発
  3. 03連携機能からの Admin API 呼び出し
  4. 04API レスポンスの検証と Webhook 処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/spree/agent-skills --skill spree-typescript-sdk

インストール後、Claude Code で/spree-typescript-sdkを入力して読み込まれたか確認します。

詳細

Spree TypeScript SDK

パッケージ対象 API認証バージョン
@spree/sdkStore API (/api/v3/store)パブリッシャブルキー + 任意の顧客 JWT / ゲストトークン2.x(現在 2.0 ベータ)
@spree/admin-sdkAdmin API (/api/v3/admin)シークレットキー または スタッフ JWT1.x ベータ
@spree/seller-sdkSeller API (/api/v3/seller)セラー JWT + セラー ID(createSellerClient、setSeller)1.x ベータ。シングルトンでは Admin SDK と同様に get/update を使用します(seller.me.get()、seller.me.update({ selected_locale })、seller.profile.update(…))。spree-marketplace を参照してください
@spree/sdk-core共通の HTTP、リトライ、エラー—内部用、公開されていません。インポートしないでください

3 つの SDK はすべて同じ構成で、SpreeError、リトライ、冪等性、エスケープハッチとしての request を共有します。Spree 6 を対象としています。Spree 6 のストアフロントには @spree/sdk 2.x が必要です。6.0 が正式リリースされるまでは、SDK とダッシュボードのパッケージは npm の beta dist-tag でプレリリースとして公開されます(pnpm add @spree/sdk@beta)。一方、@spree/cli と create-spree-app は安定版です(gems 6.0.0.beta4 と同時に 3.1.0/2.1.0)。

@spree/sdk には、@spree/sdk/types(生成された型)、@spree/sdk/zod(Zod スキーマ)、@spree/sdk/webhooks(署名検証、Node 専用)も含まれます。Admin SDK と Seller SDK にも対応する /types があり、1.0.0-beta.3 の次のリリースからは /zod サブパス(@spree/admin-sdk/zod、@spree/seller-sdk/zod)も利用できます。サブパスを解決できない場合は、インストール済みのバージョンがそれより前のものです。スキーマ名は <Type>Schema で、API シリアライザーから生成されます。zod(>= 3)はオプションのピア依存関係なので、実行時にレスポンスを検証するには、ご自身でインストールしてください。

import { OrderSchema } from '@spree/admin-sdk/zod'

const order = OrderSchema.parse(await adminClient.orders.get('or_xxx'))  // throws on a shape mismatch

@spree/sdk — Store API

import { createClient } from '@spree/sdk'

const client = createClient({
  baseUrl: 'https://shop.example.com',
  publishableKey: 'pk_xxx',
  // optional defaults, sent as X-Spree-* headers
  country: 'DE', locale: 'de', currency: 'EUR', channel: 'online',
})

const { data: products, meta } = await client.products.list({ name_cont: 'shirt', limit: 24, expand: ['default_variant'] })
const product = await client.products.get('classic-tee')         // id or slug

セッターを使うと、以降のリクエストのデフォルト値を変更できます:client.setLocale('fr')、setCurrency、setCountry、setChannel。各クライアントインスタンスは独自のデフォルト値を保持するため、サーバー側レンダリング用クライアントとブラウザ用クライアントには、それぞれ個別に設定する必要があります。

メソッドの語彙

list(params?, options?)、get(idOrSlug, params?, options?)、create(body, options?)、update(id, body, options?)、delete(id, options?)。メソッド名は常に get(show ではありません)と delete(destroy ではありません)です。ネストされたリソースでは、最初に親 ID を指定します:client.carts.items.create(cartId, body, options)。

Store SDK が公開するのは、Store API で許可されているものだけです。メソッドが型チェックを通らない場合、その API サーフェスには存在しません。

リソース利用できる機能
products、categories、collections(+ collections.products.list)、sellersカタログの読み取り(ファセットには products.filters)
markets、countries、currencies、locales、channel、policies、deliveryMethodsコンテキストと参照データ
carts(+ items、discountCodes、giftCards、fulfillments、payments、paymentSessions、storeCredits)、carts.associate、carts.completeカートとチェックアウト
orders.get(+ returns、claims)完了した注文
auth、customers.create、passwordResets、customer.*(profile、addresses、creditCards、giftCards、storeCredits、digitalLinks、orders、paymentSetupSessions)アカウント
wishlists(+ items)ウィッシュリスト
newsletterSubscribers(create、verify、requestUnsubscribe、delete)ダブルオプトインのニュースレター
account.companies、companies(+ addresses、members、invitations、orders)、companyInvitations(lookup、accept)B2B セルフサービス(spree-b2b を参照)。companies.members.create(id, { customer_email }) は常に CompanyInvitation を返します

まだラッパーがないエンドポイント(例:/customers/me/data_requests、カートの tax_identifier、po_document)には、client.request(以下を参照)からアクセスできます。

認証、ゲストトークン、リクエストごとのオプション
// Customer login: tokens aren't stored on the client; pass them per request
const { token, refresh_token, user } = await client.auth.login({ email, password })
const { data: orders } = await client.customer.orders.list({}, { token })
const fresh = await client.auth.refresh({ refresh_token })        // rotates both tokens
await client.auth.logout({ refresh_token })

// Third-party IdP (strategy registered on the server)
await client.auth.login({ provider: 'external_idp', token: idpJwt })

RequestOptions:token(Bearer JWT)、spreeToken(ゲストカート/注文トークン。X-Spree-Token として送信)、idempotencyKey、headers。

カート → 注文
let cart = await client.carts.create()
const opts = { spreeToken: cart.token }            // or { token } for a signed-in customer

cart = await client.carts.items.create(cart.id, { variant_id: 'variant_xxx', quantity: 1 }, opts)
cart = await client.carts.update(cart.id, { email, shipping_address, use_shipping: true }, opts)
// cart.requirements → [{ step, field, code, message }] (what still blocks completion)

const result = await client.carts.complete(cart.id, opts)  // Order (or_…, order.cart_id === cart.id) | OrderGroup (ogrp_…)
  • 完了後、カートエンドポイントからカートが消えます。 client.carts.get(cartId) は 404 の SpreeError(cart_not_found)をスローします。確認ページでは、またはリダイレクト方式の決済後に、client.orders.get(cartId, {}, opts) を読み取ってください。注文エンドポイントはカート ID を受け取り、同じゲストトークンを使って該当する注文を解決します。
  • チェックアウト UI はステートマシンではなく、cart.requirements に基づいて制御してください(spree-checkout を参照)。
  • Order は number、cart_id、coupon_code、payment_status、fulfillment_status、合計金額、display_* を公開します。Store シリアライザーには state と status はありません。
  • carts.complete の型は Promise<Order | OrderGroup> です。複数の販売者がいるマーケットプレイスのカートは、注文グループ(ogrp_…、orders[] を含む)に変換されます。@spree/sdk からエクスポートされる isOrderGroup ガードを使って型を絞り込んでください。const orders = isOrderGroup(result) ? result.orders : [result]。マーケットプレイスではないストアでは常に Order が返されますが、TypeScript では引き続き型を絞り込む必要があります。(@spree/admin-sdk には、管理者向けの完了処理用に独自の isOrderGroup があります。)
金額は string | null です

金額は、display_* の関連フィールドとともに小数の文字列("29.99")で表されます。ゲストに価格を表示しないチャネルでは null となり、生成される型も string | null です。null を処理し、無条件に parseFloat を使わないでください。

リストのパラメーター(Ransack)
await client.products.list({ page: 2, limit: 50, name_cont: 'shirt', sort: '-created_at', expand: ['media'], fields: ['name', 'slug'] })

page/limit/sort/expand/fields 以外のキーは q[...] の述語になります(name_cont → q[name_cont]。配列の場合は [] が付きます)。述語を filter キーの下にネストしないでください。

エラー
import { SpreeError } from '@spree/sdk'

try { await client.carts.items.create(cartId, { variant_id, quantity: 1 }, opts) }
catch (err) {
  if (err instanceof SpreeError) {
    err.status   // 422
    err.code     // 'insufficient_stock' | 'validation_error' | ...
    err.details  // { quantity: ['...'] } (attribute keys; 'base' = form-level)
  }
  throw err
}
再試行と冪等性

デフォルトでは、429/500/502/503/504 とネットワークエラーが発生した場合に、指数バックオフとジッターを使って 2 回再試行し、Retry-After に従います。再試行が有効な場合、変更を伴うリクエストには Idempotency-Key が自動的に付与されるため、POST を再試行しても処理が二重に適用されることはありません。retry: { maxRetries, baseDelay, maxDelay, retryOnStatus, retryOnNetworkError } で設定するか、retry: false で無効にできます。Next.js のキャッシュ、トレース、テストには fetch を渡してください。

@spree/admin-sdk — 管理 API

import { createAdminClient } from '@spree/admin-sdk'

// Integration (server only)
const admin = createAdminClient({ baseUrl, secretKey: process.env.SPREE_SECRET_KEY! })

const { data: orders } = await admin.orders.list({ status_eq: 'placed', sort: '-completed_at' })
const order = await admin.orders.get('or_xxx', { expand: ['items', 'payments', 'fulfillments'] })
await admin.orders.cancel('or_xxx', { cancel_reason_id: 'ocr_xxx', cancel_note: 'Customer request' })
await admin.orders.refunds.create('or_xxx', { payment_id: 'py_xxx', amount: '25.00', refund_reason_id: 'rr_xxx' })  // refund_reason_id, not reason_id
  • シークレットキー:スコープが制限されています。スコープが不足していると、details.required_scope を伴う 403 がスローされます。可能な限り範囲の狭いキーを発行してください(spree-api-v3/references/scopes.md を参照)。
  • スタッフ JWT(カスタムのバックオフィス UI 用):admin.auth.login({ email, password })(または { provider, ... })は { token, user } を返し、リフレッシュトークンは HttpOnly Cookie に設定されます。その後、admin.setToken(token) を呼び出します。admin.auth.refresh() にはトークン引数がありません(Cookie が使用されます)。管理者 JWT の有効期限は 5 分のため、admin.onUnauthorized(async () => { const { token } = await admin.auth.refresh(); admin.setToken(token); return true }) を設定してください。クライアントのデフォルト値は credentials: 'include' です。スタッフのアクセスはロールの権限キーで制御されます(details.required_permission を伴う 403)。spree-auth-permissions を参照してください。
  • マルチストアのホストでは、admin.setStore(storeId) によって X-Spree-Store-Id が送信されます。
  • 完全な CRUD に加え、ドメイン固有のアクションも利用できます:orders.complete/cancel/approve、products.clone/bulkStatusUpdate/bulkAddToCategories/...、prices.bulkUpsert、stockLevels.bulkUpsert、roles、permissions.list()、exports、imports、webhookEndpoints、apiKeys など(packages/admin-sdk/src/admin-client.ts を参照)。
  • 招待一覧には acceptance_url が含まれません。必要に応じて admin.invitations.acceptanceLink(id)(または admin.sellers.invitations.acceptanceLink(sellerId, id))でリンクを取得してください。これは書き込み権限が必要です。admin.invitations.create には role_id が必要です。
  • キーまたはスタッフメンバーに読み取り権限がない場合、注文/支払い/顧客/ギフトカード/ストアクレジットの ?expand= は通知なく無視されます。API がデータを返さなかったと判断する前に、スコープを確認してください。
  • 在庫には stockItems ではなく admin.stockLevels(sl_…)を使用します。住所では country_iso ではなく country_code/state_code を使用します。
  • 商品の価格は、シンプルな商品では prices: [{ currency: 'USD', amount: '29.99' }] の形式で指定します。バリアントごとの指定も可能です。
  • シングルトンには get を使用します:admin.me.get()(permission_keys を含む)、admin.store.get()。
  • エクスポート:admin.exports.create({ type, search_params }) を実行し、完了するまで exports.get(id) をポーリングします。その後、Authorization ヘッダーを付けて fetch でダウンロードし、Blob に格納します。.download() ヘルパーはありません。

Webhook レシーバー

// app/api/webhooks/spree/route.ts (Node runtime)
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks'
import type { Order } from '@spree/sdk'

export async function POST(request: Request) {
  const rawBody = await request.text()                       // raw bytes, before JSON.parse
  const ok = verifyWebhookSignature(
    rawBody,
    request.headers.get('x-spree-webhook-signature') ?? '',
    request.headers.get('x-spree-webhook-timestamp') ?? '',
    process.env.SPREE_WEBHOOK_SECRET!,
    300,                                                      // replay tolerance (seconds)
  )
  if (!ok) return new Response('Unauthorized', { status: 401 })

  const event = JSON.parse(rawBody) as WebhookEvent<Order>
  if (event.name === 'order.placed') await syncToERP(event.data)
  return Response.json({ ok: true })
}

order.placed を購読してください。order.completed は非推奨のエイリアスとして 6.0 を通じて引き続き送信されます(そのメタデータには deprecated_alias_of が含まれます)が、6.1 で削除されます。検証処理では node:crypto を使用します。Edge ランタイムでは、Web Crypto を使って `${timestamp}.${rawBody}` に対する HMAC-SHA256 を実装してください(spree-events-webhooks を参照)。失敗した配信はバックオフを伴って再試行され(最大 5 回、同じ event.id)、そのためハンドラーは event.id に基づいて冪等にしてください。

カスタムエンドポイント向けの SDK 拡張

SDK をフォークしたり、生の fetch でバイパスしたりしないでください。認証ヘッダー、ロケール/通貨/チャネルのデフォルト値、リトライ、冪等性、エラーの正規化が適用されなくなります。

1. 単発の呼び出し:client.request

パスはサーフェスのルート(/api/v3/store または /api/v3/admin)からの相対パスです。request は transformListParams を適用しないため、Ransack の述語は自分でラップしてください。

import type { PaginatedResponse } from '@spree/sdk'

type Brand = { id: string; name: string; slug: string }

const { data } = await client.request<PaginatedResponse<Brand>>('GET', '/brands', {
  params: { 'q[name_cont]': 'acme', page: 1, limit: 25 },
})
const created = await admin.request<Brand>('POST', '/brands', { body: { name: 'Acme' } })
2. ラップしたリソースクラス
import type { RequestFn, PaginatedResponse } from '@spree/sdk'

export class BrandsClient {
  constructor(private readonly request: RequestFn) {}
  list(params: { page?: number; limit?: number; name_cont?: string } = {}) {
    const { page, limit, ...predicates } = params
    const q = Object.fromEntries(Object.entries(predicates).map(([k, v]) => [`q[${k}]`, v]))
    return this.request<PaginatedResponse<Brand>>('GET', '/brands', { params: { page, limit, ...q } })
  }
  get(id: string) { return this.request<Brand>('GET', `/brands/${id}`) }
}

@spree/admin-sdk は RequestFn をエクスポートしないため、管理者向けのラッパーは AdminClient['request'] を使って型付けしてください。

3. クライアントへの追加
import { createClient, type Client, type ClientConfig } from '@spree/sdk'

export interface ExtendedClient extends Client { brands: BrandsClient }

export function createExtendedClient(config: ClientConfig): ExtendedClient {
  const client = createClient(config) as ExtendedClient
  client.brands = new BrandsClient(client.request)
  return client
}

組み込みリソースにメソッドを追加するには、Object.assign(base, { products: { ...base.products, republish(id: string) { ... } } }) を使用してください。リソースはプロトタイプ上にあり、破棄されてしまうため、クライアント自体を展開する { ...base } は使用しないでください。

カスタムリソースの型

spree:api_resource によって生成されるシリアライザーは、Typelizer を通じて型付けされます(API エンジンで bundle exec rake typelizer:generate を実行)。スタンドアロンアプリでは、Typelizer の出力先を独自のフォルダーに設定するか、シリアライザーに一致するインターフェースを手動で記述してください。シリアライザーが信頼できる唯一の情報源です。

MSW を使ったテスト

SDK は fetch を呼び出すだけなので、すべてのリクエストは MSW のハンドラーで確認できます:

server.use(
  http.get('https://test.spree.local/api/v3/store/products', () =>
    HttpResponse.json({ data: [{ id: 'prod_test1', name: 'Test' }], meta: { page: 1, limit: 25, count: 1, pages: 1 } })),
)
const client = createClient({ baseUrl: 'https://test.spree.local', publishableKey: 'pk_test' })

よくある問題

  • チェックアウト後に carts.get で 404 が発生する。 想定どおりです。orders.get(cartId) を使用してください。
  • 合計金額が「NaN」と表示される。 チャネルで価格が非表示になっており、フィールドが null になっています。
  • 管理者リクエストが 5 分ごとに 401 になる。 onUnauthorized を登録していないか、更新用クッキーがブロックされています。クロスオリジン構成では、HTTPS と Settings → Allowed Origins に登録したオリジンが必要です。
  • シークレットキーを使うと 403 になる。 err.details.required_scope を確認し、そのスコープを持つキーを発行してください。既存のキーのスコープは編集できません。
  • ブラウザーバンドルにシークレットキーを含めている。 絶対にやめてください。secretKey を使う @spree/admin-sdk はサーバー上でのみ使用し、ダッシュボードでは JWT フローを使用します。
  • order.completed をリッスンしている。 order.placed に切り替えてください。

詳細情報

  • Store SDK のソース:packages/sdk/src/store-client.ts。Admin SDK:packages/admin-sdk/src/admin-client.ts。Seller SDK:packages/seller-sdk/src/seller-client.ts
  • ドキュメント:node_modules/@spree/docs/dist/developer/sdk/(Store クイックスタート、admin/quickstart.md、admin/authentication.md)
  • OpenAPI:node_modules/@spree/docs/dist/api-reference/store.yaml、admin.yaml、seller.yaml
  • 関連スキル:spree-api-v3、spree-storefront、spree-checkout、spree-events-webhooks、spree-auth-permissions、spree-marketplace。

コメント