@webjsdev/core 0.7.18 → 0.7.20

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/src/expose.js DELETED
@@ -1,180 +0,0 @@
1
- /**
2
- * Expose a server action as a first-class HTTP endpoint in addition to its
3
- * internal RPC URL.
4
- *
5
- * ```js
6
- * // actions/posts.server.js
7
- * 'use server';
8
- * import { expose } from '@webjsdev/core';
9
- *
10
- * export const createPost = expose('POST /api/posts', async ({ title, body }) => {
11
- * // same function body you'd write without expose()
12
- * });
13
- * ```
14
- *
15
- * The action is reachable two ways, both backed by the exact same code:
16
- * - from a client component: `import { createPost } from '.../posts.server.js'`
17
- * - from curl / another service: `POST /api/posts` with a JSON body
18
- *
19
- * Adapter rules when invoked over HTTP:
20
- * - URL path params ({`:slug`}) and query string are merged into a single
21
- * object argument.
22
- * - For methods with a body (POST/PUT/PATCH/DELETE), the parsed JSON body
23
- * is merged on top of params/query. The function receives ONE argument:
24
- * the merged object.
25
- * - If `opts.validate` is provided, it runs BEFORE the function on EVERY
26
- * call path (the REST path here AND the internal RPC path), so a single
27
- * validator declared once guards both. The framework calls `validate(input)`
28
- * and interprets the return (see the validator contract below):
29
- * - a `{ success: true, data? }` envelope passes (the action receives
30
- * `data` when present, else the original input);
31
- * - a `{ success: false, fieldErrors, message? }` envelope FAILS with a
32
- * structured field-error result (422), without calling the action;
33
- * - a THROW fails (→ 400 on REST, sanitized error on RPC), preserving the
34
- * classic `Schema.parse`-style contract;
35
- * - any OTHER returned plain value is treated as the transformed input
36
- * (back-compat with the `validate: Schema.parse` transform style).
37
- * The zod adapter is three lines and keeps the framework zod-free:
38
- * `validate: (i) => { const r = Schema.safeParse(i);
39
- * return r.success ? { success: true, data: r.data }
40
- * : { success: false, fieldErrors: r.error.flatten().fieldErrors }; }`
41
- * - Return value becomes a JSON `Response`; throw or return a `Response`
42
- * directly for full control.
43
- *
44
- *
45
- * `cors` toggles browser CORS support for cross-origin callers:
46
- * - `true` - allow any origin (`*`), reflects requested headers
47
- * - string - allow that single origin (sets `Access-Control-Allow-Credentials: true`)
48
- * - string[] - allow-list; non-matching origins are not granted
49
- * - { origin, credentials, maxAge, headers }: full control
50
- *
51
- * When CORS is enabled, an `OPTIONS` preflight at the same path is auto-served
52
- * with `Access-Control-Allow-Methods: <route's method>, OPTIONS`.
53
- *
54
- * @param {string} pattern e.g. `"POST /api/posts"` or `"GET /api/posts/:slug"`
55
- * @param {Function} fn the async implementation
56
- * @param {{
57
- * validate?: (input: any) => any,
58
- * cors?: boolean | string | string[] | { origin: string | string[], credentials?: boolean, maxAge?: number, headers?: string[] }
59
- * }} [opts]
60
- * @returns {Function} same function, tagged with HTTP metadata
61
- */
62
- export function expose(pattern, fn, opts) {
63
- const match = /^\s*([A-Z]+)\s+(\/\S*)\s*$/.exec(pattern);
64
- if (!match) {
65
- throw new Error(
66
- `expose(): bad pattern ${JSON.stringify(pattern)}: expected "METHOD /path"`
67
- );
68
- }
69
- const [, method, path] = match;
70
- /** @type any */ (fn).__webjsHttp = {
71
- method,
72
- path,
73
- validate: opts && typeof opts.validate === 'function' ? opts.validate : null,
74
- cors: opts && 'cors' in opts ? normaliseCors(opts.cors) : null,
75
- };
76
- return fn;
77
- }
78
-
79
- /** @param {any} c */
80
- function normaliseCors(c) {
81
- if (!c) return null;
82
- if (c === true) {
83
- return { origin: '*', credentials: false, maxAge: 86400, headers: null };
84
- }
85
- if (typeof c === 'string') {
86
- return { origin: c, credentials: true, maxAge: 86400, headers: null };
87
- }
88
- if (Array.isArray(c)) {
89
- return { origin: c, credentials: true, maxAge: 86400, headers: null };
90
- }
91
- return {
92
- origin: c.origin,
93
- credentials: !!c.credentials,
94
- maxAge: typeof c.maxAge === 'number' ? c.maxAge : 86400,
95
- headers: Array.isArray(c.headers) ? c.headers : null,
96
- };
97
- }
98
-
99
- /**
100
- * Attach an input validator to a server action WITHOUT exposing it as a REST
101
- * endpoint. The validator runs SERVER-SIDE before the action body on BOTH call
102
- * paths: the internal RPC path (a client component importing the action) AND
103
- * the `expose()` REST path (if the action is also exposed). This is the shared,
104
- * declare-once validation surface (#245).
105
- *
106
- * It reuses the EXACT same `__webjsHttp` storage `expose()` writes, so the
107
- * action index's `getExposed(fn)` already surfaces the validator with no new
108
- * plumbing. The action stays a plain callable function (a direct unit-test
109
- * import still works, just without the framework running validate).
110
- *
111
- * If the action is already `expose()`d, prefer passing `{ validate }` to
112
- * `expose()` directly; calling both keeps the LAST validator attached.
113
- *
114
- * ```js
115
- * // actions/posts.server.js
116
- * 'use server';
117
- * import { validateInput } from '@webjsdev/core';
118
- *
119
- * export const createPost = validateInput(
120
- * async ({ title }) => { ... },
121
- * (input) => {
122
- * const errs = {};
123
- * if (!input?.title?.trim()) errs.title = 'Title is required';
124
- * return Object.keys(errs).length
125
- * ? { success: false, fieldErrors: errs }
126
- * : { success: true };
127
- * },
128
- * );
129
- * ```
130
- *
131
- * The validator contract (the framework calls `validate(input)`):
132
- * - returns `{ success: true, data? }` → valid; the action is called with
133
- * `data` if present, else the original input;
134
- * - returns `{ success: false, fieldErrors, message? }` (an object with a
135
- * boolean `success` of `false`, or with a `fieldErrors`) → FAILED; the
136
- * framework returns a structured `ActionResult`
137
- * `{ success: false, fieldErrors, error: message?, status: 422 }` without
138
- * calling the action body;
139
- * - THROWS → mapped to a sanitized failure result (the classic
140
- * `Schema.parse` contract);
141
- * - returns ANY OTHER plain value → treated as the validated/coerced input
142
- * and passed to the action (the `validate: Schema.parse` transform style).
143
- *
144
- * Disambiguation: a return is a result-envelope ONLY when it is an object with
145
- * a boolean `success` property OR a `fieldErrors` property; otherwise it is a
146
- * transformed input value. Caveat: if your validator returns the input
147
- * VERBATIM (the bare-transform style) and that input can itself carry a
148
- * `success` boolean or a `fieldErrors` key, it would be misread as an envelope.
149
- * Return the explicit `{ success: true, data }` envelope in that case (the zod
150
- * adapter and the recommended hand-rolled form already do, so they never
151
- * collide).
152
- *
153
- * Server-side only; import inside `.server.{js,ts}` files. The bare
154
- * `@webjsdev/core` specifier resolves to the browser entry, which excludes
155
- * `validateInput` (like `expose`), so an import from client-bound code reads
156
- * `undefined`. The validator itself lives in the `.server` file and never
157
- * reaches the client.
158
- *
159
- * @template {Function} F
160
- * @param {F} fn the async action implementation
161
- * @param {(input: any) => any} validate the validator (a plain function or a
162
- * zod-`safeParse` adapter)
163
- * @returns {F} the same function, tagged with the validator
164
- */
165
- export function validateInput(fn, validate) {
166
- if (typeof fn !== 'function') {
167
- throw new Error('validateInput(): first argument must be the action function');
168
- }
169
- if (typeof validate !== 'function') {
170
- throw new Error('validateInput(): second argument must be a validator function');
171
- }
172
- const existing = /** @type any */ (fn).__webjsHttp || {};
173
- /** @type any */ (fn).__webjsHttp = { ...existing, validate };
174
- return fn;
175
- }
176
-
177
- /** @param {unknown} fn */
178
- export function getExposed(fn) {
179
- return fn && typeof fn === 'function' ? /** @type any */ (fn).__webjsHttp || null : null;
180
- }