@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/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** —
|
|
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
|
-
//
|
|
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', {
|
|
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
|
-
**
|
|
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
|
|
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(
|
|
595
|
+
#### `groups.reorder(body)`
|
|
548
596
|
|
|
549
|
-
Reorder
|
|
597
|
+
Reorder the customer's groups.
|
|
550
598
|
|
|
551
|
-
- **Endpoint:** `POST /api/groups/
|
|
599
|
+
- **Endpoint:** `POST /api/groups/reorder`
|
|
552
600
|
- **Parameters:**
|
|
553
|
-
- `
|
|
554
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
902
|
-
variantId
|
|
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>` —
|
|
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
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
125
|
-
throw new WishlistStackApiError(
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
/**
|
|
193
|
-
|
|
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)}/
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
299
|
-
|
|
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:
|
|
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;
|