@malawistore/store-sdk 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,7 +4,51 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.2.0] - 2026-11-04
8
+
9
+ ### Added
10
+
11
+ - Bulk admin writes: `products.bulkCreate()`, `products.bulkUpdate()`,
12
+ `products.bulkDelete()`, `variants.createBulk()`, `variants.updateBulk()`,
13
+ and `inventory.setStockBulk()`. Every method accepts `atomic`, `dryRun`,
14
+ and an optional `idempotencyKey`.
15
+ - Bulk shopper writes: `cart.addItems()` (returns per-line results plus the
16
+ full updated cart), `wishlist.addBulk()`, and `wishlist.removeBulk()`.
17
+ - Product gallery admin endpoints: `products.listImages()`,
18
+ `products.uploadImages()` (multi-file), `products.deleteImage()`, and
19
+ `products.setPrimaryImage()`.
20
+ - `inventory.getStockByVariants()` batch availability read.
21
+ - `reviews.listMine()` and `reviews.listMinePage()` for the caller's review
22
+ history.
23
+ - `products.list()` / `listPage()` accept `ids` to restrict the page.
24
+ - `BulkOptions`, `BulkResult`, `BulkResponse`, `BulkSummary`, and the new
25
+ bulk/gallery wire types are exported.
26
+
27
+ ### Fixed
28
+
29
+ - `users.get()` now calls `GET /admin/users/{id}`. The previous `/users/{id}`
30
+ route does not exist (owners use `users.me()`; lookups are admin-only).
31
+ - `ApiErrorCode` now includes `BULK_LIMIT_EXCEEDED`, `DUPLICATE_ITEM`, and
32
+ `IDEMPOTENCY_CONFLICT`.
33
+
34
+ ## [0.1.3] - 2026-10-29
35
+
36
+ ### Added
37
+
38
+ - `cart.getPage()` with line filters (`q`, `product_id`, `variant_id`), sorting,
39
+ and paging metadata; cart totals still cover every line.
40
+ - `orders.listPage()` with status/currency/date/total filters and sorting.
41
+ - `wishlist.listPage()` with product search, category filtering, and sorting.
42
+ - Review filters and paging through `reviews.summary(productId, params)`.
43
+ - `ListCartParams`, `ListOrdersParams`, `ListWishlistParams`,
44
+ `ListReviewsParams`, and `CartResponse` wire types.
45
+
46
+ ### Changed
47
+
48
+ - Storefront and shopper lists default to 12 rows per page; the admin user
49
+ directory still defaults to 20. Every page remains capped at 100.
50
+ - Existing `list()`, `get()`, and `summary()` methods now accept the same
51
+ optional query parameters without changing their return shapes.
8
52
 
9
53
  ## [0.1.1] - 2026-10-28
10
54
 
package/README.md CHANGED
@@ -115,14 +115,14 @@ several parallel queries does not rotate the token multiple times.
115
115
  | `store.auth` | Register, login, refresh, logout, email verification, password reset |
116
116
  | `store.users` | Current account, user lookup, role management (admin) |
117
117
  | `store.categories` | Taxonomy reads, admin CRUD, image upload |
118
- | `store.products` | Catalog reads, admin CRUD, image upload |
119
- | `store.variants` | SKU reads and admin CRUD |
120
- | `store.cart` | Get, add, update quantity, remove, clear |
121
- | `store.inventory` | Stock lookup and admin set/reserve/release/confirm |
118
+ | `store.products` | Catalog reads, admin CRUD, bulk writes, image + gallery upload |
119
+ | `store.variants` | SKU reads, admin CRUD, bulk create/update |
120
+ | `store.cart` | Get, add (single or bulk), update quantity, remove, clear |
121
+ | `store.inventory` | Stock lookup (single or batch), bulk set, reserve/release/confirm |
122
122
  | `store.orders` | Checkout, list, get, cancel |
123
123
  | `store.payments` | Stripe payment intents and hosted Checkout sessions |
