@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/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ type RequestArgs<TBody> = {
12
12
  headers?: Record<string, string>;
13
13
  signal?: AbortSignal;
14
14
  timeoutMs?: number;
15
+ /** Marks a non-GET operation as safe to retry once on HTTP 429. */
16
+ retryable?: boolean;
15
17
  };
16
18
 
17
19
  type PaginationParams = {
@@ -51,8 +53,12 @@ declare function withPagination<TExtra extends Query>(query?: TExtra, pagination
51
53
  */
52
54
  declare function withSort<TExtra extends Query>(query?: TExtra, sort?: SortParams): (TExtra & SortParams) | undefined;
53
55
 
56
+ /**
57
+ * Create a new list. Do **not** send `variantIds` — the API may reject them.
58
+ * Create the list, then call `lists.addItems` or `lists.addItemsBatched`.
59
+ */
54
60
  type CreateListBody = {
55
- name?: string;
61
+ name: string;
56
62
  description?: string;
57
63
  groupId?: string;
58
64
  };
@@ -62,13 +68,18 @@ type UpdateListBody = {
62
68
  groupId?: string;
63
69
  };
64
70
  type AddItemsToListBody = {
65
- items?: Array<{
66
- variantId?: string;
71
+ items: Array<{
72
+ variantId: string;
73
+ /** Quantity for the item (1–999). */
67
74
  quantity?: number;
68
75
  note?: string;
69
76
  properties?: Record<string, unknown> | null;
70
77
  }>;
71
78
  };
79
+ /** Check membership for 1–100 Shopify product variant IDs. */
80
+ type ContainsVariantsBody = {
81
+ variantIds: string[];
82
+ };
72
83
  type UpdateListItemBody = {
73
84
  variantId?: string;
74
85
  quantity?: number;
@@ -96,6 +107,17 @@ type Pagination = {
96
107
  totalCount: number;
97
108
  totalPages: number;
98
109
  };
110
+ /** Pagination metadata returned by list-detail endpoints. */
111
+ type ListDetailPagination = {
112
+ page: number;
113
+ pageSize: number;
114
+ totalItems: number;
115
+ /** Compatibility alias for `totalItems`. */
116
+ totalCount: number;
117
+ totalPages: number;
118
+ hasNextPage: boolean;
119
+ hasPreviousPage: boolean;
120
+ };
99
121
  type Image = {
100
122
  altText?: string | null;
101
123
  url: string;
@@ -174,14 +196,37 @@ type GetListsResponse = {
174
196
  lists: ListSummary[];
175
197
  pagination: Pagination;
176
198
  };
177
- /** GET `/api/lists/:listId` */
178
- type GetListResponse = ListDetail;
199
+ /** GET `/api/lists/:listId` — items are paginated (max pageSize 25). */
200
+ type GetListResponse = ListDetail & {
201
+ pagination: ListDetailPagination;
202
+ };
179
203
  type ListMutationResponse = Omit<ListSummary, "featuredProducts">;
180
204
  type CreateListResponse = ListMutationResponse;
181
205
  type DuplicateListResponse = ListMutationResponse;
182
206
  type UpdateListDetailsResponse = ListMutationResponse;
183
207
  type RemoveListResponse = OkResponse;
184
- type AddItemsToListResponse = unknown;
208
+ /** POST `/api/lists/:listId/add` — newly added items only. */
209
+ type AddItemsToListResponse = {
210
+ listId: string;
211
+ addedItems: HydratedWishlistItem[];
212
+ addedCount: number;
213
+ };
214
+ /** POST `/api/lists/:listId/contains` */
215
+ type VariantMembershipMatch = {
216
+ id: string;
217
+ variantId: string;
218
+ quantity: number;
219
+ position: number;
220
+ note: string | null;
221
+ properties: Record<string, unknown> | null;
222
+ };
223
+ type ContainsVariantsResponse = {
224
+ listId: string;
225
+ present: Record<string, boolean>;
226
+ matches: Record<string, VariantMembershipMatch[]>;
227
+ };
228
+ /** @deprecated Use `AddItemsToListResponse`. */
229
+ type AddItemsToListDeltaResponse = AddItemsToListResponse;
185
230
  type UpdateListItemResponse = unknown;
186
231
  type RemoveItemFromListResponse = OkResponse;
187
232
  type ReorderListItemsResponse = {
@@ -196,12 +241,29 @@ type RevokeListSharedResponse = ListMutationResponse;
196
241
  type RequestFn$2 = <TResponse = unknown, TBody = unknown>(args: RequestArgs<TBody>) => Promise<TResponse>;
197
242
  type GetListsQuery = PaginatedQuery;
198
243
  type GetListQuery = Omit<PaginatedQuery, "query">;
244
+ type AddItemsBatchedOptions = {
245
+ /** Max items per POST (API hard max is 25). */
246
+ batchSize?: number;
247
+ };
248
+ type GetByIdAllItemsOptions = {
249
+ pageSize?: number;
250
+ sortBy?: GetListQuery["sortBy"];
251
+ sortDirection?: GetListQuery["sortDirection"];
252
+ };
199
253
  declare function createListsResource(request: RequestFn$2): {
200
254
  /** Returns all lists for the authenticated customer under the current merchant. */
201
255
  getAll: (query?: GetListsQuery) => Promise<GetListsResponse>;
202
256
  /** Fetch list details by id. */
203
- getById: (listId: string, query?: GetListQuery) => Promise<ListDetail>;
204
- /** Create a new list. */
257
+ getById: (listId: string, query?: GetListQuery) => Promise<GetListResponse>;
258
+ /**
259
+ * Fetch every item on a list by walking pages (list detail is paginated;
260
+ * max pageSize 25).
261
+ */
262
+ getByIdAllItems: (listId: string, opts?: GetByIdAllItemsOptions) => Promise<GetListResponse>;
263
+ /**
264
+ * Create a new list. Do not send `variantIds` — add items via `addItems`
265
+ * or `addItemsBatched` after create.
266
+ */
205
267
  create: (body: CreateListBody) => Promise<ListMutationResponse>;
206
268
  /** Update list name/description. */
207
269
  update: (listId: string, body: UpdateListBody) => Promise<ListMutationResponse>;
@@ -214,8 +276,22 @@ declare function createListsResource(request: RequestFn$2): {
214
276
  * The copy is named `Copy of {original name}` and is not shared.
215
277
  */
216
278
  duplicate: (listId: string, body?: DuplicateListBody) => Promise<ListMutationResponse>;
217
- /** Add one or more items to a list. */
218
- addItems: (listId: string, body: AddItemsToListBody) => Promise<unknown>;
279
+ /**
280
+ * Add one or more items to a list.
281
+ * Returns `{ listId, addedItems, addedCount }`. Prefer `addItemsBatched`
282
+ * when sending more than 25 items.
283
+ */
284
+ addItems: (listId: string, body: AddItemsToListBody) => Promise<AddItemsToListResponse>;
285
+ /**
286
+ * Check whether up to 100 variants are present without hydrating or
287
+ * paging through the list. Response keys preserve the supplied IDs.
288
+ */
289
+ containsVariants: (listId: string, body: ContainsVariantsBody) => Promise<ContainsVariantsResponse>;
290
+ /**
291
+ * Add items in sequential batches of ≤25 (API hard max).
292
+ * Merges `{ addedItems, addedCount }` across batches.
293
+ */
294
+ addItemsBatched: (listId: string, body: AddItemsToListBody, opts?: AddItemsBatchedOptions) => Promise<AddItemsToListResponse>;
219
295
  /** Update an item on a list. */
220
296
  updateItem: (listId: string, itemId: string, body: UpdateListItemBody) => Promise<unknown>;
221
297
  /** Remove an item from a list. */
@@ -230,7 +306,7 @@ declare function createListsResource(request: RequestFn$2): {
230
306
  * Legacy endpoint kept in the OpenAPI spec (`/api/wishlists/{id}/add`).
231
307
  * Prefer `addItems(listId, body)` unless your server only supports the legacy route.
232
308
  */
233
- addItemsLegacy: (id: string, body: AddItemsToListBody) => Promise<unknown>;
309
+ addItemsLegacy: (id: string, body: AddItemsToListBody) => Promise<AddItemsToListResponse>;
234
310
  };
235
311
 
236
312
  type CreateGroupBody = {
@@ -241,13 +317,20 @@ type UpdateGroupBody = {
241
317
  name?: string;
242
318
  description?: string;
243
319
  };
244
- type ReorderGroupBody = {
245
- listIds?: string[];
320
+ /**
321
+ * Body for `POST /api/groups/reorder` — reorder the customer's groups.
322
+ * `groupIds` must list every group CUID in the desired order.
323
+ */
324
+ type ReorderGroupsBody = {
325
+ groupIds: string[];
246
326
  };
327
+ /** @deprecated Use `ReorderGroupsBody` (`groupIds` on `POST /api/groups/reorder`). */
328
+ type ReorderGroupBody = ReorderGroupsBody;
247
329
 
248
330
  /**
249
331
  * List embedded on a group when `includeLists=1` is passed to `groups.getAll()`.
250
- * Items are fully hydrated (same shape as `lists.getById`).
332
+ * Items are hydrated (same shape as `lists.getById` items).
333
+ * Expect at most **10 lists × 25 items** per group.
251
334
  */
252
335
  type GroupSummaryList = {
253
336
  id: string;
@@ -267,7 +350,10 @@ type GroupSummary = {
267
350
  position: number;
268
351
  shared: boolean;
269
352
  listCount: number;
270
- /** Empty unless `includeLists=1` was requested. */
353
+ /**
354
+ * Empty unless `includeLists=1` was requested.
355
+ * Cap: 10 lists × 25 items each.
356
+ */
271
357
  lists: GroupSummaryList[];
272
358
  featuredItems: FeaturedItem[];
273
359
  createdAt: string;
@@ -317,23 +403,40 @@ type CreateGroupResponse = GroupMutationResponse;
317
403
  type DuplicateGroupResponse = GroupMutationResponse;
318
404
  type UpdateGroupDetailsResponse = GroupMutationResponse;
319
405
  type RemoveGroupResponse = OkResponse;
320
- type ReorderGroupResponse = unknown;
406
+ type ReorderGroupsResponse = {
407
+ updated: Array<{
408
+ id: string;
409
+ position: number;
410
+ }>;
411
+ };
412
+ /** @deprecated Prefer `ReorderGroupsResponse`. */
413
+ type ReorderGroupResponse = ReorderGroupsResponse;
321
414
  type MarkGroupSharedResponse = GroupMutationResponse;
322
415
  type RevokeGroupSharedResponse = GroupMutationResponse;
323
416
 
324
417
  type RequestFn$1 = <TResponse = unknown, TBody = unknown>(args: RequestArgs<TBody>) => Promise<TResponse>;
325
418
  type GetGroupsQuery = PaginatedQuery<{
326
419
  /**
327
- * When truthy (`true`, `1`, or `"1"`), each group includes its full `lists`
328
- * array with hydrated items. Sent to the API as `includeLists=1`.
420
+ * When truthy (`true`, `1`, or `"1"`), each group includes embedded `lists`
421
+ * with hydrated items (`includeLists=1`).
422
+ *
423
+ * Expect at most **10 lists × 25 items** per group. For full data use
424
+ * `groups.getById` plus `lists.getById` / `lists.getByIdAllItems`. Groups
425
+ * endpoints may return **503** until the Groups API is enabled for the merchant.
329
426
  */
330
427
  includeLists?: boolean | 1 | "1";
331
428
  }>;
332
429
  type GetGroupQuery = PaginatedQuery;
333
430
  declare function createGroupsResource(request: RequestFn$1): {
334
- /** Fetch groups for the authenticated customer. */
431
+ /**
432
+ * Fetch groups for the authenticated customer.
433
+ * May return **503** when the Groups API is disabled for this merchant.
434
+ */
335
435
  getAll: (query?: GetGroupsQuery) => Promise<GetGroupsResponse>;
336
- /** Fetch group details by id. */
436
+ /**
437
+ * Fetch group details by id.
438
+ * May return **503** when the Groups API is disabled for this merchant.
439
+ */
337
440
  getById: (groupId: string, query?: GetGroupQuery) => Promise<GroupDetail>;
338
441
  /** Create a new group. */
339
442
  create: (body: CreateGroupBody) => Promise<GroupMutationResponse>;
@@ -346,8 +449,12 @@ declare function createGroupsResource(request: RequestFn$1): {
346
449
  * The copy is named `Copy of {original name}` and is not shared.
347
450
  */
348
451
  duplicate: (groupId: string) => Promise<GroupMutationResponse>;
349
- /** Reorder a group (spec uses listIds payload). */
350
- reorder: (groupId: string, body: ReorderGroupBody) => Promise<unknown>;
452
+ /**
453
+ * Reorder the customer's groups (`POST /api/groups/reorder`).
454
+ * Body must include `groupIds` in the desired order — not list IDs, and
455
+ * not a per-group path.
456
+ */
457
+ reorder: (body: ReorderGroupsBody) => Promise<ReorderGroupsResponse>;
351
458
  /** Mark group as shared (public read-only). */
352
459
  share: (groupId: string) => Promise<GroupMutationResponse>;
353
460
  /** Revoke group sharing. */
@@ -359,7 +466,7 @@ type GetSharedListQuery = Omit<PaginatedQuery, "query">;
359
466
  type GetSharedGroupQuery = PaginatedQuery;
360
467
  declare function createSharedResource(request: RequestFn): {
361
468
  /** Public read-only view of a shared list. Requires only merchant apiKey. */
362
- getSharedList: (listId: string, query?: GetSharedListQuery) => Promise<ListDetail>;
469
+ getSharedList: (listId: string, query?: GetSharedListQuery) => Promise<GetListResponse>;
363
470
  /** Public read-only view of a shared group. Requires only merchant apiKey. */
364
471
  getSharedGroup: (groupId: string, query?: GetSharedGroupQuery) => Promise<GroupDetail>;
365
472
  };
@@ -369,6 +476,10 @@ type CreateWishlistStackClientOptions = {
369
476
  apiKey: string;
370
477
  customerAccessToken?: string;
371
478
  fetch?: FetchLike;
479
+ /** Default request timeout in ms when a call does not pass `timeoutMs`. */
480
+ defaultTimeoutMs?: number;
481
+ /** When true, retry safe reads once on HTTP 429 (default: false). */
482
+ retryOnRateLimit?: boolean;
372
483
  };
373
484
  type WishlistStackClient$1 = {
374
485
  lists: ReturnType<typeof createListsResource>;
@@ -389,6 +500,12 @@ type ApiErrorResponse = {
389
500
  type GetSharedListResponse = GetListResponse;
390
501
  type GetSharedGroupResponse = GetGroupResponse;
391
502
 
503
+ /**
504
+ * True when the value matches the add-items response shape
505
+ * (`{ listId, addedItems, addedCount }`).
506
+ */
507
+ declare function isAddItemsDeltaResponse(value: AddItemsToListResponse | unknown): value is AddItemsToListResponse;
508
+
392
509
  /**
393
510
  * Class wrapper around `createWishlistStackClient()` for users who prefer `new`.
394
511
  * Exposes `lists`, `groups`, and `shared` resources.
@@ -412,6 +529,14 @@ type WishlistStackApiErrorDetails<TBody = unknown> = {
412
529
  method: string;
413
530
  requestId?: string;
414
531
  body?: TBody;
532
+ /** Value of the `Retry-After` response header when present (seconds or HTTP-date). */
533
+ retryAfter?: string;
534
+ /** Selected rate-limit response headers when present. */
535
+ rateLimit?: {
536
+ limit?: string;
537
+ remaining?: string;
538
+ reset?: string;
539
+ };
415
540
  };
416
541
  declare class WishlistStackApiError<TBody = unknown> extends Error {
417
542
  readonly name = "WishlistStackApiError";
@@ -420,6 +545,8 @@ declare class WishlistStackApiError<TBody = unknown> extends Error {
420
545
  readonly method: string;
421
546
  readonly requestId?: string;
422
547
  readonly body?: TBody;
548
+ readonly retryAfter?: string;
549
+ readonly rateLimit?: WishlistStackApiErrorDetails["rateLimit"];
423
550
  constructor(message: string, details: WishlistStackApiErrorDetails<TBody>);
424
551
  /**
425
552
  * Best-effort extraction of API error items from the response body.
@@ -439,4 +566,14 @@ declare class WishlistStackApiError<TBody = unknown> extends Error {
439
566
  */
440
567
  declare function isWishlistStackApiError(value: unknown): value is WishlistStackApiError<unknown>;
441
568
 
442
- export { type AddItemsToListBody, type AddItemsToListResponse, type ApiErrorItem, type ApiErrorResponse, type CreateGroupBody, type CreateGroupResponse, type CreateListBody, type CreateListResponse, type CreateWishlistStackClientOptions, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, type DuplicateGroupResponse, type DuplicateListBody, type DuplicateListResponse, type FeaturedItem, type GetGroupResponse, type GetGroupsResponse, type GetListResponse, type GetListsResponse, type GetSharedGroupResponse, type GetSharedListResponse, type GroupDetail, type GroupDetailList, type GroupMutationResponse, type GroupSummary, type GroupSummaryList, type HydratedProduct, type HydratedVariant, type HydratedWishlistItem, type Id, type Image, type ListDetail, type ListMutationResponse, type ListSummary, type MarkGroupSharedResponse, type MarkListSharedResponse, type Metafield, type Money, type OkResponse, type PaginatedQuery, type Pagination, type PaginationParams, type RemoveGroupResponse, type RemoveItemFromListResponse, type RemoveListResponse, type ReorderGroupBody, type ReorderGroupResponse, type ReorderListItemsBody, type ReorderListItemsResponse, type RevokeGroupSharedResponse, type RevokeListSharedResponse, SORT_DIRECTIONS, SORT_FIELDS, type SelectedOption, type SortDirection, type SortField, type SortParams, type UpdateGroupBody, type UpdateGroupDetailsResponse, type UpdateListBody, type UpdateListDetailsResponse, type UpdateListItemBody, type UpdateListItemResponse, WishlistStackApiError, type WishlistStackClient$1 as WishlistStackClient, WishlistStackClient as WishlistStackSDK, createWishlistStackClient, isWishlistStackApiError, withPagination, withSort };
569
+ /**
570
+ * Clamp a page size to the API-allowed range (1–25) so requests
571
+ * do not send invalid `pageSize` values.
572
+ */
573
+ declare function clampPageSize(n: number): number;
574
+ /**
575
+ * Clamp a 1-based page index to at least 1.
576
+ */
577
+ declare function clampPage(n: number): number;
578
+
579
+ export { type AddItemsToListBody, type AddItemsToListDeltaResponse, type AddItemsToListResponse, type ApiErrorItem, type ApiErrorResponse, type ContainsVariantsBody, type ContainsVariantsResponse, type CreateGroupBody, type CreateGroupResponse, type CreateListBody, type CreateListResponse, type CreateWishlistStackClientOptions, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, type DuplicateGroupResponse, type DuplicateListBody, type DuplicateListResponse, type FeaturedItem, type GetGroupResponse, type GetGroupsResponse, type GetListResponse, type GetListsResponse, type GetSharedGroupResponse, type GetSharedListResponse, type GroupDetail, type GroupDetailList, type GroupMutationResponse, type GroupSummary, type GroupSummaryList, type HydratedProduct, type HydratedVariant, type HydratedWishlistItem, type Id, type Image, type ListDetail, type ListDetailPagination, type ListMutationResponse, type ListSummary, type MarkGroupSharedResponse, type MarkListSharedResponse, type Metafield, type Money, type OkResponse, type PaginatedQuery, type Pagination, type PaginationParams, type RemoveGroupResponse, type RemoveItemFromListResponse, type RemoveListResponse, type ReorderGroupBody, type ReorderGroupResponse, type ReorderGroupsBody, type ReorderGroupsResponse, type ReorderListItemsBody, type ReorderListItemsResponse, type RevokeGroupSharedResponse, type RevokeListSharedResponse, SORT_DIRECTIONS, SORT_FIELDS, type SelectedOption, type SortDirection, type SortField, type SortParams, type UpdateGroupBody, type UpdateGroupDetailsResponse, type UpdateListBody, type UpdateListDetailsResponse, type UpdateListItemBody, type UpdateListItemResponse, type VariantMembershipMatch, WishlistStackApiError, type WishlistStackClient$1 as WishlistStackClient, WishlistStackClient as WishlistStackSDK, clampPage, clampPageSize, createWishlistStackClient, isAddItemsDeltaResponse, isWishlistStackApiError, withPagination, withSort };
package/dist/index.js CHANGED
@@ -55,11 +55,15 @@ var WishlistStackApiError = class extends Error {
55
55
  __publicField(this, "method");
56
56
  __publicField(this, "requestId");
57
57
  __publicField(this, "body");
58
+ __publicField(this, "retryAfter");
59
+ __publicField(this, "rateLimit");
58
60
  this.status = details.status;
59
61
  this.url = details.url;
60
62
  this.method = details.method;
61
63
  this.requestId = details.requestId;
62
64
  this.body = details.body;
65
+ this.retryAfter = details.retryAfter;
66
+ this.rateLimit = details.rateLimit;
63
67
  }
64
68
  /**
65
69
  * Best-effort extraction of API error items from the response body.
@@ -91,6 +95,42 @@ async function safeParseJson(res) {
91
95
  return void 0;
92
96
  }
93
97
  }
98
+ function pickRequestId(res) {
99
+ 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;
100
+ }
101
+ function pickRateLimit(res) {
102
+ const limit = res.headers.get("x-ratelimit-limit") ?? void 0;
103
+ const remaining = res.headers.get("x-ratelimit-remaining") ?? void 0;
104
+ const reset = res.headers.get("x-ratelimit-reset") ?? void 0;
105
+ if (!limit && !remaining && !reset) return void 0;
106
+ return { limit, remaining, reset };
107
+ }
108
+ function errorMessageFor(status, path) {
109
+ if (status === 503 && path.includes("/api/groups")) {
110
+ return "Groups API disabled for this merchant";
111
+ }
112
+ return `Wishlist Stack API request failed (${status})`;
113
+ }
114
+ function parseRetryAfterMs(retryAfter) {
115
+ if (!retryAfter) return 0;
116
+ const asSeconds = Number(retryAfter);
117
+ if (Number.isFinite(asSeconds) && asSeconds >= 0) {
118
+ return Math.min(asSeconds * 1e3, 3e4);
119
+ }
120
+ const asDate = Date.parse(retryAfter);
121
+ if (!Number.isNaN(asDate)) {
122
+ return Math.min(Math.max(0, asDate - Date.now()), 3e4);
123
+ }
124
+ return 0;
125
+ }
126
+ function jitterDelayMs(baseMs) {
127
+ const base = Math.max(100, baseMs || 500);
128
+ const jitter = Math.floor(Math.random() * 250);
129
+ return Math.min(base + jitter, 3e4);
130
+ }
131
+ function sleep(ms) {
132
+ return new Promise((resolve) => setTimeout(resolve, ms));
133
+ }
94
134
  function createRequest(ctx) {
95
135
  return async function request(args) {
96
136
  const url = `${ctx.baseUrl}${args.path}${toQueryString(args.query)}`;
@@ -107,25 +147,40 @@ function createRequest(ctx) {
107
147
  headers["content-type"] ?? (headers["content-type"] = "application/json");
108
148
  body = JSON.stringify(args.body);
109
149
  }
110
- const controller = args.timeoutMs ? new AbortController() : void 0;
111
- const timeout = args.timeoutMs && controller ? setTimeout(() => controller.abort(), args.timeoutMs) : void 0;
112
- const signal = args.signal ? args.signal : controller ? controller.signal : void 0;
113
- const res = await ctx.fetch(url, {
114
- method: args.method,
115
- headers,
116
- body,
117
- signal
118
- });
119
- if (timeout) clearTimeout(timeout);
150
+ const timeoutMs = args.timeoutMs ?? ctx.defaultTimeoutMs;
151
+ const doFetch = async () => {
152
+ const controller = timeoutMs ? new AbortController() : void 0;
153
+ const timeout = timeoutMs && controller ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
154
+ const signal = args.signal ? args.signal : controller ? controller.signal : void 0;
155
+ try {
156
+ return await ctx.fetch(url, {
157
+ method: args.method,
158
+ headers,
159
+ body,
160
+ signal
161
+ });
162
+ } finally {
163
+ if (timeout) clearTimeout(timeout);
164
+ }
165
+ };
166
+ let res = await doFetch();
167
+ const retryable = args.method === "GET" || args.retryable === true;
168
+ if (!res.ok && res.status === 429 && ctx.retryOnRateLimit && retryable) {
169
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
170
+ await sleep(jitterDelayMs(parseRetryAfterMs(retryAfter)));
171
+ res = await doFetch();
172
+ }
120
173
  if (!res.ok) {
121
174
  const errBody = await safeParseJson(res);
122
- const requestId = res.headers.get("fly-request-id") ?? res.headers.get("x-request-id") ?? void 0;
123
- throw new WishlistStackApiError(`Wishlist Stack API request failed (${res.status})`, {
175
+ const retryAfter = res.headers.get("retry-after") ?? void 0;
176
+ throw new WishlistStackApiError(errorMessageFor(res.status, args.path), {
124
177
  status: res.status,
125
178
  url,
126
179
  method: args.method,
127
- requestId,
128
- body: errBody
180
+ requestId: pickRequestId(res),
181
+ body: errBody,
182
+ retryAfter,
183
+ rateLimit: pickRateLimit(res)
129
184
  });
130
185
  }
131
186
  if (res.status === 204) return void 0;
@@ -138,8 +193,38 @@ function createRequest(ctx) {
138
193
  };
139
194
  }
140
195
 
196
+ // src/helpers/pagination.ts
197
+ function clampPageSize(n) {
198
+ if (!Number.isFinite(n)) return MAX_PAGINATION_SIZE;
199
+ return Math.min(MAX_PAGINATION_SIZE, Math.max(1, Math.trunc(n)));
200
+ }
201
+ function clampPage(n) {
202
+ if (!Number.isFinite(n)) return 1;
203
+ return Math.max(1, Math.trunc(n));
204
+ }
205
+
141
206
  // src/resources/lists/index.ts
207
+ function chunkItems(items, batchSize) {
208
+ const size = Math.max(1, Math.min(MAX_PAGINATION_SIZE, Math.trunc(batchSize) || MAX_PAGINATION_SIZE));
209
+ const chunks = [];
210
+ for (let i = 0; i < items.length; i += size) {
211
+ chunks.push(items.slice(i, i + size));
212
+ }
213
+ return chunks.length ? chunks : [[]];
214
+ }
142
215
  function createListsResource(request) {
216
+ const getById = (listId, query) => request({
217
+ method: "GET",
218
+ path: `/api/lists/${encodeURIComponent(listId)}`,
219
+ auth: "authenticated",
220
+ query
221
+ });
222
+ const addItems = (listId, body) => request({
223
+ method: "POST",
224
+ path: `/api/lists/${encodeURIComponent(listId)}/add`,
225
+ auth: "authenticated",
226
+ body
227
+ });
143
228
  return {
144
229
  /** Returns all lists for the authenticated customer under the current merchant. */
145
230
  getAll: (query) => request({
@@ -149,13 +234,47 @@ function createListsResource(request) {
149
234
  query
150
235
  }),
151
236
  /** Fetch list details by id. */
152
- getById: (listId, query) => request({
153
- method: "GET",
154
- path: `/api/lists/${encodeURIComponent(listId)}`,
155
- auth: "authenticated",
156
- query
157
- }),
158
- /** Create a new list. */
237
+ getById,
238
+ /**
239
+ * Fetch every item on a list by walking pages (list detail is paginated;
240
+ * max pageSize 25).
241
+ */
242
+ getByIdAllItems: async (listId, opts) => {
243
+ const pageSize = clampPageSize(opts?.pageSize ?? MAX_PAGINATION_SIZE);
244
+ const sortBy = opts?.sortBy;
245
+ const sortDirection = opts?.sortDirection;
246
+ const first = await getById(listId, {
247
+ page: 1,
248
+ pageSize,
249
+ ...sortBy ? { sortBy } : {},
250
+ ...sortDirection ? { sortDirection } : {}
251
+ });
252
+ const totalPages = first.pagination.totalPages;
253
+ const allItems = [...first.items ?? []];
254
+ for (let page = 2; page <= totalPages; page++) {
255
+ const next = await getById(listId, {
256
+ page: clampPage(page),
257
+ pageSize,
258
+ ...sortBy ? { sortBy } : {},
259
+ ...sortDirection ? { sortDirection } : {}
260
+ });
261
+ allItems.push(...next.items ?? []);
262
+ }
263
+ const pagination = {
264
+ ...first.pagination,
265
+ page: 1,
266
+ pageSize
267
+ };
268
+ return {
269
+ ...first,
270
+ items: allItems,
271
+ pagination
272
+ };
273
+ },
274
+ /**
275
+ * Create a new list. Do not send `variantIds` — add items via `addItems`
276
+ * or `addItemsBatched` after create.
277
+ */
159
278
  create: (body) => request({
160
279
  method: "POST",
161
280
  path: "/api/lists",
@@ -187,13 +306,47 @@ function createListsResource(request) {
187
306
  auth: "authenticated",
188
307
  body
189
308
  }),
190
- /** Add one or more items to a list. */
191
- addItems: (listId, body) => request({
309
+ /**
310
+ * Add one or more items to a list.
311
+ * Returns `{ listId, addedItems, addedCount }`. Prefer `addItemsBatched`
312
+ * when sending more than 25 items.
313
+ */
314
+ addItems,
315
+ /**
316
+ * Check whether up to 100 variants are present without hydrating or
317
+ * paging through the list. Response keys preserve the supplied IDs.
318
+ */
319
+ containsVariants: (listId, body) => request({
192
320
  method: "POST",
193
- path: `/api/lists/${encodeURIComponent(listId)}/add`,
321
+ path: `/api/lists/${encodeURIComponent(listId)}/contains`,
194
322
  auth: "authenticated",
195
- body
323
+ body,
324
+ retryable: true
196
325
  }),
326
+ /**
327
+ * Add items in sequential batches of ≤25 (API hard max).
328
+ * Merges `{ addedItems, addedCount }` across batches.
329
+ */
330
+ addItemsBatched: async (listId, body, opts) => {
331
+ const items = body.items ?? [];
332
+ if (items.length === 0) {
333
+ throw new Error("lists.addItemsBatched requires at least one item");
334
+ }
335
+ const batchSize = opts?.batchSize ?? MAX_PAGINATION_SIZE;
336
+ const chunks = chunkItems(items, batchSize);
337
+ const merged = {
338
+ listId,
339
+ addedItems: [],
340
+ addedCount: 0
341
+ };
342
+ for (const chunk of chunks) {
343
+ const res = await addItems(listId, { items: chunk });
344
+ merged.listId = res.listId;
345
+ merged.addedItems.push(...res.addedItems);
346
+ merged.addedCount += res.addedCount;
347
+ }
348
+ return merged;
349
+ },
197
350
  /** Update an item on a list. */
198
351
  updateItem: (listId, itemId, body) => request({
199
352
  method: "PATCH",
@@ -250,14 +403,20 @@ function normalizeGetGroupsQuery(query) {
250
403
  }
251
404
  function createGroupsResource(request) {
252
405
  return {
253
- /** Fetch groups for the authenticated customer. */
406
+ /**
407
+ * Fetch groups for the authenticated customer.
408
+ * May return **503** when the Groups API is disabled for this merchant.
409
+ */
254
410
  getAll: (query) => request({
255
411
  method: "GET",
256
412
  path: "/api/groups",
257
413
  auth: "authenticated",
258
414
  query: normalizeGetGroupsQuery(query)
259
415
  }),
260
- /** Fetch group details by id. */
416
+ /**
417
+ * Fetch group details by id.
418
+ * May return **503** when the Groups API is disabled for this merchant.
419
+ */
261
420
  getById: (groupId, query) => request({
262
421
  method: "GET",
263
422
  path: `/api/groups/${encodeURIComponent(groupId)}`,
@@ -293,10 +452,14 @@ function createGroupsResource(request) {
293
452
  path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
294
453
  auth: "authenticated"
295
454
  }),
296
- /** Reorder a group (spec uses listIds payload). */
297
- reorder: (groupId, body) => request({
455
+ /**
456
+ * Reorder the customer's groups (`POST /api/groups/reorder`).
457
+ * Body must include `groupIds` in the desired order — not list IDs, and
458
+ * not a per-group path.
459
+ */
460
+ reorder: (body) => request({
298
461
  method: "POST",
299
- path: `/api/groups/${encodeURIComponent(groupId)}/reorder`,
462
+ path: "/api/groups/reorder",
300
463
  auth: "authenticated",
301
464
  body
302
465
  }),
@@ -352,7 +515,9 @@ function createWishlistStackClient(opts) {
352
515
  baseUrl: opts.baseUrl ?? BASE_URL,
353
516
  apiKey: opts.apiKey,
354
517
  customerAccessToken: opts.customerAccessToken,
355
- fetch: fetchImpl
518
+ fetch: fetchImpl,
519
+ defaultTimeoutMs: opts.defaultTimeoutMs,
520
+ retryOnRateLimit: opts.retryOnRateLimit
356
521
  });
357
522
  return {
358
523
  lists: createListsResource(request),
@@ -402,6 +567,15 @@ function withSort(query, sort) {
402
567
  return { ...query ?? {}, ...sort ?? {} };
403
568
  }
404
569
 
405
- export { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SORT_DIRECTIONS, SORT_FIELDS, WishlistStackApiError, WishlistStackClient as WishlistStackSDK, createWishlistStackClient, isWishlistStackApiError, withPagination, withSort };
570
+ // src/types/guards.ts
571
+ function isRecord2(value) {
572
+ return typeof value === "object" && value !== null;
573
+ }
574
+ function isAddItemsDeltaResponse(value) {
575
+ if (!isRecord2(value)) return false;
576
+ return typeof value.listId === "string" && Array.isArray(value.addedItems) && typeof value.addedCount === "number";
577
+ }
578
+
579
+ export { DEFAULT_PAGE, DEFAULT_PAGE_SIZE, SORT_DIRECTIONS, SORT_FIELDS, WishlistStackApiError, WishlistStackClient as WishlistStackSDK, clampPage, clampPageSize, createWishlistStackClient, isAddItemsDeltaResponse, isWishlistStackApiError, withPagination, withSort };
406
580
  //# sourceMappingURL=index.js.map
407
581
  //# sourceMappingURL=index.js.map