Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
68a3232
fix(catalogue): fix 'chidlren' typo in GraphQL query builder
Plopix Mar 12, 2026
3c25287
fix(api-caller): remove dead try/catch block that only rethrows
Plopix Mar 12, 2026
d85bb52
chore: delete dead commented-out subscription.ts file
Plopix Mar 12, 2026
88d41d8
fix(catalogue): simplify double spread to single spread in product hy…
Plopix Mar 12, 2026
146f876
fix(mass-call): use object literal instead of array for results initi…
Plopix Mar 12, 2026
be4265c
fix(package): correct module field to match actual ESM build output
Plopix Mar 12, 2026
a903151
fix(error): set name property on JSApiClientCallError for better debu…
Plopix Mar 12, 2026
46ec4d4
fix(types): replace 'any' with proper GrabOptions type in grabber and…
Plopix Mar 12, 2026
165b56f
fix(security): replace tracked .env with .env.example template
Plopix Mar 12, 2026
7920727
feat(auth): warn when no authentication credentials are configured
Plopix Mar 12, 2026
0dff69e
fix(profiling): use regex to parse Server-Timing header reliably
Plopix Mar 12, 2026
67192c6
refactor(types): rename cryptic generic parameters to descriptive nam…
Plopix Mar 12, 2026
763f4ff
feat(client): add Symbol.dispose support for automatic cleanup
Plopix Mar 12, 2026
5ed7762
feat(client): add request timeout support via AbortController
Plopix Mar 13, 2026
795dca8
refactor(types): replace any with proper types in mass call client
Plopix Mar 13, 2026
2e64734
refactor(mass-call): replace repetitive enqueue methods with generate…
Plopix Mar 13, 2026
bd8b7b7
test: add unit tests with mocked HTTP for core modules
Plopix Mar 13, 2026
4602983
test: add comprehensive error-path tests for API caller
Plopix Mar 13, 2026
0b8e1c5
ci: add GitHub Actions workflow for PR and main branch checks
Plopix Mar 13, 2026
ee3ec82
docs: add JSDoc comments to all main exported factory functions
Plopix Mar 13, 2026
447af4e
feat: review
Plopix Mar 13, 2026
3341835
fix: await onBatchDone callback in mass call client
Plopix Apr 3, 2026
4d726a5
fix: remove Promise constructor antipattern in mass call client
Plopix Apr 3, 2026
56bfe48
fix: type HTTP/2 clients Map and add null guard in grabber
Plopix Apr 3, 2026
09296f6
fix: stop sending empty auth headers when no credentials configured
Plopix Apr 3, 2026
197f0a3
refactor: extract shared lifecycleAction helper in subscription contr…
Plopix Apr 3, 2026
5ecf7d5
refactor: extract shared helpers in cart manager to eliminate duplica…
Plopix Apr 3, 2026
837dbd0
deprecate: add deprecation notice to mass call client, recommend p-li…
Plopix Apr 3, 2026
dd98cc1
feat: add statusCode getter alias on JSApiClientCallError
Plopix Apr 3, 2026
aa338dc
feat: make HTTP/2 idle timeout configurable via http2IdleTimeout option
Plopix Apr 3, 2026
ff66fd2
docs: update README with http2IdleTimeout, timeout options and status…
Plopix Apr 3, 2026
95ad6c2
clean
Plopix Apr 3, 2026
78712b0
review 1
Plopix Apr 3, 2026
13eece5
review
Plopix Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions components/js-api-client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# PROD
CRYSTALLIZE_TENANT_ID=your-tenant-id-here
CRYSTALLIZE_TENANT_IDENTIFIER=your-tenant-identifier-here
CRYSTALLIZE_ACCESS_TOKEN_ID=your-token-id-here
CRYSTALLIZE_ACCESS_TOKEN_SECRET=your-token-secret-here

# # DEV
# CRYSTALLIZE_TENANT_ID=your-dev-tenant-id-here
# CRYSTALLIZE_TENANT_IDENTIFIER=your-dev-tenant-identifier-here
# CRYSTALLIZE_ACCESS_TOKEN_ID=your-dev-token-id-here
# CRYSTALLIZE_ACCESS_TOKEN_SECRET=your-dev-token-secret-here
# CRYSTALLIZE_ORIGIN=-dev.crystallize.digital
1 change: 1 addition & 0 deletions components/js-api-client/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
.env

