@sdg.la/wishlist-stack-sdk 0.13.0 → 0.15.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,10 @@ 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
40
+ - **Fast membership checks** — `lists.containsVariants` checks up to 100 variants without hydrating a list
39
41
  - **Sort support** — Collection endpoints accept `sortBy` (`position` | `createdAt` | `updatedAt`) and `sortDirection` (`asc` | `desc`)
40
42
 
41
43
  ## Requirements
@@ -65,6 +67,9 @@ import { createWishlistStackClient } from '@sdg.la/wishlist-stack-sdk';
65
67
  const client = createWishlistStackClient({
66
68
  apiKey: 'your-merchant-api-key',
67
69
  customerAccessToken: 'customer-access-token', // required for authenticated endpoints
70
+ // optional:
71
+ // defaultTimeoutMs: 10_000,
72
+ // retryOnRateLimit: true, // safe reads retry once on HTTP 429
68
73
  });
69
74
 
70
75
  // Fetch all groups
@@ -74,6 +79,34 @@ const { groups } = await client.groups.getAll();
74
79
  const { lists } = await client.lists.getAll();
75
80
  ```
76
81
 
82
+ ### Pagination, batching, and add responses
83
+
84
+ The API paginates list detail when `pageSize` is supplied (max **25**) and
85
+ always includes an add **delta** (`{ listId, addedItems, addedCount }`). Backend
86
+ compatibility mode may additionally return the full list from add and returns
87
+ all list items when `pageSize` is omitted; the SDK ignores those additive fields.
88
+
89
+ | Concern | Guidance |
90
+ |---------|----------|
91
+ | List detail | Prefer `page` / `pageSize` ≤ 25, or `lists.getByIdAllItems` for every item |
92
+ | Adding many items | Use `lists.addItemsBatched` (chunks of ≤ 25) |
93
+ | Checking membership | Use `lists.containsVariants` for up to 100 variant IDs |
94
+ | Add response | `{ listId, addedItems, addedCount }` |
95
+ | Create + items | Do not send `variantIds` on create; create the list, then `addItems` |
96
+ | `quantity` on add | Supported **1–999** |
97
+ | `includeLists` | Cap of **10 lists × 25 items**; use `getById` helpers for full data |
98
+ | Groups | May return **503** until enabled for the merchant |
99
+
100
+ ```ts
101
+ import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
102
+
103
+ const res = await client.lists.addItemsBatched(id, { items: many });
104
+ // res.addedItems / res.addedCount — then refresh if needed:
105
+ const full = await client.lists.getByIdAllItems(id, {
106
+ pageSize: clampPageSize(25),
107
+ });
108
+ ```
109
+
77
110
  ## Usage
78
111
 
79
112
  ### Server / Node.js / Workers
@@ -175,29 +208,41 @@ try {
175
208
  console.log(error.apiErrors); // Array of { message, field? }
176
209
  console.log(error.apiErrorMessages); // Array of error message strings
177
210
  console.log(error.requestId); // Request ID for debugging
211
+ console.log(error.retryAfter); // Retry-After header (429)
212
+ console.log(error.rateLimit); // X-RateLimit-* headers when present
178
213
  }
179
214
  }
180
215
  ```
181
216
 
217
+ Groups endpoints may return **503** with message `Groups API disabled for this merchant` until the Groups API is enabled.
218
+
182
219
  ## Pagination
183
220
 
184
- Endpoints that return lists support pagination via query parameters:
221
+ Endpoints that return lists support pagination via query parameters. Keep `pageSize` in **1–25** (use `clampPageSize()`). Prefer `lists.getByIdAllItems()` when you need every item; it always supplies `pageSize`, so it behaves consistently in strict and compatibility backend modes.
185
222
 