124
- | `store.wishlist` | List, add, remove |
125
- | `store.reviews` | Product summary, create, edit, delete, admin moderation |
124
+ | `store.wishlist` | List, add, remove, bulk heart/unheart |
125
+ | `store.reviews` | Product summary, own history, create, edit, delete, moderation |
126
126
 
127
127
  ### Auth
128
128
 
@@ -160,6 +160,9 @@ console.log(detail.product.price_cents); // 12500 → K125.00
160
160
  console.log(detail.variants); // sellable SKUs
161
161
 
162
162
  const skus = await store.variants.list(detail.product.id);
163
+
164
+ // Fetch a specific set of products (e.g. a "recently viewed" rail)
165
+ const picked = await store.products.list({ ids: [id1, id2, id3] });
163
166
  ```
164
167
 
165
168
  For paginated grids use `listPage()`, which returns `{ data, meta }`:
@@ -183,8 +186,37 @@ page.meta; // page, limit, offset, total, total_pages, has_next, has_prev
183
186
 
184
187
  Categories support `q`, `parent_id`, `root_only`, `has_products`, `sort`,
185
188
  `page`, `limit` and `offset` through `categories.list()` or
186
- `categories.listPage()`. `page` wins over `offset`; the API defaults to 20
187
- rows and caps a page at 100.
189
+ `categories.listPage()`. `page` wins over `offset`; storefront and shopper
190
+ lists (products, carts, categories, orders, reviews and wishlists) default to
191
+ 12 rows. Every page is capped at 100.
192
+
193
+ The shopper lists expose the same paging envelope:
194
+
195
+ ```ts
196
+ const cart = await store.cart.getPage({ page: 1, sort: "newest" });
197
+ cart.data.items; // current page; subtotal still covers the whole cart
198
+ cart.meta; // line-level page counters
199
+
200
+ const orders = await store.orders.listPage({
201
+ status: "paid",
202
+ created_after: "2026-01-01",
203
+ sort: "total_desc",
204
+ });
205
+ const wishlist = await store.wishlist.listPage({ q: "oil", sort: "price_asc" });
206
+
207
+ const reviews = await store.reviews.summary(productId, {
208
+ rating: 5,
209
+ sort: "highest",
210
+ page: 1,
211
+ limit: 12,
212
+ });
213
+ reviews.meta; // filtered review page counters
214
+
215
+ // The caller's own review history (with product context)
216
+ const mine = await store.reviews.listMine({ sort: "newest" });
217
+ const minePage = await store.reviews.listMinePage({ page: 1, limit: 12 });
218
+ minePage.meta;
219
+ ```
188
220
 
189
221
  Variant rows include `available` (on-hand minus reservations) and `in_stock`,
190
222
  both in the product detail and `/products/:id/variants` responses.
@@ -198,6 +230,18 @@ tambala; the API converts those at charge time.
198
230
  ```ts
199
231
  await store.cart.addItem({ product_id: "…", qty: 2 });
200
232
  await store.cart.addItem({ product_id: "…", variant_id: "…", qty: 1 });
233
+
234
+ // Batch add: per-line results plus the full updated cart
235
+ const bulk = await store.cart.addItems({
236
+ atomic: true,
237
+ items: [
238
+ { product_id: "…", qty: 2 },
239
+ { product_id: "…", variant_id: "…", qty: 1 },
240
+ ],
241
+ });
242
+ bulk.results; // [{ index, id, status, data, error? }]
243
+ bulk.cart; // no follow-up GET needed
244
+
201
245
  await store.cart.setQty("cart-line-id", 3);
202
246
  await store.cart.removeItem("cart-line-id");
203
247
 
@@ -231,6 +275,10 @@ await store.wishlist.add(productId);
231
275
  const wishlist = await store.wishlist.list();
232
276
  await store.wishlist.remove(productId); // product ID, not row ID
233
277
 
278
+ // Batch heart / unheart (idempotent; already-hearted rows succeed)
279
+ await store.wishlist.addBulk({ productIds: [id1, id2] });
280
+ await store.wishlist.removeBulk({ ids: [id1, id2], dryRun: false });
281
+
234
282
  const summary = await store.reviews.summary(productId);
