JS API 客户端
与Crystallize API合作的助手和打字的公用事业.
v5是一个主要的更新:更简单的客户端,通过@crystallize/schema输入输入,以及专注于共同任务的管理者(目录,导航,水合,订单,客户,订阅,和推车).
安装
pnpm add @crystallize/js-api-client
# or
npm install @crystallize/js-api-client
# or
yarn add @crystallize/js-api-client
快开始
import { createClient } from '@crystallize/js-api-client';
const api = createClient({
tenantIdentifier: 'furniture',
// For protected APIs, provide credentials
// accessTokenId: '…',
// accessTokenSecret: '…',
// staticAuthToken: '…',
// and more
});
// Call any GraphQL you already have (string query + variables)
const { catalogue } = await api.catalogueApi(
`query Q($path: String!, $language: String!) {
catalogue(path: $path, language: $language) { name path }
}`,
{ path: '/shop', language: 'en' },
);
// Don't forget to close when using HTTP/2 option (see below)
api.close();
快速摘要
- 一个有电话的客户:
catalogueApi, (中文(简体) ),discoveryApi, (中文(简体) ),pimApi, (中文(简体) ),nextPimApi, (中文(简体) ),meApi,和商店API的呼叫器shopCartApi, (中文(简体) ),shopOrderApi, (中文(简体) ),shopCustomerApi, (中文(简体) ),shopLockApi, (中文(简体) ),shopSubscriptionContractApi, (中文(简体) ),shopBookingApi - 高级助手:
createCatalogueFetcher, (中文(简体) ),createNavigationFetcher, (中文(简体) ),createProductHydrater, (中文(简体) ),createOrderFetcher, (中文(简体) ),createOrderManager, (中文(简体) ),createCustomerManager, (中文(简体) ),createCustomerGroupManager, (中文(简体) ),createSubscriptionContractManager, (中文(简体) ),createCartManager, (中文(简体) ),createShopOrderManager, (中文(简体) ),createShopCustomerManager, (中文(简体) ),createShopLock - 公用事业:
createSignatureVerifier, (中文(简体) ),createPluginPayloadDecrypter, (中文(简体) ),createBinaryFileManager, (中文(简体) ),pricesForUsageOnTier,请求profiling - 使用对象构建图形QL
json-to-graphql-query(见下文一节) - 强打字通过
@crystallize/schema投入和产出 - 升级? 见 升级 v4 – v5 迁移
备选办法和环境
createClient(configuration, options?)
- 配置
tenantIdentifier(必须)tenantId选项accessTokenId编号 :accessTokenSecret或者说sessionIdbearerToken用于后端发行的 Bearer 令牌(同意为Authorization: Bearer …(中文(简体) )staticAuthToken只读目录/发现shopApiToken选项; 否则自动比对shopApiStaging使用中转商店 APIorigin自定义主机后缀( 默认为.crystallize.com(中文(简体) )
- 选项
useHttp2启用 HTTP/2 运输timeout请求超时以毫秒计; 需要更长时间的请求将被中止http2IdleTimeoutHTTP/2 以毫秒计的空闲超时( 默认)300000——5分钟). 对没有服务器的函数使用更短的值, 对长期运行的服务器使用更长的值profiling调用extraHeaders所有呼叫的额外请求信头shopApiToken自动牵引控制 :{ doNotFetch?: boolean; scopes?: string[]; expiresIn?: number }。 。 。 .scopes默认为['cart'];通过您使用的每一端点(例如.['cart', 'order'])在前面获得一个单一的指使,否则指使会与外延范围被重置,这是第一次叫另一个商店API终点
client.close() 当您允许 HTTP/2 优雅地结束基础会话时, 将会被调用 .
可用的 API 呼叫器
catalogueApi-目录图形QLdiscoveryApi- 发现图QL(取代了旧的搜索 API)pimApi- PIM GraphQL(经典/图克快传)nextPimApi- PIM 下图QL(适用于租户)meApi- 我图QL(/@me范围)shopCartApi- 商店API/cartGraphQL( 为您处理的文件夹)shopOrderApi, (中文(简体) ),shopCustomerApi, (中文(简体) ),shopLockApi, (中文(简体) ),shopSubscriptionContractApi, (中文(简体) ),shopBookingApi- 其他商店API终点(/order, (中文(简体) ),/customer, (中文(简体) ),/lock, (中文(简体) ),/subscription-contract, (中文(简体) ),/booking(中文(简体) )
所有的商店API的呼叫者 一个客户共享一个符号。 它在过期前5分钟被刷新,当呼叫者需要一个被缓存的代币不覆盖的端点时,它会与瞄准镜的结合进行回放.
所有呼叫者都拥有相同的签名: <T>(query: string, variables?: Record<string, unknown>) => Promise<T>。 。 。 .
认证概览
将有关全权证书交给 createClient数字 :
staticAuthToken对目录/发现只读accessTokenId+ 键accessTokenSecret(或sessionId用于PIM/Shop操作bearerToken用于后端发行的令牌——作为Authorization: Bearer …; 已接受catalogueApi, (中文(简体) ),discoveryApi, (中文(简体) ),pimApi, (中文(简体) ),nextPimApi,以及meApi在不提供其他证书的情况下,也自动用于获取商店 API 令牌.shopApiToken选项; 如果省略, 将使用您的 PIM 证书在第一个商店 API 呼叫中获取一个符号
认证优先级( 每名呼叫者, 最高第一) : sessionId → bearerToken → staticAuthToken → accessTokenId编号 :accessTokenSecret。 。 。 .
认证文件见官方文件: https://crystalize.com/learn/developer-guides/api-overview/ 认证
处理错误
API 调用错误 JSApiClientCallError 两者 code 财务报告和审定财务报表 statusCode hTTP 状态的属性 :
import { JSApiClientCallError } from '@crystallize/js-api-client';
try {
await api.pimApi(`query { … }`);
} catch (e) {
if (e instanceof JSApiClientCallError) {
console.error(`HTTP ${e.statusCode}:`, e.message);
// e.code also works (same value)
}
}
分析请求
如果有日志查询,计时和服务器计时.
import { createClient } from '@crystallize/js-api-client';
const api = createClient(
{ tenantIdentifier: 'furniture' },
{
profiling: {
onRequest: (q) => console.debug('[CRYSTALLIZE] >', q),
onRequestResolved: ({ resolutionTimeMs, serverTimeMs }, q) =>
console.debug('[CRYSTALLIZE] <', resolutionTimeMs, 'ms (server', serverTimeMs, 'ms)'),
},
},
);
图形QL 构建器: json- to- graphql- query
这个图书馆充满了赞美 json- to- graphql- query (英语) 在引擎盖下 你可以使用简单的JS对象来构建GraphQL查询. 多数帮助者接受一个对象并将其转换成图形QL字符串.
- 仍可用生弦给低级呼叫者打电话.
- 对于与目录有关的助手,我们揭露
catalogueFetcherGraphqlBuilder以组成可重复使用的碎片.
示例对象 — 查询字符串 :
import { jsonToGraphQLQuery } from 'json-to-graphql-query';
const query = jsonToGraphQLQuery({
query: {
catalogue: {
__args: { path: '/shop', language: 'en' },
name: true,
path: true,
},
},
});
高级助手
这些帮助者构建查询,使用 @crystallize/schema,并给你打电话正确的API.
目录获取器
import { createCatalogueFetcher, catalogueFetcherGraphqlBuilder as b } from '@crystallize/js-api-client';
const fetchCatalogue = createCatalogueFetcher(api);
const data = await fetchCatalogue<{ catalogue: { name: string; path: string } }>({
catalogue: {
__args: { path: '/shop', language: 'en' },
name: true,
path: true,
...b.onProduct({}, { onVariant: { sku: true, name: true } }),
},
});
导航获取器
import { createNavigationFetcher } from '@crystallize/js-api-client';
const nav = createNavigationFetcher(api);
const tree = await nav.byFolders('/', 'en', 3, /* extra root-level query */ undefined, (level) => {
if (level === 1) return { shape: { identifier: true } };
return {};
});
产品水利机
通过路径或具有可选价格背景的SKU获取产品/变量数据.
import { createProductHydrater } from '@crystallize/js-api-client';
const hydrater = createProductHydrater(api, {
marketIdentifiers: ['eu'],
priceList: 'b2b',
priceForEveryone: true,
});
const products = await hydrater.bySkus(
['SKU-1', 'SKU-2'],
'en',
/* extraQuery */ undefined,
(sku) => ({ vatType: { name: true, percent: true } }),
() => ({ priceVariants: { identifier: true, price: true } }),
);
顺序获取器
import { createOrderFetcher } from '@crystallize/js-api-client';
const orders = createOrderFetcher(api);
const order = await orders.byId('order-id', {
onOrder: { payment: { provider: true } },
onOrderItem: { subscription: { status: true } },
onCustomer: { email: true },
});
const list = await orders.byCustomerIdentifier('customer-123', { first: 20 });
已键入实例( TypeScript 通用名) :
type OrderExtras = { payment: { provider: string }[] };
type OrderItemExtras = { subscription?: { status?: string } };
type CustomerExtras = { email?: string };
const typedOrder = await orders.byId<OrderExtras, OrderItemExtras, CustomerExtras>('order-id', {
onOrder: { payment: { provider: true } },
onOrderItem: { subscription: { status: true } },
onCustomer: { email: true },
});
typedOrder.payment; // typed as array with provider
typedOrder.cart[0].subscription?.status; // typed
typedOrder.customer.email; // typed
订单管理器
创建/更新订单,设定付款或转入管道阶段. 输入以 @crystallize/schema。 。 。 .
import { createOrderManager } from '@crystallize/js-api-client';
const om = createOrderManager(api);
// Register (minimal example)
const confirmation = await om.register({
cart: [{ sku: 'SKU-1', name: 'Product', quantity: 1, price: { gross: 100, net: 80, currency: 'USD' } }],
customer: { identifier: 'customer-123' },
});
// Update payments only
await om.setPayments('order-id', [
{
provider: 'STRIPE',
amount: { gross: 100, net: 80, currency: 'USD' },
method: 'card',
},
]);
// Put in pipeline stage
await om.putInPipelineStage({ id: 'order-id', pipelineId: 'pipeline', stageId: 'stage' });
客户和客户小组管理员
import { createCustomerManager, createCustomerGroupManager } from '@crystallize/js-api-client';
const customers = createCustomerManager(api);
await customers.create({ identifier: 'cust-1', email: '[email protected]' });
await customers.update({ identifier: 'cust-1', firstName: 'John' });
const groups = createCustomerGroupManager(api);
await groups.create({ identifier: 'vip', name: 'VIP' });
订阅合同管理器
创建/更新合同并生成一个变体预填模板.
import { createSubscriptionContractManager } from '@crystallize/js-api-client';
const scm = createSubscriptionContractManager(api);
const template = await scm.createTemplateBasedOnVariantIdentity(
'/shop/my-product',
'SKU-1',
'plan-identifier',
'period-id',
'default',
'en',
);
// …tweak template and create
const created = await scm.create({
customerIdentifier: 'customer-123',
tenantId: 'tenant-id',
payment: {
/* … */
},
...template,
});
墨盒管理器 (Shop API)
托肯处理是自动的( 除非你通过) shopApiToken 设置 shopApiToken.doNotFetch: true) (中文(简体) ).
import { createCartManager } from '@crystallize/js-api-client';
const cart = createCartManager(api);
// Hydrate a cart from input
const hydrated = await cart.hydrate({
language: 'en',
items: [{ sku: 'SKU-1', quantity: 1 }],
});
// Add/remove items, abandon or place and fulfill the cart and assign the orderId
await cart.addSkuItem(hydrated.id, { sku: 'SKU-2', quantity: 2 });
await cart.setCustomer(hydrated.id, { identifier: 'customer-123', email: '[email protected]' });
await cart.setMeta(hydrated.id, { merge: true, meta: [{ key: 'source', value: 'web' }] });
await cart.abandon(hydrated.id);
await cart.place(hydrated.id);
await cart.fulfill(hydrated.id, orderId);
商店API订单
createShopOrderManager 与商店API谈话 /order 终点:将一推车转换成退货订单,记录付款情况,并通过管线移动订单。 (createOrderManager 以上针对核心API.) 输入用来自 @crystallize/schema/shop,每个方法返回顺序 id 加上您选择的字段.
import { createShopOrderManager } from '@crystallize/js-api-client';
import type { Order } from '@crystallize/schema/shop';
const orders = createShopOrderManager(api);
const order = await orders.createFromCart(cartId, {
type: 'standard',
paymentStatus: 'paid',
payments: [{ provider: 'stripe', transactionId: 'pi_123', amount: 100, method: 'card' }],
pipelines: [{ identifier: 'fulfilment', stage: 'new' }],
});
await orders.addPayments(order.id, [{ provider: 'gift-card', amount: 20, meta: [{ key: 'code', value: 'XMAS' }] }]);
await orders.setPayments(order.id, [{ provider: 'stripe', amount: 120 }]); // replaces all payments
await orders.setMeta(order.id, { meta: [{ key: 'source', value: 'web' }] }); // merge: true by default
await orders.setCustomer(order.id, { isGuest: false, identifier: 'customer-123', type: 'individual', addresses: [] });
await orders.addToStage(order.id, 'fulfilment', 'shipped');
await orders.removeFromPipeline(order.id, 'fulfilment');
const fetched = await orders.fetch<Order>(order.id, { reference: true, payments: { provider: true, meta: true } });
const history = await orders.listByCustomer('customer-123', { limit: 10, skip: 0 }, { reference: true });
payments接受任何provider字符串:它们记录了所发生的事情,没有任何指控.payments[].meta以[{ key, value }]列表,但读回为 JSON 对象({ code: 'XMAS' }) (中文(简体) ).createFromCart, (中文(简体) ),addPayments财务报告和审定财务报表setPayments在回复后继续执行命令:在回复后立即发布的读数仍可返回先前状态.fetch以JSApiClientCallError当命令不存在时.
商店 API 客户和锁
import { createShopCustomerManager, createShopLock } from '@crystallize/js-api-client';
// `/customer`: customers keyed by identifier
const customers = createShopCustomerManager(api);
await customers.upsert({ identifier: 'customer-123', email: '[email protected]', firstName: 'John' }); // create or update
await customers.addAddress('customer-123', { type: 'delivery', street: 'Main St 1', city: 'Oslo' });
await customers.setAddress('customer-123', 0, { type: 'delivery', street: 'Main St 2', city: 'Oslo' });
await customers.removeAddress('customer-123', 0);
await customers.setMeta('customer-123', { meta: [{ key: 'tier', value: 'gold' }] });
const customer = await customers.fetch('customer-123', { email: true, addresses: { city: true } });
// `/lock`: a distributed lock, e.g. so a webhook and a redirect don't both create the order
const lock = createShopLock(api);
if (await lock.acquire(`cart-${cartId}`, 30)) {
// ttl in seconds, defaults to 60; resolves false when the lock is already held
try {
// …createFromCart
} finally {
await lock.release(`cart-${cartId}`);
}
}
签字核查
使用 createSignatureVerifier 以验证用于webhooks,apps或前端呼叫的 Crystalization 签名. 核查员将HS256 JWT信封与共享的密件解码,并与其匹配 hmac sHA-256索赔——全部通过捆绑的 jose 和平台的 crypto.subtle,所以你不需要通过自己的JWT或散去执行.
import { createSignatureVerifier } from '@crystallize/js-api-client';
const verify = createSignatureVerifier({ secret: process.env.CRYSTALLIZE_SIGNATURE_SECRET! });
// POST example
await verify(signatureJwtFromHeader, {
url: request.url,
method: 'POST',
body: rawBodyString, // IMPORTANT: raw body
});
// GET webhook example (must pass the original webhook URL)
await verify(signatureJwtFromHeader, {
url: request.url, // the received URL including query params
method: 'GET',
webhookUrl: 'https://example.com/api/webhook', // the configured webhook URL in Crystallize
});
插件有效载荷解密
使用 createPluginPayloadDecrypter 解密一个 Crystallize 插件 JWE 有效载荷(外部 JWE = 嵌入式 JWS 信封 = 逐地) encryptedSecrets),并可选择性地对照JWKS验证内置JWS. 这是CLI的单一切入点,任何供应商方的整合都应依赖此.
工厂取自销售商的私人JWK(如: crystallize plugin keygen)和可选 verify 设置,并返回可重复使用的函数,每个调用接受JWE紧凑的有效载荷。 私钥和JWKS解析器是一次建造并被再用——因此对于一个处理许多webhook呼叫的服务器,在启动时创建解密器一次.
仅 RSA-OAEP 编号 : RSA-OAEP-256 与 A*GCM 内容加密在外 JWE 上被接受. 当外头带着 cty: "JWT",简文作为紧凑的JWS处理,其索赔构成信封.
签名验证为选入:通过 verify 以使之得以实现。 何时 verify (a) 删去(或在核查失败时)信封和每个外地的秘密仍被退回,以便打电话者检查; signature.verified 编号 : signature.skipped 编号 : signature.reason 告诉你是否相信结果.
import { readFile } from 'node:fs/promises';
import { createPluginPayloadDecrypter } from '@crystallize/js-api-client';
const privateJwk = JSON.parse(await readFile('./private.jwk.json', 'utf8'));
// Decrypt only — no signature check. Good for local dev / smoke tests.
const decrypt = createPluginPayloadDecrypter({ privateJwk });
const decoded = await decrypt(jweCompact);
if (decoded.envelope) {
console.log('tenant:', decoded.envelope.tenantIdentifier);
console.log('config:', decoded.envelope.config);
console.log('secrets:', decoded.secrets); // { StripeApiKey: 'sk_live_…', … }
}
以通过方式启用校验 verify 至少有一个 audience (您的插件标识符). issuer 默认为 https://api.crystallize.com 财务报告和审定财务报表 jwksUrl 默认为 ${issuer}/.well-known/jwks.json,所以生产用途是单行道:
// Production — issuer + JWKS URL default to api.crystallize.com.
const decrypt = createPluginPayloadDecrypter({
privateJwk,
verify: { audience: 'com.vendor.plugin' },
});
const verified = await decrypt(jweCompact);
if (!verified.signature.verified) {
// Signature check skipped or failed — envelope + secrets are still populated but MUST be treated as untrusted.
console.warn('signature not trusted:', verified.signature.reason);
}
其它 verify 字段 : clockTolerance (秒,默认为 30, (中文(简体) ), verifyBackendToken (还核查) envelope.backendToken 默认为 false) (中文(简体) ).
回来的 DecryptedPluginPayload 包含:
protectedHeader外 JWE 保护头innerProtectedHeader内置JWS保护头,当有效载荷被嵌入时envelope核实(或解码)JWS索赔,或null用于未记录的有效载荷plaintext当有效载荷不是一窝JWT时,原始外平面文字,否则nullsecrets——逐字解密的简单文字envelope.encryptedSecretssignature—— 说{ verified, skipped?, reason?, issuer?, audience?, algorithm? }backendToken—— 说{ verified, skipped?, reason?, claims? }何时envelope.backendToken在场,否则null
警卫:
secrets并解码envelope索赔要求包含清晰的文字证明。 不日志或将其转入共享汇.
为公用事业定价
import { pricesForUsageOnTier } from '@crystallize/js-api-client';
const usage = 1200;
const total = pricesForUsageOnTier(
usage,
[
{ threshold: 0, price: 0, currency: 'USD' },
{ threshold: 1000, price: 0.02, currency: 'USD' },
],
'graduated',
);
二进制文件管理器
通过预先签名的请求上传文件( 如图像) 。 仅限服务器侧.
import { createBinaryFileManager } from '@crystallize/js-api-client';
const files = createBinaryFileManager(api);
const mediaKey = await files.uploadImage('/absolute/path/to/picture.jpg');
const staticKey = await files.uploadFile('/absolute/path/to/static/file.pdf');
const bulkKey = await files.uploadMassOperationFile('/absolute/path/to/import.zip');
// Use the returned keys in subsequent PIM mutations
uploadImage 验证文件在创建前为图像 MEDIA 上传. 使用 uploadFile 适用于应居住在租户静态文件存储中的资产,以及 uploadMassOperationFile 由批量业务管道处理的进口货物。 打电话 uploadToTenant 需要下级控制(如自定义缓冲或上传类型)时直接.
大众呼叫客户端( 已过期)
折旧: 使用成熟的生态系统组合,如
p-limit或者说p-queue换句话说 它们提供更好的错误处理,TypeScript支持,并被积极维护.
建议使用 p- 限额的替代品
import pLimit from 'p-limit';
import { createClient } from '@crystallize/js-api-client';
const api = createClient({ tenantIdentifier: 'my-tenant', accessTokenId: '…', accessTokenSecret: '…' });
const limit = pLimit(5); // max 5 concurrent requests
const mutations = items.map((item) =>
limit(() =>
api.pimApi(
`mutation UpdateItem($id: ID!, $name: String!) { product { update(id: $id, input: { name: $name }) { id } } }`,
{ id: item.id, name: item.name },
),
),
);
const results = await Promise.allSettled(mutations);
const failed = results.filter((r) => r.status === 'rejected');
console.log(`Done: ${results.length - failed.length} succeeded, ${failed.length} failed`);
遗产使用
大众呼叫客户端仍然在运行,但会在首次使用时发出去色警告.
有时,当你有很多电话需要处理, 无论是询问还是突变, 你都想能够同步管理它们。 这是大众呼叫客户端的目的. 它将使你成为同步,管理生命周期的起重,再试,增减速度等.
这些是主要特点:
- 运行 首页 请求在批次中同步. 首页 默认的批量大小
- 如果批次有超过50%的出错,则会保存出错并延续批次大小为 1
- 如果批次中出现不到50%的出错,则会保存出错并延续当前批次大小倒数 1
- 如果没有出错,它会逐批递增(+1)请求的数量,上限为 最大缩放
- 如果错误率是100%,则根据 费博纳奇 递增
- 所有批次结束后, 您可以重试失败的请求
- 可选生命周期功能 打开批次 (同步)
- 可选生命周期功能 失败 (同步) 允许您做一些事情, 并且决定让命令( 返回真实: 默认) 或立即返回虚假和重新执行, 或任何其他行动
- 可选生命周期功能 在请求前 (同步)在每次请求前执行. 您可以返回更改的请求/承诺
- 可选生命周期功能 请求后 (同步) 每次请求后执行 。 如果需要,你也会得到结果
const client = createMassCallClient(api, { initialSpawn: 1 });
async function run() {
for (let i = 1; i <= 54; i++) {
client.enqueue.catalogueApi(`query { catalogue { id, key${i}: name } }`);
}
const successes = await client.execute();
console.log('First pass done ', successes);
console.log('Failed Count: ' + client.failureCount());
while (client.hasFailed()) {
console.log('Retrying...');
const newSuccesses = await client.retry();
console.log('Retry pass done ', newSuccesses);
}
console.log('ALL DONE!');
}
run();