186
223
  ```ts
224
+ import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
225
+
187
226
  // Paginate groups
188
227
  await client.groups.getAll({ page: 2, pageSize: 10 });
189
228
 
190
- // Include lists + hydrated items on groups.getAll
229
+ // includeLists is capped at 10 lists × 25 items
191
230
  await client.groups.getAll({ includeLists: true });
192
231
 
193
232
  // Paginate lists within a group
194
233
  await client.groups.getById('group-id', { page: 1, pageSize: 25 });
195
234
 
196
235
  // Paginate items within a list
197
- await client.lists.getById('list-id', { page: 1, pageSize: 25 });
236
+ await client.lists.getById('list-id', {
237
+ page: 1,
238
+ pageSize: clampPageSize(25),
239
+ });
240
+
241
+ // Or fetch every page:
242
+ await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
198
243
  ```
199
244
 
200
- **Pagination response structure:**
245
+ **Collection pagination response:**
201
246
 
202
247
  ```ts
203
248
  {
@@ -208,6 +253,9 @@ await client.lists.getById('list-id', { page: 1, pageSize: 25 });
208
253
  }
209
254
  ```
210
255
 
256
+ List detail uses `ListDetailPagination`, which exposes `totalItems`, the
257
+ `totalCount` compatibility alias, and next/previous-page flags.
258
+
211
259
  ## Sorting
212
260
 
213
261
  Collection endpoints accept optional sort parameters. Defaults match historical behavior (`sortBy=position`, `sortDirection=asc`). Sorting applies only to the primary collection returned by the endpoint.
@@ -242,7 +290,7 @@ Fetch all groups for the authenticated customer.
242
290
  - **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; includeLists?: boolean | 1 | "1"; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
243
291
  - **Returns:** `Promise<GetGroupsResponse>`
244
292
 
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.
293
+ 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
294
 
247
295
  ```ts
248
296
  const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday', sortBy: 'updatedAt', sortDirection: 'desc' });
@@ -544,18 +592,17 @@ const copy = await client.groups.duplicate('group-id');
544
592
 
545
593
  ---
546
594
 
547
- #### `groups.reorder(groupId, body)`
595
+ #### `groups.reorder(body)`
548
596
 
549
- Reorder lists within a group.
597
+ Reorder the customer's groups.
550
598
 
551
- - **Endpoint:** `POST /api/groups/{groupId}/reorder`
599
+ - **Endpoint:** `POST /api/groups/reorder`
552
600
  - **Parameters:**
553
- - `groupId` — `string`
554
- - `body` — `{ listIds?: string[] }`
555
- - **Returns:** `Promise<ReorderGroupResponse>`
601
+ - `body` — `{ groupIds: string[] }` (group CUIDs in the desired order)
602
+ - **Returns:** `Promise<ReorderGroupsResponse>`
556
603
 
557
604
  ```ts
558
- await client.groups.reorder('group-id', { listIds: ['list-3', 'list-1', 'list-2'] });
605
+ await client.groups.reorder({ groupIds: ['group-3', 'group-1', 'group-2'] });
559
606
  ```
560
607
 
561
608
  ---
@@ -683,16 +730,17 @@ const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10,
683
730
 
684
731
  #### `lists.getById(listId, query?)`
685
732
 
686
- Fetch a single list by ID. Items are hydrated with Shopify product data and paginated.
733
+ Fetch a single list by ID. Items are hydrated with Shopify product data. Pass pagination options for consistent paging across strict and compatibility backend modes (max `pageSize` **25**).
687
734
 
688
735
  - **Endpoint:** `GET /api/lists/{listId}`
689
736
  - **Parameters:**
690
737
  - `listId` — `string`
691
738
  - `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
692
- - **Returns:** `Promise<GetListResponse>`
739
+ - **Returns:** `Promise<GetListResponse>` (`ListDetail & { pagination: ListDetailPagination }`)
693
740
 
694
741
  ```ts
695
742
  const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sortBy: 'updatedAt', sortDirection: 'desc' });
743
+ // list.pagination.totalPages
696
744
  ```
697
745
 
698
746
  <details>
@@ -757,16 +805,50 @@ const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sort
757
805
 
758
806
  ---
759
807
 
