@marianmeres/ecsuite 1.3.2 → 2.1.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.
Files changed (50) hide show
  1. package/AGENTS.md +134 -20
  2. package/API.md +108 -25
  3. package/README.md +127 -0
  4. package/dist/adapters/http/_http.d.ts +34 -0
  5. package/dist/adapters/http/_http.js +75 -0
  6. package/dist/adapters/http/cart.d.ts +21 -0
  7. package/dist/adapters/http/cart.js +52 -0
  8. package/dist/adapters/http/customer.d.ts +22 -0
  9. package/dist/adapters/http/customer.js +35 -0
  10. package/dist/adapters/http/mod.d.ts +21 -0
  11. package/dist/adapters/http/mod.js +20 -0
  12. package/dist/adapters/http/order.d.ts +24 -0
  13. package/dist/adapters/http/order.js +43 -0
  14. package/dist/adapters/http/payment.d.ts +32 -0
  15. package/dist/adapters/http/payment.js +77 -0
  16. package/dist/adapters/http/product.d.ts +18 -0
  17. package/dist/adapters/http/product.js +30 -0
  18. package/dist/adapters/http/wishlist.d.ts +19 -0
  19. package/dist/adapters/http/wishlist.js +42 -0
  20. package/dist/adapters/mock/cart.d.ts +4 -2
  21. package/dist/adapters/mock/cart.js +3 -7
  22. package/dist/adapters/mock/customer.d.ts +3 -1
  23. package/dist/adapters/mock/customer.js +3 -2
  24. package/dist/adapters/mock/order.d.ts +3 -1
  25. package/dist/adapters/mock/order.js +7 -5
  26. package/dist/adapters/mock/payment.d.ts +3 -1
  27. package/dist/adapters/mock/payment.js +34 -20
  28. package/dist/adapters/mock/product.d.ts +3 -1
  29. package/dist/adapters/mock/product.js +3 -1
  30. package/dist/adapters/mock/wishlist.d.ts +4 -2
  31. package/dist/adapters/mock/wishlist.js +3 -7
  32. package/dist/adapters/mod.d.ts +4 -1
  33. package/dist/adapters/mod.js +4 -1
  34. package/dist/domains/base.d.ts +13 -6
  35. package/dist/domains/base.js +31 -12
  36. package/dist/domains/cart.js +17 -0
  37. package/dist/domains/customer.js +18 -4
  38. package/dist/domains/order.d.ts +33 -15
  39. package/dist/domains/order.js +34 -20
  40. package/dist/domains/payment.js +16 -2
  41. package/dist/domains/product.d.ts +29 -40
  42. package/dist/domains/product.js +99 -81
  43. package/dist/domains/wishlist.js +4 -2
  44. package/dist/suite.d.ts +49 -1
  45. package/dist/suite.js +90 -8
  46. package/dist/types/adapter.d.ts +10 -7
  47. package/dist/types/events.d.ts +6 -2
  48. package/dist/types/state.d.ts +2 -0
  49. package/docs/future-improvements.md +116 -0
  50. package/package.json +15 -6
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @module adapters/http/_http
3
+ *
4
+ * Shared primitives for the built-in HTTP adapters.
5
+ *
6
+ * Adapters throw raw HTTP errors (`Error` with `.status` and `.body`
7
+ * attached). `BaseDomainManager` catches and normalizes them to
8
+ * `DomainError` at the call site — adapters don't normalize themselves.
9
+ */
10
+ export function resolveFetch(opts) {
11
+ return opts?.fetch ?? globalThis.fetch.bind(globalThis);
12
+ }
13
+ /** Trailing-slash-safe path joining. */
14
+ export function join(base, path) {
15
+ if (!base)
16
+ return path;
17
+ if (base.endsWith("/"))
18
+ return `${base.slice(0, -1)}${path}`;
19
+ return `${base}${path}`;
20
+ }
21
+ export function authHeaders(ctx) {
22
+ return ctx.jwt ? { Authorization: `Bearer ${ctx.jwt}` } : {};
23
+ }
24
+ export function sessionHeader(ctx) {
25
+ return ctx.sessionId ? { "X-Session-ID": String(ctx.sessionId) } : {};
26
+ }
27
+ /**
28
+ * Wrap `fetch`, merge auth + session + content-type headers, and throw a raw
29
+ * HTTP error (with `.status` and `.body`) on non-OK. Returns `undefined` on
30
+ * 204 No Content, otherwise the parsed JSON body.
31
+ */
32
+ export async function requestJson(doFetch, url, init, ctx) {
33
+ const hasBody = init.body !== undefined && init.body !== null;
34
+ const res = await doFetch(url, {
35
+ ...init,
36
+ headers: {
37
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
38
+ ...(init.headers ?? {}),
39
+ ...authHeaders(ctx),
40
+ ...sessionHeader(ctx),
41
+ },
42
+ signal: ctx.signal,
43
+ });
44
+ if (!res.ok) {
45
+ const text = await res.text();
46
+ throw Object.assign(new Error(text || res.statusText), {
47
+ status: res.status,
48
+ body: text,
49
+ });
50
+ }
51
+ if (res.status === 204)
52
+ return undefined;
53
+ return (await res.json());
54
+ }
55
+ /** Require `ctx.sessionId`; throw a client-side Error if missing. */
56
+ export function requireSessionId(ctx, operation) {
57
+ if (!ctx.sessionId) {
58
+ throw Object.assign(new Error(`sessionId required for ${operation}`), { status: 400, body: `sessionId required for ${operation}` });
59
+ }
60
+ return String(ctx.sessionId);
61
+ }
62
+ /** Require `ctx.jwt`; throw a client-side Error if missing. */
63
+ export function requireJwt(ctx, operation) {
64
+ if (!ctx.jwt) {
65
+ throw Object.assign(new Error(`jwt required for ${operation}`), { status: 401, body: `jwt required for ${operation}` });
66
+ }
67
+ return ctx.jwt;
68
+ }
69
+ /** Require `ctx.customerId`; throw a client-side Error if missing. */
70
+ export function requireCustomerId(ctx, operation) {
71
+ if (!ctx.customerId) {
72
+ throw Object.assign(new Error(`customerId required for ${operation}`), { status: 400, body: `customerId required for ${operation}` });
73
+ }
74
+ return String(ctx.customerId);
75
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module adapters/http/cart
3
+ *
4
+ * Built-in {@link CartAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/cart → { data: CartData }
7
+ * POST {baseUrl}/cart → { data: CartData } body: CartItem
8
+ * PUT {baseUrl}/cart → { data: CartData } body: { product_id, quantity }
9
+ * DELETE {baseUrl}/cart?product_id=... → { data: CartData } (remove single item)
10
+ * DELETE {baseUrl}/cart → { data: CartData } (clear)
11
+ *
12
+ * All mutations require `X-Session-ID` on the request. The adapter reads
13
+ * `ctx.sessionId` and throws a client-side error if missing so failures
14
+ * surface before the network round-trip.
15
+ */
16
+ import type { CartAdapter } from "../../types/adapter.js";
17
+ import { type HttpAdapterOptions } from "./_http.js";
18
+ /** Options for {@link createHttpCartAdapter}. */
19
+ export type HttpCartAdapterOptions = HttpAdapterOptions;
20
+ /** Build a cart adapter against the conventional `/cart` REST surface. */
21
+ export declare function createHttpCartAdapter(opts?: HttpCartAdapterOptions): CartAdapter;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @module adapters/http/cart
3
+ *
4
+ * Built-in {@link CartAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/cart → { data: CartData }
7
+ * POST {baseUrl}/cart → { data: CartData } body: CartItem
8
+ * PUT {baseUrl}/cart → { data: CartData } body: { product_id, quantity }
9
+ * DELETE {baseUrl}/cart?product_id=... → { data: CartData } (remove single item)
10
+ * DELETE {baseUrl}/cart → { data: CartData } (clear)
11
+ *
12
+ * All mutations require `X-Session-ID` on the request. The adapter reads
13
+ * `ctx.sessionId` and throws a client-side error if missing so failures
14
+ * surface before the network round-trip.
15
+ */
16
+ import { join, requestJson, requireSessionId, resolveFetch, } from "./_http.js";
17
+ /** Build a cart adapter against the conventional `/cart` REST surface. */
18
+ export function createHttpCartAdapter(opts = {}) {
19
+ const base = opts.baseUrl ?? "/api/session";
20
+ const doFetch = resolveFetch(opts);
21
+ const url = () => join(base, "/cart");
22
+ return {
23
+ async fetch(ctx) {
24
+ const r = await requestJson(doFetch, url(), { method: "GET" }, ctx);
25
+ return r.data;
26
+ },
27
+ async addItem(item, ctx) {
28
+ requireSessionId(ctx, "cart.addItem");
29
+ const r = await requestJson(doFetch, url(), { method: "POST", body: JSON.stringify(item) }, ctx);
30
+ return r.data;
31
+ },
32
+ async updateItem(productId, quantity, ctx) {
33
+ requireSessionId(ctx, "cart.updateItem");
34
+ const r = await requestJson(doFetch, url(), {
35
+ method: "PUT",
36
+ body: JSON.stringify({ product_id: productId, quantity }),
37
+ }, ctx);
38
+ return r.data;
39
+ },
40
+ async removeItem(productId, ctx) {
41
+ requireSessionId(ctx, "cart.removeItem");
42
+ const qs = new URLSearchParams({ product_id: String(productId) });
43
+ const r = await requestJson(doFetch, `${url()}?${qs}`, { method: "DELETE" }, ctx);
44
+ return r.data;
45
+ },
46
+ async clear(ctx) {
47
+ requireSessionId(ctx, "cart.clear");
48
+ const r = await requestJson(doFetch, url(), { method: "DELETE" }, ctx);
49
+ return r.data;
50
+ },
51
+ };
52
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module adapters/http/customer
3
+ *
4
+ * Built-in {@link CustomerAdapter} targeting the owner-scoped customer
5
+ * REST surface:
6
+ *
7
+ * GET {baseUrl}/me/col/customer/:customerId → { model_id, data: CustomerData }
8
+ * PUT {baseUrl}/me/col/customer/:customerId → { model_id, data: CustomerData }
9
+ *
10
+ * Both calls require `Authorization: Bearer <jwt>` + a `customerId` on the
11
+ * context (typically resolved from the login subject claim).
12
+ *
13
+ * `fetchBySession` is intentionally not implemented — there is no clean
14
+ * session-scoped read endpoint on the target REST surface; consumers who
15
+ * need session-based bootstrapping should hydrate from the JWT instead.
16
+ */
17
+ import type { CustomerAdapter } from "../../types/adapter.js";
18
+ import { type HttpAdapterOptions } from "./_http.js";
19
+ /** Options for {@link createHttpCustomerAdapter}. */
20
+ export type HttpCustomerAdapterOptions = HttpAdapterOptions;
21
+ /** Build a customer adapter against the conventional owner-scoped REST surface. */
22
+ export declare function createHttpCustomerAdapter(opts?: HttpCustomerAdapterOptions): CustomerAdapter;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module adapters/http/customer
3
+ *
4
+ * Built-in {@link CustomerAdapter} targeting the owner-scoped customer
5
+ * REST surface:
6
+ *
7
+ * GET {baseUrl}/me/col/customer/:customerId → { model_id, data: CustomerData }
8
+ * PUT {baseUrl}/me/col/customer/:customerId → { model_id, data: CustomerData }
9
+ *
10
+ * Both calls require `Authorization: Bearer <jwt>` + a `customerId` on the
11
+ * context (typically resolved from the login subject claim).
12
+ *
13
+ * `fetchBySession` is intentionally not implemented — there is no clean
14
+ * session-scoped read endpoint on the target REST surface; consumers who
15
+ * need session-based bootstrapping should hydrate from the JWT instead.
16
+ */
17
+ import { join, requestJson, requireCustomerId, resolveFetch, } from "./_http.js";
18
+ /** Build a customer adapter against the conventional owner-scoped REST surface. */
19
+ export function createHttpCustomerAdapter(opts = {}) {
20
+ const base = opts.baseUrl ?? "/api/customer";
21
+ const doFetch = resolveFetch(opts);
22
+ const url = (customerId) => join(base, `/me/col/customer/${encodeURIComponent(customerId)}`);
23
+ return {
24
+ async fetch(ctx) {
25
+ const customerId = requireCustomerId(ctx, "customer.fetch");
26
+ const r = await requestJson(doFetch, url(customerId), { method: "GET" }, ctx);
27
+ return r.data;
28
+ },
29
+ async update(data, ctx) {
30
+ const customerId = requireCustomerId(ctx, "customer.update");
31
+ const r = await requestJson(doFetch, url(customerId), { method: "PUT", body: JSON.stringify(data) }, ctx);
32
+ return r.data;
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module adapters/http
3
+ *
4
+ * Built-in HTTP adapters targeting a conventional REST surface.
5
+ *
6
+ * Each factory takes `{ baseUrl?, fetch? }` and returns an adapter that
7
+ * conforms to the matching interface from `../../types/adapter.ts`.
8
+ * Adapters throw raw HTTP errors (`Error` with `.status` + `.body`);
9
+ * domain managers normalize them to `DomainError`.
10
+ *
11
+ * Authentication is carried on the context passed to each call:
12
+ * - `ctx.sessionId` → emitted as `X-Session-ID`
13
+ * - `ctx.jwt` → emitted as `Authorization: Bearer <jwt>`
14
+ */
15
+ export { type HttpAdapterOptions, } from "./_http.js";
16
+ export { createHttpCartAdapter, type HttpCartAdapterOptions } from "./cart.js";
17
+ export { createHttpWishlistAdapter, type HttpWishlistAdapterOptions, } from "./wishlist.js";
18
+ export { createHttpOrderAdapter, type HttpOrderAdapterOptions } from "./order.js";
19
+ export { createHttpCustomerAdapter, type HttpCustomerAdapterOptions, } from "./customer.js";
20
+ export { createHttpPaymentAdapter, type HttpPaymentAdapterOptions, } from "./payment.js";
21
+ export { createHttpProductAdapter, type HttpProductAdapterOptions, } from "./product.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @module adapters/http
3
+ *
4
+ * Built-in HTTP adapters targeting a conventional REST surface.
5
+ *
6
+ * Each factory takes `{ baseUrl?, fetch? }` and returns an adapter that
7
+ * conforms to the matching interface from `../../types/adapter.ts`.
8
+ * Adapters throw raw HTTP errors (`Error` with `.status` + `.body`);
9
+ * domain managers normalize them to `DomainError`.
10
+ *
11
+ * Authentication is carried on the context passed to each call:
12
+ * - `ctx.sessionId` → emitted as `X-Session-ID`
13
+ * - `ctx.jwt` → emitted as `Authorization: Bearer <jwt>`
14
+ */
15
+ export { createHttpCartAdapter } from "./cart.js";
16
+ export { createHttpWishlistAdapter, } from "./wishlist.js";
17
+ export { createHttpOrderAdapter } from "./order.js";
18
+ export { createHttpCustomerAdapter, } from "./customer.js";
19
+ export { createHttpPaymentAdapter, } from "./payment.js";
20
+ export { createHttpProductAdapter, } from "./product.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @module adapters/http/order
3
+ *
4
+ * Built-in {@link OrderAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/col/order → { data: [{ model_id, data }, ...] }
7
+ * GET {baseUrl}/col/order/:id → { model_id, data: OrderData }
8
+ * POST {baseUrl}/checkout/start → { order_id, order: OrderData, ... }
9
+ *
10
+ * `create()` only starts checkout — it calls `POST /checkout/start` and
11
+ * returns the freshly-created pending order. The multi-step completion
12
+ * flow (addresses → delivery → payment → complete) is not wrapped by this
13
+ * adapter; callers drive it via their own HTTP calls until ecsuite grows
14
+ * dedicated verbs.
15
+ *
16
+ * Read endpoints require `Authorization: Bearer <jwt>`; checkout/start
17
+ * additionally requires `X-Session-ID`.
18
+ */
19
+ import type { OrderAdapter } from "../../types/adapter.js";
20
+ import { type HttpAdapterOptions } from "./_http.js";
21
+ /** Options for {@link createHttpOrderAdapter}. */
22
+ export type HttpOrderAdapterOptions = HttpAdapterOptions;
23
+ /** Build an order adapter against the conventional `/api/order` REST surface. */
24
+ export declare function createHttpOrderAdapter(opts?: HttpOrderAdapterOptions): OrderAdapter;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @module adapters/http/order
3
+ *
4
+ * Built-in {@link OrderAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/col/order → { data: [{ model_id, data }, ...] }
7
+ * GET {baseUrl}/col/order/:id → { model_id, data: OrderData }
8
+ * POST {baseUrl}/checkout/start → { order_id, order: OrderData, ... }
9
+ *
10
+ * `create()` only starts checkout — it calls `POST /checkout/start` and
11
+ * returns the freshly-created pending order. The multi-step completion
12
+ * flow (addresses → delivery → payment → complete) is not wrapped by this
13
+ * adapter; callers drive it via their own HTTP calls until ecsuite grows
14
+ * dedicated verbs.
15
+ *
16
+ * Read endpoints require `Authorization: Bearer <jwt>`; checkout/start
17
+ * additionally requires `X-Session-ID`.
18
+ */
19
+ import { join, requestJson, requireSessionId, resolveFetch, } from "./_http.js";
20
+ /** Build an order adapter against the conventional `/api/order` REST surface. */
21
+ export function createHttpOrderAdapter(opts = {}) {
22
+ const base = opts.baseUrl ?? "/api/order";
23
+ const doFetch = resolveFetch(opts);
24
+ return {
25
+ async fetchAll(ctx) {
26
+ const r = await requestJson(doFetch, join(base, "/col/order"), { method: "GET" }, ctx);
27
+ return r.data ?? [];
28
+ },
29
+ async fetchOne(orderId, ctx) {
30
+ return await requestJson(doFetch, join(base, `/col/order/${encodeURIComponent(String(orderId))}`), { method: "GET" }, ctx);
31
+ },
32
+ async create(order, ctx) {
33
+ requireSessionId(ctx, "order.create");
34
+ const body = {
35
+ email: order.customer_email,
36
+ };
37
+ if (ctx.customerId)
38
+ body.customer_id = ctx.customerId;
39
+ const r = await requestJson(doFetch, join(base, "/checkout/start"), { method: "POST", body: JSON.stringify(body) }, ctx);
40
+ return { model_id: r.order_id, data: r.order };
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @module adapters/http/payment
3
+ *
4
+ * Built-in {@link PaymentAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/by-order/:orderId → { data: [{ model_id, data: PaymentData }, ...] }
7
+ * GET {baseUrl}/col/payment/:id → { model_id, data: PaymentData }
8
+ * POST {baseUrl}/initiate → { payment_id, redirect_url }
9
+ *
10
+ * All calls require `X-Session-ID`; read endpoints additionally take a JWT
11
+ * if present.
12
+ *
13
+ * `initiate` targets a domain-scoped entry point that shares a service seam
14
+ * with the order-checkout payment step. The server derives `amount` and
15
+ * `currency` from the order record (not from the client) and only accepts
16
+ * an initiation once the order has passed checkout validation (addresses +
17
+ * delivery set). Callers must supply `provider`, `return_url`, and
18
+ * `cancel_url` through `PaymentInitConfig` — `return_url` is typed on the
19
+ * canonical config; `cancel_url` is read off the open index signature.
20
+ *
21
+ * `capture()` is intentionally not wired — the target REST surface does not
22
+ * expose a client-facing capture endpoint (capture is driven server-side by
23
+ * provider webhooks + the checkout/complete flow). The returned adapter
24
+ * omits `capture`, so `PaymentManager.capture()` surfaces a NOT_IMPLEMENTED
25
+ * error as designed.
26
+ */
27
+ import type { PaymentAdapter } from "../../types/adapter.js";
28
+ import { type HttpAdapterOptions } from "./_http.js";
29
+ /** Options for {@link createHttpPaymentAdapter}. */
30
+ export type HttpPaymentAdapterOptions = HttpAdapterOptions;
31
+ /** Build a payment adapter against the conventional `/api/payment` REST surface. */
32
+ export declare function createHttpPaymentAdapter(opts?: HttpPaymentAdapterOptions): PaymentAdapter;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @module adapters/http/payment
3
+ *
4
+ * Built-in {@link PaymentAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/by-order/:orderId → { data: [{ model_id, data: PaymentData }, ...] }
7
+ * GET {baseUrl}/col/payment/:id → { model_id, data: PaymentData }
8
+ * POST {baseUrl}/initiate → { payment_id, redirect_url }
9
+ *
10
+ * All calls require `X-Session-ID`; read endpoints additionally take a JWT
11
+ * if present.
12
+ *
13
+ * `initiate` targets a domain-scoped entry point that shares a service seam
14
+ * with the order-checkout payment step. The server derives `amount` and
15
+ * `currency` from the order record (not from the client) and only accepts
16
+ * an initiation once the order has passed checkout validation (addresses +
17
+ * delivery set). Callers must supply `provider`, `return_url`, and
18
+ * `cancel_url` through `PaymentInitConfig` — `return_url` is typed on the
19
+ * canonical config; `cancel_url` is read off the open index signature.
20
+ *
21
+ * `capture()` is intentionally not wired — the target REST surface does not
22
+ * expose a client-facing capture endpoint (capture is driven server-side by
23
+ * provider webhooks + the checkout/complete flow). The returned adapter
24
+ * omits `capture`, so `PaymentManager.capture()` surfaces a NOT_IMPLEMENTED
25
+ * error as designed.
26
+ */
27
+ import { join, requestJson, requireSessionId, resolveFetch, } from "./_http.js";
28
+ function unwrapPayment(envelope) {
29
+ const e = envelope;
30
+ if (e && typeof e === "object" && e.data && "provider" in e.data) {
31
+ return e.data;
32
+ }
33
+ return envelope;
34
+ }
35
+ /** Build a payment adapter against the conventional `/api/payment` REST surface. */
36
+ export function createHttpPaymentAdapter(opts = {}) {
37
+ const base = opts.baseUrl ?? "/api/payment";
38
+ const doFetch = resolveFetch(opts);
39
+ return {
40
+ async fetchForOrder(orderId, ctx) {
41
+ requireSessionId(ctx, "payment.fetchForOrder");
42
+ const r = await requestJson(doFetch, join(base, `/by-order/${encodeURIComponent(String(orderId))}`), { method: "GET" }, ctx);
43
+ return (r.data ?? []).map(unwrapPayment);
44
+ },
45
+ async fetchOne(paymentId, ctx) {
46
+ const r = await requestJson(doFetch, join(base, `/col/payment/${encodeURIComponent(String(paymentId))}`), { method: "GET" }, ctx);
47
+ return unwrapPayment(r);
48
+ },
49
+ async initiate(orderId, config, ctx) {
50
+ requireSessionId(ctx, "payment.initiate");
51
+ const returnUrl = config.return_url;
52
+ const cancelUrl = config.cancel_url;
53
+ if (typeof returnUrl !== "string" || !returnUrl) {
54
+ throw Object.assign(new Error("return_url required for payment.initiate"), {
55
+ status: 400,
56
+ body: "return_url required for payment.initiate",
57
+ });
58
+ }
59
+ if (typeof cancelUrl !== "string" || !cancelUrl) {
60
+ throw Object.assign(new Error("cancel_url required for payment.initiate"), {
61
+ status: 400,
62
+ body: "cancel_url required for payment.initiate",
63
+ });
64
+ }
65
+ const r = await requestJson(doFetch, join(base, "/initiate"), {
66
+ method: "POST",
67
+ body: JSON.stringify({
68
+ order_id: orderId,
69
+ provider: config.provider,
70
+ return_url: returnUrl,
71
+ cancel_url: cancelUrl,
72
+ }),
73
+ }, ctx);
74
+ return { id: r.payment_id, redirect_url: r.redirect_url };
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @module adapters/http/product
3
+ *
4
+ * Built-in {@link ProductAdapter} targeting a generic collection REST surface:
5
+ *
6
+ * GET {baseUrl}/col/product/:id → { model_id, data: ProductData, ... }
7
+ *
8
+ * The `{ model_id, data }` model envelope is unwrapped; the adapter returns
9
+ * bare `ProductData` / `ProductData[]` to conform to the interface.
10
+ *
11
+ * There is no batch endpoint — `fetchMany` issues parallel GETs.
12
+ */
13
+ import type { ProductAdapter } from "../../types/adapter.js";
14
+ import { type HttpAdapterOptions } from "./_http.js";
15
+ /** Options for {@link createHttpProductAdapter}. */
16
+ export type HttpProductAdapterOptions = HttpAdapterOptions;
17
+ /** Build a product adapter against the conventional `/col/product` REST surface. */
18
+ export declare function createHttpProductAdapter(opts?: HttpProductAdapterOptions): ProductAdapter;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module adapters/http/product
3
+ *
4
+ * Built-in {@link ProductAdapter} targeting a generic collection REST surface:
5
+ *
6
+ * GET {baseUrl}/col/product/:id → { model_id, data: ProductData, ... }
7
+ *
8
+ * The `{ model_id, data }` model envelope is unwrapped; the adapter returns
9
+ * bare `ProductData` / `ProductData[]` to conform to the interface.
10
+ *
11
+ * There is no batch endpoint — `fetchMany` issues parallel GETs.
12
+ */
13
+ import { join, requestJson, resolveFetch, } from "./_http.js";
14
+ /** Build a product adapter against the conventional `/col/product` REST surface. */
15
+ export function createHttpProductAdapter(opts = {}) {
16
+ const base = opts.baseUrl ?? "/api/product";
17
+ const doFetch = resolveFetch(opts);
18
+ async function fetchOne(productId, ctx) {
19
+ const r = await requestJson(doFetch, join(base, `/col/product/${encodeURIComponent(String(productId))}`), { method: "GET" }, ctx);
20
+ return r.data;
21
+ }
22
+ return {
23
+ fetchOne,
24
+ async fetchMany(productIds, ctx) {
25
+ if (productIds.length === 0)
26
+ return [];
27
+ return Promise.all(productIds.map((id) => fetchOne(id, ctx)));
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module adapters/http/wishlist
3
+ *
4
+ * Built-in {@link WishlistAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/wishlist → { data: WishlistData }
7
+ * POST {baseUrl}/wishlist → { data: WishlistData, added?: boolean }
8
+ * DELETE {baseUrl}/wishlist?product_id=... → { data: WishlistData }
9
+ * DELETE {baseUrl}/wishlist → { data: WishlistData } (clear)
10
+ *
11
+ * Mutations require `X-Session-ID`. Add/toggle is idempotent on the server
12
+ * side — adding a product already present is a no-op for the wishlist state.
13
+ */
14
+ import type { WishlistAdapter } from "../../types/adapter.js";
15
+ import { type HttpAdapterOptions } from "./_http.js";
16
+ /** Options for {@link createHttpWishlistAdapter}. */
17
+ export type HttpWishlistAdapterOptions = HttpAdapterOptions;
18
+ /** Build a wishlist adapter against the conventional `/wishlist` REST surface. */
19
+ export declare function createHttpWishlistAdapter(opts?: HttpWishlistAdapterOptions): WishlistAdapter;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @module adapters/http/wishlist
3
+ *
4
+ * Built-in {@link WishlistAdapter} targeting a REST surface of the shape:
5
+ *
6
+ * GET {baseUrl}/wishlist → { data: WishlistData }
7
+ * POST {baseUrl}/wishlist → { data: WishlistData, added?: boolean }
8
+ * DELETE {baseUrl}/wishlist?product_id=... → { data: WishlistData }
9
+ * DELETE {baseUrl}/wishlist → { data: WishlistData } (clear)
10
+ *
11
+ * Mutations require `X-Session-ID`. Add/toggle is idempotent on the server
12
+ * side — adding a product already present is a no-op for the wishlist state.
13
+ */
14
+ import { join, requestJson, requireSessionId, resolveFetch, } from "./_http.js";
15
+ /** Build a wishlist adapter against the conventional `/wishlist` REST surface. */
16
+ export function createHttpWishlistAdapter(opts = {}) {
17
+ const base = opts.baseUrl ?? "/api/session";
18
+ const doFetch = resolveFetch(opts);
19
+ const url = () => join(base, "/wishlist");
20
+ return {
21
+ async fetch(ctx) {
22
+ const r = await requestJson(doFetch, url(), { method: "GET" }, ctx);
23
+ return r.data;
24
+ },
25
+ async addItem(productId, ctx) {
26
+ requireSessionId(ctx, "wishlist.addItem");
27
+ const r = await requestJson(doFetch, url(), { method: "POST", body: JSON.stringify({ product_id: productId }) }, ctx);
28
+ return r.data;
29
+ },
30
+ async removeItem(productId, ctx) {
31
+ requireSessionId(ctx, "wishlist.removeItem");
32
+ const qs = new URLSearchParams({ product_id: String(productId) });
33
+ const r = await requestJson(doFetch, `${url()}?${qs}`, { method: "DELETE" }, ctx);
34
+ return r.data;
35
+ },
36
+ async clear(ctx) {
37
+ requireSessionId(ctx, "wishlist.clear");
38
+ const r = await requestJson(doFetch, url(), { method: "DELETE" }, ctx);
39
+ return r.data;
40
+ },
41
+ };
42
+ }
@@ -2,6 +2,7 @@
2
2
  * Mock cart adapter for testing.
3
3
  */
4
4
  import type { CartData } from "@marianmeres/collection-types";
5
+ import { HTTP_ERROR } from "@marianmeres/http-utils";
5
6
  import type { CartAdapter } from "../../types/adapter.js";
6
7
  /** Mock cart adapter options */
7
8
  export interface MockCartAdapterOptions {
@@ -11,8 +12,9 @@ export interface MockCartAdapterOptions {
11
12
  delay?: number;
12
13
  /** Force errors for testing */
13
14
  forceError?: {
14
- operation?: "fetch" | "addItem" | "updateItem" | "removeItem" | "clear" | "sync";
15
- code?: string;
15
+ operation?: "fetch" | "addItem" | "updateItem" | "removeItem" | "clear";
16
+ /** HTTP error class name from `HTTP_ERROR` (default: "BadRequest") */
17
+ code?: keyof typeof HTTP_ERROR;
16
18
  message?: string;
17
19
  };
18
20
  }
@@ -11,7 +11,9 @@ export function createMockCartAdapter(options = {}) {
11
11
  const wait = () => new Promise((r) => setTimeout(r, delay));
12
12
  const maybeThrow = (operation) => {
13
13
  if (options.forceError?.operation === operation) {
14
- throw new HTTP_ERROR.BadRequest(options.forceError.message ?? `Mock error for ${operation}`);
14
+ const code = options.forceError.code ?? "BadRequest";
15
+ const Ctor = HTTP_ERROR[code] ?? HTTP_ERROR.BadRequest;
16
+ throw new Ctor(options.forceError.message ?? `Mock error for ${operation}`);
15
17
  }
16
18
  };
17
19
  return {
@@ -58,11 +60,5 @@ export function createMockCartAdapter(options = {}) {
58
60
  cart = { items: [] };
59
61
  return structuredClone(cart);
60
62
  },
61
- async sync(newCart, _ctx) {
62
- await wait();
63
- maybeThrow("sync");
64
- cart = structuredClone(newCart);
65
- return structuredClone(cart);
66
- },
67
63
  };
68
64
  }
@@ -2,6 +2,7 @@
2
2
  * Mock customer adapter for testing.
3
3
  */
4
4
  import type { CustomerData } from "@marianmeres/collection-types";
5
+ import { HTTP_ERROR } from "@marianmeres/http-utils";
5
6
  import type { CustomerAdapter } from "../../types/adapter.js";
6
7
  /** Mock customer adapter options */
7
8
  export interface MockCustomerAdapterOptions {
@@ -14,7 +15,8 @@ export interface MockCustomerAdapterOptions {
14
15
  /** Force errors for testing */
15
16
  forceError?: {
16
17
  operation?: "fetch" | "fetchBySession" | "update";
17
- code?: string;
18
+ /** HTTP error class name from `HTTP_ERROR` (default: "BadRequest") */
19
+ code?: keyof typeof HTTP_ERROR;
18
20
  message?: string;
19
21
  };
20
22
  }
@@ -14,8 +14,9 @@ export function createMockCustomerAdapter(options = {}) {
14
14
  const wait = () => new Promise((r) => setTimeout(r, delay));
15
15
  const maybeThrow = (operation) => {
16
16
  if (options.forceError?.operation === operation) {
17
- throw new HTTP_ERROR.BadRequest(options.forceError.message ??
18
- `Mock error for ${operation}`);
17
+ const code = options.forceError.code ?? "BadRequest";
18
+ const Ctor = HTTP_ERROR[code] ?? HTTP_ERROR.BadRequest;
19
+ throw new Ctor(options.forceError.message ?? `Mock error for ${operation}`);
19
20
  }
20
21
  };
21
22
  const hasFetchBySession = "guestData" in options ||
@@ -2,6 +2,7 @@
2
2
  * Mock order adapter for testing.
3
3
  */
4
4
  import type { OrderData } from "@marianmeres/collection-types";
5
+ import { HTTP_ERROR } from "@marianmeres/http-utils";
5
6
  import type { OrderAdapter } from "../../types/adapter.js";
6
7
  /** Mock order adapter options */
7
8
  export interface MockOrderAdapterOptions {
@@ -12,7 +13,8 @@ export interface MockOrderAdapterOptions {
12
13
  /** Force errors for testing */
13
14
  forceError?: {
14
15
  operation?: "fetchAll" | "fetchOne" | "create";
15
- code?: string;
16
+ /** HTTP error class name from `HTTP_ERROR` (default: "BadRequest") */
17
+ code?: keyof typeof HTTP_ERROR;
16
18
  message?: string;
17
19
  };
18
20
  }