@marianmeres/ecsuite 2.0.0 → 2.2.1

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/AGENTS.md CHANGED
@@ -58,15 +58,27 @@ src/
58
58
  │ ├── payment.ts # PaymentManager
59
59
  │ └── product.ts # ProductManager
60
60
  └── adapters/
61
- ├── mod.ts # Adapter exports
62
- └── mock/ # Mock adapters for testing
61
+ ├── mod.ts # Adapter exports (re-exports mock/* and http/*)
62
+ ├── mock/ # Mock adapters for testing
63
+ │ ├── mod.ts
64
+ │ ├── cart.ts
65
+ │ ├── wishlist.ts
66
+ │ ├── order.ts
67
+ │ ├── customer.ts
68
+ │ ├── payment.ts
69
+ │ └── product.ts
70
+ └── http/ # Built-in HTTP adapters
63
71
  ├── mod.ts
64
- ├── cart.ts
65
- ├── wishlist.ts
66
- ├── order.ts
67
- ├── customer.ts
68
- ├── payment.ts
69
- └── product.ts
72
+ ├── _http.ts # Shared fetch primitives (authHeaders, sessionHeader, requestJson)
73
+ ├── cart.ts # createHttpCartAdapter
74
+ ├── wishlist.ts # createHttpWishlistAdapter
75
+ ├── order.ts # createHttpOrderAdapter
76
+ ├── customer.ts # createHttpCustomerAdapter
77
+ ├── payment.ts # createHttpPaymentAdapter (capture omitted)
78
+ └── product.ts # createHttpProductAdapter
79
+ example/
80
+ ├── index.html # Vanilla-JS reference harness — every public verb
81
+ └── src/app.ts # imports ../../src/mod.ts
70
82
  tests/
71
83
  ├── cart.test.ts
72
84
  ├── wishlist.test.ts
@@ -74,6 +86,7 @@ tests/
74
86
  ├── customer.test.ts
75
87
  ├── payment.test.ts
76
88
  ├── product.test.ts
89
+ ├── http-adapters.test.ts # Unit tests for the built-in HTTP adapters
77
90
  └── ecsuite.test.ts
78
91
  ```
79
92
 
@@ -143,8 +156,74 @@ export {
143
156
  MockProductAdapterOptions,
144
157
  MockWishlistAdapterOptions,
145
158
  } from "./adapters/mod.ts";
159
+
160
+ // Built-in HTTP Adapters
161
+ export {
162
+ createHttpCartAdapter,
163
+ createHttpCustomerAdapter,
164
+ createHttpOrderAdapter,
165
+ createHttpPaymentAdapter,
166
+ createHttpProductAdapter,
167
+ createHttpWishlistAdapter,
168
+ HttpAdapterOptions,
169
+ HttpCartAdapterOptions,
170
+ HttpCustomerAdapterOptions,
171
+ HttpOrderAdapterOptions,
172
+ HttpPaymentAdapterOptions,
173
+ HttpProductAdapterOptions,
174
+ HttpWishlistAdapterOptions,
175
+ } from "./adapters/mod.ts";
146
176
  ```
147
177
 
178
+ ## Built-in HTTP Adapters
179
+
180
+ Each factory returns an object conforming to the matching `*Adapter`
181
+ interface in `src/types/adapter.ts`. They target a conventional commerce
182
+ REST surface; see the `README.md` "Built-in HTTP Adapters" section for the
183
+ endpoint table.
184
+
185
+ Key rules:
186
+
187
+ - Adapters are **thin** — they throw raw HTTP errors (`Error` with
188
+ `.status` and `.body` attached); the domain manager normalizes them to
189
+ `DomainError`. Don't normalize inside the adapter.
190
+ - Authentication comes off `DomainContext`:
191
+ - `ctx.jwt` → `Authorization: Bearer <jwt>`
192
+ - `ctx.sessionId` → `X-Session-ID`
193
+ - `ctx.customerId` is required for the customer adapter's
194
+ owner-scoped route.
195
+ - `_http.ts` holds the shared primitives (`authHeaders`, `sessionHeader`,
196
+ `requestJson`, `join`, `require*Id`); every adapter is ~50–70 lines.
197
+ - `createHttpOrderAdapter.create()` is a **single-call** adapter
198
+ targeting `POST /checkout/start`. The addresses → delivery → payment →
199
+ complete checkout flow is not wrapped by the adapter in v2.x; callers
200
+ drive the remaining steps directly.
201
+ - `createHttpPaymentAdapter.initiate()` posts `{ order_id, provider,
202
+ return_url, cancel_url }` to `POST {baseUrl}/initiate`. **No
203
+ `amount`/`currency` in the body** — the server derives them from the
204
+ order row (security: prevents client-side tampering), and the target
205
+ route strictly requires all four listed fields. The adapter throws a
206
+ client-side error if `return_url` or `cancel_url` is missing from the
207
+ `PaymentInitConfig`. The server additionally requires the order to have
208
+ passed checkout validation (addresses + delivery set) before initiating
209
+ payment — callers are responsible for the upstream steps.
210
+ - `createHttpPaymentAdapter` intentionally omits `capture` — consumers
211
+ calling `suite.payment.capture()` will get `NOT_IMPLEMENTED` from the
212
+ manager. Capture is typically server-driven (webhooks + checkout
213
+ complete).
214
+ - `createHttpCustomerAdapter` omits `fetchBySession` — there is no clean
215
+ session-scoped read endpoint on the target surface. Consumers should
216
+ hydrate customer state from the JWT subject claim or pass an explicit
217
+ `customerId` on context.
218
+
219
+ ## Reference Harness
220
+
221
+ `example/` ships a vanilla-JS harness that exercises every public verb of
222
+ every domain manager. It can swap between mock adapters (zero-setup
223
+ default) and the built-in HTTP adapters (real server round-trip). Runs
224
+ through `deno task example:build` / `example:watch` + any static file
225
+ server.
226
+
148
227
  ## State Machine
