@sdg.la/wishlist-stack-sdk 0.13.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 +92 -19
- package/dist/index.cjs +196 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -21
- package/dist/index.d.ts +122 -21
- package/dist/index.js +194 -35
- 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>
|
|
@@ -889,7 +942,7 @@ const ungrouped = await client.lists.duplicate('list-id', { groupId: null });
|
|
|
889
942
|
|
|
890
943
|
#### `lists.addItems(listId, body)`
|
|
891
944
|
|
|
892
|
-
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.
|
|
893
946
|
|
|
894
947
|
- **Endpoint:** `POST /api/lists/{listId}/add`
|
|
895
948
|
- **Parameters:**
|
|
@@ -900,6 +953,7 @@ Add one or more items to a list.
|
|
|
900
953
|
type AddItemsToListBody = {
|
|
901
954
|
items?: Array<{
|
|
902
955
|
variantId?: string;
|
|
956
|
+
/** Quantity 1–999. */
|
|
903
957
|
quantity?: number;
|
|
904
958
|
note?: string;
|
|
905
959
|
properties?: Record<string, unknown> | null;
|
|
@@ -907,15 +961,25 @@ type AddItemsToListBody = {
|
|
|
907
961
|
};
|
|
908
962
|
```
|
|
909
963
|
|
|
910
|
-
- **Returns:** `Promise<AddItemsToListResponse>` —
|
|
964
|
+
- **Returns:** `Promise<AddItemsToListResponse>` — `{ listId, addedItems, addedCount }`
|
|
911
965
|
|
|
912
966
|
```ts
|
|
913
|
-
await client.lists.addItems('list-id', {
|
|
967
|
+
const res = await client.lists.addItems('list-id', {
|
|
914
968
|
items: [
|
|
915
969
|
{ variantId: 'gid://shopify/ProductVariant/123', quantity: 1 },
|
|
916
970
|
{ variantId: 'gid://shopify/ProductVariant/456', note: 'Size M' },
|
|
917
971
|
],
|
|
918
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 });
|
|
919
983
|
```
|
|
920
984
|
|
|
921
985
|
---
|
|
@@ -1163,6 +1227,11 @@ import type {
|
|
|
1163
1227
|
// Errors
|
|
1164
1228
|
WishlistStackApiError,
|
|
1165
1229
|
WishlistStackApiErrorDetails,
|
|
1230
|
+
isWishlistStackApiError,
|
|
1231
|
+
|
|
1232
|
+
// Helpers
|
|
1233
|
+
clampPageSize,
|
|
1234
|
+
isAddItemsDeltaResponse,
|
|
1166
1235
|
|
|
1167
1236
|
// Groups
|
|
1168
1237
|
GetGroupsResponse,
|
|
@@ -1174,7 +1243,9 @@ import type {
|
|
|
1174
1243
|
GroupMutationResponse,
|
|
1175
1244
|
CreateGroupBody,
|
|
1176
1245
|
UpdateGroupBody,
|
|
1246
|
+
ReorderGroupsBody,
|
|
1177
1247
|
ReorderGroupBody,
|
|
1248
|
+
ReorderGroupsResponse,
|
|
1178
1249
|
DuplicateGroupResponse,
|
|
1179
1250
|
|
|
1180
1251
|
// Lists
|
|
@@ -1186,6 +1257,8 @@ import type {
|
|
|
1186
1257
|
CreateListBody,
|
|
1187
1258
|
UpdateListBody,
|
|
1188
1259
|
AddItemsToListBody,
|
|
1260
|
+
AddItemsToListResponse,
|
|
1261
|
+
AddItemsToListDeltaResponse,
|
|
1189
1262
|
UpdateListItemBody,
|
|
1190
1263
|
ReorderListItemsBody,
|
|
1191
1264
|
ReorderListItemsResponse,
|
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",
|
|
@@ -189,13 +307,33 @@ function createListsResource(request) {
|
|
|
189
307
|
auth: "authenticated",
|
|
190
308
|
body
|
|
191
309
|
}),
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
},
|
|
199
337
|
/** Update an item on a list. */
|
|
200
338
|
updateItem: (listId, itemId, body) => request({
|
|
201
339
|
method: "PATCH",
|
|
@@ -252,14 +390,20 @@ function normalizeGetGroupsQuery(query) {
|
|
|
252
390
|
}
|
|
253
391
|
function createGroupsResource(request) {
|
|
254
392
|
return {
|
|
255
|
-
/**
|
|
393
|
+
/**
|
|
394
|
+
* Fetch groups for the authenticated customer.
|
|
395
|
+
* May return **503** when the Groups API is disabled for this merchant.
|
|
396
|
+
*/
|
|
256
397
|
getAll: (query) => request({
|
|
257
398
|
method: "GET",
|
|
258
399
|
path: "/api/groups",
|
|
259
400
|
auth: "authenticated",
|
|
260
401
|
query: normalizeGetGroupsQuery(query)
|
|
261
402
|
}),
|
|
262
|
-
/**
|
|
403
|
+
/**
|
|
404
|
+
* Fetch group details by id.
|
|
405
|
+
* May return **503** when the Groups API is disabled for this merchant.
|
|
406
|
+
*/
|
|
263
407
|
getById: (groupId, query) => request({
|
|
264
408
|
method: "GET",
|
|
265
409
|
path: `/api/groups/${encodeURIComponent(groupId)}`,
|
|
@@ -295,10 +439,14 @@ function createGroupsResource(request) {
|
|
|
295
439
|
path: `/api/groups/${encodeURIComponent(groupId)}/duplicate`,
|
|
296
440
|
auth: "authenticated"
|
|
297
441
|
}),
|
|
298
|
-
/**
|
|
299
|
-
|
|
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({
|
|
300
448
|
method: "POST",
|
|
301
|
-
path:
|
|
449
|
+
path: "/api/groups/reorder",
|
|
302
450
|
auth: "authenticated",
|
|
303
451
|
body
|
|
304
452
|
}),
|
|
@@ -354,7 +502,9 @@ function createWishlistStackClient(opts) {
|
|
|
354
502
|
baseUrl: opts.baseUrl ?? BASE_URL,
|
|
355
503
|
apiKey: opts.apiKey,
|
|
356
504
|
customerAccessToken: opts.customerAccessToken,
|
|
357
|
-
fetch: fetchImpl
|
|
505
|
+
fetch: fetchImpl,
|
|
506
|
+
defaultTimeoutMs: opts.defaultTimeoutMs,
|
|
507
|
+
retryOnRateLimit: opts.retryOnRateLimit
|
|
358
508
|
});
|
|
359
509
|
return {
|
|
360
510
|
lists: createListsResource(request),
|
|
@@ -404,13 +554,25 @@ function withSort(query, sort) {
|
|
|
404
554
|
return { ...query ?? {}, ...sort ?? {} };
|
|
405
555
|
}
|
|
406
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
|
+
|
|
407
566
|
exports.DEFAULT_PAGE = DEFAULT_PAGE;
|
|
408
567
|
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
409
568
|
exports.SORT_DIRECTIONS = SORT_DIRECTIONS;
|
|
410
569
|
exports.SORT_FIELDS = SORT_FIELDS;
|
|
411
570
|
exports.WishlistStackApiError = WishlistStackApiError;
|
|
412
571
|
exports.WishlistStackSDK = WishlistStackClient;
|
|
572
|
+
exports.clampPage = clampPage;
|
|
573
|
+
exports.clampPageSize = clampPageSize;
|
|
413
574
|
exports.createWishlistStackClient = createWishlistStackClient;
|
|
575
|
+
exports.isAddItemsDeltaResponse = isAddItemsDeltaResponse;
|
|
414
576
|
exports.isWishlistStackApiError = isWishlistStackApiError;
|
|
415
577
|
exports.withPagination = withPagination;
|
|
416
578
|
exports.withSort = withSort;
|