@sdg.la/wishlist-stack-sdk 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,8 +34,9 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
34
34
  - **TypeScript-first** — Full type definitions with exported types for all requests and responses
35
35
  - **Universal** — Works in Node.js, browsers, and edge runtimes (Cloudflare Workers, Deno, etc.)
36
36
  - **Flexible API** — Choose between functional `createWishlistStackClient()` or class-based `new WishlistStackClient()`
37
- - **Built-in error handling** — Structured `WishlistStackApiError` with status codes and API error messages
38
- - **Pagination support** — All list endpoints support `page` and `pageSize` parameters
37
+ - **Built-in error handling** — Structured `WishlistStackApiError` with status codes, `Retry-After` / rate-limit headers, and API error messages
38
+ - **Pagination support** — List endpoints support `page` and `pageSize` (max **25**); helpers fetch all pages when needed
39
+ - **Batch helpers** — `lists.addItemsBatched` chunks adds to the max of 25 items per request
39
40
  - **Sort support** — Collection endpoints accept `sortBy` (`position` | `createdAt` | `updatedAt`) and `sortDirection` (`asc` | `desc`)
40
41
 
41
42
  ## Requirements
@@ -65,6 +66,9 @@ import { createWishlistStackClient } from '@sdg.la/wishlist-stack-sdk';
65
66
  const client = createWishlistStackClient({
66
67
  apiKey: 'your-merchant-api-key',
67
68
  customerAccessToken: 'customer-access-token', // required for authenticated endpoints
69
+ // optional:
70
+ // defaultTimeoutMs: 10_000,
71
+ // retryOnRateLimit: true, // single retry with jitter on HTTP 429
68
72
  });
69
73
 
70
74
  // Fetch all groups
@@ -74,6 +78,30 @@ const { groups } = await client.groups.getAll();
74
78
  const { lists } = await client.lists.getAll();
75
79
  ```
76
80
 
81
+ ### Pagination, batching, and add responses
82
+
83
+ The API paginates list detail (max `pageSize` **25**) and returns an add **delta** (`{ listId, addedItems, addedCount }`).
84
+
85
+ | Concern | Guidance |
86
+ |---------|----------|
87
+ | List detail | Prefer `page` / `pageSize` ≤ 25, or `lists.getByIdAllItems` for every item |
88
+ | Adding many items | Use `lists.addItemsBatched` (chunks of ≤ 25) |
89
+ | Add response | `{ listId, addedItems, addedCount }` |
90
+ | Create + items | Do not send `variantIds` on create; create the list, then `addItems` |
91
+ | `quantity` on add | Supported **1–999** |
92
+ | `includeLists` | Cap of **10 lists × 25 items**; use `getById` helpers for full data |
93
+ | Groups | May return **503** until enabled for the merchant |
94
+
95
+ ```ts
96
+ import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
97
+
98
+ const res = await client.lists.addItemsBatched(id, { items: many });
99
+ // res.addedItems / res.addedCount — then refresh if needed:
100
+ const full = await client.lists.getByIdAllItems(id, {
101
+ pageSize: clampPageSize(25),
102
+ });
103
+ ```
104
+
77
105
  ## Usage
78
106
 
79
107
  ### Server / Node.js / Workers
@@ -175,26 +203,38 @@ try {
175
203
  console.log(error.apiErrors); // Array of { message, field? }
176
204
  console.log(error.apiErrorMessages); // Array of error message strings
177
205
  console.log(error.requestId); // Request ID for debugging
206
+ console.log(error.retryAfter); // Retry-After header (429)
207
+ console.log(error.rateLimit); // X-RateLimit-* headers when present
178
208
  }
179
209
  }
180
210
  ```
181
211
 
212
+ Groups endpoints may return **503** with message `Groups API disabled for this merchant` until the Groups API is enabled.
213
+
182
214
  ## Pagination
183
215
 
184
- Endpoints that return lists support pagination via query parameters:
216
+ Endpoints that return lists support pagination via query parameters. Keep `pageSize` in **1–25** (use `clampPageSize()`). Prefer `lists.getByIdAllItems()` when you need every item.
185
217
 
