@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.
- package/AGENTS.md +134 -20
- package/API.md +108 -25
- package/README.md +127 -0
- package/dist/adapters/http/_http.d.ts +34 -0
- package/dist/adapters/http/_http.js +75 -0
- package/dist/adapters/http/cart.d.ts +21 -0
- package/dist/adapters/http/cart.js +52 -0
- package/dist/adapters/http/customer.d.ts +22 -0
- package/dist/adapters/http/customer.js +35 -0
- package/dist/adapters/http/mod.d.ts +21 -0
- package/dist/adapters/http/mod.js +20 -0
- package/dist/adapters/http/order.d.ts +24 -0
- package/dist/adapters/http/order.js +43 -0
- package/dist/adapters/http/payment.d.ts +32 -0
- package/dist/adapters/http/payment.js +77 -0
- package/dist/adapters/http/product.d.ts +18 -0
- package/dist/adapters/http/product.js +30 -0
- package/dist/adapters/http/wishlist.d.ts +19 -0
- package/dist/adapters/http/wishlist.js +42 -0
- package/dist/adapters/mock/cart.d.ts +4 -2
- package/dist/adapters/mock/cart.js +3 -7
- package/dist/adapters/mock/customer.d.ts +3 -1
- package/dist/adapters/mock/customer.js +3 -2
- package/dist/adapters/mock/order.d.ts +3 -1
- package/dist/adapters/mock/order.js +7 -5
- package/dist/adapters/mock/payment.d.ts +3 -1
- package/dist/adapters/mock/payment.js +34 -20
- package/dist/adapters/mock/product.d.ts +3 -1
- package/dist/adapters/mock/product.js +3 -1
- package/dist/adapters/mock/wishlist.d.ts +4 -2
- package/dist/adapters/mock/wishlist.js +3 -7
- package/dist/adapters/mod.d.ts +4 -1
- package/dist/adapters/mod.js +4 -1
- package/dist/domains/base.d.ts +13 -6
- package/dist/domains/base.js +31 -12
- package/dist/domains/cart.js +17 -0
- package/dist/domains/customer.js +18 -4
- package/dist/domains/order.d.ts +33 -15
- package/dist/domains/order.js +34 -20
- package/dist/domains/payment.js +16 -2
- package/dist/domains/product.d.ts +29 -40
- package/dist/domains/product.js +99 -81
- package/dist/domains/wishlist.js +4 -2
- package/dist/suite.d.ts +49 -1
- package/dist/suite.js +90 -8
- package/dist/types/adapter.d.ts +10 -7
- package/dist/types/events.d.ts +6 -2
- package/dist/types/state.d.ts +2 -0
- package/docs/future-improvements.md +116 -0
- 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"
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
16
|
+
/** HTTP error class name from `HTTP_ERROR` (default: "BadRequest") */
|
|
17
|
+
code?: keyof typeof HTTP_ERROR;
|
|
16
18
|
message?: string;
|
|
17
19
|
};
|
|
18
20
|
}
|