@wocart/storefront 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +2 -1
- package/USAGE.md +117 -56
- package/dist/actions.d.ts +5 -4
- package/dist/actions.js +14 -9
- package/dist/astro.d.ts +3 -1
- package/dist/astro.js +2 -1
- package/dist/client/core.d.ts +12 -2
- package/dist/client/core.js +73 -79
- package/dist/client/index.d.ts +4 -3
- package/dist/client/index.js +4 -3
- package/dist/client/orders.d.ts +5 -2
- package/dist/client/orders.js +11 -5
- package/dist/client/page-data.d.ts +1 -2
- package/dist/client/page-data.js +4 -4
- package/dist/client/pages.d.ts +1 -1
- package/dist/client/pages.js +6 -4
- package/dist/client/products.js +3 -2
- package/dist/client/shipping.d.ts +1 -2
- package/dist/client/shipping.js +1 -2
- package/dist/domain.d.ts +9 -0
- package/dist/endpoints/proxy.d.ts +5 -3
- package/dist/endpoints/proxy.js +21 -4
- package/dist/endpoints/storefront-routes.d.ts +50 -0
- package/dist/endpoints/storefront-routes.js +133 -0
- package/dist/middleware.d.ts +29 -35
- package/dist/middleware.js +32 -42
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storefront's same-origin API routes, ready to export.
|
|
3
|
+
*
|
|
4
|
+
* Every theme needs the same eight routes between the shopper's browser and
|
|
5
|
+
* the platform: OTP send and verify, cart validation, abandoned-checkout
|
|
6
|
+
* tracking and its conversion, communes, and reviews (list and submit). What
|
|
7
|
+
* each one must get right is decided here once — the cross-origin check on
|
|
8
|
+
* every POST, the shopper's address forwarded wherever the server counts or
|
|
9
|
+
* attributes per shopper, the input bounds, and the response shape the
|
|
10
|
+
* browser reads — so a theme's route file is one line:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // src/pages/api/otp/send.ts
|
|
14
|
+
* import { routes } from "@/theme/api-routes";
|
|
15
|
+
* export const POST = routes.otpSend;
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The theme decides only which client serves the request — its own,
|
|
19
|
+
* per-request client, so the language and the request lifetime are the
|
|
20
|
+
* page's. A failure answers with the status the server's category implies
|
|
21
|
+
* (see `createProxyEndpoint`), and the server's diagnostic fields ride along.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { AbandonedCheckoutInputSchema, BasketLineInputSchema, MAX_BASKET_LINES, WilayaIdSchema, } from "../domain.js";
|
|
25
|
+
import { createProxyEndpoint, } from "./proxy.js";
|
|
26
|
+
export const REVIEWS_PAGE_DEFAULT = 20;
|
|
27
|
+
export const REVIEWS_PAGE_MAX = 50;
|
|
28
|
+
const otpSendInput = z.object({ phone: z.string().min(1).max(32) });
|
|
29
|
+
const otpVerifyInput = z.object({
|
|
30
|
+
phone: z.string().min(1).max(32),
|
|
31
|
+
requestId: z.string().min(1).max(200),
|
|
32
|
+
code: z.string().min(1).max(16),
|
|
33
|
+
});
|
|
34
|
+
const cartValidateInput = z.object({
|
|
35
|
+
items: z.array(BasketLineInputSchema).min(1).max(MAX_BASKET_LINES),
|
|
36
|
+
});
|
|
37
|
+
const abandonedConvertInput = z.object({
|
|
38
|
+
sessionId: z.string().uuid(),
|
|
39
|
+
orderId: z.string().min(1).max(200),
|
|
40
|
+
orderNumber: z.string().min(1).max(100),
|
|
41
|
+
});
|
|
42
|
+
const communesInput = z.object({ wilayaId: WilayaIdSchema });
|
|
43
|
+
const reviewsListInput = z.object({
|
|
44
|
+
productId: z.string().min(1).max(200),
|
|
45
|
+
limit: z.coerce
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.min(1)
|
|
49
|
+
.catch(REVIEWS_PAGE_DEFAULT)
|
|
50
|
+
.transform((n) => Math.min(n, REVIEWS_PAGE_MAX)),
|
|
51
|
+
offset: z.coerce.number().int().min(0).catch(0),
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* The shape of a review, bounded so the body stays small. The rules — the
|
|
55
|
+
* order-number format, the rating range, the body's length — are the
|
|
56
|
+
* server's, and its refusal carries the code the form answers by.
|
|
57
|
+
*/
|
|
58
|
+
const reviewSubmitInput = z.object({
|
|
59
|
+
orderNumber: z.string().min(1).max(64),
|
|
60
|
+
productId: z.string().min(1).max(200),
|
|
61
|
+
rating: z.number(),
|
|
62
|
+
title: z.string().max(500).optional(),
|
|
63
|
+
body: z.string().max(5000),
|
|
64
|
+
});
|
|
65
|
+
export function createStorefrontRoutes(options) {
|
|
66
|
+
const route = (config) => async (context) => {
|
|
67
|
+
const { call, ...endpoint } = config;
|
|
68
|
+
return createProxyEndpoint({
|
|
69
|
+
...endpoint,
|
|
70
|
+
client: (input, headers) => call(options.client(context), input, headers),
|
|
71
|
+
})(context);
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
otpSend: route({
|
|
75
|
+
method: "POST",
|
|
76
|
+
inputSchema: otpSendInput,
|
|
77
|
+
forwardAttribution: true,
|
|
78
|
+
call: (client, { phone }, headers) => client.orders.otp.send(phone, headers),
|
|
79
|
+
}),
|
|
80
|
+
otpVerify: route({
|
|
81
|
+
method: "POST",
|
|
82
|
+
inputSchema: otpVerifyInput,
|
|
83
|
+
forwardAttribution: true,
|
|
84
|
+
call: (client, { phone, requestId, code }, headers) => client.orders.otp.verify(phone, requestId, code, headers),
|
|
85
|
+
}),
|
|
86
|
+
cartValidate: route({
|
|
87
|
+
method: "POST",
|
|
88
|
+
inputSchema: cartValidateInput,
|
|
89
|
+
successBody: (data) => ({ success: true, data }),
|
|
90
|
+
call: (client, { items }) => client.cart.validate(items),
|
|
91
|
+
}),
|
|
92
|
+
abandonedUpsert: route({
|
|
93
|
+
method: "POST",
|
|
94
|
+
inputSchema: AbandonedCheckoutInputSchema,
|
|
95
|
+
forwardAttribution: true,
|
|
96
|
+
noContent: true,
|
|
97
|
+
call: (client, input, headers) => client.orders.abandoned.upsert(input, headers),
|
|
98
|
+
}),
|
|
99
|
+
abandonedConvert: route({
|
|
100
|
+
method: "POST",
|
|
101
|
+
inputSchema: abandonedConvertInput,
|
|
102
|
+
noContent: true,
|
|
103
|
+
call: (client, { sessionId, orderId, orderNumber }) => client.orders.abandoned.convert(sessionId, orderId, orderNumber),
|
|
104
|
+
}),
|
|
105
|
+
communes: route({
|
|
106
|
+
method: "GET",
|
|
107
|
+
pathParams: { wilayaId: "number" },
|
|
108
|
+
inputSchema: communesInput,
|
|
109
|
+
call: async (client, { wilayaId }) => ({
|
|
110
|
+
success: true,
|
|
111
|
+
data: await client.geo.communes(wilayaId),
|
|
112
|
+
}),
|
|
113
|
+
}),
|
|
114
|
+
reviewsList: route({
|
|
115
|
+
method: "GET",
|
|
116
|
+
pathParams: { productId: "string" },
|
|
117
|
+
queryParams: ["limit", "offset"],
|
|
118
|
+
inputSchema: reviewsListInput,
|
|
119
|
+
successBody: ({ rows, total }) => ({ data: rows, total }),
|
|
120
|
+
call: async (client, { productId, limit, offset }) => ({
|
|
121
|
+
success: true,
|
|
122
|
+
data: await client.products.reviews.list(productId, { limit, offset }),
|
|
123
|
+
}),
|
|
124
|
+
}),
|
|
125
|
+
reviewSubmit: route({
|
|
126
|
+
method: "POST",
|
|
127
|
+
inputSchema: reviewSubmitInput,
|
|
128
|
+
successStatus: 201,
|
|
129
|
+
successBody: (data) => ({ success: true, data }),
|
|
130
|
+
call: (client, { title, ...review }) => client.products.reviews.submit(title === undefined ? review : { ...review, title }),
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Middleware engine
|
|
3
|
-
* framework-free.
|
|
2
|
+
* Middleware engine for Astro Actions called from an HTML form, framework-free.
|
|
4
3
|
*
|
|
5
|
-
* `astro:middleware` and `astro:actions` are virtual modules that only
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `astro:middleware` and `astro:actions` are virtual modules that only exist
|
|
5
|
+
* inside an Astro project, so this file runs the action and each theme's
|
|
6
|
+
* middleware wires it:
|
|
8
7
|
*
|
|
9
8
|
* ```ts
|
|
10
9
|
* // src/middleware.ts
|
|
11
10
|
* import { defineMiddleware } from "astro:middleware";
|
|
12
11
|
* import { getActionContext } from "astro:actions";
|
|
13
|
-
* import {
|
|
12
|
+
* import { runFormAction } from "@wocart/storefront/astro";
|
|
14
13
|
*
|
|
15
14
|
* export const onRequest = defineMiddleware(async (context, next) => {
|
|
16
15
|
* const { action, setActionResult, serializeActionResult } =
|
|
17
16
|
* getActionContext(context);
|
|
18
|
-
*
|
|
17
|
+
* await runFormAction({
|
|
19
18
|
* action: action && {
|
|
20
19
|
* calledFrom: action.calledFrom,
|
|
21
20
|
* name: action.name,
|
|
@@ -23,29 +22,21 @@
|
|
|
23
22
|
* },
|
|
24
23
|
* setActionResult,
|
|
25
24
|
* serializeActionResult,
|
|
26
|
-
* referer: context.request.headers.get("Referer"),
|
|
27
|
-
* pathname: context.url.pathname,
|
|
28
25
|
* });
|
|
29
|
-
* if (decision.kind === "redirect") {
|
|
30
|
-
* return context.redirect(decision.location, decision.status);
|
|
31
|
-
* }
|
|
32
26
|
* return next();
|
|
33
27
|
* });
|
|
34
28
|
* ```
|
|
35
29
|
*
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
* the same POST → getActionResult() returns the success data,
|
|
42
|
-
* the page issues Astro.redirect('/thank-you?...') which the
|
|
43
|
-
* browser follows.
|
|
30
|
+
* The page then renders from the same POST and reads the result with
|
|
31
|
+
* `getActionResult()`:
|
|
32
|
+
* - on SUCCESS it issues `Astro.redirect('/thank-you?…')`, which the browser
|
|
33
|
+
* follows;
|
|
34
|
+
* - on ERROR it renders the form again with the error in place.
|
|
44
35
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
36
|
+
* Neither path may redirect from the middleware. Astro 7 holds an action
|
|
37
|
+
* result for the response being rendered only — a redirect arrives at the next
|
|
38
|
+
* page with no result, the thank-you redirect never fires and a refused order
|
|
39
|
+
* shows no message.
|
|
49
40
|
*/
|
|
50
41
|
/**
|
|
51
42
|
* The slice of an action result the decision reads: whether it errored.
|
|
@@ -73,28 +64,31 @@ export interface ActionMiddlewareDeps<TResult extends ActionHandlerResult = Acti
|
|
|
73
64
|
setActionResult: (name: string, serialized: TSerialized) => void;
|
|
74
65
|
/** Serializes an ActionResult into the cookie value (Astro's serializeActionResult). */
|
|
75
66
|
serializeActionResult: (result: TResult) => TSerialized;
|
|
76
|
-
/**
|
|
67
|
+
/** @deprecated Unused since 1.2.0: nothing redirects. */
|
|
77
68
|
referer?: string | null;
|
|
78
|
-
/**
|
|
79
|
-
pathname
|
|
69
|
+
/** @deprecated Unused since 1.2.0: nothing redirects. */
|
|
70
|
+
pathname?: string;
|
|
80
71
|
}
|
|
81
72
|
export type PrgDecision =
|
|
82
|
-
/**
|
|
73
|
+
/** Never produced since 1.2.0 — kept so existing `decision.kind` checks compile. */
|
|
83
74
|
{
|
|
84
75
|
kind: "redirect";
|
|
85
76
|
location: string;
|
|
86
77
|
status: 303;
|
|
87
78
|
}
|
|
88
|
-
/** Render the page from this POST
|
|
79
|
+
/** Render the page from this POST. */
|
|
89
80
|
| {
|
|
90
81
|
kind: "next";
|
|
91
82
|
};
|
|
92
83
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
* Run an action called from an HTML form and store its result for the page
|
|
85
|
+
* this POST renders. Resolves to that result, or `undefined` when the request
|
|
86
|
+
* is not a form-called action (a JSON call or a plain request is Astro's).
|
|
87
|
+
*/
|
|
88
|
+
export declare function runFormAction<TResult extends ActionHandlerResult, TSerialized>(deps: Pick<ActionMiddlewareDeps<TResult, TSerialized>, "action" | "setActionResult" | "serializeActionResult">): Promise<TResult | undefined>;
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated Use `runFormAction` and call `next()`. Runs the action the same
|
|
91
|
+
* way and always answers `{ kind: "next" }`: an errored post renders in place,
|
|
92
|
+
* because a redirect drops the result (see the note at the top of this file).
|
|
99
93
|
*/
|
|
100
94
|
export declare function resolveActionRedirectDecision<TResult extends ActionHandlerResult, TSerialized>(deps: ActionMiddlewareDeps<TResult, TSerialized>): Promise<PrgDecision>;
|
package/dist/middleware.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Middleware engine
|
|
3
|
-
* framework-free.
|
|
2
|
+
* Middleware engine for Astro Actions called from an HTML form, framework-free.
|
|
4
3
|
*
|
|
5
|
-
* `astro:middleware` and `astro:actions` are virtual modules that only
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `astro:middleware` and `astro:actions` are virtual modules that only exist
|
|
5
|
+
* inside an Astro project, so this file runs the action and each theme's
|
|
6
|
+
* middleware wires it:
|
|
8
7
|
*
|
|
9
8
|
* ```ts
|
|
10
9
|
* // src/middleware.ts
|
|
11
10
|
* import { defineMiddleware } from "astro:middleware";
|
|
12
11
|
* import { getActionContext } from "astro:actions";
|
|
13
|
-
* import {
|
|
12
|
+
* import { runFormAction } from "@wocart/storefront/astro";
|
|
14
13
|
*
|
|
15
14
|
* export const onRequest = defineMiddleware(async (context, next) => {
|
|
16
15
|
* const { action, setActionResult, serializeActionResult } =
|
|
17
16
|
* getActionContext(context);
|
|
18
|
-
*
|
|
17
|
+
* await runFormAction({
|
|
19
18
|
* action: action && {
|
|
20
19
|
* calledFrom: action.calledFrom,
|
|
21
20
|
* name: action.name,
|
|
@@ -23,50 +22,41 @@
|
|
|
23
22
|
* },
|
|
24
23
|
* setActionResult,
|
|
25
24
|
* serializeActionResult,
|
|
26
|
-
* referer: context.request.headers.get("Referer"),
|
|
27
|
-
* pathname: context.url.pathname,
|
|
28
25
|
* });
|
|
29
|
-
* if (decision.kind === "redirect") {
|
|
30
|
-
* return context.redirect(decision.location, decision.status);
|
|
31
|
-
* }
|
|
32
26
|
* return next();
|
|
33
27
|
* });
|
|
34
28
|
* ```
|
|
35
29
|
*
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
* the same POST → getActionResult() returns the success data,
|
|
42
|
-
* the page issues Astro.redirect('/thank-you?...') which the
|
|
43
|
-
* browser follows.
|
|
30
|
+
* The page then renders from the same POST and reads the result with
|
|
31
|
+
* `getActionResult()`:
|
|
32
|
+
* - on SUCCESS it issues `Astro.redirect('/thank-you?…')`, which the browser
|
|
33
|
+
* follows;
|
|
34
|
+
* - on ERROR it renders the form again with the error in place.
|
|
44
35
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
36
|
+
* Neither path may redirect from the middleware. Astro 7 holds an action
|
|
37
|
+
* result for the response being rendered only — a redirect arrives at the next
|
|
38
|
+
* page with no result, the thank-you redirect never fires and a refused order
|
|
39
|
+
* shows no message.
|
|
49
40
|
*/
|
|
50
41
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
* Run an action called from an HTML form and store its result for the page
|
|
43
|
+
* this POST renders. Resolves to that result, or `undefined` when the request
|
|
44
|
+
* is not a form-called action (a JSON call or a plain request is Astro's).
|
|
45
|
+
*/
|
|
46
|
+
export async function runFormAction(deps) {
|
|
47
|
+
const { action, setActionResult, serializeActionResult } = deps;
|
|
48
|
+
if (action?.calledFrom !== "form")
|
|
49
|
+
return undefined;
|
|
50
|
+
const result = await action.handler();
|
|
51
|
+
setActionResult(action.name, serializeActionResult(result));
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @deprecated Use `runFormAction` and call `next()`. Runs the action the same
|
|
56
|
+
* way and always answers `{ kind: "next" }`: an errored post renders in place,
|
|
57
|
+
* because a redirect drops the result (see the note at the top of this file).
|
|
57
58
|
*/
|
|
58
59
|
export async function resolveActionRedirectDecision(deps) {
|
|
59
|
-
|
|
60
|
-
if (action?.calledFrom === "form") {
|
|
61
|
-
const result = await action.handler();
|
|
62
|
-
setActionResult(action.name, serializeActionResult(result));
|
|
63
|
-
if (result.error) {
|
|
64
|
-
// Redirect back to the form page so getActionResult() can surface
|
|
65
|
-
// field errors.
|
|
66
|
-
return { kind: "redirect", location: referer ?? pathname, status: 303 };
|
|
67
|
-
}
|
|
68
|
-
// Success: let next() render the page; the page will redirect to
|
|
69
|
-
// /thank-you. Calling a redirect here would drop the result cookie.
|
|
70
|
-
}
|
|
60
|
+
await runFormAction(deps);
|
|
71
61
|
return { kind: "next" };
|
|
72
62
|
}
|
package/package.json
CHANGED