@commerce-blocks/sdk 2.0.0-alpha.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -850
- package/dist/index.d.ts +345 -581
- package/dist/index.js +1343 -2004
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @commerce-blocks/sdk
|
|
2
2
|
|
|
3
|
-
ES module SDK
|
|
3
|
+
ES module SDK for product discovery — browse collections, search, recommendations, and image search via Layers API.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -11,746 +11,318 @@ npm install @commerce-blocks/sdk
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { createClient } from '@commerce-blocks/sdk'
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
layersPublicToken: 'your-layers-token',
|
|
16
|
+
const { data: client, error } = createClient({
|
|
17
|
+
token: 'your-layers-token',
|
|
19
18
|
sorts: [
|
|
20
|
-
{
|
|
21
|
-
{
|
|
19
|
+
{ name: 'Featured', code: 'featured' },
|
|
20
|
+
{ name: 'Price: Low to High', code: 'price_asc' },
|
|
21
|
+
],
|
|
22
|
+
facets: [
|
|
23
|
+
{ name: 'Color', code: 'options.color' },
|
|
24
|
+
{ name: 'Size', code: 'options.size' },
|
|
22
25
|
],
|
|
23
|
-
facets: ['options.color', 'options.size', 'vendor'],
|
|
24
|
-
|
|
25
|
-
// Opt in to Storefront API for additional product data
|
|
26
|
-
// enableStorefront: true,
|
|
27
|
-
// shop: 'your-store.myshopify.com',
|
|
28
|
-
// storefrontPublicToken: 'your-storefront-token',
|
|
29
26
|
})
|
|
30
27
|
|
|
31
|
-
if (
|
|
32
|
-
console.error('SDK init failed:', result.error.message)
|
|
33
|
-
} else {
|
|
34
|
-
const sdk = result.data
|
|
35
|
-
// Ready to use
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Configuration
|
|
40
|
-
|
|
41
|
-
### Layers
|
|
42
|
-
|
|
43
|
-
| Option | Type | Required | Description |
|
|
44
|
-
| ------------------- | ------------- | -------- | ---------------------------------------------------- |
|
|
45
|
-
| `layersPublicToken` | `string` | Yes | Layers API public token |
|
|
46
|
-
| `sorts` | `Sort[]` | Yes | Sort options (`{ label, code }`) |
|
|
47
|
-
| `facets` | `string[]` | Yes | Facet fields for filtering |
|
|
48
|
-
| `attributes` | `string[]` | No | Product attributes to fetch |
|
|
49
|
-
| `layersBaseUrl` | `string` | No | Custom API URL |
|
|
50
|
-
| `fetch` | `CustomFetch` | No | Custom fetch implementation (SSR, testing, proxying) |
|
|
51
|
-
|
|
52
|
-
### Storefront (optional)
|
|
28
|
+
if (error) throw new Error(error.message)
|
|
53
29
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
| Option | Type | Required | Description |
|
|
57
|
-
| ----------------------- | ------------- | ----------------------------- | ---------------------------------------------------- |
|
|
58
|
-
| `enableStorefront` | `boolean` | No | Enable Storefront API hydration (default: `false`) |
|
|
59
|
-
| `shop` | `string` | When `enableStorefront: true` | Store domain |
|
|
60
|
-
| `storefrontPublicToken` | `string` | When `enableStorefront: true` | Storefront API public access token |
|
|
61
|
-
| `storefrontApiVersion` | `string` | No | API version (default: `2025-01`) |
|
|
62
|
-
| `fetch` | `CustomFetch` | No | Custom fetch implementation (SSR, testing, proxying) |
|
|
63
|
-
|
|
64
|
-
Layers API supports identity tracking for personalization. Pass identity fields via request context:
|
|
30
|
+
const collection = client.collection({ handle: 'shirts' })
|
|
31
|
+
const result = await collection.execute()
|
|
65
32
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
deviceId?: string // Device identifier
|
|
70
|
-
sessionId?: string // Session identifier
|
|
71
|
-
customerId?: string // Customer identifier
|
|
33
|
+
if (result.data) {
|
|
34
|
+
console.log(result.data.products)
|
|
35
|
+
console.log(result.data.totalResults)
|
|
72
36
|
}
|
|
73
|
-
```
|
|
74
37
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Option | Type | Description |
|
|
78
|
-
| ---------------------- | ------------------------------ | ------------------------------ |
|
|
79
|
-
| `currencyCode` | `string` | Currency for price formatting |
|
|
80
|
-
| `formatPrice` | `(amount, currency) => string` | Custom price formatter |
|
|
81
|
-
| `swatches` | `Swatch[]` | Color swatch definitions |
|
|
82
|
-
| `options` | `string[]` | Product options to expose |
|
|
83
|
-
| `productMetafields` | `{ namespace, key }[]` | Product metafields to fetch |
|
|
84
|
-
| `variantMetafields` | `{ namespace, key }[]` | Variant metafields to fetch |
|
|
85
|
-
| `collectionMetafields` | `{ namespace, key }[]` | Collection metafields to fetch |
|
|
86
|
-
| `pageMetafields` | `{ namespace, key }[]` | Page metafields to fetch |
|
|
87
|
-
|
|
88
|
-
### Extensibility
|
|
38
|
+
collection.dispose()
|
|
39
|
+
```
|
|
89
40
|
|
|
90
|
-
|
|
91
|
-
| ------------------ | ---------------------------------- | ---------------------------------- |
|
|
92
|
-
| `extendProduct` | `({ base, raw, storefront }) => P` | Transform products after hydration |
|
|
93
|
-
| `extendCollection` | `(result, raw) => result` | Transform collection results |
|
|
94
|
-
| `extendSearch` | `(result, raw) => result` | Transform search results |
|
|
95
|
-
| `extendBlock` | `(result, raw) => result` | Transform blocks results |
|
|
96
|
-
| `transformFilters` | `(filters) => FilterGroup` | Custom filter transformation |
|
|
97
|
-
| `filterMap` | `FilterMap` | URL-friendly filter key mapping |
|
|
41
|
+
## Configuration
|
|
98
42
|
|
|
99
|
-
|
|
43
|
+
| Option | Type | Required | Description |
|
|
44
|
+
| --------------- | ------------------------------ | -------- | ------------------------------------ |
|
|
45
|
+
| `token` | `string` | Yes | Layers API public token |
|
|
46
|
+
| `sorts` | `Sort[]` | Yes | Sort options `{ name, code }` |
|
|
47
|
+
| `facets` | `Facet[]` | Yes | Facet fields `{ name, code }` |
|
|
48
|
+
| `attributes` | `string[]` | No | Product attributes to fetch |
|
|
49
|
+
| `baseUrl` | `string` | No | Custom API URL |
|
|
50
|
+
| `fetch` | `CustomFetch` | No | Custom fetch (SSR, testing) |
|
|
51
|
+
| `currency` | `string` | No | Currency for price formatting |
|
|
52
|
+
| `formatPrice` | `(amount, currency) => string` | No | Custom price formatter |
|
|
53
|
+
| `swatches` | `Swatch[]` | No | Color swatch definitions |
|
|
54
|
+
| `transforms` | `Transforms` | No | Post-process results (see below) |
|
|
55
|
+
| `filterAliases` | `FilterAliases` | No | URL-friendly filter key mapping |
|
|
56
|
+
| `cacheLimit` | `number` | No | Max entries in cache |
|
|
57
|
+
| `cacheLifetime` | `number` | No | TTL in milliseconds |
|
|
58
|
+
| `storage` | `StorageAdapter` | No | Custom storage adapter |
|
|
59
|
+
| `initialData` | `CacheData` | No | Pre-populate cache at init |
|
|
60
|
+
| `restoreCache` | `boolean` | No | Restore from storage (default: true) |
|
|
61
|
+
|
|
62
|
+
## Controllers
|
|
63
|
+
|
|
64
|
+
All controllers (`collection`, `blocks`, `search`, `suggest`) follow the same pattern:
|
|
65
|
+
|
|
66
|
+
- **`state`** — a `ReadonlySignal<QueryState<T>>` with `{ data, error, isFetching }`
|
|
67
|
+
- **`execute()`** — runs the query, returns `Result<T, ClientError>`
|
|
68
|
+
- **`subscribe(callback)`** — reacts to state changes without importing signals
|
|
69
|
+
- **`dispose()`** — cleans up subscriptions and aborts pending requests
|
|
70
|
+
|
|
71
|
+
### Subscribing to State
|
|
72
|
+
|
|
73
|
+
Three ways to consume controller state (pick one):
|
|
100
74
|
|
|
101
75
|
```typescript
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
isNew: raw.tags?.includes('new') ?? false,
|
|
108
|
-
rating: raw.calculated?.average_rating,
|
|
109
|
-
}),
|
|
110
|
-
filterMap: {
|
|
111
|
-
color: 'options.color',
|
|
112
|
-
size: 'options.size',
|
|
113
|
-
},
|
|
76
|
+
// 1. Controller subscribe — no signal import needed
|
|
77
|
+
const unsubscribe = controller.subscribe(({ data, error, isFetching }) => {
|
|
78
|
+
if (isFetching) showLoading()
|
|
79
|
+
if (error) showError(error.message)
|
|
80
|
+
if (data) render(data.products)
|
|
114
81
|
})
|
|
115
82
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
### Cache
|
|
123
|
-
|
|
124
|
-
| Option | Type | Description |
|
|
125
|
-
| ------------------ | -------- | -------------------------- |
|
|
126
|
-
| `cacheMaxProducts` | `number` | Max products in cache |
|
|
127
|
-
| `cacheMaxEntries` | `number` | Max query entries in cache |
|
|
128
|
-
| `cacheTtl` | `number` | TTL in milliseconds |
|
|
129
|
-
|
|
130
|
-
### Data Hydration
|
|
83
|
+
// 2. Standalone subscribe — works with any signal
|
|
84
|
+
import { subscribe } from '@commerce-blocks/sdk'
|
|
85
|
+
const unsubscribe = subscribe(controller.state, (state) => {
|
|
86
|
+
/* ... */
|
|
87
|
+
})
|
|
131
88
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
89
|
+
// 3. Direct signal access — for custom reactivity
|
|
90
|
+
import { effect } from '@commerce-blocks/sdk'
|
|
91
|
+
effect(() => {
|
|
92
|
+
const { data } = controller.state.value
|
|
93
|
+
})
|
|
94
|
+
```
|
|
136
95
|
|
|
137
|
-
|
|
96
|
+
### Shared Query Parameters
|
|
138
97
|
|
|
139
|
-
|
|
98
|
+
These parameters are available on `execute()` for all controllers except suggest:
|
|
140
99
|
|
|
141
|
-
|
|
100
|
+
| Parameter | Type | Description |
|
|
101
|
+
| ------------------ | ------------------------- | ---------------------------------- |
|
|
102
|
+
| `page` | `number` | Page number (default: 1) |
|
|
103
|
+
| `limit` | `number` | Products per page (default: 24) |
|
|
104
|
+
| `filters` | `unknown` | Filter criteria |
|
|
105
|
+
| `signal` | `AbortSignal` | Per-call abort signal |
|
|
106
|
+
| `linking` | `Record<string, unknown>` | Dynamic linking parameters |
|
|
107
|
+
| `transformRequest` | `(body) => body` | Custom request body transformation |
|
|
142
108
|
|
|
143
|
-
|
|
144
|
-
- **Reactive (callback)**: Use `controller.subscribe(callback)` — receives state updates without needing signals
|
|
145
|
-
- **Imperative**: `await controller.execute()` returns `Result<T, SdkError>` directly
|
|
109
|
+
### Collection
|
|
146
110
|
|
|
147
|
-
|
|
111
|
+
Browse products in a collection.
|
|
148
112
|
|
|
149
113
|
```typescript
|
|
150
|
-
const collection =
|
|
114
|
+
const collection = client.collection({
|
|
151
115
|
handle: 'shirts',
|
|
152
116
|
defaultSort: 'featured', // optional, uses first sort if omitted
|
|
153
117
|
})
|
|
154
118
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// Option 2: Standalone subscribe() — works with any signal
|
|
163
|
-
const unsubscribe = subscribe(collection.state, ({ data, error, isFetching }) => {
|
|
164
|
-
// same as above
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Option 3: Direct signal access (for custom reactivity)
|
|
168
|
-
effect(() => {
|
|
169
|
-
const { data, error, isFetching } = collection.state.value
|
|
170
|
-
// same as above
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
// Execute queries — returns Result<CollectionResult, SdkError>
|
|
174
|
-
const result = await collection.execute() // initial load
|
|
175
|
-
if (result.error) console.error(result.error.message)
|
|
176
|
-
if (result.data) console.log('Got', result.data.totalResults, 'results')
|
|
119
|
+
await collection.execute() // initial load
|
|
120
|
+
await collection.execute({ page: 2 }) // paginate
|
|
121
|
+
await collection.execute({ sort: 'price_asc' }) // change sort
|
|
122
|
+
await collection.execute({ filters: { color: 'Red' } })
|
|
123
|
+
await collection.execute({ includeMeta: true }) // fetch collection metadata
|
|
177
124
|
|
|
178
|
-
|
|
179
|
-
await collection.execute({ sortOrderCode: 'price_asc' }) // change sort
|
|
180
|
-
await collection.execute({ filters: { color: 'Red' } }) // with filters
|
|
181
|
-
|
|
182
|
-
// Unsubscribe
|
|
183
|
-
unsubscribe() // remove single subscription
|
|
184
|
-
collection.dispose() // cleanup all subscriptions + abort pending requests
|
|
125
|
+
collection.dispose()
|
|
185
126
|
```
|
|
186
127
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
**Options:**
|
|
190
|
-
|
|
191
|
-
| Parameter | Type | Required | Description |
|
|
192
|
-
| ------------- | ------------- | -------- | ---------------------------------------------- |
|
|
193
|
-
| `handle` | `string` | Yes | Collection URL handle |
|
|
194
|
-
| `defaultSort` | `string` | No | Default sort code (uses first configured sort) |
|
|
195
|
-
| `signal` | `AbortSignal` | No | Shared abort signal |
|
|
196
|
-
|
|
197
|
-
**Execute parameters:**
|
|
198
|
-
|
|
199
|
-
| Parameter | Type | Description |
|
|
200
|
-
| ---------------- | ------------------------- | ------------------------------------- |
|
|
201
|
-
| `page` | `number` | Page number (default: 1) |
|
|
202
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
203
|
-
| `sortOrderCode` | `string` | Sort option code |
|
|
204
|
-
| `filters` | `unknown` | Filter criteria |
|
|
205
|
-
| `signal` | `AbortSignal` | Per-call abort signal |
|
|
206
|
-
| `includeMeta` | `boolean` | Fetch collection metadata |
|
|
207
|
-
| `includeFilters` | `boolean` | Include filter counts in response |
|
|
208
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
209
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
210
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
128
|
+
Additional `execute()` params: `sort`, `includeMeta`.
|
|
211
129
|
|
|
212
|
-
###
|
|
130
|
+
### Blocks
|
|
213
131
|
|
|
214
|
-
|
|
132
|
+
Product recommendations powered by Layers blocks. Anchored to a product, collection, or cart.
|
|
215
133
|
|
|
216
134
|
```typescript
|
|
217
|
-
const blocks =
|
|
135
|
+
const blocks = client.blocks({
|
|
218
136
|
blockId: 'block-abc123',
|
|
219
|
-
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Option 1: Controller subscribe() — no signal import needed
|
|
223
|
-
const unsubscribe = blocks.subscribe(({ data, error, isFetching }) => {
|
|
224
|
-
if (data) {
|
|
225
|
-
console.log('Recommendations:', data.products)
|
|
226
|
-
console.log('Block info:', data.block) // { title, anchor_type, strategy_type, ... }
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
// Option 2: Standalone subscribe() — works with any signal
|
|
231
|
-
const unsubscribe = subscribe(blocks.state, ({ data, error, isFetching }) => {
|
|
232
|
-
// same as above
|
|
137
|
+
anchor: 'gold-necklace', // product/collection ID or handle
|
|
233
138
|
})
|
|
234
139
|
|
|
235
|
-
// Option 3: Direct signal access (for custom reactivity)
|
|
236
|
-
effect(() => {
|
|
237
|
-
const { data, error, isFetching } = blocks.state.value
|
|
238
|
-
// same as above
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// Execute queries
|
|
242
140
|
await blocks.execute()
|
|
243
|
-
await blocks.execute({ page: 2, limit: 12 })
|
|
244
|
-
|
|
245
|
-
// With cart context for personalized recommendations
|
|
246
141
|
await blocks.execute({
|
|
142
|
+
discounts: [
|
|
143
|
+
{
|
|
144
|
+
entitled: { all: true },
|
|
145
|
+
discount: { type: 'PERCENTAGE', value: 10 },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
247
148
|
context: {
|
|
248
149
|
productsInCart: [{ productId: '123', variantId: '456', quantity: 1 }],
|
|
249
150
|
geo: { country: 'US' },
|
|
250
151
|
},
|
|
251
152
|
})
|
|
252
153
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
discountEntitlements: [{ id: 'discount-123' }],
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
// Unsubscribe
|
|
259
|
-
unsubscribe() // remove single subscription
|
|
260
|
-
blocks.dispose() // cleanup all subscriptions + abort pending requests
|
|
154
|
+
// result.data.block has { id, title, anchor_type, strategy_type, ... }
|
|
155
|
+
blocks.dispose()
|
|
261
156
|
```
|
|
262
157
|
|
|
263
|
-
|
|
158
|
+
Additional `execute()` params: `discounts`, `context`.
|
|
264
159
|
|
|
265
|
-
|
|
266
|
-
| ---------- | ------------- | -------- | -------------------------------------- |
|
|
267
|
-
| `blockId` | `string` | Yes | Layers block ID |
|
|
268
|
-
| `anchorId` | `string` | No | Anchor product/collection ID or handle |
|
|
269
|
-
| `signal` | `AbortSignal` | No | Shared abort signal |
|
|
160
|
+
### Search
|
|
270
161
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
| Parameter | Type | Description |
|
|
274
|
-
| ---------------------- | ------------------------- | ------------------------------------- |
|
|
275
|
-
| `page` | `number` | Page number (default: 1) |
|
|
276
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
277
|
-
| `filters` | `unknown` | Filter criteria |
|
|
278
|
-
| `signal` | `AbortSignal` | External abort signal |
|
|
279
|
-
| `discountEntitlements` | `DiscountEntitlement[]` | Discount entitlements to apply |
|
|
280
|
-
| `context` | `BlocksContext` | Cart, geo, and custom context |
|
|
281
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
282
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
283
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
284
|
-
|
|
285
|
-
**`BlocksContext`:**
|
|
286
|
-
|
|
287
|
-
| Property | Type | Description |
|
|
288
|
-
| ---------------- | ---------------------------------------- | -------------------- |
|
|
289
|
-
| `productsInCart` | `{ productId, variantId?, quantity? }[]` | Products in the cart |
|
|
290
|
-
| `geo` | `{ country?, province?, city? }` | Geographic context |
|
|
291
|
-
| `custom` | `Record<string, unknown>` | Custom context data |
|
|
292
|
-
|
|
293
|
-
**`DiscountEntitlement`:**
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
interface DiscountEntitlement {
|
|
297
|
-
entitled: {
|
|
298
|
-
all?: boolean // Apply to all products
|
|
299
|
-
products?: string[] // Product IDs
|
|
300
|
-
variants?: (string | number)[] // Variant IDs
|
|
301
|
-
collections?: string[] // Collection handles
|
|
302
|
-
}
|
|
303
|
-
discount: {
|
|
304
|
-
type: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
|
305
|
-
value: number
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
### `sdk.autocomplete()` - Predictive Search
|
|
311
|
-
|
|
312
|
-
Creates a standalone autocomplete controller with debounced search and local caching. Only full words (completed by a trailing space) are cached — partial input filters cached results client-side.
|
|
162
|
+
Full-text search with facets. Options persist across calls — subsequent `execute()` calls merge with existing options.
|
|
313
163
|
|
|
314
164
|
```typescript
|
|
315
|
-
const
|
|
165
|
+
const search = client.search({ query: 'ring', limit: 20 })
|
|
316
166
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (data) renderSuggestions(data.matchedQueries)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
// Option 2: Standalone subscribe() — works with any signal
|
|
324
|
-
const unsubscribe = subscribe(autocomplete.state, ({ data, isFetching, error }) => {
|
|
325
|
-
// same as above
|
|
326
|
-
})
|
|
167
|
+
await search.execute()
|
|
168
|
+
await search.execute({ page: 2 }) // page persists
|
|
169
|
+
await search.execute({ filters: { vendor: 'Nike' } }) // filters update
|
|
327
170
|
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
const { data, isFetching, error } = autocomplete.state.value
|
|
331
|
-
// same as above
|
|
332
|
-
})
|
|
171
|
+
// Temporary override (doesn't persist for next call)
|
|
172
|
+
await search.execute({ query: 'shoes', temporary: true })
|
|
333
173
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
})
|
|
174
|
+
// Prepare search (caches searchId for faster execute)
|
|
175
|
+
await search.prepare()
|
|
176
|
+
await search.execute() // uses cached searchId
|
|
338
177
|
|
|
339
|
-
|
|
340
|
-
unsubscribe() // remove single subscription
|
|
341
|
-
autocomplete.dispose() // cleanup all subscriptions, abort controller, and timers
|
|
178
|
+
search.dispose()
|
|
342
179
|
```
|
|
343
180
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
| Parameter | Type | Description |
|
|
347
|
-
| ------------ | ------------- | -------------------------------------------------------- |
|
|
348
|
-
| `debounceMs` | `number` | Debounce delay (default: 300) |
|
|
349
|
-
| `signal` | `AbortSignal` | Shared abort signal (acts like `dispose()` when aborted) |
|
|
350
|
-
|
|
351
|
-
**Controller methods:**
|
|
181
|
+
Additional `execute()` params: `query`, `searchId`, `tuning`, `temporary`.
|
|
352
182
|
|
|
353
|
-
|
|
354
|
-
| ---------------- | -------------------------------------------------------- |
|
|
355
|
-
| `execute(query)` | Debounced predictive search for autocomplete |
|
|
356
|
-
| `subscribe(cb)` | Subscribe to state changes, returns unsubscribe function |
|
|
357
|
-
| `dispose()` | Cleanup all subscriptions, abort controller and timers |
|
|
183
|
+
`SearchTuning` controls matching weights: `textualWeight`, `visualWeight`, `multipleFactor`, `minimumMatch`.
|
|
358
184
|
|
|
359
|
-
###
|
|
185
|
+
### Suggest
|
|
360
186
|
|
|
361
|
-
|
|
187
|
+
Predictive search with debouncing and local caching. Only full words (trailing space) are cached — partial input filters cached results client-side.
|
|
362
188
|
|
|
363
189
|
```typescript
|
|
364
|
-
|
|
365
|
-
const search = sdk.search({ query: 'ring', limit: 20 })
|
|
366
|
-
|
|
367
|
-
// Option 1: Controller subscribe() — no signal import needed
|
|
368
|
-
const unsubscribe = search.subscribe(({ data, isFetching, error }) => {
|
|
369
|
-
if (isFetching) console.log('Searching...')
|
|
370
|
-
if (data) renderResults(data.products)
|
|
371
|
-
})
|
|
190
|
+
const suggest = client.suggest({ debounce: 300 })
|
|
372
191
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// same as above
|
|
192
|
+
suggest.subscribe(({ data }) => {
|
|
193
|
+
if (data) renderSuggestions(data.matchedQueries)
|
|
376
194
|
})
|
|
377
195
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const { data, isFetching, error } = search.state.value
|
|
381
|
-
// same as above
|
|
196
|
+
input.addEventListener('input', (e) => {
|
|
197
|
+
suggest.execute(e.target.value) // debounced automatically
|
|
382
198
|
})
|
|
383
199
|
|
|
384
|
-
|
|
385
|
-
await search.execute()
|
|
386
|
-
|
|
387
|
-
// Override and persist new options
|
|
388
|
-
await search.execute({ page: 2 }) // page=2 persists
|
|
389
|
-
await search.execute({ filters: { vendor: 'Adidas' } }) // filters updated
|
|
390
|
-
|
|
391
|
-
// Temporary override without persisting
|
|
392
|
-
await search.execute({ query: 'shoes', transient: true }) // query stays 'ring' for next call
|
|
393
|
-
|
|
394
|
-
// Prepare search (optional, caches searchId for faster execute)
|
|
395
|
-
await search.prepare()
|
|
396
|
-
await search.execute() // uses cached searchId
|
|
397
|
-
|
|
398
|
-
// Unsubscribe
|
|
399
|
-
unsubscribe()
|
|
400
|
-
search.dispose()
|
|
200
|
+
suggest.dispose()
|
|
401
201
|
```
|
|
402
202
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
|
406
|
-
|
|
|
407
|
-
| `
|
|
408
|
-
| `
|
|
409
|
-
| `page` | `number` | Page number (default: 1) |
|
|
410
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
411
|
-
| `filters` | `unknown` | Filter criteria |
|
|
412
|
-
| `tuning` | `LayersTuning` | Search tuning parameters |
|
|
413
|
-
| `signal` | `AbortSignal` | Abort signal (shared at init, per-call later) |
|
|
414
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
415
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
416
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
417
|
-
| `transient` | `boolean` | If true, overrides don't persist |
|
|
418
|
-
|
|
419
|
-
**Controller methods:**
|
|
420
|
-
|
|
421
|
-
| Method | Description |
|
|
422
|
-
| ----------------- | -------------------------------------------------------- |
|
|
423
|
-
| `prepare(query?)` | Prepare search and cache searchId for reuse |
|
|
424
|
-
| `execute(query?)` | Execute search (uses cached searchId if available) |
|
|
425
|
-
| `subscribe(cb)` | Subscribe to state changes, returns unsubscribe function |
|
|
426
|
-
| `dispose()` | Cleanup all subscriptions and abort pending requests |
|
|
203
|
+
| Option | Type | Description |
|
|
204
|
+
| ------------------- | ------------- | ------------------------------------------- |
|
|
205
|
+
| `debounce` | `number` | Debounce delay in ms (default: 300) |
|
|
206
|
+
| `excludeInputQuery` | `boolean` | Remove user's input from suggestions |
|
|
207
|
+
| `excludeQueries` | `string[]` | Custom strings to filter from suggestions |
|
|
208
|
+
| `signal` | `AbortSignal` | Shared abort signal (acts like `dispose()`) |
|
|
427
209
|
|
|
428
|
-
|
|
210
|
+
### Image Search
|
|
429
211
|
|
|
430
|
-
|
|
431
|
-
| ---------------- | -------- | ------------------------------------------- |
|
|
432
|
-
| `textualWeight` | `number` | Weight for text-based matching (0-1) |
|
|
433
|
-
| `visualWeight` | `number` | Weight for visual similarity matching (0-1) |
|
|
434
|
-
| `multipleFactor` | `number` | Factor for multiple keyword matching |
|
|
435
|
-
| `minimumMatch` | `number` | Minimum match threshold |
|
|
436
|
-
|
|
437
|
-
### `sdk.uploadImage()` - Upload Image for Search
|
|
438
|
-
|
|
439
|
-
Returns a reactive signal that uploads an image and resolves to an image ID for use with `imageSearch()`. Subscribe with `effect()` like other signal-based methods.
|
|
212
|
+
Upload an image, then search by it.
|
|
440
213
|
|
|
441
214
|
```typescript
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (isFetching) console.log('Uploading...')
|
|
450
|
-
if (error) console.error('Upload failed:', error.message)
|
|
451
|
-
if (data) console.log('Image ID:', data.imageId)
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
// Option 2: Direct signal access (for custom reactivity)
|
|
455
|
-
effect(() => {
|
|
456
|
-
const { data, error, isFetching } = state.value
|
|
457
|
-
// same as above
|
|
215
|
+
const upload = client.uploadImage({ image: file })
|
|
216
|
+
upload.subscribe(({ data }) => {
|
|
217
|
+
if (!data) return
|
|
218
|
+
const results = client.searchByImage({ imageId: data.imageId })
|
|
219
|
+
results.subscribe(({ data }) => {
|
|
220
|
+
if (data) console.log('Similar:', data.products)
|
|
221
|
+
})
|
|
458
222
|
})
|
|
459
223
|
```
|
|
460
224
|
|
|
461
|
-
###
|
|
225
|
+
### Abort Signals
|
|
462
226
|
|
|
463
|
-
|
|
227
|
+
Controllers support two levels of abort:
|
|
464
228
|
|
|
465
229
|
```typescript
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
filters: { vendor: 'Nike' },
|
|
470
|
-
})
|
|
230
|
+
// Shared signal — cancels everything when component unmounts
|
|
231
|
+
const ac = new AbortController()
|
|
232
|
+
const search = client.search({ query: 'ring', signal: ac.signal })
|
|
471
233
|
|
|
472
|
-
//
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
})
|
|
234
|
+
// Per-call signal — cancels only this request
|
|
235
|
+
const req = new AbortController()
|
|
236
|
+
await search.execute({ page: 2, signal: req.signal })
|
|
476
237
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
const { data, error, isFetching } = state.value
|
|
480
|
-
// same as above
|
|
481
|
-
})
|
|
238
|
+
// Either aborting cancels the request (they're linked internally)
|
|
239
|
+
ac.abort() // cancels all pending + acts like dispose()
|
|
482
240
|
```
|
|
483
241
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
| Parameter | Type | Required | Description |
|
|
487
|
-
| --------- | -------------- | -------- | ------------------------------- |
|
|
488
|
-
| `imageId` | `string` | Yes | Image ID from `uploadImage()` |
|
|
489
|
-
| `page` | `number` | No | Page number (default: 1) |
|
|
490
|
-
| `limit` | `number` | No | Products per page (default: 24) |
|
|
491
|
-
| `filters` | `unknown` | No | Filter criteria |
|
|
492
|
-
| `tuning` | `LayersTuning` | No | Search tuning parameters |
|
|
493
|
-
| `signal` | `AbortSignal` | No | External abort signal |
|
|
242
|
+
Collection and blocks auto-cancel the previous request when a new `execute()` starts.
|
|
494
243
|
|
|
495
|
-
|
|
244
|
+
## Product Card
|
|
496
245
|
|
|
497
|
-
|
|
246
|
+
Reactive controller for product cards with variant selection and availability logic. All derived values are computed signals that auto-update when inputs change.
|
|
498
247
|
|
|
499
248
|
```typescript
|
|
500
|
-
|
|
501
|
-
ids: ['gid://shopify/Product/123', 'gid://shopify/Product/456'],
|
|
502
|
-
meta: {
|
|
503
|
-
collection: 'shirts', // optional: fetch collection metadata
|
|
504
|
-
page: 'about', // optional: fetch page metadata
|
|
505
|
-
includeFilters: true, // optional: include available filters
|
|
506
|
-
},
|
|
507
|
-
})
|
|
249
|
+
import { createProductCard, effect } from '@commerce-blocks/sdk'
|
|
508
250
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
console.log('Collection:', data.collection)
|
|
514
|
-
console.log('Page:', data.page)
|
|
515
|
-
}
|
|
251
|
+
const card = createProductCard({
|
|
252
|
+
product,
|
|
253
|
+
selectedOptions: [{ name: 'Size', value: '7' }],
|
|
254
|
+
breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
|
|
516
255
|
})
|
|
517
256
|
|
|
518
|
-
//
|
|
257
|
+
// Reactive signals
|
|
519
258
|
effect(() => {
|
|
520
|
-
|
|
521
|
-
|
|
259
|
+
console.log('Variant:', card.selectedVariant.value)
|
|
260
|
+
console.log('Options:', card.options.value) // OptionGroup[]
|
|
261
|
+
console.log('Images:', card.images.value)
|
|
262
|
+
console.log('Price:', card.price.value) // PriceData
|
|
522
263
|
})
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
**Parameters:**
|
|
526
|
-
|
|
527
|
-
| Parameter | Type | Required | Description |
|
|
528
|
-
| --------------------- | ------------- | -------- | ---------------------------------------- |
|
|
529
|
-
| `ids` | `string[]` | Yes | Product GIDs |
|
|
530
|
-
| `meta.collection` | `string` | No | Collection handle to fetch metadata |
|
|
531
|
-
| `meta.page` | `string` | No | Page handle to fetch metadata |
|
|
532
|
-
| `meta.includeFilters` | `boolean` | No | Include available filters for collection |
|
|
533
|
-
| `signal` | `AbortSignal` | No | External abort signal |
|
|
534
|
-
|
|
535
|
-
## Abort Signals
|
|
536
|
-
|
|
537
|
-
Controllers support two levels of abort signals:
|
|
538
|
-
|
|
539
|
-
1. **Shared signal** — passed at initialization, affects all operations
|
|
540
|
-
2. **Per-call signal** — passed to `execute()` or `prepare()`, affects only that call
|
|
541
|
-
|
|
542
|
-
Both signals are linked internally using `linkedAbort()` — when either signal aborts, the request cancels. This allows component-level cancellation (shared) alongside request-level cancellation (per-call).
|
|
543
|
-
|
|
544
|
-
```typescript
|
|
545
|
-
// Shared signal at init — cancels everything when component unmounts
|
|
546
|
-
const componentController = new AbortController()
|
|
547
264
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
265
|
+
// Actions
|
|
266
|
+
card.selectOption({ name: 'Size', value: 'L' })
|
|
267
|
+
card.setSelectedOptions([{ name: 'Size', value: 'L' }]) // merge by name
|
|
268
|
+
card.setSelectedVariant(12345) // select by numeric variant ID
|
|
269
|
+
card.setCarouselPosition(3) // 1-based manual override
|
|
552
270
|
|
|
553
|
-
|
|
554
|
-
|
|
271
|
+
card.subscribe(({ selectedVariant, options, price }) => {
|
|
272
|
+
// Called on any state change
|
|
555
273
|
})
|
|
556
274
|
|
|
557
|
-
|
|
558
|
-
const requestController = new AbortController()
|
|
559
|
-
await search.execute({ page: 2, signal: requestController.signal })
|
|
560
|
-
requestController.abort() // cancels only this request
|
|
561
|
-
|
|
562
|
-
// Shared signal abort cancels everything
|
|
563
|
-
componentController.abort() // cancels all pending requests + acts like dispose()
|
|
275
|
+
card.dispose()
|
|
564
276
|
```
|
|
565
277
|
|
|
566
|
-
**
|
|
567
|
-
|
|
568
|
-
| Scenario | Behavior |
|
|
569
|
-
| -------------------- | ----------------------------------------- |
|
|
570
|
-
| Only shared signal | All requests use shared signal |
|
|
571
|
-
| Only per-call signal | That request uses per-call signal |
|
|
572
|
-
| Both signals | Request cancels if either aborts |
|
|
573
|
-
| Neither signal | Controller manages its own internal abort |
|
|
574
|
-
|
|
575
|
-
**Auto-cancellation:**
|
|
278
|
+
**Reactive state** (all `ReadonlySignal`): `variants`, `selectedVariant`, `options`, `images`, `price`, `priceRange`, `carouselPosition`, `isSelectionComplete`.
|
|
576
279
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
## Response Types
|
|
280
|
+
Options include availability status baked in:
|
|
580
281
|
|
|
581
282
|
```typescript
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
error: SdkError | null
|
|
586
|
-
isFetching: boolean
|
|
283
|
+
interface OptionGroup {
|
|
284
|
+
name: string
|
|
285
|
+
values: OptionValue[]
|
|
587
286
|
}
|
|
588
287
|
|
|
589
|
-
interface
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
resultsPerPage?: number
|
|
595
|
-
facets: Record<string, Record<string, number>>
|
|
596
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
597
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
598
|
-
attributionToken: string
|
|
599
|
-
collection?: StorefrontCollection
|
|
288
|
+
interface OptionValue {
|
|
289
|
+
value: string
|
|
290
|
+
status: 'available' | 'backorderable' | 'sold-out' | 'unavailable'
|
|
291
|
+
selected: boolean
|
|
292
|
+
swatch: Swatch | null
|
|
600
293
|
}
|
|
601
294
|
|
|
602
|
-
interface
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
page: number
|
|
607
|
-
resultsPerPage?: number
|
|
608
|
-
facets: Record<string, Record<string, number>>
|
|
609
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
610
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
611
|
-
attributionToken: string
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
interface BlocksResult {
|
|
615
|
-
products: Product[]
|
|
616
|
-
totalResults: number
|
|
617
|
-
totalPages: number
|
|
618
|
-
page: number
|
|
619
|
-
resultsPerPage?: number
|
|
620
|
-
facets: Record<string, Record<string, number>>
|
|
621
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
622
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
623
|
-
attributionToken: string
|
|
624
|
-
block?: BlocksInfo // { id, title, anchor_type, strategy_type, strategy_key }
|
|
295
|
+
interface PriceData {
|
|
296
|
+
price: Price | null
|
|
297
|
+
compareAtPrice: Price | null
|
|
298
|
+
isOnSale: boolean
|
|
625
299
|
}
|
|
626
300
|
|
|
627
|
-
interface
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
interface Price {
|
|
633
|
-
amount: number
|
|
634
|
-
currencyCode: string
|
|
635
|
-
formatted: string
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
interface StorefrontResult {
|
|
639
|
-
products: Product[]
|
|
640
|
-
collection?: StorefrontCollection
|
|
641
|
-
page?: StorefrontPage
|
|
301
|
+
interface PriceRangeData {
|
|
302
|
+
priceRange: PriceRange
|
|
303
|
+
compareAtPriceRange: PriceRange | null
|
|
642
304
|
}
|
|
643
305
|
```
|
|
644
306
|
|
|
645
|
-
##
|
|
307
|
+
## Filters
|
|
646
308
|
|
|
647
|
-
|
|
309
|
+
Build filters using the DSL:
|
|
648
310
|
|
|
649
311
|
```typescript
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (result.error) {
|
|
653
|
-
switch (result.error._tag) {
|
|
654
|
-
case 'NetworkError':
|
|
655
|
-
// Connection issues, timeouts, aborted requests
|
|
656
|
-
console.log(result.error.code) // 'TIMEOUT' | 'CONNECTION_FAILED' | 'ABORTED' | ...
|
|
657
|
-
break
|
|
658
|
-
case 'ApiError':
|
|
659
|
-
// Server errors, rate limits
|
|
660
|
-
console.log(result.error.source) // 'layers' | 'storefront'
|
|
661
|
-
console.log(result.error.status) // HTTP status code
|
|
662
|
-
break
|
|
663
|
-
case 'ValidationError':
|
|
664
|
-
// Invalid parameters
|
|
665
|
-
console.log(result.error.operation) // which method failed
|
|
666
|
-
console.log(result.error.fields) // [{ field, code, message }]
|
|
667
|
-
break
|
|
668
|
-
case 'ConfigError':
|
|
669
|
-
// SDK configuration issues
|
|
670
|
-
console.log(result.error.field) // which config field
|
|
671
|
-
break
|
|
672
|
-
}
|
|
673
|
-
} else {
|
|
674
|
-
const data = result.data
|
|
675
|
-
}
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
### Error Types
|
|
679
|
-
|
|
680
|
-
**`NetworkError`** — connection failures, timeouts, aborted requests
|
|
681
|
-
|
|
682
|
-
| Field | Type | Description |
|
|
683
|
-
| -------------- | ------------------ | ------------------------------------------------------------------------------- |
|
|
684
|
-
| `code` | `NetworkErrorCode` | `TIMEOUT`, `CONNECTION_FAILED`, `DNS_FAILED`, `SSL_ERROR`, `ABORTED`, `OFFLINE` |
|
|
685
|
-
| `message` | `string` | Human-readable description |
|
|
686
|
-
| `retryable` | `boolean` | Whether the request can be retried |
|
|
687
|
-
| `retryAfterMs` | `number?` | Suggested retry delay in milliseconds |
|
|
688
|
-
|
|
689
|
-
**`ApiError`** — server errors, rate limits, GraphQL errors
|
|
690
|
-
|
|
691
|
-
| Field | Type | Description |
|
|
692
|
-
| -------------- | -------------- | -------------------------------------------------- |
|
|
693
|
-
| `code` | `ApiErrorCode` | `NOT_FOUND`, `RATE_LIMITED`, `GRAPHQL_ERROR`, etc. |
|
|
694
|
-
| `source` | `ApiSource` | `'layers'` or `'storefront'` |
|
|
695
|
-
| `status` | `number?` | HTTP status code |
|
|
696
|
-
| `retryable` | `boolean` | Whether the request can be retried |
|
|
697
|
-
| `retryAfterMs` | `number?` | Suggested retry delay |
|
|
698
|
-
|
|
699
|
-
**`ValidationError`** — invalid parameters passed to SDK methods
|
|
700
|
-
|
|
701
|
-
| Field | Type | Description |
|
|
702
|
-
| ----------- | ------------------------ | ------------------------------------- |
|
|
703
|
-
| `operation` | `string` | Which method failed (e.g. `'search'`) |
|
|
704
|
-
| `fields` | `ValidationFieldError[]` | `[{ field, code, message }]` |
|
|
312
|
+
import { filter, and, or, eq, gte, lte, inValues } from '@commerce-blocks/sdk'
|
|
705
313
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
| ---------- | --------- | --------------------------- |
|
|
710
|
-
| `field` | `string` | Which config field is wrong |
|
|
711
|
-
| `expected` | `string?` | What was expected |
|
|
712
|
-
|
|
713
|
-
### Error Helpers
|
|
714
|
-
|
|
715
|
-
Always check `isRetryable()` before calling `getRetryDelay()`. `getRetryDelay()` returns `undefined` for non-retryable errors — it's not meant to be used standalone.
|
|
716
|
-
|
|
717
|
-
```typescript
|
|
718
|
-
import { isRetryable, getRetryDelay } from '@commerce-blocks/sdk'
|
|
719
|
-
|
|
720
|
-
if (result.error && isRetryable(result.error)) {
|
|
721
|
-
const delay = getRetryDelay(result.error) ?? 1000
|
|
722
|
-
setTimeout(() => collection.execute(), delay)
|
|
723
|
-
}
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
## Filtering
|
|
727
|
-
|
|
728
|
-
Build filters using the DSL helpers:
|
|
729
|
-
|
|
730
|
-
```typescript
|
|
731
|
-
import { filter, and, or, eq, inValues, gte, lte } from '@commerce-blocks/sdk'
|
|
732
|
-
|
|
733
|
-
// Single filter
|
|
734
|
-
const colorFilter = filter(eq('options.color', 'Red'))
|
|
735
|
-
|
|
736
|
-
// Multiple conditions (AND)
|
|
737
|
-
const multiFilter = filter(and(eq('options.color', 'Red'), eq('options.size', 'Medium')))
|
|
738
|
-
|
|
739
|
-
// Multiple values (OR)
|
|
740
|
-
const multiValue = filter(or(eq('vendor', 'Nike'), eq('vendor', 'Adidas')))
|
|
314
|
+
await collection.execute({
|
|
315
|
+
filters: filter(and(eq('options.color', 'Red'), eq('options.size', 'Medium'))),
|
|
316
|
+
})
|
|
741
317
|
|
|
742
318
|
// Price range
|
|
743
|
-
|
|
319
|
+
filter(and(gte('price', 50), lte('price', 200)))
|
|
744
320
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
filters: multiFilter,
|
|
748
|
-
})
|
|
321
|
+
// Multiple values
|
|
322
|
+
filter(or(eq('vendor', 'Nike'), eq('vendor', 'Adidas')))
|
|
749
323
|
```
|
|
750
324
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
| Function | Description |
|
|
325
|
+
| Operator | Description |
|
|
754
326
|
| ------------------------------ | ----------------------- |
|
|
755
327
|
| `eq(property, value)` | Equals |
|
|
756
328
|
| `notEq(property, value)` | Not equals |
|
|
@@ -763,293 +335,181 @@ await collection.execute({
|
|
|
763
335
|
| `exists(property)` | Property exists |
|
|
764
336
|
| `notExists(property)` | Property does not exist |
|
|
765
337
|
|
|
766
|
-
|
|
338
|
+
## Transforms and Filter Aliases
|
|
767
339
|
|
|
768
|
-
|
|
340
|
+
Configure once at init — applied automatically to all results. The `product` transform extends every `Product` with custom fields via the generic `Product<T>` type:
|
|
769
341
|
|
|
770
342
|
```typescript
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
343
|
+
import type { Product } from '@commerce-blocks/sdk'
|
|
344
|
+
|
|
345
|
+
type MyProduct = Product<{ description: string; rating: number }>
|
|
346
|
+
|
|
347
|
+
const { data: client } = createClient({
|
|
348
|
+
// ...config
|
|
349
|
+
attributes: ['body_html'],
|
|
350
|
+
transforms: {
|
|
351
|
+
product: ({ raw }) => ({
|
|
352
|
+
description: raw.body_html ?? '',
|
|
353
|
+
rating: raw.calculated?.average_rating ?? 0,
|
|
354
|
+
}),
|
|
355
|
+
collection: (result, raw) => result,
|
|
356
|
+
search: (result, raw) => result,
|
|
357
|
+
block: (result, raw) => result,
|
|
358
|
+
filters: (filters) => filters,
|
|
359
|
+
},
|
|
360
|
+
filterAliases: {
|
|
774
361
|
color: 'options.color',
|
|
775
362
|
size: 'options.size',
|
|
776
363
|
brand: { property: 'vendor', values: { nike: 'Nike', adidas: 'Adidas' } },
|
|
777
364
|
},
|
|
778
365
|
})
|
|
779
366
|
|
|
780
|
-
//
|
|
781
|
-
await collection.execute({
|
|
782
|
-
|
|
783
|
-
})
|
|
367
|
+
// Aliases resolve automatically
|
|
368
|
+
await collection.execute({ filters: { color: 'Red', brand: 'nike' } })
|
|
369
|
+
// Products now include description and rating from the product transform
|
|
784
370
|
```
|
|
785
371
|
|
|
786
|
-
##
|
|
787
|
-
|
|
788
|
-
### `createProductCard()` - Reactive Product Card Controller
|
|
372
|
+
## Error Handling
|
|
789
373
|
|
|
790
|
-
|
|
374
|
+
All methods return `{ data, error }` instead of throwing. Errors are discriminated by `_tag`:
|
|
791
375
|
|
|
792
376
|
```typescript
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const card = createProductCard({
|
|
796
|
-
product,
|
|
797
|
-
selectedOptions: [{ name: 'Size', value: '7' }],
|
|
798
|
-
breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
// Subscribe to reactive state
|
|
802
|
-
effect(() => {
|
|
803
|
-
console.log('Selected variant:', card.selectedVariant.value)
|
|
804
|
-
console.log('Available options:', card.options.value)
|
|
805
|
-
})
|
|
377
|
+
const result = await collection.execute()
|
|
806
378
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// Unsubscribe
|
|
826
|
-
card.dispose()
|
|
379
|
+
if (result.error) {
|
|
380
|
+
switch (result.error._tag) {
|
|
381
|
+
case 'NetworkError':
|
|
382
|
+
// code: 'TIMEOUT' | 'CONNECTION_FAILED' | 'DNS_FAILED' | 'SSL_ERROR' | 'ABORTED' | 'OFFLINE'
|
|
383
|
+
break
|
|
384
|
+
case 'ApiError':
|
|
385
|
+
// code: 'NOT_FOUND' | 'RATE_LIMITED' | 'UNAUTHORIZED' | ...
|
|
386
|
+
// status: HTTP status code, source: 'layers'
|
|
387
|
+
break
|
|
388
|
+
case 'ValidationError':
|
|
389
|
+
// operation: which method failed, fields: [{ field, code, message }]
|
|
390
|
+
break
|
|
391
|
+
case 'ConfigError':
|
|
392
|
+
// field: which config field, expected: what was expected
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
827
396
|
```
|
|
828
397
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
| Parameter | Type | Required | Description |
|
|
832
|
-
| ----------------- | ----------------- | -------- | -------------------------------------------------------- |
|
|
833
|
-
| `product` | `Product` | Yes | Product to display |
|
|
834
|
-
| `selectedOptions` | `ProductOption[]` | No | Initially selected options |
|
|
835
|
-
| `breakoutOptions` | `ProductOption[]` | No | Options to filter variants (auto-set from variant tiles) |
|
|
836
|
-
|
|
837
|
-
**Reactive State (ReadonlySignal):**
|
|
838
|
-
|
|
839
|
-
| Property | Type | Description |
|
|
840
|
-
| ----------------- | ---------------------------------------- | --------------------------------------- |
|
|
841
|
-
| `product` | `Product` | Original product (static) |
|
|
842
|
-
| `variants` | `ReadonlySignal<ProductVariant[]>` | Variants matching breakoutOptions |
|
|
843
|
-
| `selectedVariant` | `ReadonlySignal<ProductVariant \| null>` | Variant matching all selected options |
|
|
844
|
-
| `selectedOptions` | `ReadonlySignal<ProductOption[]>` | Combined breakout + selected options |
|
|
845
|
-
| `options` | `ReadonlySignal<RichProductOption[]>` | Available options from visible variants |
|
|
846
|
-
| `optionNames` | `ReadonlySignal<string[]>` | Option names (excludes breakout) |
|
|
847
|
-
| `images` | `ReadonlySignal<Image[]>` | Variant image or product images |
|
|
848
|
-
| `carouselIndex` | `ReadonlySignal<number>` | Index of selected variant's image |
|
|
849
|
-
|
|
850
|
-
**Actions:**
|
|
851
|
-
|
|
852
|
-
| Method | Description |
|
|
853
|
-
| ----------------------------- | ------------------------------- |
|
|
854
|
-
| `selectOption(name, value)` | Select a single option by name |
|
|
855
|
-
| `setSelectedOptions(options)` | Merge options by name |
|
|
856
|
-
| `setBreakoutOptions(options)` | Replace breakout filter options |
|
|
857
|
-
| `dispose()` | Cleanup controller |
|
|
858
|
-
|
|
859
|
-
**Query Methods:**
|
|
860
|
-
|
|
861
|
-
| Method | Returns | Description |
|
|
862
|
-
| -------------------------------- | ------------------------ | ------------------------------------------------------ |
|
|
863
|
-
| `getOptionValues(name)` | `string[]` | All values for an option from visible variants |
|
|
864
|
-
| `getSwatches(name)` | `Swatch[]` | Swatch definitions for an option |
|
|
865
|
-
| `getVariantByOptions(opts)` | `ProductVariant \| null` | Find variant by options |
|
|
866
|
-
| `isOptionAvailable(name, value)` | `boolean` | Check if selecting option results in available variant |
|
|
867
|
-
| `isVariantAvailable(variant)` | `boolean` | Check if variant is available for sale |
|
|
868
|
-
|
|
869
|
-
## Singleton Access
|
|
870
|
-
|
|
871
|
-
After initialization, access the SDK anywhere:
|
|
398
|
+
`isRetryable` classifies errors by tag, code, and status — use it standalone or as a `shouldRetry` predicate:
|
|
872
399
|
|
|
873
400
|
```typescript
|
|
874
|
-
import {
|
|
401
|
+
import { isRetryable } from '@commerce-blocks/sdk'
|
|
875
402
|
|
|
876
|
-
if (
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
const sdk = result.data
|
|
880
|
-
// Use sdk
|
|
881
|
-
}
|
|
403
|
+
if (result.error && isRetryable(result.error)) {
|
|
404
|
+
const delay = result.error.retryAfterMs ?? 1000
|
|
405
|
+
setTimeout(() => collection.execute(), delay)
|
|
882
406
|
}
|
|
883
407
|
```
|
|
884
408
|
|
|
885
|
-
##
|
|
409
|
+
## Cache and Storage
|
|
886
410
|
|
|
887
|
-
The
|
|
411
|
+
The client exposes a reactive cache:
|
|
888
412
|
|
|
889
413
|
```typescript
|
|
890
|
-
const {
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
const querySignal = store.queries.get('cache-key')
|
|
899
|
-
store.queries.invalidate('browse:*') // invalidate by pattern
|
|
900
|
-
|
|
901
|
-
// Collection metadata
|
|
902
|
-
const meta = store.collections.get('shirts')
|
|
903
|
-
|
|
904
|
-
// Page metadata
|
|
905
|
-
const page = store.pages.get('about')
|
|
906
|
-
store.pages.set('about', { title: 'About Us', body: '...' })
|
|
907
|
-
store.pages.delete('about')
|
|
908
|
-
|
|
909
|
-
// Persistence
|
|
910
|
-
store.persist() // save to storage
|
|
911
|
-
store.restore() // restore from storage
|
|
912
|
-
store.clear() // clear all caches
|
|
913
|
-
|
|
914
|
-
// Stats
|
|
915
|
-
console.log(store.stats) // { products, queries, collections, pages }
|
|
414
|
+
const { cache } = client
|
|
415
|
+
|
|
416
|
+
cache.get('cache-key') // CacheEntry<QueryResult> | null
|
|
417
|
+
cache.invalidate('browse') // invalidate keys containing 'browse'
|
|
418
|
+
cache.persist() // save to storage
|
|
419
|
+
cache.restore() // restore from storage
|
|
420
|
+
cache.clear() // clear all
|
|
421
|
+
cache.stats.entries // current entry count
|
|
916
422
|
```
|
|
917
423
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
Configure how the SDK persists cache data. By default, the SDK uses `localStorage` in browsers.
|
|
921
|
-
|
|
922
|
-
### Built-in Adapters
|
|
424
|
+
### Storage Adapters
|
|
923
425
|
|
|
924
426
|
```typescript
|
|
925
|
-
import { localStorageAdapter,
|
|
427
|
+
import { localStorageAdapter, fileStorage } from '@commerce-blocks/sdk'
|
|
926
428
|
|
|
927
|
-
// Browser
|
|
429
|
+
// Browser (returns null if unavailable)
|
|
928
430
|
const browserAdapter = localStorageAdapter('my-cache-key')
|
|
929
431
|
|
|
930
|
-
// Node.js
|
|
432
|
+
// Node.js
|
|
931
433
|
import fs from 'fs'
|
|
932
|
-
const nodeAdapter =
|
|
434
|
+
const nodeAdapter = fileStorage('./cache.json', fs)
|
|
933
435
|
```
|
|
934
436
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
Implement the `StorageAdapter` interface:
|
|
437
|
+
Custom adapter — implement `StorageAdapter`:
|
|
938
438
|
|
|
939
439
|
```typescript
|
|
940
|
-
|
|
941
|
-
read()
|
|
942
|
-
write(data
|
|
943
|
-
remove()
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// Example: sessionStorage adapter
|
|
947
|
-
const sessionAdapter: StorageAdapter = {
|
|
948
|
-
read: () => sessionStorage.getItem('my-key'),
|
|
949
|
-
write: (data) => sessionStorage.setItem('my-key', data),
|
|
950
|
-
remove: () => sessionStorage.removeItem('my-key'),
|
|
440
|
+
const adapter: StorageAdapter = {
|
|
441
|
+
read: () => sessionStorage.getItem('key'),
|
|
442
|
+
write: (data) => sessionStorage.setItem('key', data),
|
|
443
|
+
remove: () => sessionStorage.removeItem('key'),
|
|
951
444
|
}
|
|
952
445
|
```
|
|
953
446
|
|
|
954
|
-
##
|
|
447
|
+
## Signals
|
|
955
448
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
For advanced use cases bypassing SDK caching. Combines `prepareSearch` + `search` into a single call.
|
|
449
|
+
The SDK re-exports `@preact/signals-core` primitives for reactive state:
|
|
959
450
|
|
|
960
451
|
```typescript
|
|
961
|
-
import {
|
|
962
|
-
|
|
963
|
-
const clientResult = layersClient({
|
|
964
|
-
layersPublicToken: 'your-token',
|
|
965
|
-
sorts: [{ label: 'Featured', code: 'featured' }],
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
if (clientResult.data) {
|
|
969
|
-
const result = await search(clientResult.data, 'red dress', {
|
|
970
|
-
pagination: { page: 1, limit: 20 },
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
if (result.data) {
|
|
974
|
-
console.log('Raw Layers results:', result.data.results)
|
|
975
|
-
}
|
|
976
|
-
}
|
|
452
|
+
import { signal, computed, effect, batch } from '@commerce-blocks/sdk'
|
|
977
453
|
```
|
|
978
454
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
Build cache keys for manual store operations:
|
|
455
|
+
Controller `state` is a `ReadonlySignal<QueryState<T>>`:
|
|
982
456
|
|
|
983
457
|
```typescript
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
blocksKey('block-abc', { anchorId: 'gold-necklace', page: 1 })
|
|
990
|
-
productsKey(['gid://shopify/Product/1', 'gid://shopify/Product/2'])
|
|
458
|
+
interface QueryState<T> {
|
|
459
|
+
data: T | null
|
|
460
|
+
error: ClientError | null
|
|
461
|
+
isFetching: boolean
|
|
462
|
+
}
|
|
991
463
|
```
|
|
992
464
|
|
|
993
|
-
|
|
465
|
+
All controllers return the same `QueryResult` shape in `data`:
|
|
994
466
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
467
|
+
```typescript
|
|
468
|
+
interface QueryResult {
|
|
469
|
+
products: Product[]
|
|
470
|
+
totalResults: number
|
|
471
|
+
totalPages: number
|
|
472
|
+
page: number
|
|
473
|
+
facets: Record<string, Record<string, number>>
|
|
474
|
+
priceRange?: PriceRange
|
|
475
|
+
attributionToken?: string
|
|
476
|
+
}
|
|
1002
477
|
```
|
|
1003
478
|
|
|
1004
|
-
|
|
479
|
+
Blocks results add a `block` field with `{ id, title, anchor_type, strategy_type, strategy_key }`.
|
|
1005
480
|
|
|
1006
|
-
|
|
1007
|
-
- `inBrowser(page, fn)` — runs a function inside the browser via `page.evaluate()`
|
|
1008
|
-
|
|
1009
|
-
### Writing tests
|
|
481
|
+
## Singleton Access
|
|
1010
482
|
|
|
1011
|
-
|
|
483
|
+
After initialization, access the client anywhere:
|
|
1012
484
|
|
|
1013
485
|
```typescript
|
|
1014
|
-
import {
|
|
1015
|
-
import { setupSdk, inBrowser } from './helpers'
|
|
1016
|
-
|
|
1017
|
-
test.describe('SDK: feature()', () => {
|
|
1018
|
-
test.beforeEach(async ({ page }) => {
|
|
1019
|
-
await setupSdk(page)
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1022
|
-
test('does something', async ({ page }) => {
|
|
1023
|
-
const result = await inBrowser(page, async () => {
|
|
1024
|
-
const { createSdk, devConfig } = (window as any).SDK
|
|
1025
|
-
const { data: sdk } = createSdk(devConfig)
|
|
1026
|
-
if (!sdk) return { error: 'init failed' }
|
|
1027
|
-
|
|
1028
|
-
const controller = sdk.collection({ handle: 'shirts' })
|
|
1029
|
-
const { data, error } = await controller.execute({ limit: 3 })
|
|
1030
|
-
controller.dispose()
|
|
486
|
+
import { getClient, isInitialized } from '@commerce-blocks/sdk'
|
|
1031
487
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
expect(result.error).toBeUndefined()
|
|
1037
|
-
expect(result.count).toBeGreaterThan(0)
|
|
1038
|
-
})
|
|
1039
|
-
})
|
|
488
|
+
if (isInitialized()) {
|
|
489
|
+
const { data: client } = getClient()
|
|
490
|
+
// Use client
|
|
491
|
+
}
|
|
1040
492
|
```
|
|
1041
493
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
- All SDK calls happen inside `inBrowser()` — you can't access SDK objects directly in Node
|
|
1045
|
-
- Return plain objects from `inBrowser()`, not class instances or signals
|
|
1046
|
-
- Always `dispose()` controllers to prevent leaks between tests
|
|
1047
|
-
- Use `devConfig` from the test harness for real API credentials
|
|
494
|
+
## Response Types
|
|
1048
495
|
|
|
1049
|
-
|
|
496
|
+
The SDK exports all types from `@commerce-blocks/sdk`:
|
|
1050
497
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
498
|
+
```typescript
|
|
499
|
+
import type {
|
|
500
|
+
Client,
|
|
501
|
+
ClientConfig,
|
|
502
|
+
QueryState,
|
|
503
|
+
QueryResult,
|
|
504
|
+
Product,
|
|
505
|
+
ProductBase,
|
|
506
|
+
ProductVariant,
|
|
507
|
+
OptionGroup,
|
|
508
|
+
OptionValue,
|
|
509
|
+
PriceData,
|
|
510
|
+
PriceRangeData,
|
|
511
|
+
ClientError,
|
|
512
|
+
NetworkError,
|
|
513
|
+
ApiError,
|
|
514
|
+
} from '@commerce-blocks/sdk'
|
|
515
|
+
```
|