235
283
  await store.reviews.create(productId, { rating: 5, title: "Great", body: "…" });
236
284
  await store.reviews.update(reviewId, { rating: 4 });
@@ -256,6 +304,38 @@ await store.inventory.setStock({ product_id: product.id, qty: 50 });
256
304
  await store.users.setRole(userId, { role: "admin" });
257
305
  await store.reviews.deleteAny(reviewId); // moderation
258
306
 
307
+ // Bulk product + variant + stock writes. Per-item failures stay inside
308
+ // the 200 envelope; atomic rolls the whole batch back.
309
+ await store.products.bulkCreate({ items: [draft1, draft2] });
310
+ await store.products.bulkUpdate({
311
+ updates: [{ id: productId, patch: { is_active: false } }],
312
+ });
313
+ await store.products.bulkDelete({ ids: [id1, id2], dryRun: true });
314
+
315
+ await store.variants.createBulk(product.id, { items: [sku1, sku2] });
316
+ await store.variants.updateBulk({
317
+ updates: [{ id: skuId, patch: { price_cents: 2500 } }],
318
+ });
319
+
320
+ await store.inventory.setStockBulk({
321
+ atomic: true,
322
+ items: [
323
+ { product_id: id1, qty: 50 },
324
+ { product_id: id1, variant_id: skuId, qty: 20 },
325
+ ],
326
+ });
327
+
328
+ // Batch availability read; unknown ids land in `notFound`
329
+ const stock = await store.inventory.getStockByVariants([skuId, otherSkuId]);
330
+ stock.stock;
331
+ stock.notFound;
332
+
333
+ // Bulk writes accept an optional replay-safe key (24h window)
334
+ await store.products.bulkCreate({
335
+ idempotencyKey: "catalog-import-2026-11-04",
336
+ items: [draft1, draft2],
337
+ });
338
+
259
339
  // Admin user directory: filters + pagination
260
340
  const users = await store.users.list({ role: "customer", q: "aisha", sort: "newest" });
261
341
  const page = await store.users.listPage({ email_verified: false, page: 1, limit: 50 });
@@ -272,6 +352,12 @@ Cloudinary finishes. The method resolves as soon as the job is accepted.
272
352
  const file = fileInput.files![0];
273
353
  await store.products.uploadImage(productId, file);
274
354
 
355
+ // Gallery: list, batch upload, reorder/primary, delete
356
+ const images = await store.products.listImages(productId);
357
+ await store.products.uploadImages(productId, [file, otherFile]); // up to 20
358
+ await store.products.setPrimaryImage(productId, images[0].id);
359
+ await store.products.deleteImage(productId, images[0].id);
360
+
275
361
  // Node: wrap a Buffer
276
362
  import { readFile } from "node:fs/promises";
277
363
  const data = await readFile("./photo.jpg");
package/dist/index.cjs CHANGED
@@ -433,6 +433,11 @@ var AuthResource = class {
433
433
  }
434
434
  };
435
435
 
436
+ // src/headers.ts
437
+ function idempotencyHeaders(key) {
438
+ return key ? { "Idempotency-Key": key } : void 0;
439
+ }
440
+
436
441
  // src/path.ts
