@sdg.la/wishlist-stack-sdk 0.12.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>
@@ -846,16 +899,25 @@ await client.lists.remove('list-id');
846
899
 
847
900
  ---
848
901
 
849
- #### `lists.duplicate(listId)`
902
+ #### `lists.duplicate(listId, body?)`
850
903
 
851
- Duplicate a list in a single call, including all of its items. The copy stays in the same group (if any), is named `Copy of {original name}`, and is not shared.
904
+ Duplicate a list in a single call, including all of its items (notes and custom `properties` preserved). By default the copy stays in the same group as the source. Pass an optional body to place it in another group or leave it ungrouped. The copy is named `Copy of {original name}` and is not shared.
852
905
 
853
906
  - **Endpoint:** `POST /api/lists/{listId}/duplicate`
854
- - **Parameters:** `listId` — `string`
907
+ - **Parameters:**
908
+ - `listId` — `string`
909
+ - `body?` — `DuplicateListBody` (`{ groupId?: string | null }`)
855
910
  - **Returns:** `Promise<DuplicateListResponse>`
856
911
 
857
912
  ```ts
913
+ // Same group as source (default)
858
914
  const copy = await client.lists.duplicate('list-id');
915
+
916
+ // Place copy in another group (cross-project)
917
+ const crossProject = await client.lists.duplicate('list-id', { groupId: 'other-group-id' });
918
+
919
+ // Leave copy ungrouped
920
+ const ungrouped = await client.lists.duplicate('list-id', { groupId: null });
859
921
  ```
860
922
 
861
923
  <details>
@@ -880,7 +942,7 @@ const copy = await client.lists.duplicate('list-id');
880
942
 
881
943
  #### `lists.addItems(listId, body)`
882
944
 
883
- 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.
884
946
 
885
947
  - **Endpoint:** `POST /api/lists/{listId}/add`
886
948
  - **Parameters:**
