@commerce-blocks/sdk 2.0.0-alpha.2 → 2.0.0-alpha.3
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 +313 -850
- package/dist/index.d.ts +278 -573
- package/dist/index.js +1156 -1980
- package/package.json +1 -1
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,321 @@ 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
|
+
| `options` | `string[]` | No | Product options to expose |
|
|
55
|
+
| `productMetafields` | `{ namespace, key }[]` | No | Product metafields to fetch |
|
|
56
|
+
| `variantMetafields` | `{ namespace, key }[]` | No | Variant metafields to fetch |
|
|
57
|
+
| `transforms` | `Transforms` | No | Post-process results (see below) |
|
|
58
|
+
| `filterAliases` | `FilterAliases` | No | URL-friendly filter key mapping |
|
|
59
|
+
| `cacheLimit` | `number` | No | Max entries in cache |
|
|
60
|
+
| `cacheLifetime` | `number` | No | TTL in milliseconds |
|
|
61
|
+
| `storage` | `StorageAdapter` | No | Custom storage adapter |
|
|
62
|
+
| `initialData` | `CacheData` | No | Pre-populate cache at init |
|
|
63
|
+
| `restoreCache` | `boolean` | No | Restore from storage (default: true) |
|
|
64
|
+
|
|
65
|
+
## Controllers
|
|
66
|
+
|
|
67
|
+
All controllers (`collection`, `blocks`, `search`, `suggest`) follow the same pattern:
|
|
68
|
+
|
|
69
|
+
- **`state`** — a `ReadonlySignal<QueryState<T>>` with `{ data, error, isFetching }`
|
|
70
|
+
- **`execute()`** — runs the query, returns `Result<T, ClientError>`
|
|
71
|
+
- **`subscribe(callback)`** — reacts to state changes without importing signals
|
|
72
|
+
- **`dispose()`** — cleans up subscriptions and aborts pending requests
|
|
73
|
+
|
|
74
|
+
### Subscribing to State
|
|
75
|
+
|
|
76
|
+
Three ways to consume controller state (pick one):
|
|
100
77
|
|
|
101
78
|
```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
|
-
},
|
|
79
|
+
// 1. Controller subscribe — no signal import needed
|
|
80
|
+
const unsubscribe = controller.subscribe(({ data, error, isFetching }) => {
|
|
81
|
+
if (isFetching) showLoading()
|
|
82
|
+
if (error) showError(error.message)
|
|
83
|
+
if (data) render(data.products)
|
|
114
84
|
})
|
|
115
85
|
|
|
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
|
|
86
|
+
// 2. Standalone subscribe — works with any signal
|
|
87
|
+
import { subscribe } from '@commerce-blocks/sdk'
|
|
88
|
+
const unsubscribe = subscribe(controller.state, (state) => {
|
|
89
|
+
/* ... */
|
|
90
|
+
})
|
|
131
91
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
92
|
+
// 3. Direct signal access — for custom reactivity
|
|
93
|
+
import { effect } from '@commerce-blocks/sdk'
|
|
94
|
+
effect(() => {
|
|
95
|
+
const { data } = controller.state.value
|
|
96
|
+
})
|
|
97
|
+
```
|
|
136
98
|
|
|
137
|
-
|
|
99
|
+
### Shared Query Parameters
|
|
138
100
|
|
|
139
|
-
|
|
101
|
+
These parameters are available on `execute()` for all controllers except suggest:
|
|
140
102
|
|
|
141
|
-
|
|
103
|
+
| Parameter | Type | Description |
|
|
104
|
+
| ------------------ | ------------------------- | ---------------------------------- |
|
|
105
|
+
| `page` | `number` | Page number (default: 1) |
|
|
106
|
+
| `limit` | `number` | Products per page (default: 24) |
|
|
107
|
+
| `filters` | `unknown` | Filter criteria |
|
|
108
|
+
| `signal` | `AbortSignal` | Per-call abort signal |
|
|
109
|
+
| `linking` | `Record<string, unknown>` | Dynamic linking parameters |
|
|
110
|
+
| `transformRequest` | `(body) => body` | Custom request body transformation |
|
|
142
111
|
|
|
143
|
-
|
|
144
|
-
- **Reactive (callback)**: Use `controller.subscribe(callback)` — receives state updates without needing signals
|
|
145
|
-
- **Imperative**: `await controller.execute()` returns `Result<T, SdkError>` directly
|
|
112
|
+
### Collection
|
|
146
113
|
|
|
147
|
-
|
|
114
|
+
Browse products in a collection.
|
|
148
115
|
|
|
149
116
|
```typescript
|
|
150
|
-
const collection =
|
|
117
|
+
const collection = client.collection({
|
|
151
118
|
handle: 'shirts',
|
|
152
119
|
defaultSort: 'featured', // optional, uses first sort if omitted
|
|
153
120
|
})
|
|
154
121
|
|
|
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')
|
|
122
|
+
await collection.execute() // initial load
|
|
123
|
+
await collection.execute({ page: 2 }) // paginate
|
|
124
|
+
await collection.execute({ sort: 'price_asc' }) // change sort
|
|
125
|
+
await collection.execute({ filters: { color: 'Red' } })
|
|
126
|
+
await collection.execute({ includeMeta: true }) // fetch collection metadata
|
|
177
127
|
|
|
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
|
|
128
|
+
collection.dispose()
|
|
185
129
|
```
|
|
186
130
|
|
|
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 |
|
|
131
|
+
Additional `execute()` params: `sort`, `includeMeta`.
|
|
211
132
|
|
|
212
|
-
###
|
|
133
|
+
### Blocks
|
|
213
134
|
|
|
214
|
-
|
|
135
|
+
Product recommendations powered by Layers blocks. Anchored to a product, collection, or cart.
|
|
215
136
|
|
|
216
137
|
```typescript
|
|
217
|
-
const blocks =
|
|
138
|
+
const blocks = client.blocks({
|
|
218
139
|
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
|
|
140
|
+
anchor: 'gold-necklace', // product/collection ID or handle
|
|
233
141
|
})
|
|
234
142
|
|
|
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
143
|
await blocks.execute()
|
|
243
|
-
await blocks.execute({ page: 2, limit: 12 })
|
|
244
|
-
|
|
245
|
-
// With cart context for personalized recommendations
|
|
246
144
|
await blocks.execute({
|
|
145
|
+
discounts: [
|
|
146
|
+
{
|
|
147
|
+
entitled: { all: true },
|
|
148
|
+
discount: { type: 'PERCENTAGE', value: 10 },
|
|
149
|
+
},
|
|
150
|
+
],
|
|
247
151
|
context: {
|
|
248
152
|
productsInCart: [{ productId: '123', variantId: '456', quantity: 1 }],
|
|
249
153
|
geo: { country: 'US' },
|
|
250
154
|
},
|
|
251
155
|
})
|
|
252
156
|
|
|
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
|
|
157
|
+
// result.data.block has { id, title, anchor_type, strategy_type, ... }
|
|
158
|
+
blocks.dispose()
|
|
261
159
|
```
|
|
262
160
|
|
|
263
|
-
|
|
161
|
+
Additional `execute()` params: `discounts`, `context`.
|
|
264
162
|
|
|
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 |
|
|
163
|
+
### Search
|
|
270
164
|
|
|
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.
|
|
165
|
+
Full-text search with facets. Options persist across calls — subsequent `execute()` calls merge with existing options.
|
|
313
166
|
|
|
314
167
|
```typescript
|
|
315
|
-
const
|
|
168
|
+
const search = client.search({ query: 'ring', limit: 20 })
|
|
316
169
|
|
|
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
|
-
})
|
|
170
|
+
await search.execute()
|
|
171
|
+
await search.execute({ page: 2 }) // page persists
|
|
172
|
+
await search.execute({ filters: { vendor: 'Nike' } }) // filters update
|
|
327
173
|
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
const { data, isFetching, error } = autocomplete.state.value
|
|
331
|
-
// same as above
|
|
332
|
-
})
|
|
174
|
+
// Temporary override (doesn't persist for next call)
|
|
175
|
+
await search.execute({ query: 'shoes', temporary: true })
|
|
333
176
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
})
|
|
177
|
+
// Prepare search (caches searchId for faster execute)
|
|
178
|
+
await search.prepare()
|
|
179
|
+
await search.execute() // uses cached searchId
|
|
338
180
|
|
|
339
|
-
|
|
340
|
-
unsubscribe() // remove single subscription
|
|
341
|
-
autocomplete.dispose() // cleanup all subscriptions, abort controller, and timers
|
|
181
|
+
search.dispose()
|
|
342
182
|
```
|
|
343
183
|
|
|
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:**
|
|
184
|
+
Additional `execute()` params: `query`, `searchId`, `tuning`, `temporary`.
|
|
352
185
|
|
|
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 |
|
|
186
|
+
`SearchTuning` controls matching weights: `textualWeight`, `visualWeight`, `multipleFactor`, `minimumMatch`.
|
|
358
187
|
|
|
359
|
-
###
|
|
188
|
+
### Suggest
|
|
360
189
|
|
|
361
|
-
|
|
190
|
+
Predictive search with debouncing and local caching. Only full words (trailing space) are cached — partial input filters cached results client-side.
|
|
362
191
|
|
|
363
192
|
```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
|
-
})
|
|
193
|
+
const suggest = client.suggest({ debounce: 300 })
|
|
372
194
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// same as above
|
|
195
|
+
suggest.subscribe(({ data }) => {
|
|
196
|
+
if (data) renderSuggestions(data.matchedQueries)
|
|
376
197
|
})
|
|
377
198
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const { data, isFetching, error } = search.state.value
|
|
381
|
-
// same as above
|
|
199
|
+
input.addEventListener('input', (e) => {
|
|
200
|
+
suggest.execute(e.target.value) // debounced automatically
|
|
382
201
|
})
|
|
383
202
|
|
|
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()
|
|
203
|
+
suggest.dispose()
|
|
401
204
|
```
|
|
402
205
|
|
|
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 |
|
|
206
|
+
| Option | Type | Description |
|
|
207
|
+
| ------------------- | ------------- | ------------------------------------------- |
|
|
208
|
+
| `debounce` | `number` | Debounce delay in ms (default: 300) |
|
|
209
|
+
| `excludeInputQuery` | `boolean` | Remove user's input from suggestions |
|
|
210
|
+
| `excludeQueries` | `string[]` | Custom strings to filter from suggestions |
|
|
211
|
+
| `signal` | `AbortSignal` | Shared abort signal (acts like `dispose()`) |
|
|
427
212
|
|
|
428
|
-
|
|
213
|
+
### Image Search
|
|
429
214
|
|
|
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.
|
|
215
|
+
Upload an image, then search by it.
|
|
440
216
|
|
|
441
217
|
```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
|
|
218
|
+
const upload = client.uploadImage({ image: file })
|
|
219
|
+
upload.subscribe(({ data }) => {
|
|
220
|
+
if (!data) return
|
|
221
|
+
const results = client.searchByImage({ imageId: data.imageId })
|
|
222
|
+
results.subscribe(({ data }) => {
|
|
223
|
+
if (data) console.log('Similar:', data.products)
|
|
224
|
+
})
|
|
458
225
|
})
|
|
459
226
|
```
|
|
460
227
|
|
|
461
|
-
###
|
|
228
|
+
### Abort Signals
|
|
462
229
|
|
|
463
|
-
|
|
230
|
+
Controllers support two levels of abort:
|
|
464
231
|
|
|
465
232
|
```typescript
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
filters: { vendor: 'Nike' },
|
|
470
|
-
})
|
|
233
|
+
// Shared signal — cancels everything when component unmounts
|
|
234
|
+
const ac = new AbortController()
|
|
235
|
+
const search = client.search({ query: 'ring', signal: ac.signal })
|
|
471
236
|
|
|
472
|
-
//
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
})
|
|
237
|
+
// Per-call signal — cancels only this request
|
|
238
|
+
const req = new AbortController()
|
|
239
|
+
await search.execute({ page: 2, signal: req.signal })
|
|
476
240
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
const { data, error, isFetching } = state.value
|
|
480
|
-
// same as above
|
|
481
|
-
})
|
|
241
|
+
// Either aborting cancels the request (they're linked internally)
|
|
242
|
+
ac.abort() // cancels all pending + acts like dispose()
|
|
482
243
|
```
|
|
483
244
|
|
|
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 |
|
|
245
|
+
Collection and blocks auto-cancel the previous request when a new `execute()` starts.
|
|
494
246
|
|
|
495
|
-
|
|
247
|
+
## Product Card
|
|
496
248
|
|
|
497
|
-
|
|
249
|
+
Reactive controller for product cards with variant selection and availability logic. All derived values are computed signals that auto-update when inputs change.
|
|
498
250
|
|
|
499
251
|
```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
|
-
})
|
|
252
|
+
import { createProductCard, effect } from '@commerce-blocks/sdk'
|
|
508
253
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
console.log('Collection:', data.collection)
|
|
514
|
-
console.log('Page:', data.page)
|
|
515
|
-
}
|
|
254
|
+
const card = createProductCard({
|
|
255
|
+
product,
|
|
256
|
+
selectedOptions: [{ name: 'Size', value: '7' }],
|
|
257
|
+
breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
|
|
516
258
|
})
|
|
517
259
|
|
|
518
|
-
//
|
|
260
|
+
// Reactive signals
|
|
519
261
|
effect(() => {
|
|
520
|
-
|
|
521
|
-
|
|
262
|
+
console.log('Variant:', card.selectedVariant.value)
|
|
263
|
+
console.log('Options:', card.options.value) // OptionGroup[]
|
|
264
|
+
console.log('Images:', card.images.value)
|
|
265
|
+
console.log('Price:', card.price.value) // PriceData
|
|
522
266
|
})
|
|
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
267
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
268
|
+
// Actions
|
|
269
|
+
card.selectOption({ name: 'Size', value: 'L' })
|
|
270
|
+
card.setSelectedOptions([{ name: 'Size', value: 'L' }]) // merge by name
|
|
271
|
+
card.setSelectedVariant(12345) // select by numeric variant ID
|
|
272
|
+
card.setCarouselPosition(3) // 1-based manual override
|
|
552
273
|
|
|
553
|
-
|
|
554
|
-
|
|
274
|
+
card.subscribe(({ selectedVariant, options, price }) => {
|
|
275
|
+
// Called on any state change
|
|
555
276
|
})
|
|
556
277
|
|
|
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()
|
|
278
|
+
card.dispose()
|
|
564
279
|
```
|
|
565
280
|
|
|
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:**
|
|
281
|
+
**Reactive state** (all `ReadonlySignal`): `variants`, `selectedVariant`, `options`, `images`, `price`, `priceRange`, `carouselPosition`, `isSelectionComplete`.
|
|
576
282
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
## Response Types
|
|
283
|
+
Options include availability status baked in:
|
|
580
284
|
|
|
581
285
|
```typescript
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
error: SdkError | null
|
|
586
|
-
isFetching: boolean
|
|
286
|
+
interface OptionGroup {
|
|
287
|
+
name: string
|
|
288
|
+
values: OptionValue[]
|
|
587
289
|
}
|
|
588
290
|
|
|
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
|
|
291
|
+
interface OptionValue {
|
|
292
|
+
value: string
|
|
293
|
+
status: 'available' | 'backorderable' | 'sold-out' | 'unavailable'
|
|
294
|
+
selected: boolean
|
|
295
|
+
swatch: Swatch | null
|
|
600
296
|
}
|
|
601
297
|
|
|
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 }
|
|
298
|
+
interface PriceData {
|
|
299
|
+
price: Price | null
|
|
300
|
+
compareAtPrice: Price | null
|
|
301
|
+
isOnSale: boolean
|
|
625
302
|
}
|
|
626
303
|
|
|
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
|
|
304
|
+
interface PriceRangeData {
|
|
305
|
+
priceRange: PriceRange
|
|
306
|
+
compareAtPriceRange: PriceRange | null
|
|
642
307
|
}
|
|
643
308
|
```
|
|
644
309
|
|
|
645
|
-
##
|
|
310
|
+
## Filters
|
|
646
311
|
|
|
647
|
-
|
|
312
|
+
Build filters using the DSL:
|
|
648
313
|
|
|
649
314
|
```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 }]` |
|
|
315
|
+
import { filter, and, or, eq, gte, lte, inValues } from '@commerce-blocks/sdk'
|
|
705
316
|
|
|
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')))
|
|
317
|
+
await collection.execute({
|
|
318
|
+
filters: filter(and(eq('options.color', 'Red'), eq('options.size', 'Medium'))),
|
|
319
|
+
})
|
|
741
320
|
|
|
742
321
|
// Price range
|
|
743
|
-
|
|
322
|
+
filter(and(gte('price', 50), lte('price', 200)))
|
|
744
323
|
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
filters: multiFilter,
|
|
748
|
-
})
|
|
324
|
+
// Multiple values
|
|
325
|
+
filter(or(eq('vendor', 'Nike'), eq('vendor', 'Adidas')))
|
|
749
326
|
```
|
|
750
327
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
| Function | Description |
|
|
328
|
+
| Operator | Description |
|
|
754
329
|
| ------------------------------ | ----------------------- |
|
|
755
330
|
| `eq(property, value)` | Equals |
|
|
756
331
|
| `notEq(property, value)` | Not equals |
|
|
@@ -763,293 +338,181 @@ await collection.execute({
|
|
|
763
338
|
| `exists(property)` | Property exists |
|
|
764
339
|
| `notExists(property)` | Property does not exist |
|
|
765
340
|
|
|
766
|
-
|
|
341
|
+
## Transforms and Filter Aliases
|
|
767
342
|
|
|
768
|
-
|
|
343
|
+
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
344
|
|
|
770
345
|
```typescript
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
346
|
+
import type { Product } from '@commerce-blocks/sdk'
|
|
347
|
+
|
|
348
|
+
type MyProduct = Product<{ description: string; rating: number }>
|
|
349
|
+
|
|
350
|
+
const { data: client } = createClient({
|
|
351
|
+
// ...config
|
|
352
|
+
attributes: ['body_html'],
|
|
353
|
+
transforms: {
|
|
354
|
+
product: ({ raw }) => ({
|
|
355
|
+
description: raw.body_html ?? '',
|
|
356
|
+
rating: raw.calculated?.average_rating ?? 0,
|
|
357
|
+
}),
|
|
358
|
+
collection: (result, raw) => result,
|
|
359
|
+
search: (result, raw) => result,
|
|
360
|
+
block: (result, raw) => result,
|
|
361
|
+
filters: (filters) => filters,
|
|
362
|
+
},
|
|
363
|
+
filterAliases: {
|
|
774
364
|
color: 'options.color',
|
|
775
365
|
size: 'options.size',
|
|
776
366
|
brand: { property: 'vendor', values: { nike: 'Nike', adidas: 'Adidas' } },
|
|
777
367
|
},
|
|
778
368
|
})
|
|
779
369
|
|
|
780
|
-
//
|
|
781
|
-
await collection.execute({
|
|
782
|
-
|
|
783
|
-
})
|
|
370
|
+
// Aliases resolve automatically
|
|
371
|
+
await collection.execute({ filters: { color: 'Red', brand: 'nike' } })
|
|
372
|
+
// Products now include description and rating from the product transform
|
|
784
373
|
```
|
|
785
374
|
|
|
786
|
-
##
|
|
787
|
-
|
|
788
|
-
### `createProductCard()` - Reactive Product Card Controller
|
|
375
|
+
## Error Handling
|
|
789
376
|
|
|
790
|
-
|
|
377
|
+
All methods return `{ data, error }` instead of throwing. Errors are discriminated by `_tag`:
|
|
791
378
|
|
|
792
379
|
```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
|
-
})
|
|
380
|
+
const result = await collection.execute()
|
|
806
381
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
// Unsubscribe
|
|
826
|
-
card.dispose()
|
|
382
|
+
if (result.error) {
|
|
383
|
+
switch (result.error._tag) {
|
|
384
|
+
case 'NetworkError':
|
|
385
|
+
// code: 'TIMEOUT' | 'CONNECTION_FAILED' | 'DNS_FAILED' | 'SSL_ERROR' | 'ABORTED' | 'OFFLINE'
|
|
386
|
+
break
|
|
387
|
+
case 'ApiError':
|
|
388
|
+
// code: 'NOT_FOUND' | 'RATE_LIMITED' | 'UNAUTHORIZED' | ...
|
|
389
|
+
// status: HTTP status code, source: 'layers'
|
|
390
|
+
break
|
|
391
|
+
case 'ValidationError':
|
|
392
|
+
// operation: which method failed, fields: [{ field, code, message }]
|
|
393
|
+
break
|
|
394
|
+
case 'ConfigError':
|
|
395
|
+
// field: which config field, expected: what was expected
|
|
396
|
+
break
|
|
397
|
+
}
|
|
398
|
+
}
|
|
827
399
|
```
|
|
828
400
|
|
|
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:
|
|
401
|
+
Retryable errors (`TIMEOUT`, `CONNECTION_FAILED`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`) expose `retryable` and `retryAfterMs`:
|
|
872
402
|
|
|
873
403
|
```typescript
|
|
874
|
-
import {
|
|
404
|
+
import { isRetryable } from '@commerce-blocks/sdk'
|
|
875
405
|
|
|
876
|
-
if (
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
const sdk = result.data
|
|
880
|
-
// Use sdk
|
|
881
|
-
}
|
|
406
|
+
if (result.error && isRetryable(result.error)) {
|
|
407
|
+
const delay = result.error.retryAfterMs ?? 1000
|
|
408
|
+
setTimeout(() => collection.execute(), delay)
|
|
882
409
|
}
|
|
883
410
|
```
|
|
884
411
|
|
|
885
|
-
##
|
|
412
|
+
## Cache and Storage
|
|
886
413
|
|
|
887
|
-
The
|
|
414
|
+
The client exposes a reactive cache:
|
|
888
415
|
|
|
889
416
|
```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 }
|
|
417
|
+
const { cache } = client
|
|
418
|
+
|
|
419
|
+
cache.get('cache-key') // CacheEntry<QueryResult> | null
|
|
420
|
+
cache.invalidate('browse:*') // invalidate by pattern
|
|
421
|
+
cache.persist() // save to storage
|
|
422
|
+
cache.restore() // restore from storage
|
|
423
|
+
cache.clear() // clear all
|
|
424
|
+
cache.stats.entries // current entry count
|
|
916
425
|
```
|
|
917
426
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
Configure how the SDK persists cache data. By default, the SDK uses `localStorage` in browsers.
|
|
921
|
-
|
|
922
|
-
### Built-in Adapters
|
|
427
|
+
### Storage Adapters
|
|
923
428
|
|
|
924
429
|
```typescript
|
|
925
|
-
import { localStorageAdapter,
|
|
430
|
+
import { localStorageAdapter, fileStorage } from '@commerce-blocks/sdk'
|
|
926
431
|
|
|
927
|
-
// Browser
|
|
432
|
+
// Browser (returns null if unavailable)
|
|
928
433
|
const browserAdapter = localStorageAdapter('my-cache-key')
|
|
929
434
|
|
|
930
|
-
// Node.js
|
|
435
|
+
// Node.js
|
|
931
436
|
import fs from 'fs'
|
|
932
|
-
const nodeAdapter =
|
|
437
|
+
const nodeAdapter = fileStorage('./cache.json', fs)
|
|
933
438
|
```
|
|
934
439
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
Implement the `StorageAdapter` interface:
|
|
440
|
+
Custom adapter — implement `StorageAdapter`:
|
|
938
441
|
|
|
939
442
|
```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'),
|
|
443
|
+
const adapter: StorageAdapter = {
|
|
444
|
+
read: () => sessionStorage.getItem('key'),
|
|
445
|
+
write: (data) => sessionStorage.setItem('key', data),
|
|
446
|
+
remove: () => sessionStorage.removeItem('key'),
|
|
951
447
|
}
|
|
952
448
|
```
|
|
953
449
|
|
|
954
|
-
##
|
|
450
|
+
## Signals
|
|
955
451
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
For advanced use cases bypassing SDK caching. Combines `prepareSearch` + `search` into a single call.
|
|
452
|
+
The SDK re-exports `@preact/signals-core` primitives for reactive state:
|
|
959
453
|
|
|
960
454
|
```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
|
-
}
|
|
455
|
+
import { signal, computed, effect, batch } from '@commerce-blocks/sdk'
|
|
977
456
|
```
|
|
978
457
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
Build cache keys for manual store operations:
|
|
458
|
+
Controller `state` is a `ReadonlySignal<QueryState<T>>`:
|
|
982
459
|
|
|
983
460
|
```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'])
|
|
461
|
+
interface QueryState<T> {
|
|
462
|
+
data: T | null
|
|
463
|
+
error: ClientError | null
|
|
464
|
+
isFetching: boolean
|
|
465
|
+
}
|
|
991
466
|
```
|
|
992
467
|
|
|
993
|
-
|
|
468
|
+
All controllers return the same `QueryResult` shape in `data`:
|
|
994
469
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
470
|
+
```typescript
|
|
471
|
+
interface QueryResult {
|
|
472
|
+
products: Product[]
|
|
473
|
+
totalResults: number
|
|
474
|
+
totalPages: number
|
|
475
|
+
page: number
|
|
476
|
+
facets: Record<string, Record<string, number>>
|
|
477
|
+
priceRange?: PriceRange
|
|
478
|
+
attributionToken?: string
|
|
479
|
+
}
|
|
1002
480
|
```
|
|
1003
481
|
|
|
1004
|
-
|
|
482
|
+
Blocks results add a `block` field with `{ id, title, anchor_type, strategy_type, strategy_key }`.
|
|
1005
483
|
|
|
1006
|
-
|
|
1007
|
-
- `inBrowser(page, fn)` — runs a function inside the browser via `page.evaluate()`
|
|
1008
|
-
|
|
1009
|
-
### Writing tests
|
|
484
|
+
## Singleton Access
|
|
1010
485
|
|
|
1011
|
-
|
|
486
|
+
After initialization, access the client anywhere:
|
|
1012
487
|
|
|
1013
488
|
```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()
|
|
489
|
+
import { getClient, isInitialized } from '@commerce-blocks/sdk'
|
|
1031
490
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
expect(result.error).toBeUndefined()
|
|
1037
|
-
expect(result.count).toBeGreaterThan(0)
|
|
1038
|
-
})
|
|
1039
|
-
})
|
|
491
|
+
if (isInitialized()) {
|
|
492
|
+
const { data: client } = getClient()
|
|
493
|
+
// Use client
|
|
494
|
+
}
|
|
1040
495
|
```
|
|
1041
496
|
|
|
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
|
|
497
|
+
## Response Types
|
|
1048
498
|
|
|
1049
|
-
|
|
499
|
+
The SDK exports all types from `@commerce-blocks/sdk`:
|
|
1050
500
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
501
|
+
```typescript
|
|
502
|
+
import type {
|
|
503
|
+
Client,
|
|
504
|
+
ClientConfig,
|
|
505
|
+
QueryState,
|
|
506
|
+
QueryResult,
|
|
507
|
+
Product,
|
|
508
|
+
ProductBase,
|
|
509
|
+
ProductVariant,
|
|
510
|
+
OptionGroup,
|
|
511
|
+
OptionValue,
|
|
512
|
+
PriceData,
|
|
513
|
+
PriceRangeData,
|
|
514
|
+
ClientError,
|
|
515
|
+
NetworkError,
|
|
516
|
+
ApiError,
|
|
517
|
+
} from '@commerce-blocks/sdk'
|
|
518
|
+
```
|