@sdg.la/wishlist-stack-sdk 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -22
- package/dist/index.cjs +200 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -24
- package/dist/index.d.ts +130 -24
- package/dist/index.js +198 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,8 +34,9 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
|
|
|
34
34
|
- **TypeScript-first** — Full type definitions with exported types for all requests and responses
|
|
35
35
|
- **Universal** — Works in Node.js, browsers, and edge runtimes (Cloudflare Workers, Deno, etc.)
|
|
36
36
|
- **Flexible API** — Choose between functional `createWishlistStackClient()` or class-based `new WishlistStackClient()`
|
|
37
|
-
- **Built-in error handling** — Structured `WishlistStackApiError` with status codes and API error messages
|
|
38
|
-
- **Pagination support** —
|
|
37
|
+
- **Built-in error handling** — Structured `WishlistStackApiError` with status codes, `Retry-After` / rate-limit headers, and API error messages
|
|
38
|
+
- **Pagination support** — List endpoints support `page` and `pageSize` (max **25**); helpers fetch all pages when needed
|
|
39
|
+
- **Batch helpers** — `lists.addItemsBatched` chunks adds to the max of 25 items per request
|
|
39
40
|
- **Sort support** — Collection endpoints accept `sortBy` (`position` | `createdAt` | `updatedAt`) and `sortDirection` (`asc` | `desc`)
|
|
40
41
|
|
|
41
42
|
## Requirements
|
|
@@ -65,6 +66,9 @@ import { createWishlistStackClient } from '@sdg.la/wishlist-stack-sdk';
|
|
|
65
66
|
const client = createWishlistStackClient({
|
|
66
67
|
apiKey: 'your-merchant-api-key',
|
|
67
68
|
customerAccessToken: 'customer-access-token', // required for authenticated endpoints
|
|
69
|
+
// optional:
|
|
70
|
+
// defaultTimeoutMs: 10_000,
|
|
71
|
+
// retryOnRateLimit: true, // single retry with jitter on HTTP 429
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
// Fetch all groups
|
|
@@ -74,6 +78,30 @@ const { groups } = await client.groups.getAll();
|
|
|
74
78
|
const { lists } = await client.lists.getAll();
|
|
75
79
|
```
|
|
76
80
|
|
|
81
|
+
### Pagination, batching, and add responses
|
|
82
|
+
|
|
83
|
+
The API paginates list detail (max `pageSize` **25**) and returns an add **delta** (`{ listId, addedItems, addedCount }`).
|
|
84
|
+
|
|
85
|
+
| Concern | Guidance |
|
|
86
|
+
|---------|----------|
|
|
87
|
+
| List detail | Prefer `page` / `pageSize` ≤ 25, or `lists.getByIdAllItems` for every item |
|
|
88
|
+
| Adding many items | Use `lists.addItemsBatched` (chunks of ≤ 25) |
|
|
89
|
+
| Add response | `{ listId, addedItems, addedCount }` |
|
|
90
|
+
| Create + items | Do not send `variantIds` on create; create the list, then `addItems` |
|
|
91
|
+
| `quantity` on add | Supported **1–999** |
|
|
92
|
+
| `includeLists` | Cap of **10 lists × 25 items**; use `getById` helpers for full data |
|
|
93
|
+
| Groups | May return **503** until enabled for the merchant |
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
|
|
97
|
+
|
|
98
|
+
const res = await client.lists.addItemsBatched(id, { items: many });
|
|
99
|
+
// res.addedItems / res.addedCount — then refresh if needed:
|
|
100
|
+
const full = await client.lists.getByIdAllItems(id, {
|
|
101
|
+
pageSize: clampPageSize(25),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
77
105
|
## Usage
|
|
78
106
|
|
|
79
107
|
### Server / Node.js / Workers
|
|
@@ -175,26 +203,38 @@ try {
|
|
|
175
203
|
console.log(error.apiErrors); // Array of { message, field? }
|
|
176
204
|
console.log(error.apiErrorMessages); // Array of error message strings
|
|
177
205
|
console.log(error.requestId); // Request ID for debugging
|
|
206
|
+
console.log(error.retryAfter); // Retry-After header (429)
|
|
207
|
+
console.log(error.rateLimit); // X-RateLimit-* headers when present
|
|
178
208
|
}
|
|
179
209
|
}
|
|
180
210
|
```
|
|
181
211
|
|
|
212
|
+
Groups endpoints may return **503** with message `Groups API disabled for this merchant` until the Groups API is enabled.
|
|
213
|
+
|
|
182
214
|
## Pagination
|
|
183
215
|
|
|
184
|
-
Endpoints that return lists support pagination via query parameters
|
|
216
|
+
Endpoints that return lists support pagination via query parameters. Keep `pageSize` in **1–25** (use `clampPageSize()`). Prefer `lists.getByIdAllItems()` when you need every item.
|
|
185
217
|
|
|
186
218
|
```ts
|
|
219
|
+
import { clampPageSize } from '@sdg.la/wishlist-stack-sdk';
|
|
220
|
+
|
|
187
221
|
// Paginate groups
|
|
188
222
|
await client.groups.getAll({ page: 2, pageSize: 10 });
|
|
189
223
|
|
|
190
|
-
//
|
|
224
|
+
// includeLists is capped at 10 lists × 25 items
|
|
191
225
|
await client.groups.getAll({ includeLists: true });
|
|
192
226
|
|
|
193
227
|
// Paginate lists within a group
|
|
194
228
|
await client.groups.getById('group-id', { page: 1, pageSize: 25 });
|
|
195
229
|
|
|
196
230
|
// Paginate items within a list
|
|
197
|
-
await client.lists.getById('list-id', {
|
|
231
|
+
await client.lists.getById('list-id', {
|
|
232
|
+
page: 1,
|
|
233
|
+
pageSize: clampPageSize(25),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Or fetch every page:
|
|
237
|
+
await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
|
|
198
238
|
```
|
|
199
239
|
|
|
200
240
|
**Pagination response structure:**
|
|
@@ -242,7 +282,7 @@ Fetch all groups for the authenticated customer.
|
|
|
242
282
|
- **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; includeLists?: boolean | 1 | "1"; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
243
283
|
- **Returns:** `Promise<GetGroupsResponse>`
|
|
244
284
|
|
|
245
|
-
Pass `includeLists: true` (sent as `includeLists=1`) to embed
|
|
285
|
+
Pass `includeLists: true` (sent as `includeLists=1`) to embed lists under each group with hydrated items. Expect at most **10 lists × 25 items** per group — use `groups.getById` + `lists.getById` / `lists.getByIdAllItems` for full data. Without `includeLists`, `lists` is an empty array and only `featuredItems` are populated. Groups may return **503** until enabled for the merchant.
|
|
246
286
|
|
|
247
287
|
```ts
|
|
248
288
|
const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday', sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
@@ -544,18 +584,17 @@ const copy = await client.groups.duplicate('group-id');
|
|
|
544
584
|
|
|
545
585
|
---
|
|
546
586
|
|
|
547
|
-
#### `groups.reorder(
|
|
587
|
+
#### `groups.reorder(body)`
|
|
548
588
|
|
|
549
|
-
Reorder
|
|
589
|
+
Reorder the customer's groups.
|
|
550
590
|
|
|
551
|
-
- **Endpoint:** `POST /api/groups/
|
|
591
|
+
- **Endpoint:** `POST /api/groups/reorder`
|
|
552
592
|
- **Parameters:**
|
|
553
|
-
- `
|
|
554
|
-
|
|
555
|
-
- **Returns:** `Promise<ReorderGroupResponse>`
|
|
593
|
+
- `body` — `{ groupIds: string[] }` (group CUIDs in the desired order)
|
|
594
|
+
- **Returns:** `Promise<ReorderGroupsResponse>`
|
|
556
595
|
|
|
557
596
|
```ts
|
|
558
|
-
await client.groups.reorder(
|
|
597
|
+
await client.groups.reorder({ groupIds: ['group-3', 'group-1', 'group-2'] });
|
|
559
598
|
```
|
|
560
599
|
|
|
561
600
|
---
|
|
@@ -683,16 +722,17 @@ const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10,
|
|
|
683
722
|
|
|
684
723
|
#### `lists.getById(listId, query?)`
|
|
685
724
|
|
|
686
|
-
Fetch a single list by ID. Items are hydrated with Shopify product data and paginated.
|
|
725
|
+
Fetch a single list by ID. Items are hydrated with Shopify product data and paginated. The response includes `pagination` (max `pageSize` **25**).
|
|
687
726
|
|
|
688
727
|
- **Endpoint:** `GET /api/lists/{listId}`
|
|
689
728
|
- **Parameters:**
|
|
690
729
|
- `listId` — `string`
|
|
691
730
|
- `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
692
|
-
- **Returns:** `Promise<GetListResponse>`
|
|
731
|
+
- **Returns:** `Promise<GetListResponse>` (`ListDetail & { pagination: Pagination }`)
|
|
693
732
|
|
|
694
733
|
```ts
|
|
695
734
|
const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
735
|
+
// list.pagination.totalPages
|
|
696
736
|
```
|
|
697
737
|
|
|
698
738
|
<details>
|
|
@@ -757,9 +797,19 @@ const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sort
|
|
|
757
797
|
|
|
758
798
|
---
|
|
759
799
|
|
|
800
|
+
#### `lists.getByIdAllItems(listId, opts?)`
|
|
801
|
+
|
|
802
|
+
Walk every page of `lists.getById` and return one list with all items concatenated. Use when you need the full wishlist.
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
const full = await client.lists.getByIdAllItems('list-id', { pageSize: 25 });
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
|
|
760
810
|
#### `lists.create(body)`
|
|
761
811
|
|
|
762
|
-
Create a new list, optionally assigned to a group.
|
|
812
|
+
Create a new list, optionally assigned to a group. Do **not** send `variantIds` on create (the API rejects them). Create the list, then call `addItems` / `addItemsBatched`.
|
|
763
813
|
|
|
764
814
|
- **Endpoint:** `POST /api/lists`
|
|
765
815
|
- **Parameters:** `body` — `{ name?: string; description?: string; groupId?: string }`
|
|
@@ -767,6 +817,9 @@ Create a new list, optionally assigned to a group.
|
|
|
767
817
|
|
|
768
818
|
```ts
|
|
769
819
|
const list = await client.lists.create({ name: 'Gift Ideas', groupId: 'group-id' });
|
|
820
|
+
await client.lists.addItems(list.id, {
|
|
821
|
+
items: [{ variantId: 'gid://shopify/ProductVariant/123', quantity: 2 }],
|
|
822
|
+
});
|
|
770
823
|
```
|
|
771
824
|
|
|
772
825
|
<details>
|
|
@@ -846,16 +899,25 @@ await client.lists.remove('list-id');
|
|
|
846
899
|
|
|
847
900
|
---
|
|
848
901
|
|
|
849
|
-
#### `lists.duplicate(listId)`
|
|
902
|
+
#### `lists.duplicate(listId, body?)`
|
|
850
903
|
|
|
851
|
-
Duplicate a list in a single call, including all of its items.
|
|
904
|
+
Duplicate a list in a single call, including all of its items (notes and custom `properties` preserved). By default the copy stays in the same group as the source. Pass an optional body to place it in another group or leave it ungrouped. The copy is named `Copy of {original name}` and is not shared.
|
|
852
905
|
|
|
853
906
|
- **Endpoint:** `POST /api/lists/{listId}/duplicate`
|
|
854
|
-
- **Parameters:**
|
|
907
|
+
- **Parameters:**
|
|
908
|
+
- `listId` — `string`
|
|
909
|
+
- `body?` — `DuplicateListBody` (`{ groupId?: string | null }`)
|
|
855
910
|
- **Returns:** `Promise<DuplicateListResponse>`
|
|
856
911
|
|
|
857
912
|
```ts
|
|
913
|
+
// Same group as source (default)
|
|
858
914
|
const copy = await client.lists.duplicate('list-id');
|
|
915
|
+
|
|
916
|
+
// Place copy in another group (cross-project)
|
|
917
|
+
const crossProject = await client.lists.duplicate('list-id', { groupId: 'other-group-id' });
|
|
918
|
+
|
|
919
|
+
// Leave copy ungrouped
|
|
920
|
+
const ungrouped = await client.lists.duplicate('list-id', { groupId: null });
|
|
859
921
|
```
|
|
860
922
|
|
|
861
923
|
<details>
|
|
@@ -880,7 +942,7 @@ const copy = await client.lists.duplicate('list-id');
|
|
|
880
942
|
|
|
881
943
|
#### `lists.addItems(listId, body)`
|
|
882
944
|
|
|
883
|
-
Add one or more items to a list.
|
|
945
|
+
Add one or more items to a list. Prefer `lists.addItemsBatched` when sending more than **25** items.
|
|
884
946
|
|
|
885
947
|
- **Endpoint:** `POST /api/lists/{listId}/add`
|
|
886
948
|
- **Parameters:**
|
|
@@ -891,6 +953,7 @@ Add one or more items to a list.
|
|
|
891
953
|
type AddItemsToListBody = {
|
|
892
954
|
items?: Array<{
|
|
893
955
|
variantId?: string;
|
|
956
|
+
/** Quantity 1–999. */
|
|
894
957
|
quantity?: number;
|
|
895
958
|
note?: string;
|
|
896
959
|
properties?: Record<string, unknown> | null;
|
|
@@ -898,15 +961,25 @@ type AddItemsToListBody = {
|
|
|
898
961
|
};
|
|
899
962
|
```
|
|
900
963
|
|
|
901
|
-
- **Returns:** `Promise<AddItemsToListResponse>` —
|
|
964
|
+
- **Returns:** `Promise<AddItemsToListResponse>` — `{ listId, addedItems, addedCount }`
|
|
902
965
|
|
|
903
966
|
```ts
|
|
904
|
-
await client.lists.addItems('list-id', {
|
|
967
|
+
const res = await client.lists.addItems('list-id', {
|
|
905
968
|
items: [
|
|
906
969
|
{ variantId: 'gid://shopify/ProductVariant/123', quantity: 1 },
|
|
907
970
|
{ variantId: 'gid://shopify/ProductVariant/456', note: 'Size M' },
|
|
908
971
|
],
|
|
909
972
|
});
|
|
973
|
+
|
|
974
|
+
console.log(res.addedCount, res.addedItems);
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
#### `lists.addItemsBatched(listId, body, opts?)`
|
|
978
|
+
|
|
979
|
+
Chunks `items` into sequential POSTs of ≤25 (configurable via `opts.batchSize`) and merges `{ addedItems, addedCount }`.
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
const res = await client.lists.addItemsBatched('list-id', { items: many });
|
|
910
983
|
```
|
|
911
984
|
|
|
912
985
|
---
|
|
@@ -1154,6 +1227,11 @@ import type {
|
|
|
1154
1227
|
// Errors
|
|
1155
1228
|
WishlistStackApiError,
|
|
1156
1229
|
WishlistStackApiErrorDetails,
|
|
1230
|
+
isWishlistStackApiError,
|
|
1231
|
+
|
|
1232
|
+
// Helpers
|
|
1233
|
+
clampPageSize,
|
|
1234
|
+
isAddItemsDeltaResponse,
|
|
1157
1235
|
|
|
1158
1236
|
// Groups
|
|
1159
1237
|
GetGroupsResponse,
|
|
@@ -1165,7 +1243,9 @@ import type {
|
|
|
1165
1243
|
GroupMutationResponse,
|
|
1166
1244
|
CreateGroupBody,
|
|
1167
1245
|
UpdateGroupBody,
|
|
1246
|
+
ReorderGroupsBody,
|
|
1168
1247
|
ReorderGroupBody,
|
|
1248
|
+
ReorderGroupsResponse,
|
|
1169
1249
|
DuplicateGroupResponse,
|
|
1170
1250
|
|
|
1171
1251
|
// Lists
|
|
@@ -1177,9 +1257,12 @@ import type {
|
|
|
1177
1257
|
CreateListBody,
|
|
1178
1258
|
UpdateListBody,
|
|
1179
1259
|
AddItemsToListBody,
|
|
1260
|
+
AddItemsToListResponse,
|
|
1261
|
+
AddItemsToListDeltaResponse,
|
|
1180
1262
|
UpdateListItemBody,
|
|
1181
1263
|
ReorderListItemsBody,
|
|
1182
1264
|
ReorderListItemsResponse,
|
|
1265
|
+
DuplicateListBody,
|
|
1183
1266
|
DuplicateListResponse,
|
|
1184
1267
|
|
|
1185
1268
|
// Shared
|
package/dist/index.cjs
CHANGED
|
@@ -57,11 +57,15 @@ var WishlistStackApiError = class extends Error {
|
|
|
57
57
|
__publicField(this, "method");
|
|
58
58
|
__publicField(this, "requestId");
|
|
59
59
|
__publicField(this, "body");
|
|
60
|
+
__publicField(this, "retryAfter");
|
|
61
|
+
__publicField(this, "rateLimit");
|
|
60
62
|
this.status = details.status;
|
|
61
63
|
this.url = details.url;
|
|
62
64
|
this.method = details.method;
|
|
63
65
|
this.requestId = details.requestId;
|
|
64
66
|
this.body = details.body;
|
|
67
|
+
this.retryAfter = details.retryAfter;
|
|
68
|
+
this.rateLimit = details.rateLimit;
|
|
65
69
|
}
|
|
66
70
|
/**
|
|
67
71
|
* Best-effort extraction of API error items from the response body.
|
|
@@ -93,6 +97,42 @@ async function safeParseJson(res) {
|
|
|
93
97
|
return void 0;
|
|
94
98
|
}
|
|
95
99
|
}
|
|
100
|
+
function pickRequestId(res) {
|
|
101
|
+
return res.headers.get("fly-request-id") ?? res.headers.get("x-request-id") ?? res.headers.get("x-nf-request-id") ?? res.headers.get("x-netlify-request-id") ?? void 0;
|
|
102
|
+
}
|
|
103
|
+
function pickRateLimit(res) {
|
|
104
|
+
const limit = res.headers.get("x-ratelimit-limit") ?? void 0;
|
|
105
|
+
const remaining = res.headers.get("x-ratelimit-remaining") ?? void 0;
|
|
106
|
+
const reset = res.headers.get("x-ratelimit-reset") ?? void 0;
|
|
107
|
+
if (!limit && !remaining && !reset) return void 0;
|
|
108
|
+
return { limit, remaining, reset };
|
|
109
|
+
}
|
|
110
|
+
function errorMessageFor(status, path) {
|
|
111
|
+
if (status === 503 && path.includes("/api/groups")) {
|
|
112
|
+
return "Groups API disabled for this merchant";
|
|
113
|
+
}
|
|
114
|
+
return `Wishlist Stack API request failed (${status})`;
|
|
115
|
+
}
|
|
116
|
+
function parseRetryAfterMs(retryAfter) {
|
|
117
|
+
if (!retryAfter) return 0;
|
|
118
|
+
const asSeconds = Number(retryAfter);
|
|
119
|
+
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
120
|
+
return Math.min(asSeconds * 1e3, 3e4);
|
|
121
|
+
}
|
|
122
|
+
const asDate = Date.parse(retryAfter);
|
|
123
|
+
if (!Number.isNaN(asDate)) {
|
|
124
|
+
return Math.min(Math.max(0, asDate - Date.now()), 3e4);
|
|
125
|
+
}
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
function jitterDelayMs(baseMs) {
|
|
129
|
+
const base = Math.max(100, baseMs || 500);
|
|
130
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
131
|
+
return Math.min(base + jitter, 3e4);
|
|
132
|
+
}
|
|
133
|
+
function sleep(ms) {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
96
136
|
function createRequest(ctx) {
|
|
97
137
|
return async function request(args) {
|
|
98
138
|
const url = `${ctx.baseUrl}${args.path}${toQueryString(args.query)}`;
|
|
@@ -109,25 +149,39 @@ function createRequest(ctx) {
|
|
|
109
149
|
headers["content-type"] ?? (headers["content-type"] = "application/json");
|
|
110
150
|
body = JSON.stringify(args.body);
|
|
111
151
|
}
|
|
112
|
-
const
|
|
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
|
+
if (!res.ok && res.status === 429 && ctx.retryOnRateLimit) {
|
|
170
|
+
const retryAfter = res.headers.get("retry-after") ?? void 0;
|
|
171
|
+
await sleep(jitterDelayMs(parseRetryAfterMs(retryAfter)));
|
|
172
|
+
res = await doFetch();
|
|
173
|
+
}
|
|
122
174
|
if (!res.ok) {
|
|
123
175
|
const errBody = await safeParseJson(res);
|
|
124
|
-
const
|
|
125
|
-
throw new WishlistStackApiError(
|
|
176
|
+
const retryAfter = res.headers.get("retry-after") ?? void 0;
|
|
177
|
+
throw new WishlistStackApiError(errorMessageFor(res.status, args.path), {
|
|
126
178
|
status: res.status,
|
|
127
179
|
url,
|
|
128
180
|
method: args.method,
|
|
129
|
-
requestId,
|
|
130
|
-
body: errBody
|
|
181
|
+
requestId: pickRequestId(res),
|
|
182
|
+
body: errBody,
|
|
183
|
+
retryAfter,
|
|
184
|
+
rateLimit: pickRateLimit(res)
|
|
131
185
|
});
|
|
132
186
|
}
|
|
133
187
|
if (res.status === 204) return void 0;
|
|
@@ -140,8 +194,38 @@ function createRequest(ctx) {
|
|
|
140
194
|
};
|
|
141
195
|
}
|
|
142
196
|
|
|
197
|
+
// src/helpers/pagination.ts
|
|
198
|
+
function clampPageSize(n) {
|
|
199
|
+
if (!Number.isFinite(n)) return MAX_PAGINATION_SIZE;
|
|
200
|
+
return Math.min(MAX_PAGINATION_SIZE, Math.max(1, Math.trunc(n)));
|
|
201
|
+
}
|
|
202
|
+
function clampPage(n) {
|
|
203
|
+
if (!Number.isFinite(n)) return 1;
|
|
204
|
+
return Math.max(1, Math.trunc(n));
|
|
205
|
+
}
|
|
206
|
+
|
|
143
207
|
// src/resources/lists/index.ts
|
|
208
|
+
function chunkItems(items, batchSize) {
|
|
209
|
+
const size = Math.max(1, Math.min(MAX_PAGINATION_SIZE, Math.trunc(batchSize) || MAX_PAGINATION_SIZE));
|
|
210
|
+
const chunks = [];
|
|
211
|
+
for (let i = 0; i < items.length; i += size) {
|
|
212
|
+
chunks.push(items.slice(i, i + size));
|
|
213
|
+
}
|
|
214
|
+
return chunks.length ? chunks : [[]];
|
|
215
|
+
}
|
|
144
216
|
function createListsResource(request) {
|
|
217
|
+
const getById = (listId, query) => request({
|
|
218
|
+
method: "GET",
|
|
219
|
+
path: `/api/lists/${encodeURIComponent(listId)}`,
|
|
220
|
+
auth: "authenticated",
|
|
221
|
+
query
|
|
222
|
+
});
|
|
223
|
+
const addItems = (listId, body) => request({
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: `/api/lists/${encodeURIComponent(listId)}/add`,
|
|
226
|
+
auth: "authenticated",
|
|
227
|
+
body
|
|
228
|
+
});
|
|
145
229
|
return {
|
|
146
230
|
/** Returns all lists for the authenticated customer under the current merchant. */
|
|
147
231
|
getAll: (query) => request({
|
|
@@ -151,13 +235,47 @@ function createListsResource(request) {
|
|
|
151
235
|
query
|
|
152
236
|
}),
|
|
153
237
|
/** Fetch list details by id. */
|
|
154
|
-
getById
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
238
|
+
getById,
|
|
239
|
+
/**
|
|
240
|
+
* Fetch every item on a list by walking pages (list detail is paginated;
|
|
241
|
+
* max pageSize 25).
|
|
242
|
+
*/
|
|
243
|
+
getByIdAllItems: async (listId, opts) => {
|
|
244
|
+
const pageSize = clampPageSize(opts?.pageSize ?? MAX_PAGINATION_SIZE);
|
|
245
|
+
const sortBy = opts?.sortBy;
|
|
246
|
+
const sortDirection = opts?.sortDirection;
|
|
247
|
+
const first = await getById(listId, {
|
|
248
|
+
page: 1,
|
|
249
|
+
pageSize,
|
|
250
|
+
...sortBy ? { sortBy } : {},
|
|
251
|
+
...sortDirection ? { sortDirection } : {}
|
|
252
|
+
});
|
|
253
|
+
const totalPages = first.pagination.totalPages;
|
|
254
|
+
const allItems = [...first.items ?? []];
|
|
255
|
+
for (let page = 2; page <= totalPages; page++) {
|
|
256
|
+
const next = await getById(listId, {
|
|
257
|
+
page: clampPage(page),
|
|
258
|
+
pageSize,
|
|
259
|
+
...sortBy ? { sortBy } : {},
|
|
260
|
+
...sortDirection ? { sortDirection } : {}
|
|
261
|
+
});
|
|
262
|
+
allItems.push(...next.items ?? []);
|
|
263
|
+
}
|
|
264
|
+
const pagination = {
|
|
265
|
+
...first.pagination,
|
|
266
|
+
page: 1,
|
|
267
|
+
pageSize
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
...first,
|
|
271
|
+
items: allItems,
|
|
272
|
+
pagination
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
/**
|
|
276
|
+
* Create a new list. Do not send `variantIds` — add items via `addItems`
|
|
277
|
+
* or `addItemsBatched` after create.
|
|
278
|
+
*/
|
|
161
279
|
create: (body) => request({
|
|
162
280
|
method: "POST",
|
|
163
281
|
path: "/api/lists",
|
|
@@ -179,21 +297,43 @@ function createListsResource(request) {
|
|
|
179
297
|
}),
|
|
180
298
|
/**
|
|
181
299
|
* Duplicate a list (including all items) in a single call.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
300
|
+
* By default the copy stays in the same group as the source. Pass `groupId`
|
|
301
|
+
* to place it in another group, or `groupId: null` to leave it ungrouped.
|
|
302
|
+
* The copy is named `Copy of {original name}` and is not shared.
|
|
184
303
|
*/
|
|
185
|
-
duplicate: (listId) => request({
|
|
304
|
+
duplicate: (listId, body) => request({
|
|
186
305
|
method: "POST",
|
|
187
306
|
path: `/api/lists/${encodeURIComponent(listId)}/duplicate`,
|
|
188
|
-
auth: "authenticated"
|
|
189
|
-
}),
|
|
190
|
-
/** Add one or more items to a list. */
|
|
191
|
-
addItems: (listId, body) => request({
|
|
192
|
-
method: "POST",
|
|
193
|
-
path: `/api/lists/${encodeURIComponent(listId)}/add`,
|
|
194
307
|
auth: "authenticated",
|
|
195
308
|
body
|
|
196
309
|
}),
|
|
310
|
+
/**
|
|
311
|
+
* Add one or more items to a list.
|
|
312
|
+
* Returns `{ listId, addedItems, addedCount }`. Prefer `addItemsBatched`
|
|
313
|
+
* when sending more than 25 items.
|
|
314
|
+
*/
|
|
315
|
+
addItems,
|
|
316
|
+
/**
|
|
317
|
+
* Add items in sequential batches of ≤25 (API hard max).
|
|
318
|
+
* Merges `{ addedItems, addedCount }` across batches.
|
|
319
|
+
*/
|
|
320
|
+
addItemsBatched: async (listId, body, opts) => {
|
|
321
|
+
const items = body.items ?? [];
|
|
322
|
+
const batchSize = opts?.batchSize ?? MAX_PAGINATION_SIZE;
|
|
323
|
+
const chunks = chunkItems(items, batchSize);
|
|
324
|
+
const merged = {
|
|
325
|
+
listId,
|
|
326
|
+
addedItems: [],
|
|
327
|
+
addedCount: 0
|
|
328
|
+
};
|
|
329
|
+
for (const chunk of chunks) {
|
|
330
|
+
const res = await addItems(listId, { items: chunk });
|
|
331
|
+
merged.listId = res.listId;
|
|
332
|
+
merged.addedItems.push(...res.addedItems);
|
|
333
|
+
merged.addedCount += res.addedCount;
|
|
334
|
+
}
|
|
335
|
+
return merged;
|
|
336
|
+
},
|
|
197
337
|
/** Update an item on a list. */
|
|
198
338
|
updateItem: (listId, itemId, body) => request({
|
|
199
339
|
method: "PATCH",
|
|
@@ -250,14 +390,20 @@ function normalizeGetGroupsQuery(query) {
|
|
|
250
390
|
}
|
|
251
391
|
function createGroupsResource(request) {
|
|
252
392
|
return {
|
|
253
|
-
/**
|
|
393
|
+
/**
|
|
394
|
+
* Fetch groups for the authenticated customer.
|
|
395
|
+
* May return **503** when the Groups API is disabled for this merchant.
|
|
396
|
+
*/
|
|
254
397
|
getAll: (query) => request({
|
|
255
398
|
method: "GET",
|
|
256
399
|
path: "/api/groups",
|
|
257
400
|
auth: "authenticated",
|
|
258
401
|
query: normalizeGetGroupsQuery(query)
|
|
259
402
|
}),
|
|
260
|
-
/**
|
|
403
|
+
/**
|
|
404
|
+
* Fetch group details by id.
|
|
405
|
+
* May return **503** when the Groups API is disabled for this merchant.
|
|
406
|
+
*/
|
|
261
407
|
getById: (groupId, query) => request({
|
|
262
408
|
method: "GET",
|
|
263
409
|
path: `/api/groups/${encodeURIComponent(groupId)}`,
|
|
@@ -293,10 +439,14 @@ function createGroupsResource(request) {
|
|
|
293
439
|
path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
|
|
294
440
|
auth: "authenticated"
|
|
295
441
|
}),
|
|
296
|
-
/**
|
|
297
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Reorder the customer's groups (`POST /api/groups/reorder`).
|
|
444
|
+
* Body must include `groupIds` in the desired order — not list IDs, and
|
|
445
|
+
* not a per-group path.
|
|
446
|
+
*/
|
|
447
|
+
reorder: (body) => request({
|
|
298
448
|
method: "POST",
|
|
299
|
-
path:
|
|
449
|
+
path: "/api/groups/reorder",
|
|
300
450
|
auth: "authenticated",
|
|
301
451
|
body
|
|
302
452
|
}),
|
|
@@ -352,7 +502,9 @@ function createWishlistStackClient(opts) {
|
|
|
352
502
|
baseUrl: opts.baseUrl ?? BASE_URL,
|
|
353
503
|
apiKey: opts.apiKey,
|
|
354
504
|
customerAccessToken: opts.customerAccessToken,
|
|
355
|
-
fetch: fetchImpl
|
|
505
|
+
fetch: fetchImpl,
|
|
506
|
+
defaultTimeoutMs: opts.defaultTimeoutMs,
|
|
507
|
+
retryOnRateLimit: opts.retryOnRateLimit
|
|
356
508
|
});
|
|
357
509
|
return {
|
|
358
510
|
lists: createListsResource(request),
|
|
@@ -402,13 +554,25 @@ function withSort(query, sort) {
|
|
|
402
554
|
return { ...query ?? {}, ...sort ?? {} };
|
|
403
555
|
}
|
|
404
556
|
|
|
557
|
+
// src/types/guards.ts
|
|
558
|
+
function isRecord2(value) {
|
|
559
|
+
return typeof value === "object" && value !== null;
|
|
560
|
+
}
|
|
561
|
+
function isAddItemsDeltaResponse(value) {
|
|
562
|
+
if (!isRecord2(value)) return false;
|
|
563
|
+
return typeof value.listId === "string" && Array.isArray(value.addedItems) && typeof value.addedCount === "number";
|
|
564
|
+
}
|
|
565
|
+
|
|
405
566
|
exports.DEFAULT_PAGE = DEFAULT_PAGE;
|
|
406
567
|
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
407
568
|
exports.SORT_DIRECTIONS = SORT_DIRECTIONS;
|
|
408
569
|
exports.SORT_FIELDS = SORT_FIELDS;
|
|
409
570
|
exports.WishlistStackApiError = WishlistStackApiError;
|
|
410
571
|
exports.WishlistStackSDK = WishlistStackClient;
|
|
572
|
+
exports.clampPage = clampPage;
|
|
573
|
+
exports.clampPageSize = clampPageSize;
|
|
411
574
|
exports.createWishlistStackClient = createWishlistStackClient;
|
|
575
|
+
exports.isAddItemsDeltaResponse = isAddItemsDeltaResponse;
|
|
412
576
|
exports.isWishlistStackApiError = isWishlistStackApiError;
|
|
413
577
|
exports.withPagination = withPagination;
|
|
414
578
|
exports.withSort = withSort;
|