@spree/sdk 0.6.6 → 0.6.8

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 CHANGED
@@ -27,7 +27,7 @@ const client = createSpreeClient({
27
27
  // Browse products (Store API)
28
28
  const products = await client.store.products.list({
29
29
  limit: 10,
30
- expand: 'variants,images',
30
+ expand: ['variants', 'images'],
31
31
  });
32
32
 
33
33
  // Get a single product
@@ -150,12 +150,12 @@ const products = await client.store.products.list({
150
150
  limit: 25,
151
151
  name_cont: 'shirt',
152
152
  sort: 'price asc',
153
- expand: 'variants,images,taxons',
153
+ expand: ['variants', 'images', 'taxons'],
154
154
  });
155
155
 
156
156
  // Get single product by ID or slug
157
157
  const product = await client.store.products.get('spree-tote', {
158
- expand: 'variants,images',
158
+ expand: ['variants', 'images'],
159
159
  });
160
160
 
161
161
  // Get available filters (price range, availability, options, taxons)
@@ -169,12 +169,12 @@ const filters = await client.store.products.filters({
169
169
  ```typescript
170
170
  // List taxonomies
171
171
  const taxonomies = await client.store.taxonomies.list({
172
- expand: 'taxons',
172
+ expand: ['taxons'],
173
173
  });
174
174
 
175
175
  // Get taxonomy with taxons
176
176
  const categories = await client.store.taxonomies.get('tax_123', {
177
- expand: 'root,taxons',
177
+ expand: ['root', 'taxons'],
178
178
  });
179
179
 
180
180
  // List taxons with filtering
@@ -185,14 +185,14 @@ const taxons = await client.store.taxons.list({
185
185
 
186
186
  // Get single taxon by ID or permalink
187
187
  const taxon = await client.store.taxons.get('categories/clothing', {
188
- expand: 'ancestors,children', // For breadcrumbs and subcategories
188
+ expand: ['ancestors', 'children'], // For breadcrumbs and subcategories
189
189
  });
190
190
 
191
191
  // List products in a category
192
192
  const categoryProducts = await client.store.taxons.products.list('categories/clothing', {
193
193
  page: 1,
194
194
  limit: 12,
195
- expand: 'images,default_variant',
195
+ expand: ['images', 'default_variant'],
196
196
  });
197
197
  ```
198
198
 
@@ -225,7 +225,7 @@ const options = { orderToken: cart.order_token };
225
225
 
226
226
  // Get order by ID or number
227
227
  const order = await client.store.orders.get('R123456789', {
228
- expand: 'line_items,shipments',
228
+ expand: ['line_items', 'shipments'],
229
229
  }, options);
230
230
 
231
231
  // Update order (email, addresses)
@@ -364,14 +364,37 @@ const completed = await client.store.orders.paymentSessions.complete(
364
364
  console.log(completed.status); // 'completed'
365
365
  ```
366
366
 
367
+ ### Markets
368
+
369
+ ```typescript
370
+ // List all markets
371
+ const { data: markets } = await client.store.markets.list();
372
+ // [{ id: "mkt_xxx", name: "North America", currency: "USD", default_locale: "en", ... }]
373
+
374
+ // Get a single market
375
+ const market = await client.store.markets.get('mkt_xxx');
376
+
377
+ // Resolve which market applies for a country
378
+ const market = await client.store.markets.resolve('DE');
379
+ // => { id: "mkt_xxx", name: "Europe", currency: "EUR", default_locale: "de", ... }
380
+
381
+ // List countries in a market
382
+ const { data: countries } = await client.store.markets.countries.list('mkt_xxx');
383
+
384
+ // Get a country in a market (with states for address forms)
385
+ const country = await client.store.markets.countries.get('mkt_xxx', 'DE', {
386
+ expand: ['states'],
387
+ });
388
+ ```
389
+
367
390
  ### Geography
368
391
 
369
392
  ```typescript
370
393
  // List countries available for checkout
371
394
  const { data: countries } = await client.store.countries.list();
372
395
 
373
- // Get country by ISO code (includes states)
374
- const usa = await client.store.countries.get('US');
396
+ // Get country by ISO code (with states)
397
+ const usa = await client.store.countries.get('US', { expand: ['states'] });
375
398
  console.log(usa.states); // Array of states
376
399
  ```
377
400
 
@@ -448,7 +471,7 @@ const { data: wishlists } = await client.store.wishlists.list({}, options);
448
471
 
449
472
  // Get wishlist by ID
450
473
  const wishlist = await client.store.wishlists.get('wl_xxx', {
451
- expand: 'wished_items',
474
+ expand: ['wished_items'],
452
475
  }, options);
453
476
 
454
477
  // Create wishlist
@@ -501,6 +524,7 @@ The SDK uses a resource builder pattern for nested resources:
501
524
  | `store.customer` | `addresses` | `list`, `get`, `create`, `update`, `delete`, `markAsDefault` |
502
525
  | `store.customer` | `creditCards` | `list`, `get`, `delete` |
503
526
  | `store.customer` | `giftCards` | `list`, `get` |
527
+ | `store.markets` | `countries` | `list`, `get` |
504
528
  | `store.taxons` | `products` | `list` |
505
529
  | `store.wishlists` | `items` | `create`, `update`, `delete` |
506
530
 
@@ -511,6 +535,7 @@ await client.store.orders.lineItems.create(orderId, params, options);
511
535
  await client.store.orders.payments.list(orderId, options);
512
536
  await client.store.orders.shipments.update(orderId, shipmentId, params, options);
513
537
  await client.store.customer.addresses.list({}, options);
538
+ await client.store.markets.countries.list(marketId);
514
539
  await client.store.taxons.products.list(taxonId, params, options);
515
540
  await client.store.wishlists.items.create(wishlistId, params, options);
516
541
  ```
@@ -608,6 +633,7 @@ The SDK exports all Store API types:
608
633
  - `StoreState` - State/province
609
634
  - `StoreAddress` - Customer address
610
635
  - `StoreCustomer` - Customer profile
636
+ - `StoreMarket` - Market configuration (currency, locales, countries)
611
637
  - `StoreStore` - Store configuration
612
638
 
613
639
  ### Commerce Types
package/dist/index.cjs CHANGED
@@ -21,20 +21,23 @@ function calculateDelay(attempt, config) {
21
21
  function sleep(ms) {
22
22
  return new Promise((resolve) => setTimeout(resolve, ms));
23
23
  }
24
- function shouldRetryOnStatus(method, status, config) {
25
- const isIdempotent = method === "GET" || method === "HEAD";
24
+ function generateIdempotencyKey() {
25
+ return `spree-sdk-retry-${crypto.randomUUID()}`;
26
+ }
27
+ function shouldRetryOnStatus(method, status, config, hasIdempotencyKey) {
28
+ const isIdempotent = method === "GET" || method === "HEAD" || hasIdempotencyKey;
26
29
  if (isIdempotent) {
27
30
  return config.retryOnStatus.includes(status);
28
31
  }
29
32
  return status === 429;
30
33
  }
31
- function shouldRetryOnNetworkError(method, config) {
34
+ function shouldRetryOnNetworkError(method, config, hasIdempotencyKey) {
32
35
  if (!config.retryOnNetworkError) return false;
33
- return method === "GET" || method === "HEAD";
36
+ return method === "GET" || method === "HEAD" || hasIdempotencyKey;
34
37
  }
35
38
  function createRequestFn(config, basePath, auth, defaults) {
36
39
  return async function request(method, path, options = {}) {
37
- const { token, orderToken, headers = {}, body, params } = options;
40
+ const { token, orderToken, idempotencyKey, headers = {}, body, params } = options;
38
41
  const locale = options.locale ?? defaults?.locale;
39
42
  const currency = options.currency ?? defaults?.currency;
40
43
  const country = options.country ?? defaults?.country;
@@ -50,14 +53,13 @@ function createRequestFn(config, basePath, auth, defaults) {
50
53
  }
51
54
  });
52
55
  }
53
- if (orderToken) {
54
- url.searchParams.set("order_token", orderToken);
55
- }
56
56
  const requestHeaders = {
57
57
  "Content-Type": "application/json",
58
- [auth.headerName]: auth.headerValue,
59
58
  ...headers
60
59
  };
60
+ if (auth.headerName && auth.headerValue) {
61
+ requestHeaders[auth.headerName] = auth.headerValue;
62
+ }
61
63
  if (token) {
62
64
  requestHeaders["Authorization"] = `Bearer ${token}`;
63
65
  }
@@ -73,6 +75,12 @@ function createRequestFn(config, basePath, auth, defaults) {
73
75
  if (country) {
74
76
  requestHeaders["x-spree-country"] = country;
75
77
  }
78
+ const isMutating = method !== "GET" && method !== "HEAD";
79
+ const effectiveIdempotencyKey = idempotencyKey ?? (isMutating && config.retryConfig ? generateIdempotencyKey() : void 0);
80
+ if (effectiveIdempotencyKey) {
81
+ requestHeaders["Idempotency-Key"] = effectiveIdempotencyKey;
82
+ }
83
+ const hasIdempotencyKey = !!effectiveIdempotencyKey;
76
84
  const maxAttempts = config.retryConfig ? config.retryConfig.maxRetries + 1 : 1;
77
85
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
78
86
  try {
@@ -83,7 +91,7 @@ function createRequestFn(config, basePath, auth, defaults) {
83
91
  });
84
92
  if (!response.ok) {
85
93
  const isLastAttempt = attempt >= maxAttempts - 1;
86
- if (!isLastAttempt && config.retryConfig && shouldRetryOnStatus(method, response.status, config.retryConfig)) {
94
+ if (!isLastAttempt && config.retryConfig && shouldRetryOnStatus(method, response.status, config.retryConfig, hasIdempotencyKey)) {
87
95
  const retryAfter = response.headers.get("Retry-After");
88
96
  const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, config.retryConfig.maxDelay) : calculateDelay(attempt, config.retryConfig);
89
97
  await sleep(delay);
@@ -101,7 +109,7 @@ function createRequestFn(config, basePath, auth, defaults) {
101
109
  throw error;
102
110
  }
103
111
  const isLastAttempt = attempt >= maxAttempts - 1;
104
- if (!isLastAttempt && config.retryConfig && shouldRetryOnNetworkError(method, config.retryConfig)) {
112
+ if (!isLastAttempt && config.retryConfig && shouldRetryOnNetworkError(method, config.retryConfig, hasIdempotencyKey)) {
105
113
  const delay = calculateDelay(attempt, config.retryConfig);
106
114
  await sleep(delay);
107
115
  continue;
@@ -138,6 +146,10 @@ function transformListParams(params) {
138
146
  }
139
147
 
140
148
  // src/store-client.ts
149
+ function expandParams(params) {
150
+ if (!params?.expand?.length) return void 0;
151
+ return { expand: params.expand.join(",") };
152
+ }
141
153
  var StoreClient = class {
142
154
  request;
143
155
  constructor(request) {
@@ -161,15 +173,6 @@ var StoreClient = class {
161
173
  refresh: (options) => this.request("POST", "/auth/refresh", options)
162
174
  };
163
175
  // ============================================
164
- // Store
165
- // ============================================
166
- store = {
167
- /**
168
- * Get current store information
169
- */
170
- get: (options) => this.request("GET", "/store", options)
171
- };
172
- // ============================================
173
176
  // Products
174
177
  // ============================================
175
178
  products = {
@@ -178,14 +181,14 @@ var StoreClient = class {
178
181
  */
179
182
  list: (params, options) => this.request("GET", "/products", {
180
183
  ...options,
181
- params: params ? transformListParams(params) : void 0
184
+ params: transformListParams({ ...params })
182
185
  }),
183
186
  /**
184
187
  * Get a product by ID or slug
185
188
  */
186
189
  get: (idOrSlug, params, options) => this.request("GET", `/products/${idOrSlug}`, {
187
190
  ...options,
188
- params
191
+ params: expandParams(params)
189
192
  }),
190
193
  /**
191
194
  * Get available filters for products
@@ -205,14 +208,14 @@ var StoreClient = class {
205
208
  */
206
209
  list: (params, options) => this.request("GET", "/taxonomies", {
207
210
  ...options,
208
- params
211
+ params: transformListParams({ ...params })
209
212
  }),
210
213
  /**
211
214
  * Get a taxonomy by ID
212
215
  */
213
216
  get: (id, params, options) => this.request("GET", `/taxonomies/${id}`, {
214
217
  ...options,
215
- params
218
+ params: expandParams(params)
216
219
  })
217
220
  };
218
221
  taxons = {
@@ -221,14 +224,14 @@ var StoreClient = class {
221
224
  */
222
225
  list: (params, options) => this.request("GET", "/taxons", {
223
226
  ...options,
224
- params: params ? transformListParams(params) : void 0
227
+ params: transformListParams({ ...params })
225
228
  }),
226
229
  /**
227
230
  * Get a taxon by ID or permalink
228
231
  */
229
232
  get: (idOrPermalink, params, options) => this.request("GET", `/taxons/${idOrPermalink}`, {
230
233
  ...options,
231
- params
234
+ params: expandParams(params)
232
235
  }),
233
236
  /**
234
237
  * Nested resource: Products in a taxon
@@ -243,7 +246,7 @@ var StoreClient = class {
243
246
  `/taxons/${taxonId}/products`,
244
247
  {
245
248
  ...options,
246
- params: params ? transformListParams(params) : void 0
249
+ params: transformListParams({ ...params })
247
250
  }
248
251
  )
249
252
  }
@@ -264,7 +267,7 @@ var StoreClient = class {
264
267
  */
265
268
  get: (iso, params, options) => this.request("GET", `/countries/${iso}`, {
266
269
  ...options,
267
- params
270
+ params: expandParams(params)
268
271
  })
269
272
  };
270
273
  currencies = {
@@ -321,7 +324,7 @@ var StoreClient = class {
321
324
  get: (marketId, iso, params, options) => this.request(
322
325
  "GET",
323
326
  `/markets/${marketId}/countries/${iso}`,
324
- { ...options, params }
327
+ { ...options, params: expandParams(params) }
325
328
  )
326
329
  }
327
330
  };
@@ -358,7 +361,7 @@ var StoreClient = class {
358
361
  */
359
362
  get: (idOrNumber, params, options) => this.request("GET", `/orders/${idOrNumber}`, {
360
363
  ...options,
361
- params
364
+ params: expandParams(params)
362
365
  }),
363
366
  /**
364
367
  * Update an order
@@ -576,7 +579,7 @@ var StoreClient = class {
576
579
  list: (params, options) => this.request(
577
580
  "GET",
578
581
  "/customer/addresses",
579
- { ...options, params }
582
+ { ...options, params: transformListParams({ ...params }) }
580
583
  ),
581
584
  /**
582
585
  * Get an address by ID
@@ -618,7 +621,7 @@ var StoreClient = class {
618
621
  list: (params, options) => this.request(
619
622
  "GET",
620
623
  "/customer/credit_cards",
621
- { ...options, params }
624
+ { ...options, params: transformListParams({ ...params }) }
622
625
  ),
623
626
  /**
624
627
  * Get a credit card by ID
@@ -640,7 +643,7 @@ var StoreClient = class {
640
643
  list: (params, options) => this.request(
641
644
  "GET",
642
645
  "/customer/gift_cards",
643
- { ...options, params }
646
+ { ...options, params: transformListParams({ ...params }) }
644
647
  ),
645
648
  /**
646
649
  * Get a gift card by ID
@@ -656,7 +659,7 @@ var StoreClient = class {
656
659
  */
657
660
  list: (params, options) => this.request("GET", "/customer/orders", {
658
661
  ...options,
659
- params: params ? transformListParams(params) : void 0
662
+ params: transformListParams({ ...params })
660
663
  })
661
664
  },
662
665
  /**
@@ -696,14 +699,14 @@ var StoreClient = class {
696
699
  */
697
700
  list: (params, options) => this.request("GET", "/wishlists", {
698
701
  ...options,
699
- params
702
+ params: transformListParams({ ...params })
700
703
  }),
701
704
  /**
702
705
  * Get a wishlist by ID
703
706
  */
704
707
  get: (id, params, options) => this.request("GET", `/wishlists/${id}`, {
705
708
  ...options,
706
- params
709
+ params: expandParams(params)
707
710
  }),
708
711
  /**
709
712
  * Create a wishlist
@@ -772,6 +775,9 @@ var SpreeClient = class {
772
775
  admin;
773
776
  _defaults;
774
777
  constructor(config) {
778
+ if (!config.publishableKey && !config.secretKey) {
779
+ throw new Error("SpreeClient requires at least one of publishableKey or secretKey");
780
+ }
775
781
  const baseUrl = config.baseUrl.replace(/\/$/, "");
776
782
  const fetchFn = config.fetch || fetch.bind(globalThis);
777
783
  this._defaults = {
@@ -795,7 +801,7 @@ var SpreeClient = class {
795
801
  const storeRequestFn = createRequestFn(
796
802
  requestConfig,
797
803
  "/api/v3/store",
798
- { headerName: "x-spree-api-key", headerValue: config.publishableKey },
804
+ config.publishableKey ? { headerName: "x-spree-api-key", headerValue: config.publishableKey } : { headerName: "", headerValue: "" },
799
805
  this._defaults
800
806
  );
801
807
  const adminRequestFn = createRequestFn(