186
218
  ```ts
219
+ import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
220
+
187
221
  // Paginate groups
188
222
  await client.groups.getAll({ page: 2, pageSize: 10 });
189
223
 
190
- // Include lists + hydrated items on groups.getAll
224
+ // includeLists is capped at 10 lists × 25 items
191
225
  await client.groups.getAll({ includeLists: true });
192
226
 
193
227
  // Paginate lists within a group
194
228
  await client.groups.getById('group-id', { page: 1, pageSize: 25 });
195
229
 
196
230
  // Paginate items within a list
197
- await client.lists.getById('list-id', { page: 1, pageSize: 25 });
231
+ await client.lists.getById('list-id', {
232
+ page: 1,
233
+ pageSize: clampPageSize(25),
234
+ });
235
+
236
+ // Or fetch every page:
237
+ await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
198
238
  ```
199
239
 
200
240
  **Pagination response structure:**
@@ -242,7 +282,7 @@ Fetch all groups for the authenticated customer.
242
282
  - **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; includeLists?: boolean | 1 | "1"; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
243
283
  - **Returns:** `Promise<GetGroupsResponse>`
244
284
 
245
- Pass `includeLists: true` (sent as `includeLists=1`) to embed every list under each group with fully hydrated items — useful for loading projects/boards in one call instead of per-group `getById` fan-out. Without it, `lists` is an empty array and only `featuredItems` are populated.
285
+ Pass `includeLists: true` (sent as `includeLists=1`) to embed lists under each group with hydrated items. Expect at most **10 lists × 25 items** per group — use `groups.getById` + `lists.getById` / `lists.getByIdAllItems` for full data. Without `includeLists`, `lists` is an empty array and only `featuredItems` are populated. Groups may return **503** until enabled for the merchant.
246
286
 
247
287
  ```ts
248
288
  const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday', sortBy: 'updatedAt', sortDirection: 'desc' });
@@ -544,18 +584,17 @@ const copy = await client.groups.duplicate('group-id');
544
584
 
545
585
  ---
546
586
 
547
- #### `groups.reorder(groupId, body)`
587
+ #### `groups.reorder(body)`
548
588
 
549
- Reorder lists within a group.
589
+ Reorder the customer's groups.
550
590
 
551
- - **Endpoint:** `POST /api/groups/{groupId}/reorder`
591
+ - **Endpoint:** `POST /api/groups/reorder`
552
592
  - **Parameters:**
553
- - `groupId` — `string`
554
- - `body` — `{ listIds?: string[] }`
555
- - **Returns:** `Promise<ReorderGroupResponse>`
593
+ - `body` — `{ groupIds: string[] }` (group CUIDs in the desired order)
594
+ - **Returns:** `Promise<ReorderGroupsResponse>`
556
595
 
557
596
  ```ts
558
- await client.groups.reorder('group-id', { listIds: ['list-3', 'list-1', 'list-2'] });
597
+ await client.groups.reorder({ groupIds: ['group-3', 'group-1', 'group-2'] });
559
598
  ```
560
599
 
561
600
  ---
@@ -683,16 +722,17 @@ const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10,
683
722
 
684
723
  #### `lists.getById(listId, query?)`
685
724
 
686
- Fetch a single list by ID. Items are hydrated with Shopify product data and paginated.
725
+ Fetch a single list by ID. Items are hydrated with Shopify product data and paginated. The response includes `pagination` (max `pageSize` **25**).
687
726
 
688
727
  - **Endpoint:** `GET /api/lists/{listId}`
689
728
  - **Parameters:**
690
729
  - `listId` — `string`
691
730
  - `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
692
- - **Returns:** `Promise<GetListResponse>`
731
+ - **Returns:** `Promise<GetListResponse>` (`ListDetail & { pagination: Pagination }`)
693
732
 
694
733
  ```ts
695
734
  const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sortBy: 'updatedAt', sortDirection: 'desc' });
735
+ // list.pagination.totalPages
696
736
  ```
697
737
 
698
738
  <details>
@@ -757,9 +797,19 @@ const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sort
757
797
 
758
798
  ---
759
799
 
800
+ #### `lists.getByIdAllItems(listId, opts?)`
801
+
802
+ Walk every page of `lists.getById` and return one list with all items concatenated. Use when you need the full wishlist.
803
+
804
+ ```ts
805
+ const full = await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
806
+ ```
807
+
808
+ ---
809
+
760
810
  #### `lists.create(body)`
761
811
 
762
- Create a new list, optionally assigned to a group.
812
+ Create a new list, optionally assigned to a group. Do **not** send `variantIds` on create (the API rejects them). Create the list, then call `addItems` / `addItemsBatched`.
763
813
 
764
814
  - **Endpoint:** `POST /api/lists`
765
815
  - **Parameters:** `body` — `{ name?: string; description?: string; groupId?: string }`