@@ -891,6 +953,7 @@ Add one or more items to a list.
891
953
  type AddItemsToListBody = {
892
954
  items?: Array<{
893
955
  variantId?: string;
956
+ /** Quantity 1–999. */
894
957
  quantity?: number;
895
958
  note?: string;
896
959
  properties?: Record<string, unknown> | null;
@@ -898,15 +961,25 @@ type AddItemsToListBody = {
898
961
  };
899
962
  ```
900
963
 
901
- - **Returns:** `Promise<AddItemsToListResponse>` — the updated list with hydrated items (same shape as `getById`)
964
+ - **Returns:** `Promise<AddItemsToListResponse>` — `{ listId, addedItems, addedCount }`
902
965
 
903
966
  ```ts
904
- await client.lists.addItems('list-id', {
967
+ const res = await client.lists.addItems('list-id', {
905
968
  items: [
906
969
  { variantId: 'gid://shopify/ProductVariant/123', quantity: 1 },
907
970
  { variantId: 'gid://shopify/ProductVariant/456', note: 'Size M' },
908
971
  ],
909
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 });
910
983
  ```
911
984
 
912
985
  ---
@@ -1154,6 +1227,11 @@ import type {
1154
1227
  // Errors
1155
1228
  WishlistStackApiError,
1156
1229
  WishlistStackApiErrorDetails,
1230
+ isWishlistStackApiError,
1231
+
1232
+ // Helpers
1233
+ clampPageSize,
1234
+ isAddItemsDeltaResponse,
1157
1235
 
1158
1236
  // Groups
1159
1237
  GetGroupsResponse,
@@ -1165,7 +1243,9 @@ import type {
1165
1243
  GroupMutationResponse,
1166
1244
  CreateGroupBody,
1167
1245
  UpdateGroupBody,
1246
+ ReorderGroupsBody,
1168
1247
  ReorderGroupBody,
1248
+ ReorderGroupsResponse,
1169
1249
  DuplicateGroupResponse,
1170
1250
 
1171
1251
  // Lists
@@ -1177,9 +1257,12 @@ import type {
1177
1257
  CreateListBody,
1178
1258
  UpdateListBody,
1179
1259
  AddItemsToListBody,
1260
+ AddItemsToListResponse,
1261
+ AddItemsToListDeltaResponse,
1180
1262
  UpdateListItemBody,
1181
1263
  ReorderListItemsBody,
1182
1264
  ReorderListItemsResponse,
1265
+ DuplicateListBody,
1183
1266
  DuplicateListResponse,
1184
1267
 
1185
1268
  // Shared
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",
@@ -179,21 +297,43 @@ function createListsResource(request) {
179
297
  }),
180
298
  /**
181
299
  * Duplicate a list (including all items) in a single call.
182
- * The copy stays in the same group (if any), is named `Copy of {original name}`,
183
- * and is not shared.
300
+ * By default the copy stays in the same group as the source. Pass `groupId`
301
+ * to place it in another group, or `groupId: null` to leave it ungrouped.
302
+ * The copy is named `Copy of {original name}` and is not shared.
184
303
  */
185
- duplicate: (listId) => request({
304
+ duplicate: (listId, body) => request({
186
305
  method: "POST",
187
306
  path: `/api/lists/${encodeURIComponent(listId)}/duplicate`,
188
- auth: "authenticated"
189
- }),
190
- /** Add one or more items to a list. */
191
- addItems: (listId, body) => request({
192
- method: "POST",
193
- path: `/api/lists/${encodeURIComponent(listId)}/add`,
194
307
  auth: "authenticated",
195
308
  body
196
309
  }),
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
+ },
197
337
  /** Update an item on a list. */
198
338
  updateItem: (listId, itemId, body) => request({
199
339
  method: "PATCH",
@@ -250,14 +390,20 @@ function normalizeGetGroupsQuery(query) {
250
390
  }
251
391
  function createGroupsResource(request) {
252
392
  return {
253
- /** 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
+ */
254
397
  getAll: (query) => request({
255
398
  method: "GET",
256
399
  path: "/api/groups",
257
400
  auth: "authenticated",
258
401
  query: normalizeGetGroupsQuery(query)
259
402
  }),
260
- /** 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
+ */
261
407
  getById: (groupId, query) => request({
262
408
  method: "GET",
263
409
  path: `/api/groups/${encodeURIComponent(groupId)}`,
@@ -293,10 +439,14 @@ function createGroupsResource(request) {
293
439
  path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
294
440
  auth: "authenticated"
295
441
  }),
296
- /** Reorder a group (spec uses listIds payload). */
297
- 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({
298
448
  method: "POST",
299
- path: `/api/groups/${encodeURIComponent(groupId)}/reorder`,
449
+ path: "/api/groups/reorder",
300
450
  auth: "authenticated",
301
451
  body
302
452
  }),
@@ -352,7 +502,9 @@ function createWishlistStackClient(opts) {
352
502
  baseUrl: opts.baseUrl ?? BASE_URL,
353
503
  apiKey: opts.apiKey,
354
504
  customerAccessToken: opts.customerAccessToken,
355
- fetch: fetchImpl
505
+ fetch: fetchImpl,
506
+ defaultTimeoutMs: opts.defaultTimeoutMs,
507
+ retryOnRateLimit: opts.retryOnRateLimit
356
508
  });
357
509
  return {
358
510
  lists: createListsResource(request),
@@ -402,13 +554,25 @@ function withSort(query, sort) {
402
554
  return { ...query ?? {}, ...sort ?? {} };
403
555
  }
404
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
+
405
566
  exports.DEFAULT_PAGE = DEFAULT_PAGE;
406
567
  exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
407
568
  exports.SORT_DIRECTIONS = SORT_DIRECTIONS;
408
569
  exports.SORT_FIELDS = SORT_FIELDS;
409
570
  exports.WishlistStackApiError = WishlistStackApiError;
410
571
  exports.WishlistStackSDK = WishlistStackClient;
572
+ exports.clampPage = clampPage;
573
+ exports.clampPageSize = clampPageSize;
411
574
  exports.createWishlistStackClient = createWishlistStackClient;
575
+ exports.isAddItemsDeltaResponse = isAddItemsDeltaResponse;
412
576
  exports.isWishlistStackApiError = isWishlistStackApiError;
413
577
  exports.withPagination = withPagination;
414
578
  exports.withSort = withSort;