@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 +87 -8
- package/README.md +57 -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/mod.d.ts +4 -1
- package/dist/adapters/mod.js +4 -1
- package/dist/types/state.d.ts +2 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
├──
|
|
65
|
-
├──
|
|
66
|
-
├──
|
|
67
|
-
├──
|
|
68
|
-
├──
|
|
69
|
-
|
|
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
|
+
}
|
package/dist/adapters/mod.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module adapters
|
|
3
3
|
*
|
|
4
|
-
* Adapter exports
|
|
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";
|
package/dist/adapters/mod.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module adapters
|
|
3
3
|
*
|
|
4
|
-
* Adapter exports
|
|
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";
|
package/dist/types/state.d.ts
CHANGED
|
@@ -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
|
}
|