@@ -767,6 +817,9 @@ Create a new list, optionally assigned to a group.
767
817
 
768
818
  ```ts
769
819
  const list = await client.lists.create({ name: 'Gift Ideas', groupId: 'group-id' });
820
+ await client.lists.addItems(list.id, {
821
+ items: [{ variantId: 'gid://shopify/ProductVariant/123', quantity: 2 }],
822
+ });
770
823
  ```
771
824
 
772
825
  <details>
@@ -889,7 +942,7 @@ const ungrouped = await client.lists.duplicate('list-id', { groupId: null });
889
942
 
890
943
  #### `lists.addItems(listId, body)`
891
944
 
892
- Add one or more items to a list.
945
+ Add one or more items to a list. Prefer `lists.addItemsBatched` when sending more than **25** items.
893
946
 
894
947
  - **Endpoint:** `POST /api/lists/{listId}/add`
895
948
  - **Parameters:**
@@ -900,6 +953,7 @@ Add one or more items to a list.
900
953
  type AddItemsToListBody = {
901
954
  items?: Array<{
902
955
  variantId?: string;
956
+ /** Quantity 1–999. */
903
957
  quantity?: number;
904
958
  note?: string;
905
959
  properties?: Record<string, unknown> | null;
@@ -907,15 +961,25 @@ type AddItemsToListBody = {
907
961
  };
908
962
  ```
909
963
 
910
- - **Returns:** `Promise<AddItemsToListResponse>` — the updated list with hydrated items (same shape as `getById`)
964
+ - **Returns:** `Promise<AddItemsToListResponse>` — `{ listId, addedItems, addedCount }`
911
965
 
912
966
  ```ts
913
- await client.lists.addItems('list-id', {
967
+ const res = await client.lists.addItems('list-id', {
914
968
  items: [
915
969
  { variantId: 'gid://shopify/ProductVariant/123', quantity: 1 },
916
970
  { variantId: 'gid://shopify/ProductVariant/456', note: 'Size M' },
917
971
  ],
918
972
  });
973
+
974
+ console.log(res.addedCount, res.addedItems);
975
+ ```
976
+
977
+ #### `lists.addItemsBatched(listId, body, opts?)`
978
+
979
+ Chunks `items` into sequential POSTs of ≤25 (configurable via `opts.batchSize`) and merges `{ addedItems, addedCount }`.
980
+
981
+ ```ts
982
+ const res = await client.lists.addItemsBatched('list-id', { items: many });
919
983
  ```
920
984
 
921
985
  ---
@@ -1163,6 +1227,11 @@ import type {
1163
1227
  // Errors
1164
1228
  WishlistStackApiError,
1165
1229
  WishlistStackApiErrorDetails,
1230
+ isWishlistStackApiError,
1231
+
1232
+ // Helpers
1233
+ clampPageSize,
1234
+ isAddItemsDeltaResponse,
1166
1235
 
1167
1236
  // Groups
1168
1237
  GetGroupsResponse,
@@ -1174,7 +1243,9 @@ import type {
1174
1243
  GroupMutationResponse,
1175
1244
  CreateGroupBody,
1176
1245
  UpdateGroupBody,
1246
+ ReorderGroupsBody,
1177
1247
  ReorderGroupBody,
1248
+ ReorderGroupsResponse,
1178
1249
  DuplicateGroupResponse,
1179
1250
 
1180
1251
  // Lists
@@ -1186,6 +1257,8 @@ import type {
1186
1257
  CreateListBody,
1187
1258
  UpdateListBody,
1188
1259
  AddItemsToListBody,
1260
+ AddItemsToListResponse,
1261
+ AddItemsToListDeltaResponse,
1189
1262
  UpdateListItemBody,
1190
1263
  ReorderListItemsBody,
1191
1264
  ReorderListItemsResponse,
package/dist/index.cjs CHANGED
@@ -57,11 +57,15 @@ var WishlistStackApiError = class extends Error {
57
57
  __publicField(this, "method");
58
58
  __publicField(this, "requestId");
59
59
  __publicField(this, "body");
60
+ __publicField(this, "retryAfter");
61
+ __publicField(this, "rateLimit");
60
62
  this.status = details.status;
61
63
  this.url = details.url;
62
64
  this.method = details.method;
63
65
  this.requestId = details.requestId;
64
66
  this.body = details.body;
67
+ this.retryAfter = details.retryAfter;
68
+ this.rateLimit = details.rateLimit;
65
69
  }
66
70
  /**
67
71
  * Best-effort extraction of API error items from the response body.
@@ -93,6 +97,42 @@ async function safeParseJson(res) {
93
97
  return void 0;
94
98
  }
95
99
  }
100
+ function pickRequestId(res) {
101
+ return res.headers.get("fly-request-id") ?? res.headers.get("x-request-id") ?? res.headers.get("x-nf-request-id") ?? res.headers.get("x-netlify-request-id") ?? void 0;
102
+ }
103
+ function pickRateLimit(res) {
104
+ const limit = res.headers.get("x-ratelimit-limit") ?? void 0;
105
+ const remaining = res.headers.get("x-ratelimit-remaining") ?? void 0;
106
+ const reset = res.headers.get("x-ratelimit-reset") ?? void 0;
107
+ if (!limit && !remaining && !reset) return void 0;
108
+ return { limit, remaining, reset };
109
+ }
110
+ function errorMessageFor(status, path) {
111
+ if (status === 503 && path.includes("/api/groups")) {
112
+ return "Groups API disabled for this merchant";
113
+ }
114
+ return `Wishlist Stack API request failed (${status})`;
115
+ }
116
+ function parseRetryAfterMs(retryAfter) {
117
+ if (!retryAfter) return 0;
118
+ const asSeconds = Number(retryAfter);
119
+ if (Number.isFinite(asSeconds) && asSeconds >= 0) {
120
+ return Math.min(asSeconds * 1e3, 3e4);
121
+ }
122
+ const asDate = Date.parse(retryAfter);
123
+ if (!Number.isNaN(asDate)) {
124
+ return Math.min(Math.max(0, asDate - Date.now()), 3e4);
125
+ }
126
+ return 0;
127
+ }
128
+ function jitterDelayMs(baseMs) {
129
+ const base = Math.max(100, baseMs || 500);
130
+ const jitter = Math.floor(Math.random() * 250);
131
+ return Math.min(base + jitter, 3e4);
132
+ }
133
+ function sleep(ms) {
134
+ return new Promise((resolve) => setTimeout(resolve, ms));
135
+ }
96
136
  function createRequest(ctx) {
97
137
  return async function request(args) {
98
138
  const url = `${ctx.baseUrl}${args.path}${toQueryString(args.query)}`;
@@ -109,25 +149,39 @@ function createRequest(ctx) {
109
149
  headers["content-type"] ?? (headers["content-type"] = "application/json");
110
150
  body = JSON.stringify(args.body);
111
151
  }
112
- const controller = args.timeoutMs ? new AbortController() : void 0;
113
- const timeout = args.timeoutMs && controller ? setTimeout(() => controller.abort(), args.timeoutMs) : void 0;
114
- const signal = args.signal ? args.signal : controller ? controller.signal : void 0;
115
- const res = await ctx.fetch(url, {
116
- method: args.method,
117
- headers,
118
- body,
119
- signal
120
- });
121
- if (timeout) clearTimeout(timeout);
152
+ const timeoutMs = args.timeoutMs ?? ctx.defaultTimeoutMs;
153
+ const doFetch = async () => {
154
+ const controller = timeoutMs ? new AbortController() : void 0;
155
+ const timeout = timeoutMs && controller ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
156
+ const signal = args.signal ? args.signal : controller ? controller.signal : void 0;
157
+ try {
158
+ return await ctx.fetch(url, {
159
+ method: args.method,
160
+ headers,
161
+ body,
162
+ signal
163
+ });
164
+ } finally {
165
+ if (timeout) clearTimeout(timeout);
166
+ }
167
+ };
168
+ let res = await doFetch();
169
+ if (!res.ok && res.status === 429 && ctx.retryOnRateLimit) {
170
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
171
+ await sleep(jitterDelayMs(parseRetryAfterMs(retryAfter)));
172
+ res = await doFetch();
173
+ }
122
174
  if (!res.ok) {
123
175
  const errBody = await safeParseJson(res);
124
- const requestId = res.headers.get("fly-request-id") ?? res.headers.get("x-request-id") ?? void 0;
125
- throw new WishlistStackApiError(`Wishlist Stack API request failed (${res.status})`, {
176
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
177
+ throw new WishlistStackApiError(errorMessageFor(res.status, args.path), {
126
178
  status: res.status,
127
179
  url,
128
180
  method: args.method,
129
- requestId,
130
- body: errBody
181
+ requestId: pickRequestId(res),
182
+ body: errBody,
183
+ retryAfter,
184
+ rateLimit: pickRateLimit(res)
131
185
  });
132
186
  }
133
187
  if (res.status === 204) return void 0;
@@ -140,8 +194,38 @@ function createRequest(ctx) {
140
194
  };
141
195
  }
142
196
 
197
+ // src/helpers/pagination.ts
198
+ function clampPageSize(n) {
199
+ if (!Number.isFinite(n)) return MAX_PAGINATION_SIZE;
200
+ return Math.min(MAX_PAGINATION_SIZE, Math.max(1, Math.trunc(n)));
201
+ }
202
+ function clampPage(n) {
203
+ if (!Number.isFinite(n)) return 1;
204
+ return Math.max(1, Math.trunc(n));
205
+ }
206
+
143
207
  // src/resources/lists/index.ts
208
+ function chunkItems(items, batchSize) {
209
+ const size = Math.max(1, Math.min(MAX_PAGINATION_SIZE, Math.trunc(batchSize) || MAX_PAGINATION_SIZE));
210
+ const chunks = [];
211
+ for (let i = 0; i < items.length; i += size) {
212
+ chunks.push(items.slice(i, i + size));
213
+ }
214
+ return chunks.length ? chunks : [[]];
215
+ }
144
216
  function createListsResource(request) {
217
+ const getById = (listId, query) => request({
218
+ method: "GET",
219
+ path: `/api/lists/${encodeURIComponent(listId)}`,
220
+ auth: "authenticated",
221
+ query
222
+ });
223
+ const addItems = (listId, body) => request({
224
+ method: "POST",
225
+ path: `/api/lists/${encodeURIComponent(listId)}/add`,
226
+ auth: "authenticated",
227
+ body
228
+ });
145
229
  return {
146
230
  /** Returns all lists for the authenticated customer under the current merchant. */
147
231
  getAll: (query) => request({
@@ -151,13 +235,47 @@ function createListsResource(request) {
151
235
  query
152
236
  }),
153
237
  /** Fetch list details by id. */
154
- getById: (listId, query) => request({
155
- method: "GET",
156
- path: `/api/lists/${encodeURIComponent(listId)}`,
157
- auth: "authenticated",
158
- query
159
- }),
160
- /** Create a new list. */
238
+ getById,
239
+ /**
240
+ * Fetch every item on a list by walking pages (list detail is paginated;
241
+ * max pageSize 25).
242
+ */
243
+ getByIdAllItems: async (listId, opts) => {
244
+ const pageSize = clampPageSize(opts?.pageSize ?? MAX_PAGINATION_SIZE);
245
+ const sortBy = opts?.sortBy;
246
+ const sortDirection = opts?.sortDirection;
247
+ const first = await getById(listId, {
248
+ page: 1,
249
+ pageSize,
250
+ ...sortBy ? { sortBy } : {},
251
+ ...sortDirection ? { sortDirection } : {}
252
+ });
253
+ const totalPages = first.pagination.totalPages;
254
+ const allItems = [...first.items ?? []];
255
+ for (let page = 2; page <= totalPages; page++) {
256
+ const next = await getById(listId, {
257
+ page: clampPage(page),
258
+ pageSize,
259
+ ...sortBy ? { sortBy } : {},
260
+ ...sortDirection ? { sortDirection } : {}
261
+ });
262
+ allItems.push(...next.items ?? []);
263
+ }
264
+ const pagination = {
265
+ ...first.pagination,
266
+ page: 1,
267
+ pageSize
268
+ };
269
+ return {
270
+ ...first,
271
+ items: allItems,
272
+ pagination
273
+ };
274
+ },
275
+ /**
276
+ * Create a new list. Do not send `variantIds` — add items via `addItems`
277
+ * or `addItemsBatched` after create.
278
+ */
161
279
  create: (body) => request({
162
280
  method: "POST",
163
281
  path: "/api/lists",
@@ -189,13 +307,33 @@ function createListsResource(request) {
189
307
  auth: "authenticated",
190
308
  body
191
309
  }),
192
- /** Add one or more items to a list. */
193
- addItems: (listId, body) => request({
194
- method: "POST",
195
- path: `/api/lists/${encodeURIComponent(listId)}/add`,
196
- auth: "authenticated",
197
- body
198
- }),
310
+ /**
311
+ * Add one or more items to a list.
312
+ * Returns `{ listId, addedItems, addedCount }`. Prefer `addItemsBatched`
313
+ * when sending more than 25 items.
314
+ */
315
+ addItems,
316
+ /**
317
+ * Add items in sequential batches of ≤25 (API hard max).
318
+ * Merges `{ addedItems, addedCount }` across batches.
319
+ */
320
+ addItemsBatched: async (listId, body, opts) => {
321
+ const items = body.items ?? [];
322
+ const batchSize = opts?.batchSize ?? MAX_PAGINATION_SIZE;
323
+ const chunks = chunkItems(items, batchSize);
324
+ const merged = {
325
+ listId,
326
+ addedItems: [],
327
+ addedCount: 0
328
+ };
329
+ for (const chunk of chunks) {
330
+ const res = await addItems(listId, { items: chunk });
331
+ merged.listId = res.listId;
332
+ merged.addedItems.push(...res.addedItems);
333
+ merged.addedCount += res.addedCount;
334
+ }
335
+ return merged;
336
+ },
199
337
  /** Update an item on a list. */
200
338
  updateItem: (listId, itemId, body) => request({
201
339
  method: "PATCH",
@@ -252,14 +390,20 @@ function normalizeGetGroupsQuery(query) {
252
390
  }
253
391
  function createGroupsResource(request) {
254
392
  return {
255
- /** Fetch groups for the authenticated customer. */
393
+ /**
394
+ * Fetch groups for the authenticated customer.
395
+ * May return **503** when the Groups API is disabled for this merchant.
396
+ */
256
397
  getAll: (query) => request({
257
398
  method: "GET",
258
399
  path: "/api/groups",
259
400
  auth: "authenticated",
260
401
  query: normalizeGetGroupsQuery(query)
261
402
  }),
262
- /** Fetch group details by id. */
403
+ /**
404
+ * Fetch group details by id.
405
+ * May return **503** when the Groups API is disabled for this merchant.
406
+ */
263
407
  getById: (groupId, query) => request({
264
408
  method: "GET",
265
409
  path: `/api/groups/${encodeURIComponent(groupId)}`,
@@ -295,10 +439,14 @@ function createGroupsResource(request) {
295
439
  path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
296
440
  auth: "authenticated"
297
441
  }),
298
- /** Reorder a group (spec uses listIds payload). */
299
- reorder: (groupId, body) => request({
442
+ /**
443
+ * Reorder the customer's groups (`POST /api/groups/reorder`).
444
+ * Body must include `groupIds` in the desired order — not list IDs, and
445
+ * not a per-group path.
446
+ */
447
+ reorder: (body) => request({
300
448
  method: "POST",
301
- path: `/api/groups/${encodeURIComponent(groupId)}/reorder`,
449
+ path: "/api/groups/reorder",
302
450
  auth: "authenticated",
303
451
  body
304
452
  }),
@@ -354,7 +502,9 @@ function createWishlistStackClient(opts) {
354
502
  baseUrl: opts.baseUrl ?? BASE_URL,
355
503
  apiKey: opts.apiKey,
356
504
  customerAccessToken: opts.customerAccessToken,
357
- fetch: fetchImpl
505
+ fetch: fetchImpl,
506
+ defaultTimeoutMs: opts.defaultTimeoutMs,
507
+ retryOnRateLimit: opts.retryOnRateLimit
358
508
  });
359
509
  return {
360
510
  lists: createListsResource(request),
@@ -404,13 +554,25 @@ function withSort(query, sort) {
404
554
  return { ...query ?? {}, ...sort ?? {} };
405
555
  }
406
556
 
557
+ // src/types/guards.ts
558
+ function isRecord2(value) {
559
+ return typeof value === "object" && value !== null;
560
+ }
561
+ function isAddItemsDeltaResponse(value) {
562
+ if (!isRecord2(value)) return false;
563
+ return typeof value.listId === "string" && Array.isArray(value.addedItems) && typeof value.addedCount === "number";
564
+ }
565
+
407
566
  exports.DEFAULT_PAGE = DEFAULT_PAGE;
408
567
  exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
409
568
  exports.SORT_DIRECTIONS = SORT_DIRECTIONS;
410
569
  exports.SORT_FIELDS = SORT_FIELDS;
411
570
  exports.WishlistStackApiError = WishlistStackApiError;
412
571
  exports.WishlistStackSDK = WishlistStackClient;
572
+ exports.clampPage = clampPage;
573
+ exports.clampPageSize = clampPageSize;
413
574
  exports.createWishlistStackClient = createWishlistStackClient;
575
+ exports.isAddItemsDeltaResponse = isAddItemsDeltaResponse;
414
576
  exports.isWishlistStackApiError = isWishlistStackApiError;
415
577
  exports.withPagination = withPagination;
416
578
  exports.withSort = withSort;