@commerce-blocks/sdk 2.0.0-alpha.1 → 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 -758
- package/dist/index.d.ts +328 -636
- package/dist/index.js +1176 -1946
- 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,654 +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) |
|
|
28
|
+
if (error) throw new Error(error.message)
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Storefront API is opt-in. Enable it for collection/page metadata and Shopify-specific fields (metafields, variant detail).
|
|
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
|
-
|
|
75
|
-
### Product
|
|
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
37
|
|
|
88
|
-
|
|
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
|
-
- **Imperative**: `await controller.execute()` returns `Result<T, SdkError>` directly
|
|
112
|
+
### Collection
|
|
145
113
|
|
|
146
|
-
|
|
114
|
+
Browse products in a collection.
|
|
147
115
|
|
|
148
116
|
```typescript
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const collection = sdk.collection({
|
|
117
|
+
const collection = client.collection({
|
|
152
118
|
handle: 'shirts',
|
|
153
119
|
defaultSort: 'featured', // optional, uses first sort if omitted
|
|
154
120
|
})
|
|
155
121
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (data) console.log('Products:', data.products)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Execute queries — returns Result<CollectionResult, SdkError>
|
|
165
|
-
const result = await collection.execute() // initial load
|
|
166
|
-
if (result.error) console.error(result.error.message)
|
|
167
|
-
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
|
|
168
127
|
|
|
169
|
-
await collection.execute({ page: 2 }) // pagination
|
|
170
|
-
await collection.execute({ sortOrderCode: 'price_asc' }) // change sort
|
|
171
|
-
await collection.execute({ filters: { color: 'Red' } }) // with filters
|
|
172
|
-
|
|
173
|
-
// Cleanup
|
|
174
128
|
collection.dispose()
|
|
175
129
|
```
|
|
176
130
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
| Parameter | Type | Required | Description |
|
|
180
|
-
| ------------- | -------- | -------- | ---------------------------------------------- |
|
|
181
|
-
| `handle` | `string` | Yes | Collection URL handle |
|
|
182
|
-
| `defaultSort` | `string` | No | Default sort code (uses first configured sort) |
|
|
131
|
+
Additional `execute()` params: `sort`, `includeMeta`.
|
|
183
132
|
|
|
184
|
-
|
|
133
|
+
### Blocks
|
|
185
134
|
|
|
186
|
-
|
|
187
|
-
| ---------------- | ------------------------- | ------------------------------------- |
|
|
188
|
-
| `page` | `number` | Page number (default: 1) |
|
|
189
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
190
|
-
| `sortOrderCode` | `string` | Sort option code |
|
|
191
|
-
| `filters` | `unknown` | Filter criteria |
|
|
192
|
-
| `signal` | `AbortSignal` | External abort signal |
|
|
193
|
-
| `includeMeta` | `boolean` | Fetch collection metadata |
|
|
194
|
-
| `includeFilters` | `boolean` | Include filter counts in response |
|
|
195
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
196
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
197
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
198
|
-
|
|
199
|
-
### `sdk.blocks()` - Product Recommendations
|
|
200
|
-
|
|
201
|
-
Creates a blocks controller for product recommendations powered by Layers blocks. Blocks can be anchored to a product, collection, or cart for contextual recommendations.
|
|
135
|
+
Product recommendations powered by Layers blocks. Anchored to a product, collection, or cart.
|
|
202
136
|
|
|
203
137
|
```typescript
|
|
204
|
-
const blocks =
|
|
138
|
+
const blocks = client.blocks({
|
|
205
139
|
blockId: 'block-abc123',
|
|
206
|
-
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
// Subscribe to state changes
|
|
210
|
-
effect(() => {
|
|
211
|
-
const { data, error, isFetching } = blocks.state.value
|
|
212
|
-
if (data) {
|
|
213
|
-
console.log('Recommendations:', data.products)
|
|
214
|
-
console.log('Block info:', data.block) // { title, anchor_type, strategy_type, ... }
|
|
215
|
-
}
|
|
140
|
+
anchor: 'gold-necklace', // product/collection ID or handle
|
|
216
141
|
})
|
|
217
142
|
|
|
218
|
-
// Execute queries
|
|
219
143
|
await blocks.execute()
|
|
220
|
-
await blocks.execute({ page: 2, limit: 12 })
|
|
221
|
-
|
|
222
|
-
// With cart context for personalized recommendations
|
|
223
144
|
await blocks.execute({
|
|
145
|
+
discounts: [
|
|
146
|
+
{
|
|
147
|
+
entitled: { all: true },
|
|
148
|
+
discount: { type: 'PERCENTAGE', value: 10 },
|
|
149
|
+
},
|
|
150
|
+
],
|
|
224
151
|
context: {
|
|
225
152
|
productsInCart: [{ productId: '123', variantId: '456', quantity: 1 }],
|
|
226
153
|
geo: { country: 'US' },
|
|
227
154
|
},
|
|
228
155
|
})
|
|
229
156
|
|
|
230
|
-
//
|
|
231
|
-
await blocks.execute({
|
|
232
|
-
discountEntitlements: [{ id: 'discount-123' }],
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
// Cleanup
|
|
157
|
+
// result.data.block has { id, title, anchor_type, strategy_type, ... }
|
|
236
158
|
blocks.dispose()
|
|
237
159
|
```
|
|
238
160
|
|
|
239
|
-
|
|
161
|
+
Additional `execute()` params: `discounts`, `context`.
|
|
240
162
|
|
|
241
|
-
|
|
242
|
-
| -------------- | -------- | -------- | -------------------------------- |
|
|
243
|
-
| `blockId` | `string` | Yes | Layers block ID |
|
|
244
|
-
| `anchorId` | `string` | No | Anchor product/collection ID |
|
|
245
|
-
| `anchorHandle` | `string` | No | Anchor product/collection handle |
|
|
163
|
+
### Search
|
|
246
164
|
|
|
247
|
-
|
|
165
|
+
Full-text search with facets. Options persist across calls — subsequent `execute()` calls merge with existing options.
|
|
248
166
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
| `page` | `number` | Page number (default: 1) |
|
|
252
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
253
|
-
| `filters` | `unknown` | Filter criteria |
|
|
254
|
-
| `signal` | `AbortSignal` | External abort signal |
|
|
255
|
-
| `discountEntitlements` | `DiscountEntitlement[]` | Discount entitlements to apply |
|
|
256
|
-
| `context` | `BlocksContext` | Cart, geo, and custom context |
|
|
257
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
258
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
259
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
167
|
+
```typescript
|
|
168
|
+
const search = client.search({ query: 'ring', limit: 20 })
|
|
260
169
|
|
|
261
|
-
|
|
170
|
+
await search.execute()
|
|
171
|
+
await search.execute({ page: 2 }) // page persists
|
|
172
|
+
await search.execute({ filters: { vendor: 'Nike' } }) // filters update
|
|
262
173
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
| `productsInCart` | `{ productId, variantId?, quantity? }[]` | Products in the cart |
|
|
266
|
-
| `geo` | `{ country?, province?, city? }` | Geographic context |
|
|
267
|
-
| `custom` | `Record<string, unknown>` | Custom context data |
|
|
174
|
+
// Temporary override (doesn't persist for next call)
|
|
175
|
+
await search.execute({ query: 'shoes', temporary: true })
|
|
268
176
|
|
|
269
|
-
|
|
177
|
+
// Prepare search (caches searchId for faster execute)
|
|
178
|
+
await search.prepare()
|
|
179
|
+
await search.execute() // uses cached searchId
|
|
270
180
|
|
|
271
|
-
|
|
272
|
-
interface DiscountEntitlement {
|
|
273
|
-
entitled: {
|
|
274
|
-
all?: boolean // Apply to all products
|
|
275
|
-
products?: string[] // Product IDs
|
|
276
|
-
variants?: (string | number)[] // Variant IDs
|
|
277
|
-
collections?: string[] // Collection handles
|
|
278
|
-
}
|
|
279
|
-
discount: {
|
|
280
|
-
type: 'PERCENTAGE' | 'FIXED_AMOUNT'
|
|
281
|
-
value: number
|
|
282
|
-
}
|
|
283
|
-
}
|
|
181
|
+
search.dispose()
|
|
284
182
|
```
|
|
285
183
|
|
|
286
|
-
|
|
184
|
+
Additional `execute()` params: `query`, `searchId`, `tuning`, `temporary`.
|
|
185
|
+
|
|
186
|
+
`SearchTuning` controls matching weights: `textualWeight`, `visualWeight`, `multipleFactor`, `minimumMatch`.
|
|
287
187
|
|
|
288
|
-
|
|
188
|
+
### Suggest
|
|
189
|
+
|
|
190
|
+
Predictive search with debouncing and local caching. Only full words (trailing space) are cached — partial input filters cached results client-side.
|
|
289
191
|
|
|
290
192
|
```typescript
|
|
291
|
-
const
|
|
193
|
+
const suggest = client.suggest({ debounce: 300 })
|
|
292
194
|
|
|
293
|
-
|
|
294
|
-
effect(() => {
|
|
295
|
-
const { data, isFetching, error } = autocomplete.state.value
|
|
296
|
-
if (isFetching) console.log('Loading suggestions...')
|
|
195
|
+
suggest.subscribe(({ data }) => {
|
|
297
196
|
if (data) renderSuggestions(data.matchedQueries)
|
|
298
197
|
})
|
|
299
198
|
|
|
300
|
-
// Wire to input (debounced automatically)
|
|
301
199
|
input.addEventListener('input', (e) => {
|
|
302
|
-
|
|
200
|
+
suggest.execute(e.target.value) // debounced automatically
|
|
303
201
|
})
|
|
304
202
|
|
|
305
|
-
|
|
306
|
-
autocomplete.dispose()
|
|
203
|
+
suggest.dispose()
|
|
307
204
|
```
|
|
308
205
|
|
|
309
|
-
|
|
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()`) |
|
|
310
212
|
|
|
311
|
-
|
|
312
|
-
| ------------ | ------------- | ---------------------------------------------------------- |
|
|
313
|
-
| `debounceMs` | `number` | Debounce delay (default: 300) |
|
|
314
|
-
| `signal` | `AbortSignal` | External abort signal (acts like `dispose()` when aborted) |
|
|
213
|
+
### Image Search
|
|
315
214
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
| Method | Description |
|
|
319
|
-
| ---------------- | -------------------------------------------- |
|
|
320
|
-
| `execute(query)` | Debounced predictive search for autocomplete |
|
|
321
|
-
| `dispose()` | Cleanup abort controller and timers |
|
|
322
|
-
|
|
323
|
-
### `sdk.search()` - Search Products
|
|
324
|
-
|
|
325
|
-
Creates a search controller for full text search with prepare/execute flow.
|
|
215
|
+
Upload an image, then search by it.
|
|
326
216
|
|
|
327
217
|
```typescript
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (data) renderResults(data.products)
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
// Prepare search (optional, caches searchId for reuse)
|
|
338
|
-
await search.prepare({ query: 'ring' })
|
|
339
|
-
|
|
340
|
-
// Execute search with pagination and filters
|
|
341
|
-
form.addEventListener('submit', async (e) => {
|
|
342
|
-
e.preventDefault()
|
|
343
|
-
const result = await search.execute({
|
|
344
|
-
query: inputValue,
|
|
345
|
-
limit: 20,
|
|
346
|
-
filters: { color: 'Blue' },
|
|
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)
|
|
347
224
|
})
|
|
348
|
-
if (result.data) console.log('Results:', result.data.products)
|
|
349
225
|
})
|
|
350
|
-
|
|
351
|
-
// Cleanup
|
|
352
|
-
search.dispose()
|
|
353
226
|
```
|
|
354
227
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
| Method | Description |
|
|
358
|
-
| ---------------- | -------------------------------------------------- |
|
|
359
|
-
| `prepare(query)` | Prepare search and cache searchId for reuse |
|
|
360
|
-
| `execute(query)` | Execute search (uses cached searchId if available) |
|
|
361
|
-
| `dispose()` | Cleanup abort controller |
|
|
362
|
-
|
|
363
|
-
**Search parameters:**
|
|
364
|
-
|
|
365
|
-
| Parameter | Type | Description |
|
|
366
|
-
| ---------------- | ------------------------- | ------------------------------------- |
|
|
367
|
-
| `query` | `string` | Search query |
|
|
368
|
-
| `page` | `number` | Page number (default: 1) |
|
|
369
|
-
| `limit` | `number` | Products per page (default: 24) |
|
|
370
|
-
| `filters` | `unknown` | Filter criteria |
|
|
371
|
-
| `tuning` | `LayersTuning` | Search tuning parameters |
|
|
372
|
-
| `signal` | `AbortSignal` | External abort signal |
|
|
373
|
-
| `dynamicLinking` | `Record<string, unknown>` | Custom dynamic linking parameters |
|
|
374
|
-
| `params` | `Record<string, unknown>` | Additional request parameters |
|
|
375
|
-
| `transformBody` | `(body) => body` | Custom request body mutation function |
|
|
228
|
+
### Abort Signals
|
|
376
229
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
| Property | Type | Description |
|
|
380
|
-
| ---------------- | -------- | ------------------------------------------- |
|
|
381
|
-
| `textualWeight` | `number` | Weight for text-based matching (0-1) |
|
|
382
|
-
| `visualWeight` | `number` | Weight for visual similarity matching (0-1) |
|
|
383
|
-
| `multipleFactor` | `number` | Factor for multiple keyword matching |
|
|
384
|
-
| `minimumMatch` | `number` | Minimum match threshold |
|
|
385
|
-
|
|
386
|
-
### `sdk.uploadImage()` - Upload Image for Search
|
|
387
|
-
|
|
388
|
-
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.
|
|
230
|
+
Controllers support two levels of abort:
|
|
389
231
|
|
|
390
232
|
```typescript
|
|
391
|
-
|
|
392
|
-
const
|
|
233
|
+
// Shared signal — cancels everything when component unmounts
|
|
234
|
+
const ac = new AbortController()
|
|
235
|
+
const search = client.search({ query: 'ring', signal: ac.signal })
|
|
393
236
|
|
|
394
|
-
|
|
237
|
+
// Per-call signal — cancels only this request
|
|
238
|
+
const req = new AbortController()
|
|
239
|
+
await search.execute({ page: 2, signal: req.signal })
|
|
395
240
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (isFetching) console.log('Uploading...')
|
|
399
|
-
if (error) console.error('Upload failed:', error.message)
|
|
400
|
-
if (data) console.log('Image ID:', data.imageId)
|
|
401
|
-
})
|
|
241
|
+
// Either aborting cancels the request (they're linked internally)
|
|
242
|
+
ac.abort() // cancels all pending + acts like dispose()
|
|
402
243
|
```
|
|
403
244
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
Returns a reactive signal that searches products using a previously uploaded image. Same signal-based pattern as `uploadImage()` and `storefront()`.
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
const state = sdk.imageSearch({
|
|
410
|
-
imageId: 'uploaded-image-id',
|
|
411
|
-
limit: 20,
|
|
412
|
-
filters: { vendor: 'Nike' },
|
|
413
|
-
})
|
|
245
|
+
Collection and blocks auto-cancel the previous request when a new `execute()` starts.
|
|
414
246
|
|
|
415
|
-
|
|
416
|
-
const { data, error, isFetching } = state.value
|
|
417
|
-
if (data) console.log('Similar products:', data.products)
|
|
418
|
-
})
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
**Parameters:**
|
|
422
|
-
|
|
423
|
-
| Parameter | Type | Required | Description |
|
|
424
|
-
| --------- | -------------- | -------- | ------------------------------- |
|
|
425
|
-
| `imageId` | `string` | Yes | Image ID from `uploadImage()` |
|
|
426
|
-
| `page` | `number` | No | Page number (default: 1) |
|
|
427
|
-
| `limit` | `number` | No | Products per page (default: 24) |
|
|
428
|
-
| `filters` | `unknown` | No | Filter criteria |
|
|
429
|
-
| `tuning` | `LayersTuning` | No | Search tuning parameters |
|
|
430
|
-
| `signal` | `AbortSignal` | No | External abort signal |
|
|
431
|
-
|
|
432
|
-
### `sdk.storefront()` - Fetch Products and Metadata
|
|
247
|
+
## Product Card
|
|
433
248
|
|
|
434
|
-
|
|
249
|
+
Reactive controller for product cards with variant selection and availability logic. All derived values are computed signals that auto-update when inputs change.
|
|
435
250
|
|
|
436
251
|
```typescript
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
},
|
|
252
|
+
import { createProductCard, effect } from '@commerce-blocks/sdk'
|
|
253
|
+
|
|
254
|
+
const card = createProductCard({
|
|
255
|
+
product,
|
|
256
|
+
selectedOptions: [{ name: 'Size', value: '7' }],
|
|
257
|
+
breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
|
|
444
258
|
})
|
|
445
259
|
|
|
260
|
+
// Reactive signals
|
|
446
261
|
effect(() => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
console.log('Page:', data.page)
|
|
452
|
-
}
|
|
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
|
|
453
266
|
})
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**Parameters:**
|
|
457
|
-
|
|
458
|
-
| Parameter | Type | Required | Description |
|
|
459
|
-
| --------------------- | ------------- | -------- | ---------------------------------------- |
|
|
460
|
-
| `ids` | `string[]` | Yes | Product GIDs |
|
|
461
|
-
| `meta.collection` | `string` | No | Collection handle to fetch metadata |
|
|
462
|
-
| `meta.page` | `string` | No | Page handle to fetch metadata |
|
|
463
|
-
| `meta.includeFilters` | `boolean` | No | Include available filters for collection |
|
|
464
|
-
| `signal` | `AbortSignal` | No | External abort signal |
|
|
465
|
-
|
|
466
|
-
## Abort Signals
|
|
467
267
|
|
|
468
|
-
|
|
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
|
|
469
273
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Cancel from outside
|
|
474
|
-
await search.execute({ query: 'ring', signal: controller.signal })
|
|
475
|
-
controller.abort() // cancels the request
|
|
476
|
-
|
|
477
|
-
// With autocomplete — signal acts like dispose()
|
|
478
|
-
const autocomplete = sdk.autocomplete({ signal: controller.signal })
|
|
479
|
-
controller.abort() // cancels debounce + pending request
|
|
274
|
+
card.subscribe(({ selectedVariant, options, price }) => {
|
|
275
|
+
// Called on any state change
|
|
276
|
+
})
|
|
480
277
|
|
|
481
|
-
|
|
482
|
-
await collection.execute({ page: 2, signal: controller.signal })
|
|
278
|
+
card.dispose()
|
|
483
279
|
```
|
|
484
280
|
|
|
485
|
-
|
|
281
|
+
**Reactive state** (all `ReadonlySignal`): `variants`, `selectedVariant`, `options`, `images`, `price`, `priceRange`, `carouselPosition`, `isSelectionComplete`.
|
|
486
282
|
|
|
487
|
-
|
|
283
|
+
Options include availability status baked in:
|
|
488
284
|
|
|
489
285
|
```typescript
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
error: SdkError | null
|
|
494
|
-
isFetching: boolean
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
interface CollectionResult {
|
|
498
|
-
products: Product[]
|
|
499
|
-
totalResults: number
|
|
500
|
-
totalPages: number
|
|
501
|
-
page: number
|
|
502
|
-
resultsPerPage?: number
|
|
503
|
-
facets: Record<string, Record<string, number>>
|
|
504
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
505
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
506
|
-
attributionToken: string
|
|
507
|
-
collection?: StorefrontCollection
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
interface SearchResult {
|
|
511
|
-
products: Product[]
|
|
512
|
-
totalResults: number
|
|
513
|
-
totalPages: number
|
|
514
|
-
page: number
|
|
515
|
-
resultsPerPage?: number
|
|
516
|
-
facets: Record<string, Record<string, number>>
|
|
517
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
518
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
519
|
-
attributionToken: string
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
interface BlocksResult {
|
|
523
|
-
products: Product[]
|
|
524
|
-
totalResults: number
|
|
525
|
-
totalPages: number
|
|
526
|
-
page: number
|
|
527
|
-
resultsPerPage?: number
|
|
528
|
-
facets: Record<string, Record<string, number>>
|
|
529
|
-
facetRanges?: Record<string, { min: number; max: number }>
|
|
530
|
-
priceRange?: PriceRange // Formatted min/max prices from result set
|
|
531
|
-
attributionToken: string
|
|
532
|
-
block?: BlocksInfo // { id, title, anchor_type, strategy_type, strategy_key }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
interface PriceRange {
|
|
536
|
-
min: Price
|
|
537
|
-
max: Price
|
|
286
|
+
interface OptionGroup {
|
|
287
|
+
name: string
|
|
288
|
+
values: OptionValue[]
|
|
538
289
|
}
|
|
539
290
|
|
|
540
|
-
interface
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
291
|
+
interface OptionValue {
|
|
292
|
+
value: string
|
|
293
|
+
status: 'available' | 'backorderable' | 'sold-out' | 'unavailable'
|
|
294
|
+
selected: boolean
|
|
295
|
+
swatch: Swatch | null
|
|
544
296
|
}
|
|
545
297
|
|
|
546
|
-
interface
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
298
|
+
interface PriceData {
|
|
299
|
+
price: Price | null
|
|
300
|
+
compareAtPrice: Price | null
|
|
301
|
+
isOnSale: boolean
|
|
550
302
|
}
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
## Error Handling
|
|
554
|
-
|
|
555
|
-
All methods return `Result<T, E>` instead of throwing. Errors are discriminated by `_tag`:
|
|
556
303
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (result.error) {
|
|
561
|
-
switch (result.error._tag) {
|
|
562
|
-
case 'NetworkError':
|
|
563
|
-
// Connection issues, timeouts, aborted requests
|
|
564
|
-
console.log(result.error.code) // 'TIMEOUT' | 'CONNECTION_FAILED' | 'ABORTED' | ...
|
|
565
|
-
break
|
|
566
|
-
case 'ApiError':
|
|
567
|
-
// Server errors, rate limits
|
|
568
|
-
console.log(result.error.source) // 'layers' | 'storefront'
|
|
569
|
-
console.log(result.error.status) // HTTP status code
|
|
570
|
-
break
|
|
571
|
-
case 'ValidationError':
|
|
572
|
-
// Invalid parameters
|
|
573
|
-
console.log(result.error.operation) // which method failed
|
|
574
|
-
console.log(result.error.fields) // [{ field, code, message }]
|
|
575
|
-
break
|
|
576
|
-
case 'ConfigError':
|
|
577
|
-
// SDK configuration issues
|
|
578
|
-
console.log(result.error.field) // which config field
|
|
579
|
-
break
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
582
|
-
const data = result.data
|
|
304
|
+
interface PriceRangeData {
|
|
305
|
+
priceRange: PriceRange
|
|
306
|
+
compareAtPriceRange: PriceRange | null
|
|
583
307
|
}
|
|
584
308
|
```
|
|
585
309
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
**`NetworkError`** — connection failures, timeouts, aborted requests
|
|
589
|
-
|
|
590
|
-
| Field | Type | Description |
|
|
591
|
-
| -------------- | ------------------ | ------------------------------------------------------------------------------- |
|
|
592
|
-
| `code` | `NetworkErrorCode` | `TIMEOUT`, `CONNECTION_FAILED`, `DNS_FAILED`, `SSL_ERROR`, `ABORTED`, `OFFLINE` |
|
|
593
|
-
| `message` | `string` | Human-readable description |
|
|
594
|
-
| `retryable` | `boolean` | Whether the request can be retried |
|
|
595
|
-
| `retryAfterMs` | `number?` | Suggested retry delay in milliseconds |
|
|
596
|
-
|
|
597
|
-
**`ApiError`** — server errors, rate limits, GraphQL errors
|
|
598
|
-
|
|
599
|
-
| Field | Type | Description |
|
|
600
|
-
| -------------- | -------------- | -------------------------------------------------- |
|
|
601
|
-
| `code` | `ApiErrorCode` | `NOT_FOUND`, `RATE_LIMITED`, `GRAPHQL_ERROR`, etc. |
|
|
602
|
-
| `source` | `ApiSource` | `'layers'` or `'storefront'` |
|
|
603
|
-
| `status` | `number?` | HTTP status code |
|
|
604
|
-
| `retryable` | `boolean` | Whether the request can be retried |
|
|
605
|
-
| `retryAfterMs` | `number?` | Suggested retry delay |
|
|
606
|
-
|
|
607
|
-
**`ValidationError`** — invalid parameters passed to SDK methods
|
|
608
|
-
|
|
609
|
-
| Field | Type | Description |
|
|
610
|
-
| ----------- | ------------------------ | ------------------------------------- |
|
|
611
|
-
| `operation` | `string` | Which method failed (e.g. `'search'`) |
|
|
612
|
-
| `fields` | `ValidationFieldError[]` | `[{ field, code, message }]` |
|
|
310
|
+
## Filters
|
|
613
311
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
| Field | Type | Description |
|
|
617
|
-
| ---------- | --------- | --------------------------- |
|
|
618
|
-
| `field` | `string` | Which config field is wrong |
|
|
619
|
-
| `expected` | `string?` | What was expected |
|
|
620
|
-
|
|
621
|
-
### Error Helpers
|
|
622
|
-
|
|
623
|
-
Always check `isRetryable()` before calling `getRetryDelay()`. `getRetryDelay()` returns `undefined` for non-retryable errors — it's not meant to be used standalone.
|
|
312
|
+
Build filters using the DSL:
|
|
624
313
|
|
|
625
314
|
```typescript
|
|
626
|
-
import {
|
|
627
|
-
|
|
628
|
-
if (result.error && isRetryable(result.error)) {
|
|
629
|
-
const delay = getRetryDelay(result.error) ?? 1000
|
|
630
|
-
setTimeout(() => collection.execute(), delay)
|
|
631
|
-
}
|
|
632
|
-
```
|
|
315
|
+
import { filter, and, or, eq, gte, lte, inValues } from '@commerce-blocks/sdk'
|
|
633
316
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
```typescript
|
|
639
|
-
import { filter, and, or, eq, inValues, gte, lte } from '@commerce-blocks/sdk'
|
|
640
|
-
|
|
641
|
-
// Single filter
|
|
642
|
-
const colorFilter = filter(eq('options.color', 'Red'))
|
|
643
|
-
|
|
644
|
-
// Multiple conditions (AND)
|
|
645
|
-
const multiFilter = filter(and(eq('options.color', 'Red'), eq('options.size', 'Medium')))
|
|
646
|
-
|
|
647
|
-
// Multiple values (OR)
|
|
648
|
-
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
|
+
})
|
|
649
320
|
|
|
650
321
|
// Price range
|
|
651
|
-
|
|
322
|
+
filter(and(gte('price', 50), lte('price', 200)))
|
|
652
323
|
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
filters: multiFilter,
|
|
656
|
-
})
|
|
324
|
+
// Multiple values
|
|
325
|
+
filter(or(eq('vendor', 'Nike'), eq('vendor', 'Adidas')))
|
|
657
326
|
```
|
|
658
327
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
| Function | Description |
|
|
328
|
+
| Operator | Description |
|
|
662
329
|
| ------------------------------ | ----------------------- |
|
|
663
330
|
| `eq(property, value)` | Equals |
|
|
664
331
|
| `notEq(property, value)` | Not equals |
|
|
@@ -671,293 +338,181 @@ await collection.execute({
|
|
|
671
338
|
| `exists(property)` | Property exists |
|
|
672
339
|
| `notExists(property)` | Property does not exist |
|
|
673
340
|
|
|
674
|
-
|
|
341
|
+
## Transforms and Filter Aliases
|
|
675
342
|
|
|
676
|
-
|
|
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:
|
|
677
344
|
|
|
678
345
|
```typescript
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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: {
|
|
682
364
|
color: 'options.color',
|
|
683
365
|
size: 'options.size',
|
|
684
366
|
brand: { property: 'vendor', values: { nike: 'Nike', adidas: 'Adidas' } },
|
|
685
367
|
},
|
|
686
368
|
})
|
|
687
369
|
|
|
688
|
-
//
|
|
689
|
-
await collection.execute({
|
|
690
|
-
|
|
691
|
-
})
|
|
370
|
+
// Aliases resolve automatically
|
|
371
|
+
await collection.execute({ filters: { color: 'Red', brand: 'nike' } })
|
|
372
|
+
// Products now include description and rating from the product transform
|
|
692
373
|
```
|
|
693
374
|
|
|
694
|
-
##
|
|
695
|
-
|
|
696
|
-
### `createProductCard()` - Reactive Product Card Controller
|
|
375
|
+
## Error Handling
|
|
697
376
|
|
|
698
|
-
|
|
377
|
+
All methods return `{ data, error }` instead of throwing. Errors are discriminated by `_tag`:
|
|
699
378
|
|
|
700
379
|
```typescript
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const card = createProductCard({
|
|
704
|
-
product,
|
|
705
|
-
selectedOptions: [{ name: 'Size', value: '7' }],
|
|
706
|
-
breakoutOptions: [{ name: 'Stone', value: 'Ruby' }],
|
|
707
|
-
})
|
|
708
|
-
|
|
709
|
-
// Subscribe to reactive state
|
|
710
|
-
effect(() => {
|
|
711
|
-
console.log('Selected variant:', card.selectedVariant.value)
|
|
712
|
-
console.log('Available options:', card.options.value)
|
|
713
|
-
})
|
|
380
|
+
const result = await collection.execute()
|
|
714
381
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// Cleanup
|
|
734
|
-
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
|
+
}
|
|
735
399
|
```
|
|
736
400
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
| Parameter | Type | Required | Description |
|
|
740
|
-
| ----------------- | ----------------- | -------- | -------------------------------------------------------- |
|
|
741
|
-
| `product` | `Product` | Yes | Product to display |
|
|
742
|
-
| `selectedOptions` | `ProductOption[]` | No | Initially selected options |
|
|
743
|
-
| `breakoutOptions` | `ProductOption[]` | No | Options to filter variants (auto-set from variant tiles) |
|
|
744
|
-
|
|
745
|
-
**Reactive State (ReadonlySignal):**
|
|
746
|
-
|
|
747
|
-
| Property | Type | Description |
|
|
748
|
-
| ----------------- | ---------------------------------------- | --------------------------------------- |
|
|
749
|
-
| `product` | `Product` | Original product (static) |
|
|
750
|
-
| `variants` | `ReadonlySignal<ProductVariant[]>` | Variants matching breakoutOptions |
|
|
751
|
-
| `selectedVariant` | `ReadonlySignal<ProductVariant \| null>` | Variant matching all selected options |
|
|
752
|
-
| `selectedOptions` | `ReadonlySignal<ProductOption[]>` | Combined breakout + selected options |
|
|
753
|
-
| `options` | `ReadonlySignal<RichProductOption[]>` | Available options from visible variants |
|
|
754
|
-
| `optionNames` | `ReadonlySignal<string[]>` | Option names (excludes breakout) |
|
|
755
|
-
| `images` | `ReadonlySignal<Image[]>` | Variant image or product images |
|
|
756
|
-
| `carouselIndex` | `ReadonlySignal<number>` | Index of selected variant's image |
|
|
757
|
-
|
|
758
|
-
**Actions:**
|
|
759
|
-
|
|
760
|
-
| Method | Description |
|
|
761
|
-
| ----------------------------- | ------------------------------- |
|
|
762
|
-
| `selectOption(name, value)` | Select a single option by name |
|
|
763
|
-
| `setSelectedOptions(options)` | Merge options by name |
|
|
764
|
-
| `setBreakoutOptions(options)` | Replace breakout filter options |
|
|
765
|
-
| `dispose()` | Cleanup controller |
|
|
766
|
-
|
|
767
|
-
**Query Methods:**
|
|
768
|
-
|
|
769
|
-
| Method | Returns | Description |
|
|
770
|
-
| -------------------------------- | ------------------------ | ------------------------------------------------------ |
|
|
771
|
-
| `getOptionValues(name)` | `string[]` | All values for an option from visible variants |
|
|
772
|
-
| `getSwatches(name)` | `Swatch[]` | Swatch definitions for an option |
|
|
773
|
-
| `getVariantByOptions(opts)` | `ProductVariant \| null` | Find variant by options |
|
|
774
|
-
| `isOptionAvailable(name, value)` | `boolean` | Check if selecting option results in available variant |
|
|
775
|
-
| `isVariantAvailable(variant)` | `boolean` | Check if variant is available for sale |
|
|
776
|
-
|
|
777
|
-
## Singleton Access
|
|
778
|
-
|
|
779
|
-
After initialization, access the SDK anywhere:
|
|
401
|
+
Retryable errors (`TIMEOUT`, `CONNECTION_FAILED`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`) expose `retryable` and `retryAfterMs`:
|
|
780
402
|
|
|
781
403
|
```typescript
|
|
782
|
-
import {
|
|
404
|
+
import { isRetryable } from '@commerce-blocks/sdk'
|
|
783
405
|
|
|
784
|
-
if (
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
const sdk = result.data
|
|
788
|
-
// Use sdk
|
|
789
|
-
}
|
|
406
|
+
if (result.error && isRetryable(result.error)) {
|
|
407
|
+
const delay = result.error.retryAfterMs ?? 1000
|
|
408
|
+
setTimeout(() => collection.execute(), delay)
|
|
790
409
|
}
|
|
791
410
|
```
|
|
792
411
|
|
|
793
|
-
##
|
|
412
|
+
## Cache and Storage
|
|
794
413
|
|
|
795
|
-
The
|
|
414
|
+
The client exposes a reactive cache:
|
|
796
415
|
|
|
797
416
|
```typescript
|
|
798
|
-
const {
|
|
799
|
-
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
const querySignal = store.queries.get('cache-key')
|
|
807
|
-
store.queries.invalidate('browse:*') // invalidate by pattern
|
|
808
|
-
|
|
809
|
-
// Collection metadata
|
|
810
|
-
const meta = store.collections.get('shirts')
|
|
811
|
-
|
|
812
|
-
// Page metadata
|
|
813
|
-
const page = store.pages.get('about')
|
|
814
|
-
store.pages.set('about', { title: 'About Us', body: '...' })
|
|
815
|
-
store.pages.delete('about')
|
|
816
|
-
|
|
817
|
-
// Persistence
|
|
818
|
-
store.persist() // save to storage
|
|
819
|
-
store.restore() // restore from storage
|
|
820
|
-
store.clear() // clear all caches
|
|
821
|
-
|
|
822
|
-
// Stats
|
|
823
|
-
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
|
|
824
425
|
```
|
|
825
426
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
Configure how the SDK persists cache data. By default, the SDK uses `localStorage` in browsers.
|
|
829
|
-
|
|
830
|
-
### Built-in Adapters
|
|
427
|
+
### Storage Adapters
|
|
831
428
|
|
|
832
429
|
```typescript
|
|
833
|
-
import { localStorageAdapter,
|
|
430
|
+
import { localStorageAdapter, fileStorage } from '@commerce-blocks/sdk'
|
|
834
431
|
|
|
835
|
-
// Browser
|
|
432
|
+
// Browser (returns null if unavailable)
|
|
836
433
|
const browserAdapter = localStorageAdapter('my-cache-key')
|
|
837
434
|
|
|
838
|
-
// Node.js
|
|
435
|
+
// Node.js
|
|
839
436
|
import fs from 'fs'
|
|
840
|
-
const nodeAdapter =
|
|
437
|
+
const nodeAdapter = fileStorage('./cache.json', fs)
|
|
841
438
|
```
|
|
842
439
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
Implement the `StorageAdapter` interface:
|
|
440
|
+
Custom adapter — implement `StorageAdapter`:
|
|
846
441
|
|
|
847
442
|
```typescript
|
|
848
|
-
|
|
849
|
-
read()
|
|
850
|
-
write(data
|
|
851
|
-
remove()
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Example: sessionStorage adapter
|
|
855
|
-
const sessionAdapter: StorageAdapter = {
|
|
856
|
-
read: () => sessionStorage.getItem('my-key'),
|
|
857
|
-
write: (data) => sessionStorage.setItem('my-key', data),
|
|
858
|
-
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'),
|
|
859
447
|
}
|
|
860
448
|
```
|
|
861
449
|
|
|
862
|
-
##
|
|
450
|
+
## Signals
|
|
863
451
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
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:
|
|
867
453
|
|
|
868
454
|
```typescript
|
|
869
|
-
import {
|
|
870
|
-
|
|
871
|
-
const clientResult = layersClient({
|
|
872
|
-
layersPublicToken: 'your-token',
|
|
873
|
-
sorts: [{ label: 'Featured', code: 'featured' }],
|
|
874
|
-
})
|
|
875
|
-
|
|
876
|
-
if (clientResult.data) {
|
|
877
|
-
const result = await search(clientResult.data, 'red dress', {
|
|
878
|
-
pagination: { page: 1, limit: 20 },
|
|
879
|
-
})
|
|
880
|
-
|
|
881
|
-
if (result.data) {
|
|
882
|
-
console.log('Raw Layers results:', result.data.results)
|
|
883
|
-
}
|
|
884
|
-
}
|
|
455
|
+
import { signal, computed, effect, batch } from '@commerce-blocks/sdk'
|
|
885
456
|
```
|
|
886
457
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
Build cache keys for manual store operations:
|
|
458
|
+
Controller `state` is a `ReadonlySignal<QueryState<T>>`:
|
|
890
459
|
|
|
891
460
|
```typescript
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
blocksKey('block-abc', { anchorHandle: 'gold-necklace', page: 1 })
|
|
898
|
-
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
|
+
}
|
|
899
466
|
```
|
|
900
467
|
|
|
901
|
-
|
|
468
|
+
All controllers return the same `QueryResult` shape in `data`:
|
|
902
469
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
+
}
|
|
910
480
|
```
|
|
911
481
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
- `setupSdk(page)` — navigates to the test harness and waits for `window.SDK_READY`
|
|
915
|
-
- `inBrowser(page, fn)` — runs a function inside the browser via `page.evaluate()`
|
|
482
|
+
Blocks results add a `block` field with `{ id, title, anchor_type, strategy_type, strategy_key }`.
|
|
916
483
|
|
|
917
|
-
|
|
484
|
+
## Singleton Access
|
|
918
485
|
|
|
919
|
-
|
|
486
|
+
After initialization, access the client anywhere:
|
|
920
487
|
|
|
921
488
|
```typescript
|
|
922
|
-
import {
|
|
923
|
-
import { setupSdk, inBrowser } from './helpers'
|
|
924
|
-
|
|
925
|
-
test.describe('SDK: feature()', () => {
|
|
926
|
-
test.beforeEach(async ({ page }) => {
|
|
927
|
-
await setupSdk(page)
|
|
928
|
-
})
|
|
929
|
-
|
|
930
|
-
test('does something', async ({ page }) => {
|
|
931
|
-
const result = await inBrowser(page, async () => {
|
|
932
|
-
const { createSdk, devConfig } = (window as any).SDK
|
|
933
|
-
const { data: sdk } = createSdk(devConfig)
|
|
934
|
-
if (!sdk) return { error: 'init failed' }
|
|
935
|
-
|
|
936
|
-
const controller = sdk.collection({ handle: 'shirts' })
|
|
937
|
-
const { data, error } = await controller.execute({ limit: 3 })
|
|
938
|
-
controller.dispose()
|
|
939
|
-
|
|
940
|
-
if (error) return { error: error._tag }
|
|
941
|
-
return { count: data.products.length }
|
|
942
|
-
})
|
|
489
|
+
import { getClient, isInitialized } from '@commerce-blocks/sdk'
|
|
943
490
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
491
|
+
if (isInitialized()) {
|
|
492
|
+
const { data: client } = getClient()
|
|
493
|
+
// Use client
|
|
494
|
+
}
|
|
948
495
|
```
|
|
949
496
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
- All SDK calls happen inside `inBrowser()` — you can't access SDK objects directly in Node
|
|
953
|
-
- Return plain objects from `inBrowser()`, not class instances or signals
|
|
954
|
-
- Always `dispose()` controllers to prevent leaks between tests
|
|
955
|
-
- Use `devConfig` from the test harness for real API credentials
|
|
497
|
+
## Response Types
|
|
956
498
|
|
|
957
|
-
|
|
499
|
+
The SDK exports all types from `@commerce-blocks/sdk`:
|
|
958
500
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
```
|