yarn.lock
package-lock.json
55 changes: 50 additions & 5 deletions components/js-api-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ api.close();
- `origin` custom host suffix (defaults to `.crystallize.com`)
- options
- `useHttp2` enable HTTP/2 transport
- `timeout` request timeout in milliseconds; requests that take longer will be aborted
- `http2IdleTimeout` HTTP/2 idle timeout in milliseconds (default `300000` — 5 minutes). Use a shorter value for serverless functions, a longer one for long-running servers
- `profiling` callbacks
- `extraHeaders` extra request headers for all calls
- `shopApiToken` controls auto-fetch: `{ doNotFetch?: boolean; scopes?: string[]; expiresIn?: number }`
Expand All @@ -89,6 +91,23 @@ Pass the relevant credentials to `createClient`:

See the official docs for auth: https://crystallize.com/learn/developer-guides/api-overview/authentication

### Error handling

API call errors throw a `JSApiClientCallError` with both `code` and `statusCode` properties for the HTTP status:

```typescript
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)
}
}
```

## Profiling requests

Log queries, timings and server timing if available.
Expand Down Expand Up @@ -378,7 +397,36 @@ const bulkKey = await files.uploadMassOperationFile('/absolute/path/to/import.zi

[crystallizeobject]: crystallize_marketing|folder|625619f6615e162541535959

## Mass Call Client
## Mass Call Client (Deprecated)

> **Deprecated:** Use mature ecosystem packages like [`p-limit`](https://www.npmjs.com/package/p-limit) or [`p-queue`](https://www.npmjs.com/package/p-queue) instead. They provide better error handling, TypeScript support, and are actively maintained.

### Recommended alternative using p-limit

```typescript
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`);
```

### Legacy usage

The mass call client is still functional but will emit a deprecation warning on first use.

Sometimes, when you have many calls to do, whether they are queries or mutations, you want to be able to manage them asynchronously. This is the purpose of the Mass Call Client. It will let you be asynchronous, managing the heavy lifting of lifecycle, retry, incremental increase or decrease of the pace, etc.

Expand All @@ -396,8 +444,7 @@ These are the main features:
- Optional lifecycle function *afterRequest* (sync) to execute after each request. You also get the result in there, if needed

```javascript
// import { createMassCallClient } from '@crystallize/js-api-client';
const client = createMassCallClient(api, { initialSpawn: 1 }); // api created via createClient(...)
const client = createMassCallClient(api, { initialSpawn: 1 });

async function run() {
for (let i = 1; i <= 54; i++) {
Expand All @@ -416,5 +463,3 @@ async function run() {
}
run();
```

Full example: https://github.com/CrystallizeAPI/libraries/blob/main/components/js-api-client/src/examples/dump-tenant.ts
26 changes: 26 additions & 0 deletions components/js-api-client/UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
# Upgrade Guide

## From v5

### `extraHeaders` type widened

The `extraHeaders` option on `createClient` now accepts `Record<string, string> | Headers | [string, string][]` instead of only `Record<string, string>`. This is **not a breaking change** — all existing code continues to work. If you were casting headers to `Record<string, string>`, you can now pass `Headers` instances or tuple arrays directly.

### HTTP/2 stability

The HTTP/2 transport now guards against double-settlement of promises when abort signals fire after a request has already completed. No API changes — this is a reliability fix.

### `JSApiClientCallError.statusCode` alias

A read-only `statusCode` getter was added as an alias for `code`, following the Node.js convention. Both properties return the same numeric HTTP status.

### `http2IdleTimeout` option

You can now configure the HTTP/2 session idle timeout via `createClient(config, { http2IdleTimeout: 60000 })`. The default remains 300 000 ms (5 minutes).

### `timeout` option

A request-level timeout can be set via `createClient(config, { timeout: 10000 })`. When set, requests that exceed the timeout are aborted with an `AbortError`.

---

# Upgrade Guide to v5

This guide helps you migrate from v4 to v5 of `@crystallize/js-api-client`.
Expand Down
18 changes: 9 additions & 9 deletions components/js-api-client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@crystallize/js-api-client",
"license": "MIT",
"version": "5.3.0",
"version": "6.0.0",
"type": "module",
"author": "Crystallize <hello@crystallize.com> (https://crystallize.com)",
"contributors": [
Expand All @@ -26,20 +26,20 @@
},
"types": "./dist/index.d.ts",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"module": "./dist/index.js",
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/node": "^24.2.0",
"dotenv": "^16.6.1",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"@tsconfig/node22": "^22.0.5",
"@types/node": "^25.5.2",
"dotenv": "^17.4.0",
"tsup": "^8.5.1",
"typescript": "^6.0.2",
"vitest": "^4.1.2"
},
"dependencies": {
"@crystallize/schema": "workspace:*",
"json-to-graphql-query": "^2.3.0",
"mime-lite": "^1.0.3",
"zod": "^4.1.12"
"zod": "^4.3.6"
},
"browser": {
"fs": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ export type CatalogueFetcherGrapqhqlOnFolder<OC = unknown> = {
onChildren?: OC;
};

/**
* Creates a catalogue fetcher that executes queries against the Crystallize Catalogue API using JSON-based query objects.
* Use this when you want to build catalogue queries programmatically instead of writing raw GraphQL strings.
*
* @param client - A Crystallize client instance created via `createClient`.
* @returns A function that accepts a JSON query object and optional variables, and returns the catalogue data.
*
* @example
* ```ts
* const fetcher = createCatalogueFetcher(client);
* const data = await fetcher({
* catalogue: {
* __args: { path: '/my-product', language: 'en' },
* name: true,
* path: true,
* },
* });
* ```
*/
export const createCatalogueFetcher = (client: ClientInterface) => {
return <T = unknown>(query: object, variables?: VariablesType): Promise<T> => {
return client.catalogueApi<T>(jsonToGraphQLQuery({ query }), variables);
Expand Down Expand Up @@ -64,7 +83,7 @@ function onFolder<OF = unknown>(onFolder?: OF, c?: CatalogueFetcherGrapqhqlOnFol
const children = () => {
if (c?.onChildren) {
return {
chidlren: {
children: {
...c.onChildren,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,20 @@ function buildNestedNavigationQuery(
return jsonToGraphQLQuery({ query });
}

/**
* Creates a navigation fetcher that builds nested tree queries for folder-based or topic-based navigation.
* Use this to retrieve hierarchical navigation structures from the Crystallize catalogue.
*
* @param client - A Crystallize client instance created via `createClient`.
* @returns An object with `byFolders` and `byTopics` methods for fetching navigation trees at a given depth.
*
* @example
* ```ts
* const nav = createNavigationFetcher(client);
* const folderTree = await nav.byFolders('/', 'en', 3);
* const topicTree = await nav.byTopics('/', 'en', 2);
* ```
*/
export function createNavigationFetcher(client: ClientInterface): {
byFolders: TreeFetcher;
byTopics: TreeFetcher;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function byPaths(client: ClientInterface, options?: ProductHydraterOptions): Pro
}, {} as any);

const query = {
...{ ...productListQuery },
...productListQuery,
...(extraQuery !== undefined ? extraQuery : {}),
};

Expand Down Expand Up @@ -140,6 +140,21 @@ function bySkus(client: ClientInterface, options?: ProductHydraterOptions): Prod
};
}

/**
* Creates a product hydrater that fetches full product data from the catalogue by paths or SKUs.
* Use this to enrich a list of product references with complete variant, pricing, and attribute data.
*
* @param client - A Crystallize client instance created via `createClient`.
* @param options - Optional settings for market identifiers, price lists, and price-for-everyone inclusion.
* @returns An object with `byPaths` and `bySkus` methods for hydrating products.
*
* @example
* ```ts
* const hydrater = createProductHydrater(client);
* const products = await hydrater.byPaths(['/shop/my-product'], 'en');
* const productsBySkus = await hydrater.bySkus(['SKU-001', 'SKU-002'], 'en');
* ```
*/
export function createProductHydrater(client: ClientInterface, options?: ProductHydraterOptions) {
return {
byPaths: byPaths(client, options),
Expand Down
Loading
Loading