808
+ #### `lists.getByIdAllItems(listId, opts?)`
809
+
810
+ Walk every page of `lists.getById` and return one list with all items concatenated. Use when you need the full wishlist.
811
+
812
+ ```ts
813
+ const full = await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
814
+ ```
815
+
816
+ ---
817
+
818
+ #### `lists.containsVariants(listId, body)`
819
+
820
+ Check whether up to **100** Shopify product variants are present without product hydration or paging through the list. Numeric IDs and ProductVariant GIDs are accepted; response keys preserve the supplied IDs. `matches` contains lightweight item records for property-aware duplicate and move flows.
821
+
822
+ - **Endpoint:** `POST /api/lists/{listId}/contains`
823
+ - **Parameters:** `body` — `{ variantIds: string[] }`
824
+ - **Returns:** `Promise<ContainsVariantsResponse>`
825
+
826
+ ```ts
827
+ const { present, matches } = await client.lists.containsVariants('list-id', {
828
+ variantIds: ['46932429275374', 'gid://shopify/ProductVariant/46932429275375'],
829
+ });
830
+
831
+ if (present['46932429275374']) {
832
+ const existingItems = matches['46932429275374'];
833
+ // Each match includes id, quantity, position, note, and properties.
834
+ }
835
+ ```
836
+
837
+ ---
838
+
760
839
  #### `lists.create(body)`
761
840
 
762
- Create a new list, optionally assigned to a group.
841
+ 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
842
 
764
843
  - **Endpoint:** `POST /api/lists`
765
- - **Parameters:** `body` — `{ name?: string; description?: string; groupId?: string }`
844
+ - **Parameters:** `body` — `{ name: string; description?: string; groupId?: string }`
766
845
  - **Returns:** `Promise<CreateListResponse>`
767
846
 
768
847
  ```ts
769
848
  const list = await client.lists.create({ name: 'Gift Ideas', groupId: 'group-id' });
849
+ await client.lists.addItems(list.id, {
850
+ items: [{ variantId: 'gid://shopify/ProductVariant/123', quantity: 2 }],
851
+ });
770
852
  ```
771
853
 
772
854
  <details>
@@ -889,7 +971,7 @@ const ungrouped = await client.lists.duplicate('list-id', { groupId: null });
889
971
 
890
972
  #### `lists.addItems(listId, body)`
891
973
 
892
- Add one or more items to a list.
974
+ Add one or more items to a list. Prefer `lists.addItemsBatched` when sending more than **25** items.
893
975
 
894
976
  - **Endpoint:** `POST /api/lists/{listId}/add`
895
977
  - **Parameters:**
@@ -898,8 +980,9 @@ Add one or more items to a list.
898
980
 
899
981
  ```ts
900
982
  type AddItemsToListBody = {
901
- items?: Array<{
902
- variantId?: string;
983
+ items: Array<{
984
+ variantId: string;
985
+ /** Quantity 1–999. */
903
986
  quantity?: number;
904
987
  note?: string;
905
988
  properties?: Record<string, unknown> | null;
@@ -907,15 +990,25 @@ type AddItemsToListBody = {
907
990
  };
908
991
  ```
909
992
 
910
- - **Returns:** `Promise<AddItemsToListResponse>` — the updated list with hydrated items (same shape as `getById`)
993
+ - **Returns:** `Promise<AddItemsToListResponse>` — `{ listId, addedItems, addedCount }`
911
994
 
912
995
  ```ts
913
- await client.lists.addItems('list-id', {
996
+ const res = await client.lists.addItems('list-id', {
914
997
  items: [
915
998
  { variantId: 'gid://shopify/ProductVariant/123', quantity: 1 },
916
999
  { variantId: 'gid://shopify/ProductVariant/456', note: 'Size M' },
917
1000
  ],
918
1001
  });
1002
+
1003
+ console.log(res.addedCount, res.addedItems);
1004
+ ```
1005
+
1006
+ #### `lists.addItemsBatched(listId, body, opts?)`
1007
+
1008
+ Chunks `items` into sequential POSTs of ≤25 (configurable via `opts.batchSize`) and merges `{ addedItems, addedCount }`.
1009
+
1010
+ ```ts
1011
+ const res = await client.lists.addItemsBatched('list-id', { items: many });
919
1012
  ```
920
1013
 
921
1014
  ---
