@sdg.la/wishlist-stack-sdk 0.10.1 → 0.12.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/AUTH_EXAMPLE.md +209 -0
- package/README.md +228 -14
- package/dist/index.cjs +43 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -21
- package/dist/index.d.ts +90 -21
- package/dist/index.js +41 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/AUTH_EXAMPLE.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Getting a customer access token (Shopify Liquid storefront)
|
|
2
|
+
|
|
3
|
+
Authenticated SDK endpoints require a `customerAccessToken`. In a Liquid theme, you typically run client-side JavaScript (theme asset or inline script) to perform the OAuth 2.0 + PKCE flow and pass the resulting `access_token` into `createWishlistStackClient({ customerAccessToken })`.
|
|
4
|
+
|
|
5
|
+
**Official Shopify documentation:**
|
|
6
|
+
|
|
7
|
+
- [Getting started with the Customer Account API](https://shopify.dev/docs/storefronts/headless/building-with-the-customer-account-api/getting-started) — setup, credentials, OAuth overview
|
|
8
|
+
- [Customer Account API reference (OAuth)](https://shopify.dev/docs/api/customer/latest) — authorize/token endpoints, PKCE, `prompt=none` silent auth
|
|
9
|
+
|
|
10
|
+
> **Example only.** This snippet is adapted from Shopify's documentation for illustration. It is not maintained or supported by this SDK. Your theme, Customer Account API settings, and Shopify session behavior may differ — mileage may vary. For Hydrogen storefronts, use [`@sdg.la/wishlist-stack-hydrogen`](../hydrogen/README.md) instead.
|
|
11
|
+
|
|
12
|
+
## Flow
|
|
13
|
+
|
|
14
|
+
1. Handle OAuth callback — exchange `code` for tokens
|
|
15
|
+
2. Refresh existing tokens when possible
|
|
16
|
+
3. Otherwise redirect with `prompt=none` for silent auth (requires an existing Shopify login session)
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Store ID and client ID from Customer Account API settings in Shopify admin
|
|
21
|
+
- Redirect URI registered in admin (must match `redirectUri` in the script)
|
|
22
|
+
- Customer logged in via the account link for silent auth to succeed
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
(async function () {
|
|
28
|
+
const storeId = "YOUR_STORE_ID";
|
|
29
|
+
const clientId = ""; // Client id from headless environment
|
|
30
|
+
const redirectUri = window.location.origin;
|
|
31
|
+
|
|
32
|
+
function generateRandomCode() {
|
|
33
|
+
const array = new Uint8Array(32);
|
|
34
|
+
crypto.getRandomValues(array);
|
|
35
|
+
return String.fromCharCode.apply(null, Array.from(array));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function base64UrlEncode(str) {
|
|
39
|
+
const base64 = btoa(str);
|
|
40
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function convertBufferToString(hash) {
|
|
44
|
+
const uintArray = new Uint8Array(hash);
|
|
45
|
+
const numberArray = Array.from(uintArray);
|
|
46
|
+
return String.fromCharCode(...numberArray);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function generateCodeVerifier() {
|
|
50
|
+
const rando = generateRandomCode();
|
|
51
|
+
return base64UrlEncode(rando);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function generateCodeChallenge(codeVerifier) {
|
|
55
|
+
const digestOp = await crypto.subtle.digest(
|
|
56
|
+
{ name: "SHA-256" },
|
|
57
|
+
new TextEncoder().encode(codeVerifier),
|
|
58
|
+
);
|
|
59
|
+
const hash = convertBufferToString(digestOp);
|
|
60
|
+
return base64UrlEncode(hash);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function generateState() {
|
|
64
|
+
const timestamp = Date.now().toString();
|
|
65
|
+
const randomString = Math.random().toString(36).substring(2);
|
|
66
|
+
return timestamp + randomString;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function generateNonce(length) {
|
|
70
|
+
const characters =
|
|
71
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
72
|
+
let nonce = "";
|
|
73
|
+
for (let i = 0; i < length; i++) {
|
|
74
|
+
const randomIndex = Math.floor(Math.random() * characters.length);
|
|
75
|
+
nonce += characters.charAt(randomIndex);
|
|
76
|
+
}
|
|
77
|
+
return nonce;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Step 1: Check for authorization code in URL (callback handler) ──
|
|
81
|
+
|
|
82
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
83
|
+
const code = urlParams.get("code");
|
|
84
|
+
|
|
85
|
+
if (code) {
|
|
86
|
+
console.log("Authorization code found, exchanging for tokens...");
|
|
87
|
+
|
|
88
|
+
const codeVerifier = localStorage.getItem("code-verifier");
|
|
89
|
+
if (!codeVerifier) {
|
|
90
|
+
console.error("No code-verifier found in localStorage. Cannot exchange code.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clean up URL
|
|
95
|
+
urlParams.delete("code");
|
|
96
|
+
urlParams.delete("state");
|
|
97
|
+
const newUrl = `${window.location.pathname}${urlParams.toString() ? `?${urlParams.toString()}` : ""}${window.location.hash}`;
|
|
98
|
+
window.history.replaceState({}, "", newUrl);
|
|
99
|
+
localStorage.removeItem("code-verifier");
|
|
100
|
+
|
|
101
|
+
// Exchange code for tokens
|
|
102
|
+
const body = new URLSearchParams();
|
|
103
|
+
body.append("grant_type", "authorization_code");
|
|
104
|
+
body.append("client_id", clientId);
|
|
105
|
+
body.append("redirect_uri", redirectUri);
|
|
106
|
+
body.append("code", code);
|
|
107
|
+
body.append("code_verifier", codeVerifier);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(
|
|
111
|
+
`https://shopify.com/authentication/${storeId}/oauth/token`,
|
|
112
|
+
{
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
115
|
+
body,
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
|
|
120
|
+
if (!data.access_token) {
|
|
121
|
+
console.error("Token exchange failed:", data);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { access_token, expires_in, id_token, refresh_token } = data;
|
|
126
|
+
localStorage.setItem(
|
|
127
|
+
"customer-access-data",
|
|
128
|
+
JSON.stringify({ access_token, expires_in, id_token, refresh_token }),
|
|
129
|
+
);
|
|
130
|
+
console.log("Access token obtained successfully!");
|
|
131
|
+
console.log("Access Token:", access_token);
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error exchanging code for tokens:", error);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Step 2: Check for existing tokens and try refresh ──
|
|
141
|
+
|
|
142
|
+
const existingData = localStorage.getItem("customer-access-data");
|
|
143
|
+
if (existingData) {
|
|
144
|
+
const parsed = JSON.parse(existingData);
|
|
145
|
+
if (parsed.refresh_token) {
|
|
146
|
+
console.log("Found existing refresh token, attempting refresh...");
|
|
147
|
+
|
|
148
|
+
const body = new URLSearchParams();
|
|
149
|
+
body.append("grant_type", "refresh_token");
|
|
150
|
+
body.append("client_id", clientId);
|
|
151
|
+
body.append("refresh_token", parsed.refresh_token);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(
|
|
155
|
+
`https://shopify.com/authentication/${storeId}/oauth/token`,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
159
|
+
body,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
|
|
164
|
+
if (data.access_token) {
|
|
165
|
+
localStorage.setItem(
|
|
166
|
+
"customer-access-data",
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
access_token: data.access_token,
|
|
169
|
+
expires_in: data.expires_in,
|
|
170
|
+
id_token: data.id_token || parsed.id_token,
|
|
171
|
+
refresh_token: data.refresh_token,
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
console.log("Token refreshed successfully!");
|
|
175
|
+
console.log("Access Token:", data.access_token);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn("Refresh failed, will attempt silent auth:", error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const state = await generateState();
|
|
185
|
+
const nonce = await generateNonce(16);
|
|
186
|
+
const verifier = await generateCodeVerifier();
|
|
187
|
+
const challenge = await generateCodeChallenge(verifier);
|
|
188
|
+
localStorage.setItem("code-verifier", verifier);
|
|
189
|
+
|
|
190
|
+
const authUrl = new URL(
|
|
191
|
+
`https://shopify.com/authentication/${storeId}/oauth/authorize`,
|
|
192
|
+
);
|
|
193
|
+
authUrl.searchParams.append("scope", "openid email customer-account-api:full");
|
|
194
|
+
authUrl.searchParams.append("client_id", clientId);
|
|
195
|
+
authUrl.searchParams.append("response_type", "code");
|
|
196
|
+
authUrl.searchParams.append("redirect_uri", redirectUri);
|
|
197
|
+
authUrl.searchParams.append("state", state);
|
|
198
|
+
authUrl.searchParams.append("nonce", nonce);
|
|
199
|
+
authUrl.searchParams.append("code_challenge", challenge);
|
|
200
|
+
authUrl.searchParams.append("code_challenge_method", "S256");
|
|
201
|
+
authUrl.searchParams.append("prompt", "none"); // Silent auth - no login UI
|
|
202
|
+
|
|
203
|
+
console.log("Redirecting for silent auth...");
|
|
204
|
+
console.log("If you see a login_required error in the URL after redirect,");
|
|
205
|
+
console.log("it means no Shopify session exists - log in via the account link first.");
|
|
206
|
+
|
|
207
|
+
window.location.href = authUrl.toString();
|
|
208
|
+
})();
|
|
209
|
+
```
|
package/README.md
CHANGED
|
@@ -14,9 +14,12 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
|
|
|
14
14
|
- [Usage](#usage)
|
|
15
15
|
- [Server / Node.js / Workers](#server--nodejs--workers)
|
|
16
16
|
- [Browser](#browser)
|
|
17
|
+
- [Raw HTML (script module)](#raw-html-script-module)
|
|
18
|
+
- [Shopify Liquid authentication](#shopify-liquid-authentication)
|
|
17
19
|
- [Class API](#class-api)
|
|
18
20
|
- [Error Handling](#error-handling)
|
|
19
21
|
- [Pagination](#pagination)
|
|
22
|
+
- [Sorting](#sorting)
|
|
20
23
|
- [API Reference](#api-reference)
|
|
21
24
|
- [Groups](#groups)
|
|
22
25
|
- [Lists](#lists)
|
|
@@ -33,6 +36,7 @@ TypeScript/JavaScript SDK for the Wishlist Stack API.
|
|
|
33
36
|
- **Flexible API** — Choose between functional `createWishlistStackClient()` or class-based `new WishlistStackClient()`
|
|
34
37
|
- **Built-in error handling** — Structured `WishlistStackApiError` with status codes and API error messages
|
|
35
38
|
- **Pagination support** — All list endpoints support `page` and `pageSize` parameters
|
|
39
|
+
- **Sort support** — Collection endpoints accept `sortBy` (`position` | `createdAt` | `updatedAt`) and `sortDirection` (`asc` | `desc`)
|
|
36
40
|
|
|
37
41
|
## Requirements
|
|
38
42
|
|
|
@@ -102,6 +106,35 @@ const { lists } = await client.lists.getAll();
|
|
|
102
106
|
|
|
103
107
|
> **Note:** If you use a strict Content Security Policy (CSP), you must allow your API domain in `connect-src`.
|
|
104
108
|
|
|
109
|
+
### Raw HTML (script module)
|
|
110
|
+
|
|
111
|
+
You can load the SDK in plain HTML without a bundler by using a `<script type="module">` tag and importing from a CDN:
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<script type="module">
|
|
115
|
+
import { createWishlistStackClient } from "https://cdn.jsdelivr.net/npm/@sdg.la/wishlist-stack-sdk@0.10.1/+esm";
|
|
116
|
+
|
|
117
|
+
window.wishlistClient = createWishlistStackClient({
|
|
118
|
+
apiKey: "YOUR_MERCHANT_API_KEY",
|
|
119
|
+
customerAccessToken: "...",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Example usage:
|
|
123
|
+
await window.wishlistClient.groups.getAll();
|
|
124
|
+
await window.wishlistClient.lists.getAll();
|
|
125
|
+
</script>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Pin the version in the CDN URL (as shown above) so updates do not break your integration unexpectedly.
|
|
129
|
+
|
|
130
|
+
> **Note:** With a strict CSP, allow `https://cdn.jsdelivr.net` in `script-src` (for the module import) and your API domain in `connect-src`.
|
|
131
|
+
|
|
132
|
+
### Shopify Liquid authentication
|
|
133
|
+
|
|
134
|
+
Authenticated SDK calls require a customer access token. For Liquid themes, obtain one via Shopify's Customer Account API OAuth flow in the browser.
|
|
135
|
+
|
|
136
|
+
See [AUTH_EXAMPLE.md](./AUTH_EXAMPLE.md) for an example-only reference implementation adapted from Shopify's docs.
|
|
137
|
+
|
|
105
138
|
### Class API
|
|
106
139
|
|
|
107
140
|
If you prefer a class-based API, use `WishlistStackSDK`:
|
|
@@ -154,6 +187,9 @@ Endpoints that return lists support pagination via query parameters:
|
|
|
154
187
|
// Paginate groups
|
|
155
188
|
await client.groups.getAll({ page: 2, pageSize: 10 });
|
|
156
189
|
|
|
190
|
+
// Include lists + hydrated items on groups.getAll
|
|
191
|
+
await client.groups.getAll({ includeLists: true });
|
|
192
|
+
|
|
157
193
|
// Paginate lists within a group
|
|
158
194
|
await client.groups.getById('group-id', { page: 1, pageSize: 25 });
|
|
159
195
|
|
|
@@ -172,6 +208,24 @@ await client.lists.getById('list-id', { page: 1, pageSize: 25 });
|
|
|
172
208
|
}
|
|
173
209
|
```
|
|
174
210
|
|
|
211
|
+
## Sorting
|
|
212
|
+
|
|
213
|
+
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.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Recently updated groups first
|
|
217
|
+
await client.groups.getAll({ sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
218
|
+
|
|
219
|
+
// Newest lists first
|
|
220
|
+
await client.lists.getAll({ sortBy: 'createdAt', sortDirection: 'desc' });
|
|
221
|
+
|
|
222
|
+
// Sort items in a list by updatedAt
|
|
223
|
+
await client.lists.getById('list-id', { sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
224
|
+
|
|
225
|
+
// Shared endpoints support the same params
|
|
226
|
+
await client.shared.getSharedList('list-id', { sortBy: 'createdAt', sortDirection: 'asc' });
|
|
227
|
+
```
|
|
228
|
+
|
|
175
229
|
## API Reference
|
|
176
230
|
|
|
177
231
|
### Groups
|
|
@@ -185,11 +239,16 @@ await client.lists.getById('list-id', { page: 1, pageSize: 25 });
|
|
|
185
239
|
Fetch all groups for the authenticated customer.
|
|
186
240
|
|
|
187
241
|
- **Endpoint:** `GET /api/groups`
|
|
188
|
-
- **Parameters:** `query?` — `{ page?: number; pageSize?: number
|
|
242
|
+
- **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; includeLists?: boolean | 1 | "1"; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
189
243
|
- **Returns:** `Promise<GetGroupsResponse>`
|
|
190
244
|
|
|
245
|
+
Pass `includeLists: true` (sent as `includeLists=1`) to embed every list under each group with fully hydrated items — useful for loading projects/boards in one call instead of per-group `getById` fan-out. Without it, `lists` is an empty array and only `featuredItems` are populated.
|
|
246
|
+
|
|
191
247
|
```ts
|
|
192
|
-
const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday' });
|
|
248
|
+
const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 10, query: 'holiday', sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
249
|
+
|
|
250
|
+
// Include lists + hydrated items
|
|
251
|
+
const { groups: withLists } = await client.groups.getAll({ includeLists: true });
|
|
193
252
|
```
|
|
194
253
|
|
|
195
254
|
<details>
|
|
@@ -205,6 +264,7 @@ const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 1
|
|
|
205
264
|
"position": 1,
|
|
206
265
|
"shared": false,
|
|
207
266
|
"listCount": 2,
|
|
267
|
+
"lists": [],
|
|
208
268
|
"createdAt": "2025-01-10T08:00:00.000Z",
|
|
209
269
|
"updatedAt": "2025-01-18T12:00:00.000Z",
|
|
210
270
|
"featuredItems": [
|
|
@@ -231,6 +291,75 @@ const { groups, pagination } = await client.groups.getAll({ page: 1, pageSize: 1
|
|
|
231
291
|
|
|
232
292
|
</details>
|
|
233
293
|
|
|
294
|
+
<details>
|
|
295
|
+
<summary>Example response with <code>includeLists: true</code></summary>
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"groups": [
|
|
300
|
+
{
|
|
301
|
+
"id": "cml8drad90001js09weonjojg",
|
|
302
|
+
"name": "Holiday Lists",
|
|
303
|
+
"description": "All my holiday wishlists",
|
|
304
|
+
"position": 1,
|
|
305
|
+
"shared": false,
|
|
306
|
+
"listCount": 1,
|
|
307
|
+
"lists": [
|
|
308
|
+
{
|
|
309
|
+
"id": "cml8drg8x0003js093j4ua6p8",
|
|
310
|
+
"name": "Gift Ideas",
|
|
311
|
+
"description": "Birthday gift ideas",
|
|
312
|
+
"position": 1,
|
|
313
|
+
"shared": false,
|
|
314
|
+
"itemCount": 1,
|
|
315
|
+
"items": [
|
|
316
|
+
{
|
|
317
|
+
"id": "item_1",
|
|
318
|
+
"quantity": 1,
|
|
319
|
+
"position": 1,
|
|
320
|
+
"userNote": null,
|
|
321
|
+
"product": {
|
|
322
|
+
"id": "gid://shopify/Product/1",
|
|
323
|
+
"title": "Example Product",
|
|
324
|
+
"description": "",
|
|
325
|
+
"availableForSale": true,
|
|
326
|
+
"handle": "example-product",
|
|
327
|
+
"productType": null,
|
|
328
|
+
"category": null,
|
|
329
|
+
"tags": [],
|
|
330
|
+
"onlineStoreUrl": null,
|
|
331
|
+
"metafields": [],
|
|
332
|
+
"vendor": null,
|
|
333
|
+
"variant": {
|
|
334
|
+
"id": "46932429275374",
|
|
335
|
+
"title": "Default",
|
|
336
|
+
"price": { "amount": "10.00", "currencyCode": "USD" },
|
|
337
|
+
"availableForSale": true,
|
|
338
|
+
"currentlyNotInStock": false,
|
|
339
|
+
"selectedOptions": [],
|
|
340
|
+
"metafields": []
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
],
|
|
347
|
+
"featuredItems": [],
|
|
348
|
+
"createdAt": "2025-01-10T08:00:00.000Z",
|
|
349
|
+
"updatedAt": "2025-01-18T12:00:00.000Z"
|
|
350
|
+
}
|
|
351
|
+
],
|
|
352
|
+
"pagination": {
|
|
353
|
+
"page": 1,
|
|
354
|
+
"pageSize": 10,
|
|
355
|
+
"totalCount": 1,
|
|
356
|
+
"totalPages": 1
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
</details>
|
|
362
|
+
|
|
234
363
|
---
|
|
235
364
|
|
|
236
365
|
#### `groups.getById(groupId, query?)`
|
|
@@ -240,11 +369,11 @@ Fetch a single group by ID. Lists within the group are paginated.
|
|
|
240
369
|
- **Endpoint:** `GET /api/groups/{groupId}`
|
|
241
370
|
- **Parameters:**
|
|
242
371
|
- `groupId` — `string`
|
|
243
|
-
- `query?` — `{ page?: number; pageSize?: number
|
|
372
|
+
- `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
244
373
|
- **Returns:** `Promise<GetGroupResponse>`
|
|
245
374
|
|
|
246
375
|
```ts
|
|
247
|
-
const group = await client.groups.getById('group-id', { page: 1, pageSize: 25, query: 'gift' });
|
|
376
|
+
const group = await client.groups.getById('group-id', { page: 1, pageSize: 25, query: 'gift', sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
248
377
|
```
|
|
249
378
|
|
|
250
379
|
<details>
|
|
@@ -383,6 +512,38 @@ await client.groups.remove('group-id');
|
|
|
383
512
|
|
|
384
513
|
---
|
|
385
514
|
|
|
515
|
+
#### `groups.duplicate(groupId)`
|
|
516
|
+
|
|
517
|
+
Duplicate a group in a single call, including all of its lists and items. The copy is named `Copy of {original name}` and is not shared.
|
|
518
|
+
|
|
519
|
+
- **Endpoint:** `POST /api/groups/{groupId}/duplicate`
|
|
520
|
+
- **Parameters:** `groupId` — `string`
|
|
521
|
+
- **Returns:** `Promise<DuplicateGroupResponse>`
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
const copy = await client.groups.duplicate('group-id');
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
<details>
|
|
528
|
+
<summary>Example response</summary>
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"id": "cmlbe49qk0001ju094x1ya9co",
|
|
533
|
+
"name": "Copy of Holiday Lists",
|
|
534
|
+
"description": "All my holiday wishlists",
|
|
535
|
+
"position": 2,
|
|
536
|
+
"shared": false,
|
|
537
|
+
"listCount": 2,
|
|
538
|
+
"createdAt": "2025-01-22T09:15:00.000Z",
|
|
539
|
+
"updatedAt": "2025-01-22T09:15:00.000Z"
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
</details>
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
386
547
|
#### `groups.reorder(groupId, body)`
|
|
387
548
|
|
|
388
549
|
Reorder lists within a group.
|
|
@@ -472,11 +633,11 @@ await client.groups.unshare('group-id');
|
|
|
472
633
|
Fetch all lists for the authenticated customer.
|
|
473
634
|
|
|
474
635
|
- **Endpoint:** `GET /api/lists`
|
|
475
|
-
- **Parameters:** `query?` — `{ page?: number; pageSize?: number
|
|
636
|
+
- **Parameters:** `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
476
637
|
- **Returns:** `Promise<GetListsResponse>`
|
|
477
638
|
|
|
478
639
|
```ts
|
|
479
|
-
const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10 });
|
|
640
|
+
const { lists, pagination } = await client.lists.getAll({ page: 1, pageSize: 10, sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
480
641
|
```
|
|
481
642
|
|
|
482
643
|
<details>
|
|
@@ -527,11 +688,11 @@ Fetch a single list by ID. Items are hydrated with Shopify product data and pagi
|
|
|
527
688
|
- **Endpoint:** `GET /api/lists/{listId}`
|
|
528
689
|
- **Parameters:**
|
|
529
690
|
- `listId` — `string`
|
|
530
|
-
- `query?` — `{ page?: number; pageSize?: number }`
|
|
691
|
+
- `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
531
692
|
- **Returns:** `Promise<GetListResponse>`
|
|
532
693
|
|
|
533
694
|
```ts
|
|
534
|
-
const list = await client.lists.getById('list-id', { page: 1, pageSize: 25 });
|
|
695
|
+
const list = await client.lists.getById('list-id', { page: 1, pageSize: 25, sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
535
696
|
```
|
|
536
697
|
|
|
537
698
|
<details>
|
|
@@ -685,6 +846,38 @@ await client.lists.remove('list-id');
|
|
|
685
846
|
|
|
686
847
|
---
|
|
687
848
|
|
|
849
|
+
#### `lists.duplicate(listId)`
|
|
850
|
+
|
|
851
|
+
Duplicate a list in a single call, including all of its items. The copy stays in the same group (if any), is named `Copy of {original name}`, and is not shared.
|
|
852
|
+
|
|
853
|
+
- **Endpoint:** `POST /api/lists/{listId}/duplicate`
|
|
854
|
+
- **Parameters:** `listId` — `string`
|
|
855
|
+
- **Returns:** `Promise<DuplicateListResponse>`
|
|
856
|
+
|
|
857
|
+
```ts
|
|
858
|
+
const copy = await client.lists.duplicate('list-id');
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
<details>
|
|
862
|
+
<summary>Example response</summary>
|
|
863
|
+
|
|
864
|
+
```json
|
|
865
|
+
{
|
|
866
|
+
"id": "cml8drg8x0003js093j4ua6p8",
|
|
867
|
+
"name": "Copy of Gift Ideas",
|
|
868
|
+
"description": "Birthday gift ideas",
|
|
869
|
+
"position": 2,
|
|
870
|
+
"shared": false,
|
|
871
|
+
"itemCount": 3,
|
|
872
|
+
"createdAt": "2025-01-22T09:15:00.000Z",
|
|
873
|
+
"updatedAt": "2025-01-22T09:15:00.000Z"
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
</details>
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
688
881
|
#### `lists.addItems(listId, body)`
|
|
689
882
|
|
|
690
883
|
Add one or more items to a list.
|
|
@@ -918,30 +1111,34 @@ Public read-only endpoints for accessing shared lists and groups. These require
|
|
|
918
1111
|
|
|
919
1112
|
---
|
|
920
1113
|
|
|
921
|
-
#### `shared.getSharedList(listId)`
|
|
1114
|
+
#### `shared.getSharedList(listId, query?)`
|
|
922
1115
|
|
|
923
1116
|
Fetch a public shared list with hydrated product details.
|
|
924
1117
|
|
|
925
1118
|
- **Endpoint:** `GET /api/shared/list/{listId}`
|
|
926
|
-
- **Parameters:**
|
|
1119
|
+
- **Parameters:**
|
|
1120
|
+
- `listId` — `string`
|
|
1121
|
+
- `query?` — `{ page?: number; pageSize?: number; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
927
1122
|
- **Returns:** `Promise<GetSharedListResponse>` — same response shape as [`lists.getById`](#listsgetbyidlistid-query)
|
|
928
1123
|
|
|
929
1124
|
```ts
|
|
930
|
-
const list = await client.shared.getSharedList('list-id');
|
|
1125
|
+
const list = await client.shared.getSharedList('list-id', { sortBy: 'updatedAt', sortDirection: 'desc' });
|
|
931
1126
|
```
|
|
932
1127
|
|
|
933
1128
|
---
|
|
934
1129
|
|
|
935
|
-
#### `shared.getSharedGroup(groupId)`
|
|
1130
|
+
#### `shared.getSharedGroup(groupId, query?)`
|
|
936
1131
|
|
|
937
1132
|
Fetch a public shared group and its lists.
|
|
938
1133
|
|
|
939
1134
|
- **Endpoint:** `GET /api/shared/group/{groupId}`
|
|
940
|
-
- **Parameters:**
|
|
1135
|
+
- **Parameters:**
|
|
1136
|
+
- `groupId` — `string`
|
|
1137
|
+
- `query?` — `{ page?: number; pageSize?: number; query?: string; sortBy?: 'position' | 'createdAt' | 'updatedAt'; sortDirection?: 'asc' | 'desc' }`
|
|
941
1138
|
- **Returns:** `Promise<GetSharedGroupResponse>` — same response shape as [`groups.getById`](#groupsgetbyidgroupid-query)
|
|
942
1139
|
|
|
943
1140
|
```ts
|
|
944
|
-
const group = await client.shared.getSharedGroup('group-id');
|
|
1141
|
+
const group = await client.shared.getSharedGroup('group-id', { sortBy: 'createdAt', sortDirection: 'asc' });
|
|
945
1142
|
```
|
|
946
1143
|
|
|
947
1144
|
## TypeScript Types
|
|
@@ -962,23 +1159,28 @@ import type {
|
|
|
962
1159
|
GetGroupsResponse,
|
|
963
1160
|
GetGroupResponse,
|
|
964
1161
|
GroupSummary,
|
|
1162
|
+
GroupSummaryList,
|
|
965
1163
|
GroupDetail,
|
|
966
1164
|
GroupDetailList,
|
|
1165
|
+
GroupMutationResponse,
|
|
967
1166
|
CreateGroupBody,
|
|
968
1167
|
UpdateGroupBody,
|
|
969
1168
|
ReorderGroupBody,
|
|
1169
|
+
DuplicateGroupResponse,
|
|
970
1170
|
|
|
971
1171
|
// Lists
|
|
972
1172
|
GetListsResponse,
|
|
973
1173
|
GetListResponse,
|
|
974
1174
|
ListSummary,
|
|
975
1175
|
ListDetail,
|
|
1176
|
+
ListMutationResponse,
|
|
976
1177
|
CreateListBody,
|
|
977
1178
|
UpdateListBody,
|
|
978
1179
|
AddItemsToListBody,
|
|
979
1180
|
UpdateListItemBody,
|
|
980
1181
|
ReorderListItemsBody,
|
|
981
1182
|
ReorderListItemsResponse,
|
|
1183
|
+
DuplicateListResponse,
|
|
982
1184
|
|
|
983
1185
|
// Shared
|
|
984
1186
|
GetSharedListResponse,
|
|
@@ -1030,10 +1232,22 @@ type GroupSummary = {
|
|
|
1030
1232
|
position: number;
|
|
1031
1233
|
shared: boolean;
|
|
1032
1234
|
listCount: number;
|
|
1235
|
+
/** Empty unless `includeLists` was requested on `groups.getAll()`. */
|
|
1236
|
+
lists: GroupSummaryList[];
|
|
1033
1237
|
featuredItems: FeaturedItem[];
|
|
1034
1238
|
createdAt: string;
|
|
1035
1239
|
updatedAt: string;
|
|
1036
1240
|
};
|
|
1241
|
+
|
|
1242
|
+
type GroupSummaryList = {
|
|
1243
|
+
id: string;
|
|
1244
|
+
name: string;
|
|
1245
|
+
description: string | null;
|
|
1246
|
+
position: number;
|
|
1247
|
+
shared: boolean;
|
|
1248
|
+
itemCount: number;
|
|
1249
|
+
items: HydratedWishlistItem[];
|
|
1250
|
+
};
|
|
1037
1251
|
```
|
|
1038
1252
|
|
|
1039
1253
|
</details>
|