149
228
 
150
229
  ```
package/README.md CHANGED
@@ -144,6 +144,63 @@ const suite = createECSuite({
144
144
  });
145
145
  ```
146
146
 
147
+ ## Built-in HTTP Adapters
148
+
149
+ For consumers whose backend exposes the conventional commerce REST surface,
150
+ ecsuite ships ready-to-use HTTP adapters for every domain. Each factory
151
+ takes `{ baseUrl?, fetch? }`; authentication is carried on the context
152
+ passed into each call (`ctx.sessionId` → `X-Session-ID`; `ctx.jwt` →
153
+ `Authorization: Bearer <jwt>`).
154
+
155
+ ```typescript
156
+ import {
157
+ createECSuite,
158
+ createHttpCartAdapter,
159
+ createHttpCustomerAdapter,
160
+ createHttpOrderAdapter,
161
+ createHttpPaymentAdapter,
162
+ createHttpProductAdapter,
163
+ createHttpWishlistAdapter,
164
+ } from "@marianmeres/ecsuite";
165
+
166
+ const suite = createECSuite({
167
+ context: { sessionId: mySessionId, jwt: myJwt, customerId: myCustomerId },
168
+ adapters: {
169
+ cart: createHttpCartAdapter({ baseUrl: "/api/session" }),
170
+ wishlist: createHttpWishlistAdapter({ baseUrl: "/api/session" }),
171
+ order: createHttpOrderAdapter({ baseUrl: "/api/order" }),
172
+ customer: createHttpCustomerAdapter({ baseUrl: "/api/customer" }),
173
+ payment: createHttpPaymentAdapter({ baseUrl: "/api/payment" }),
174
+ product: createHttpProductAdapter({ baseUrl: "/api/product" }),
175
+ },
176
+ });
177
+ ```
178
+
179
+ Expected endpoints per adapter (all mutations require `X-Session-ID`, all
180
+ owner-scoped reads require a JWT):
181
+
182
+ | Adapter | Endpoints |
183
+ | -------- | --------------------------------------------------------------------------------------------------- |
184
+ | cart | `GET/POST/PUT/DELETE {baseUrl}/cart` (DELETE with optional `?product_id=` for single-item remove) |
185
+ | wishlist | `GET/POST/DELETE {baseUrl}/wishlist` (DELETE with optional `?product_id=` for single-item remove) |
186
+ | order | `GET {baseUrl}/col/order/mod`, `GET {baseUrl}/col/order/mod/:id`, `POST {baseUrl}/checkout/start` |
187
+ | customer | `GET/PUT {baseUrl}/me/col/customer/mod/:customerId` |
188
+ | payment | `GET {baseUrl}/by-order/:orderId`, `GET {baseUrl}/col/payment/mod/:id`, `POST {baseUrl}/initiate` (body: `{ order_id, provider, return_url, cancel_url }` — server derives amount/currency from the order record) |
189
+ | product | `GET {baseUrl}/col/product/mod/:id` (`fetchMany` = parallel single fetches — no batch endpoint assumed) |
190
+
191
+ Adapters throw raw HTTP errors (`Error` with `.status` and `.body`
192
+ attached); the domain manager normalizes them to `DomainError`. Responses
193
+ may use `{ model_id, data }` model envelopes — adapters unwrap them
194
+ transparently.
195
+
196
+ `PaymentAdapter.capture` is intentionally omitted from
197
+ `createHttpPaymentAdapter`; capture is typically driven server-side by
198
+ provider webhooks + checkout completion. Calls to `suite.payment.capture()`
199
+ will surface as `NOT_IMPLEMENTED`.
200
+
201
+ See [`example/`](./example/) for a vanilla-JS reference harness exercising
202
+ every public verb against either the HTTP adapters or the mock adapters.
203
+
147
204
  ## Events
