@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 +145 -23
- package/dist/index.cjs +208 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +161 -24
- package/dist/index.d.ts +161 -24
- package/dist/index.js +206 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
66
|
-
variantId
|
|
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
|
-
|
|
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<
|
|
204
|
-
/**
|
|
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
|
-
/**
|
|
218
|
-
|
|
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<
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
328
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
350
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
123
|
-
throw new WishlistStackApiError(
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
/**
|
|
191
|
-
|
|
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)}/
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
297
|
-
|
|
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:
|
|
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
|
-
|
|
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
|