@@ -1163,6 +1256,11 @@ import type {
1163
1256
  // Errors
1164
1257
  WishlistStackApiError,
1165
1258
  WishlistStackApiErrorDetails,
1259
+ isWishlistStackApiError,
1260
+
1261
+ // Helpers
1262
+ clampPageSize,
1263
+ isAddItemsDeltaResponse,
1166
1264
 
1167
1265
  // Groups
1168
1266
  GetGroupsResponse,
@@ -1174,7 +1272,9 @@ import type {
1174
1272
  GroupMutationResponse,
1175
1273
  CreateGroupBody,
1176
1274
  UpdateGroupBody,
1275
+ ReorderGroupsBody,
1177
1276
  ReorderGroupBody,
1277
+ ReorderGroupsResponse,
1178
1278
  DuplicateGroupResponse,
1179
1279
 
1180
1280
  // Lists
@@ -1186,6 +1286,10 @@ import type {
1186
1286
  CreateListBody,
1187
1287
  UpdateListBody,
1188
1288
  AddItemsToListBody,
1289
+ AddItemsToListResponse,
1290
+ AddItemsToListDeltaResponse,
1291
+ ContainsVariantsBody,
1292
+ ContainsVariantsResponse,
1189
1293
  UpdateListItemBody,
1190
1294
  ReorderListItemsBody,
1191
1295
  ReorderListItemsResponse,
@@ -1206,6 +1310,7 @@ import type {
1206
1310
 
1207
1311
  // Common
1208
1312
  Pagination,
1313
+ ListDetailPagination,
1209
1314
  PaginationParams,
1210
1315
  OkResponse,
1211
1316
  Image,
@@ -1231,6 +1336,23 @@ type Pagination = {
1231
1336
 
1232
1337
  </details>
1233
1338
 
1339
+ <details>
1340
+ <summary><code>ListDetailPagination</code></summary>
1341
+
1342
+ ```ts
1343
+ type ListDetailPagination = {
1344
+ page: number;
1345
+ pageSize: number;
1346
+ totalItems: number;
1347
+ totalCount: number; // compatibility alias for totalItems
1348
+ totalPages: number;
1349
+ hasNextPage: boolean;
1350
+ hasPreviousPage: boolean;
1351
+ };
1352
+ ```
1353
+
1354
+ </details>
1355
+
1234
1356
  <details>
1235
1357
  <summary><code>GroupSummary</code></summary>
1236
1358
 
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,40 @@ 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
+ const retryable = args.method === "GET" || args.retryable === true;
170
+ if (!res.ok && res.status === 429 && ctx.retryOnRateLimit && retryable) {
171
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
172
+ await sleep(jitterDelayMs(parseRetryAfterMs(retryAfter)));
173
+ res = await doFetch();
174
+ }
122
175
  if (!res.ok) {
123
176
  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})`, {
177
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
178
+ throw new WishlistStackApiError(errorMessageFor(res.status, args.path), {
126
179
  status: res.status,
127
180
  url,
128
181
  method: args.method,
129
- requestId,
130
- body: errBody
182
+ requestId: pickRequestId(res),
183
+ body: errBody,
184
+ retryAfter,
185
+ rateLimit: pickRateLimit(res)
131
186
  });
132
187
  }
133
188
  if (res.status === 204) return void 0;
@@ -140,8 +195,38 @@ function createRequest(ctx) {
140
195
  };
141
196
  }
142
197
 
198
+ // src/helpers/pagination.ts
199
+ function clampPageSize(n) {
200
+ if (!Number.isFinite(n)) return MAX_PAGINATION_SIZE;
201
+ return Math.min(MAX_PAGINATION_SIZE, Math.max(1, Math.trunc(n)));
202
+ }
203
+ function clampPage(n) {
204
+ if (!Number.isFinite(n)) return 1;
205
+ return Math.max(1, Math.trunc(n));
206
+ }
207
+
143
208
  // src/resources/lists/index.ts
209
+ function chunkItems(items, batchSize) {
210
+ const size = Math.max(1, Math.min(MAX_PAGINATION_SIZE, Math.trunc(batchSize) || MAX_PAGINATION_SIZE));
211
+ const chunks = [];
212
+ for (let i = 0; i < items.length; i += size) {
213
+ chunks.push(items.slice(i, i + size));
214
+ }
215
+ return chunks.length ? chunks : [[]];
216
+ }
144
217
  function createListsResource(request) {
218
+ const getById = (listId, query) => request({
219
+ method: "GET",
220
+ path: `/api/lists/${encodeURIComponent(listId)}`,
221
+ auth: "authenticated",
222
+ query
223
+ });
224
+ const addItems = (listId, body) => request({
225
+ method: "POST",
226
+ path: `/api/lists/${encodeURIComponent(listId)}/add`,
227
+ auth: "authenticated",
228
+ body
229
+ });
145
230
  return {
146
231
  /** Returns all lists for the authenticated customer under the current merchant. */
147
232
  getAll: (query) => request({
@@ -151,13 +236,47 @@ function createListsResource(request) {
151
236
  query
152
237
  }),
153
238
  /** 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. */
239
+ getById,
240
+ /**
241
+ * Fetch every item on a list by walking pages (list detail is paginated;
242
+ * max pageSize 25).
243
+ */
244
+ getByIdAllItems: async (listId, opts) => {
245
+ const pageSize = clampPageSize(opts?.pageSize ?? MAX_PAGINATION_SIZE);
246
+ const sortBy = opts?.sortBy;
247
+ const sortDirection = opts?.sortDirection;
248
+ const first = await getById(listId, {
249
+ page: 1,
250
+ pageSize,
251
+ ...sortBy ? { sortBy } : {},
252
+ ...sortDirection ? { sortDirection } : {}
253
+ });
254
+ const totalPages = first.pagination.totalPages;
255
+ const allItems = [...first.items ?? []];
256
+ for (let page = 2; page <= totalPages; page++) {
257
+ const next = await getById(listId, {
258
+ page: clampPage(page),
259
+ pageSize,
260
+ ...sortBy ? { sortBy } : {},
261
+ ...sortDirection ? { sortDirection } : {}
262
+ });
263
+ allItems.push(...next.items ?? []);
264
+ }
265
+ const pagination = {
266
+ ...first.pagination,
267
+ page: 1,
268
+ pageSize
269
+ };
270
+ return {
271
+ ...first,
272
+ items: allItems,
273
+ pagination
274
+ };
275
+ },
276
+ /**
277
+ * Create a new list. Do not send `variantIds` — add items via `addItems`
278
+ * or `addItemsBatched` after create.
279
+ */
161
280
  create: (body) => request({
162
281
  method: "POST",
163
282
  path: "/api/lists",
@@ -189,13 +308,47 @@ function createListsResource(request) {
189
308
  auth: "authenticated",
190
309
  body
191
310
  }),
192
- /** Add one or more items to a list. */
193
- addItems: (listId, body) => request({
311
+ /**
312
+ * Add one or more items to a list.
313
+ * Returns `{ listId, addedItems, addedCount }`. Prefer `addItemsBatched`
314
+ * when sending more than 25 items.
315
+ */
316
+ addItems,
317
+ /**
318
+ * Check whether up to 100 variants are present without hydrating or
319
+ * paging through the list. Response keys preserve the supplied IDs.
320
+ */
321
+ containsVariants: (listId, body) => request({
194
322
  method: "POST",
195
- path: `/api/lists/${encodeURIComponent(listId)}/add`,
323
+ path: `/api/lists/${encodeURIComponent(listId)}/contains`,
196
324
  auth: "authenticated",
197
- body
325
+ body,
326
+ retryable: true
198
327
  }),
328
+ /**
329
+ * Add items in sequential batches of ≤25 (API hard max).
330
+ * Merges `{ addedItems, addedCount }` across batches.
331
+ */
332
+ addItemsBatched: async (listId, body, opts) => {
333
+ const items = body.items ?? [];
334
+ if (items.length === 0) {
335
+ throw new Error("lists.addItemsBatched requires at least one item");
336
+ }
337
+ const batchSize = opts?.batchSize ?? MAX_PAGINATION_SIZE;
338
+ const chunks = chunkItems(items, batchSize);
339
+ const merged = {
340
+ listId,
341
+ addedItems: [],
342
+ addedCount: 0
343
+ };
344
+ for (const chunk of chunks) {
345
+ const res = await addItems(listId, { items: chunk });
346
+ merged.listId = res.listId;
347
+ merged.addedItems.push(...res.addedItems);
348
+ merged.addedCount += res.addedCount;
349
+ }
350
+ return merged;
351
+ },
199
352
  /** Update an item on a list. */
200
353
  updateItem: (listId, itemId, body) => request({
201
354
  method: "PATCH",
@@ -252,14 +405,20 @@ function normalizeGetGroupsQuery(query) {
252
405
  }
253
406
  function createGroupsResource(request) {
254
407
  return {
255
- /** Fetch groups for the authenticated customer. */
408
+ /**
409
+ * Fetch groups for the authenticated customer.
410
+ * May return **503** when the Groups API is disabled for this merchant.
411
+ */
256
412
  getAll: (query) => request({
257
413
  method: "GET",
258
414
  path: "/api/groups",
259
415
  auth: "authenticated",
260
416
  query: normalizeGetGroupsQuery(query)
261
417
  }),
262
- /** Fetch group details by id. */
418
+ /**
419
+ * Fetch group details by id.
420
+ * May return **503** when the Groups API is disabled for this merchant.
421
+ */
263
422
  getById: (groupId, query) => request({
264
423
  method: "GET",
265
424
  path: `/api/groups/${encodeURIComponent(groupId)}`,
@@ -295,10 +454,14 @@ function createGroupsResource(request) {
295
454
  path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
296
455
  auth: "authenticated"
297
456
  }),
298
- /** Reorder a group (spec uses listIds payload). */
299
- reorder: (groupId, body) => request({
457
+ /**
458
+ * Reorder the customer's groups (`POST /api/groups/reorder`).
459
+ * Body must include `groupIds` in the desired order — not list IDs, and
460
+ * not a per-group path.
461
+ */
462
+ reorder: (body) => request({
300
463
  method: "POST",
301
- path: `/api/groups/${encodeURIComponent(groupId)}/reorder`,
464
+ path: "/api/groups/reorder",
302
465
  auth: "authenticated",
303
466
  body
304
467
  }),
@@ -354,7 +517,9 @@ function createWishlistStackClient(opts) {
354
517
  baseUrl: opts.baseUrl ?? BASE_URL,
355
518
  apiKey: opts.apiKey,
356
519
  customerAccessToken: opts.customerAccessToken,
357
- fetch: fetchImpl
520
+ fetch: fetchImpl,
521
+ defaultTimeoutMs: opts.defaultTimeoutMs,
522
+ retryOnRateLimit: opts.retryOnRateLimit
358
523
  });
359
524
  return {
360
525
  lists: createListsResource(request),
@@ -404,13 +569,25 @@ function withSort(query, sort) {
404
569
  return { ...query ?? {}, ...sort ?? {} };
405
570
  }
406
571
 
572
+ // src/types/guards.ts
573
+ function isRecord2(value) {
574
+ return typeof value === "object" && value !== null;
575
+ }
576
+ function isAddItemsDeltaResponse(value) {
577
+ if (!isRecord2(value)) return false;
578
+ return typeof value.listId === "string" && Array.isArray(value.addedItems) && typeof value.addedCount === "number";
579
+ }
580
+
407
581
  exports.DEFAULT_PAGE = DEFAULT_PAGE;
408
582
  exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
409
583
  exports.SORT_DIRECTIONS = SORT_DIRECTIONS;
410
584
  exports.SORT_FIELDS = SORT_FIELDS;
411
585
  exports.WishlistStackApiError = WishlistStackApiError;
412
586
  exports.WishlistStackSDK = WishlistStackClient;
587
+ exports.clampPage = clampPage;
588
+ exports.clampPageSize = clampPageSize;
413
589
  exports.createWishlistStackClient = createWishlistStackClient;
590
+ exports.isAddItemsDeltaResponse = isAddItemsDeltaResponse;
414
591
  exports.isWishlistStackApiError = isWishlistStackApiError;
415
592
  exports.withPagination = withPagination;
416
593
  exports.withSort = withSort;