437
442
  function segment(value) {
438
443
  return encodeURIComponent(value);
@@ -444,9 +449,34 @@ var CartResource = class {
444
449
  this.http = http;
445
450
  }
446
451
  http;
447
- /** Active cart, created empty on first use. */
448
- get() {
449
- return this.http.request("GET", "/cart");
452
+ /** Active cart, created empty on first use (paging meta is dropped). */
453
+ get(params = {}) {
454
+ return this.http.request("GET", "/cart", {
455
+ query: {
456
+ q: params.q,
457
+ product_id: params.product_id,
458
+ variant_id: params.variant_id,
459
+ sort: params.sort,
460
+ page: params.page,
461
+ limit: params.limit,
462
+ offset: params.offset
463
+ }
464
+ });
465
+ }
466
+ /** Same as `get`, plus the `meta` paging counters. */
467
+ getPage(params = {}) {
468
+ return this.http.request("GET", "/cart", {
469
+ query: {
470
+ q: params.q,
471
+ product_id: params.product_id,
472
+ variant_id: params.variant_id,
473
+ sort: params.sort,
474
+ page: params.page,
475
+ limit: params.limit,
476
+ offset: params.offset
477
+ },
478
+ unwrap: false
479
+ });
450
480
  }
451
481
  /** Validates against the catalog and snapshots the current price. */
452
482
  addItem(input) {
@@ -455,6 +485,22 @@ var CartResource = class {
455
485
  retry: false
456
486
  });
457
487
  }
488
+ /**
489
+ * Adds up to MAX_BULK_ITEMS lines in one request. Per-line results carry
490
+ * the written line, plus the full updated cart.
491
+ */
492
+ addItems(input) {
493
+ const { idempotencyKey, ...body } = input;
494
+ return this.http.request(
495
+ "POST",
496
+ "/cart/items/bulk",
497
+ {
498
+ body,
499
+ headers: idempotencyHeaders(idempotencyKey),
500
+ retry: false
501
+ }
502
+ );
503
+ }
458
504
  /** `qty: 0` removes the line. */
459
505
  setQty(itemId, qty) {
460
506
  return this.http.request(
@@ -477,12 +523,11 @@ var CartResource = class {
477
523
  };
478
524
 
479
525
  // src/upload.ts
480
- function createUploadForm(file, field = "file") {
481
- const form = new FormData();
526
+ function appendFile(form, file, field) {
482
527
  if (typeof Blob !== "undefined" && file instanceof Blob) {
483
528
  const name = typeof File !== "undefined" && file instanceof File ? file.name : "upload";
484
529
  form.append(field, file, name);
485
- return form;
530
+ return;
486
531
  }
487
532
  if (typeof file !== "object" || file === null || !("data" in file) || typeof file.name !== "string") {
488
533
  throw new TypeError(
@@ -493,6 +538,19 @@ function createUploadForm(file, field = "file") {
493
538
  type: file.type ?? "application/octet-stream"
494
539
  });
495
540
  form.append(field, blob, file.name);
541
+ }
542
+ function createUploadForm(file, field = "file") {
543
+ const form = new FormData();
544
+ appendFile(form, file, field);
545
+ return form;
546
+ }
547
+ function createUploadFormMany(files, field = "files") {
548
+ const list = Array.isArray(files) ? files : [files];
549
+ if (list.length === 0) {
550
+ throw new TypeError("At least one UploadFile is required");
551
+ }
552
+ const form = new FormData();
553
+ for (const file of list) appendFile(form, file, field);
496
554
  return form;
497
555
  }
498
556
 
@@ -597,6 +655,21 @@ var InventoryResource = class {
597
655
  }
598
656
  });
599
657
  }
658
+ /**
659
+ * Reads availability for up to MAX_BULK_ITEMS variants in one request.
660
+ * Unknown ids are returned in `notFound` instead of failing.
661
+ */
662
+ getStockByVariants(variantIds) {
663
+ return this.http.request(
664
+ "GET",
665
+ "/admin/inventory/stock",
666
+ {
667
+ query: {
668
+ variantIds: Array.isArray(variantIds) ? variantIds : [variantIds]
669
+ }
670
+ }
671
+ );
672
+ }
600
673
  /** Upserts the on-hand quantity (restocking). */
601
674
  setStock(input) {
602
675
  return this.http.request("POST", "/admin/inventory/stock", {
@@ -604,6 +677,19 @@ var InventoryResource = class {
604
677
  retry: false
605
678
  });
606
679
  }
680
+ /** Sets on-hand stock for many SKUs; `atomic` commits all or none. */
681
+ setStockBulk(input) {
682
+ const { idempotencyKey, ...body } = input;
683
+ return this.http.request(
684
+ "POST",
685
+ "/admin/inventory/stock/bulk",
686
+ {
687
+ body,
688
+ headers: idempotencyHeaders(idempotencyKey),
689
+ retry: false
690
+ }
691
+ );
692
+ }
607
693
  /** Holds all lines atomically; one short line fails the whole order. */
608
694
  reserve(input) {
609
695
  return this.http.request(
@@ -645,9 +731,40 @@ var OrdersResource = class {
645
731
  retry: false
646
732
  });
647
733
  }
648
- /** Newest first. */
649
- list() {
650
- return this.http.request("GET", "/orders");
734
+ /** Newest first (paging meta is dropped). */
735
+ list(params = {}) {
736
+ return this.http.request("GET", "/orders", {
737
+ query: {
738
+ status: params.status,
739
+ currency: params.currency,
740
+ created_after: params.created_after,
741
+ created_before: params.created_before,
742
+ min_total_cents: params.min_total_cents,
743
+ max_total_cents: params.max_total_cents,
744
+ sort: params.sort,
745
+ page: params.page,
746
+ limit: params.limit,
747
+ offset: params.offset
748
+ }
749
+ });
750
+ }
751
+ /** Same filters as `list`, plus the `meta` paging counters. */
752
+ listPage(params = {}) {
753
+ return this.http.request("GET", "/orders", {
754
+ query: {
755
+ status: params.status,
756
+ currency: params.currency,
757
+ created_after: params.created_after,
758
+ created_before: params.created_before,
759
+ min_total_cents: params.min_total_cents,
760
+ max_total_cents: params.max_total_cents,
761
+ sort: params.sort,
762
+ page: params.page,
763
+ limit: params.limit,
764
+ offset: params.offset
765
+ },
766
+ unwrap: false
767
+ });
651
768
  }
652
769
  get(id) {
653
770
  return this.http.request("GET", `/orders/${segment(id)}`);
@@ -701,6 +818,7 @@ var ProductsResource = class {
701
818
  return this.http.request("GET", "/products", {
702
819
  query: {
703
820
  q: params.q,
821
+ ids: params.ids,
704
822
  category_id: params.category_id,
705
823
  category_slug: params.category_slug,
706
824
  min_price_cents: params.min_price_cents,
@@ -724,6 +842,7 @@ var ProductsResource = class {
724
842
  return this.http.request("GET", "/products", {
725
843
  query: {
726
844
  q: params.q,
845
+ ids: params.ids,
727
846
  category_id: params.category_id,
728
847
  category_slug: params.category_slug,
729
848
  min_price_cents: params.min_price_cents,
@@ -789,6 +908,86 @@ var ProductsResource = class {
789
908
  { form: createUploadForm(file), retry: false }
790
909
  );
791
910
  }
911
+ /**
912
+ * Admin only. Creates products in bulk. Per-item failures stay inside
913
+ * the 200 envelope; `atomic` rolls the whole batch back.
914
+ */
915
+ bulkCreate(input) {
916
+ const { idempotencyKey, ...body } = input;
917
+ return this.http.request(
918
+ "POST",
919
+ "/admin/products/bulk",
920
+ {
921
+ body,
922
+ headers: idempotencyHeaders(idempotencyKey),
923
+ retry: false
924
+ }
925
+ );
926
+ }
927
+ /** Admin only. Applies one partial patch per product id. */
928
+ bulkUpdate(input) {
929
+ const { idempotencyKey, ...body } = input;
930
+ return this.http.request(
931
+ "PATCH",
932
+ "/admin/products/bulk",
933
+ {
934
+ body,
935
+ headers: idempotencyHeaders(idempotencyKey),
936
+ retry: false
937
+ }
938
+ );
939
+ }
940
+ /**
941
+ * Admin only. POST rather than DELETE so the id list can travel in JSON.
942
+ * `dryRun` reports `wouldDelete` without writing.
943
+ */
944
+ bulkDelete(input) {
945
+ const { idempotencyKey, ...body } = input;
946
+ return this.http.request(
947
+ "POST",
948
+ "/admin/products/bulk-delete",
949
+ {
950
+ body,
951
+ headers: idempotencyHeaders(idempotencyKey),
952
+ retry: false
953
+ }
954
+ );
955
+ }
956
+ /** Admin only. Gallery rows, including uploads still queued (empty url). */
957
+ async listImages(id) {
958
+ const result = await this.http.request(
959
+ "GET",
960
+ `/admin/products/${segment(id)}/images`
961
+ );
962
+ return result.images;
963
+ }
964
+ /**
965
+ * Admin only. Multipart with repeated `files` parts (up to 20 per
966
+ * request). Each accepted file is queued asynchronously (HTTP 202).
967
+ */
968
+ async uploadImages(id, files) {
969
+ return this.http.request(
970
+ "POST",
971
+ `/admin/products/${segment(id)}/images`,
972
+ { form: createUploadFormMany(files), retry: false }
973
+ );
974
+ }
975
+ /** Admin only. Promotes the next ready image when the primary is deleted. */
976
+ deleteImage(id, imageId) {
977
+ return this.http.request(
978
+ "DELETE",
979
+ `/admin/products/${segment(id)}/images/${segment(imageId)}`,
980
+ { retry: false }
981
+ );
982
+ }
983
+ /** Admin only. Points the storefront primary image at one gallery row. */
984
+ setPrimaryImage(id, imageId) {
985
+ return this.http.request(
986
+ "PUT",
987
+ `/admin/products/${segment(id)}/images/${segment(imageId)}/primary`,
988
+ { retry: false }
989
+ );
990
+ }
792
991
  };
793
992
 
794
993
  // src/resources/reviews.ts
@@ -797,14 +996,47 @@ var ReviewsResource = class {
797
996
  this.http = http;
798
997
  }
799
998
  http;
800
- /** Public aggregate plus the newest reviews for a product. */
801
- summary(productId) {
999
+ /** Public aggregate plus one filtered review page (`summary.meta`). */
1000
+ summary(productId, params = {}) {
802
1001
  return this.http.request(
803
1002
  "GET",
804
1003
  `/products/${segment(productId)}/reviews`,
805
- { skipAuth: true }
1004
+ {
1005
+ query: {
1006
+ q: params.q,
1007
+ rating: params.rating,
1008
+ sort: params.sort,
1009
+ page: params.page,
1010
+ limit: params.limit,
1011
+ offset: params.offset
1012
+ },
1013
+ skipAuth: true
1014
+ }
806
1015
  );
807
1016
  }
1017
+ /** The caller's own reviews with product context (paging meta dropped). */
1018
+ listMine(params = {}) {
1019
+ return this.http.request("GET", "/reviews/me", {
1020
+ query: {
1021
+ sort: params.sort,
1022
+ page: params.page,
1023
+ limit: params.limit,
1024
+ offset: params.offset
1025
+ }
1026
+ });
1027
+ }
1028
+ /** Same as `listMine`, plus the `meta` paging counters. */
1029
+ listMinePage(params = {}) {
1030
+ return this.http.request("GET", "/reviews/me", {
1031
+ query: {
1032
+ sort: params.sort,
1033
+ page: params.page,
1034
+ limit: params.limit,
1035
+ offset: params.offset
1036
+ },
1037
+ unwrap: false
1038
+ });
1039
+ }
808
1040
  /** One review per user per product. */
809
1041
  create(productId, input) {
810
1042
  return this.http.request(
@@ -849,8 +1081,9 @@ var UsersResource = class {
849
1081
  me() {
850
1082
  return this.http.request("GET", "/users/me");
851
1083
  }
1084
+ /** Admin only. There is no public `GET /users/{id}` — owners use `me()`. */
852
1085
  get(id) {
853
- return this.http.request("GET", `/users/${segment(id)}`);
1086
+ return this.http.request("GET", `/admin/users/${segment(id)}`);
854
1087
  }
855
1088
  /** Admin only. User directory without envelope metadata. */
856
1089
  list(params = {}) {
@@ -930,6 +1163,32 @@ var VariantsResource = class {
930
1163
  { retry: false }
931
1164
  );
932
1165
  }
1166
+ /** Admin only. Creates up to MAX_BULK_ITEMS SKUs for one product. */
1167
+ createBulk(productId, input) {
1168
+ const { idempotencyKey, ...body } = input;
1169
+ return this.http.request(
1170
+ "POST",
1171
+ `/products/${segment(productId)}/variants/bulk`,
1172
+ {
1173
+ body,
1174
+ headers: idempotencyHeaders(idempotencyKey),
1175
+ retry: false
1176
+ }
1177
+ );
1178
+ }
1179
+ /** Admin only. Applies one partial patch per variant id. */
1180
+ updateBulk(input) {
1181
+ const { idempotencyKey, ...body } = input;
1182
+ return this.http.request(
1183
+ "PATCH",
1184
+ "/admin/variants/bulk",
1185
+ {
1186
+ body,
1187
+ headers: idempotencyHeaders(idempotencyKey),
1188
+ retry: false
1189
+ }
1190
+ );
1191
+ }
933
1192
  };
934
1193
 
935
1194
  // src/resources/wishlist.ts
@@ -938,9 +1197,32 @@ var WishlistResource = class {
938
1197
  this.http = http;
939
1198
  }
940
1199
  http;
941
- /** Newest first. */
942
- list() {
943
- return this.http.request("GET", "/wishlist");
1200
+ /** Newest first (paging meta is dropped). */
1201
+ list(params = {}) {
1202
+ return this.http.request("GET", "/wishlist", {
1203
+ query: {
1204
+ q: params.q,
1205
+ category_id: params.category_id,
1206
+ sort: params.sort,
1207
+ page: params.page,
1208
+ limit: params.limit,
1209
+ offset: params.offset
1210
+ }
1211
+ });
1212
+ }
1213
+ /** Same filters as `list`, plus the `meta` paging counters. */
1214
+ listPage(params = {}) {
1215
+ return this.http.request("GET", "/wishlist", {
1216
+ query: {
1217
+ q: params.q,
1218
+ category_id: params.category_id,
1219
+ sort: params.sort,
1220
+ page: params.page,
1221
+ limit: params.limit,
1222
+ offset: params.offset
1223
+ },
1224
+ unwrap: false
1225
+ });
944
1226
  }
945
1227
  /** Idempotent: re-hearting succeeds silently. */
946
1228
  add(productId) {
@@ -957,6 +1239,35 @@ var WishlistResource = class {
957
1239
  { retry: false }
958
1240
  );
959
1241
  }
1242
+ /** Hearts up to MAX_BULK_ITEMS products; already-hearted rows succeed. */
1243
+ addBulk(input) {
1244
+ const { idempotencyKey, ...body } = input;
1245
+ return this.http.request(
1246
+ "POST",
1247
+ "/wishlist/bulk",
1248
+ {
1249
+ body,
1250
+ headers: idempotencyHeaders(idempotencyKey),
1251
+ retry: false
1252
+ }
1253
+ );
1254
+ }
1255
+ /**
1256
+ * Unhearts by product id. `dryRun` reports `wouldDelete` without
1257
+ * writing; already-removed rows succeed.
1258
+ */
1259
+ removeBulk(input) {
1260
+ const { idempotencyKey, ...body } = input;
1261
+ return this.http.request(
1262
+ "POST",
1263
+ "/wishlist/bulk-delete",
1264
+ {
1265
+ body,
1266
+ headers: idempotencyHeaders(idempotencyKey),
1267
+ retry: false
1268
+ }
1269
+ );
1270
+ }
960
1271
  };
961
1272
 
962
1273
  // src/client.ts
@@ -1118,6 +1429,7 @@ exports.MalawiStoreError = MalawiStoreError;
1118
1429
  exports.buildQuery = buildQuery;
1119
1430
  exports.constructStripeEvent = constructStripeEvent;
1120
1431
  exports.createUploadForm = createUploadForm;
1432
+ exports.createUploadFormMany = createUploadFormMany;
1121
1433
  exports.isMalawiStoreError = isMalawiStoreError;
1122
1434
  exports.verifyStripeSignature = verifyStripeSignature;
1123
1435
  //# sourceMappingURL=index.cjs.map