148
205
 
149
206
  Subscribe to domain events:
@@ -0,0 +1,34 @@
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
+ import type { DomainContext } from "../../types/state.js";
11
+ /** Options shared by every built-in HTTP adapter factory. */
12
+ export interface HttpAdapterOptions {
13
+ /** Base URL of the mounted REST app. Each adapter has its own default. */
14
+ baseUrl?: string;
15
+ /** Override the `fetch` implementation (useful for tests / SSR). */
16
+ fetch?: typeof fetch;
17
+ }
18
+ export declare function resolveFetch(opts?: HttpAdapterOptions): typeof fetch;
19
+ /** Trailing-slash-safe path joining. */
20
+ export declare function join(base: string, path: string): string;
21
+ export declare function authHeaders(ctx: DomainContext): HeadersInit;
22
+ export declare function sessionHeader(ctx: DomainContext): HeadersInit;
23
+ /**
24
+ * Wrap `fetch`, merge auth + session + content-type headers, and throw a raw
25
+ * HTTP error (with `.status` and `.body`) on non-OK. Returns `undefined` on
26
+ * 204 No Content, otherwise the parsed JSON body.
27
+ */
28
+ export declare function requestJson<T>(doFetch: typeof fetch, url: string, init: RequestInit, ctx: DomainContext): Promise<T>;
29
+ /** Require `ctx.sessionId`; throw a client-side Error if missing. */
30
+ export declare function requireSessionId(ctx: DomainContext, operation: string): string;
31
+ /** Require `ctx.jwt`; throw a client-side Error if missing. */
32
+ export declare function requireJwt(ctx: DomainContext, operation: string): string;
33
+ /** Require `ctx.customerId`; throw a client-side Error if missing. */
34
+ export declare function requireCustomerId(ctx: DomainContext, operation: string): string;
@@ -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/mod/:customerId → { model_id, data: CustomerData }
8
+ * PUT {baseUrl}/me/col/customer/mod/: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/mod/:customerId → { model_id, data: CustomerData }
8
+ * PUT {baseUrl}/me/col/customer/mod/: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/mod/${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/mod → { data: [{ model_id, data }, ...] }
7
+ * GET {baseUrl}/col/order/mod/: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/mod → { data: [{ model_id, data }, ...] }
7
+ * GET {baseUrl}/col/order/mod/: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/mod"), { method: "GET" }, ctx);
27
+ return r.data ?? [];
28
+ },
29
+ async fetchOne(orderId, ctx) {
30
+ return await requestJson(doFetch, join(base, `/col/order/mod/${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/mod/: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/mod/: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/mod/${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/mod/: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/mod` 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/mod/: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/mod` 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/mod/${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
+ }
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * @module adapters
3
3
  *
4
- * Adapter exports including mock adapters for testing.
4
+ * Adapter exports. Includes:
5
+ * - Mock adapters for testing (see `./mock/`)
6
+ * - Built-in HTTP adapters for the conventional REST surface (see `./http/`)
5
7
  */
6
8
  export * from "./mock/mod.js";
9
+ export * from "./http/mod.js";
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * @module adapters
3
3
  *
4
- * Adapter exports including mock adapters for testing.
4
+ * Adapter exports. Includes:
5
+ * - Mock adapters for testing (see `./mock/`)
6
+ * - Built-in HTTP adapters for the conventional REST surface (see `./http/`)
5
7
  */
6
8
  export * from "./mock/mod.js";
9
+ export * from "./http/mod.js";
@@ -35,6 +35,8 @@ export interface DomainContext {
35
35
  customerId?: UUID;
36
36
  /** Optional session ID */
37
37
  sessionId?: UUID;
38
+ /** Optional JWT forwarded to HTTP adapters as `Authorization: Bearer <jwt>`. */
39
+ jwt?: string;
38
40
  /** Additional context properties for adapter-specific needs */
39
41
  [key: string]: unknown;
40
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/ecsuite",
3
- "version": "2.0.0",
3
+ "version": "2.2.1",
4
4
  "type": "module",
5
5
  "main": "dist/mod.js",
6
6
  "types": "dist/mod.d.ts",