webspresso 0.0.62 → 0.0.63
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/README.md +53 -2
- package/core/compileSchema.js +6 -2
- package/core/orm/index.js +3 -1
- package/core/orm/utils/nanoid.js +53 -0
- package/package.json +1 -1
- package/src/file-router.js +27 -0
- package/templates/skills/webspresso-usage/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -1030,7 +1030,40 @@ module.exports = async function handler(req, res) {
|
|
|
1030
1030
|
};
|
|
1031
1031
|
```
|
|
1032
1032
|
|
|
1033
|
-
**
|
|
1033
|
+
**Object export (`handler`, `middleware`, `schema`):**
|
|
1034
|
+
|
|
1035
|
+
Use a single export object when you need **named middleware** from `createApp({ middlewares })` and/or a **Zod** `schema`. Order at runtime: **`req.db`** (if configured) → **schema** (`req.input`) → **middleware** chain → **handler**.
|
|
1036
|
+
|
|
1037
|
+
```javascript
|
|
1038
|
+
// pages/api/search.post.js
|
|
1039
|
+
module.exports = {
|
|
1040
|
+
middleware: ['requireAuth'],
|
|
1041
|
+
schema: ({ z }) => ({
|
|
1042
|
+
body: z.object({ q: z.string() }),
|
|
1043
|
+
}),
|
|
1044
|
+
handler: async (req, res) => {
|
|
1045
|
+
const { q } = req.input.body;
|
|
1046
|
+
const results = [];
|
|
1047
|
+
return res.json({ results, q });
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Register `requireAuth` (and any other names) on the app:
|
|
1053
|
+
|
|
1054
|
+
```javascript
|
|
1055
|
+
createApp({
|
|
1056
|
+
pagesDir: './pages',
|
|
1057
|
+
middlewares: {
|
|
1058
|
+
requireAuth: (req, res, next) => {
|
|
1059
|
+
if (!req.session?.user) return res.status(401).json({ error: 'Unauthorized' });
|
|
1060
|
+
next();
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
**With Schema Validation (same object shape):**
|
|
1034
1067
|
|
|
1035
1068
|
```javascript
|
|
1036
1069
|
// pages/api/posts.post.js
|
|
@@ -1066,7 +1099,25 @@ module.exports = {
|
|
|
1066
1099
|
| `query` | Validates query string parameters |
|
|
1067
1100
|
| `response` | Response schema (for documentation, not enforced) |
|
|
1068
1101
|
|
|
1069
|
-
All schemas use [Zod](https://zod.dev) for validation. Invalid requests
|
|
1102
|
+
All schemas use [Zod](https://zod.dev) for validation. Invalid requests receive **`400 JSON`**: `{ error: 'Validation Error', issues: [...] }` (no `handler` / `middleware` run).
|
|
1103
|
+
|
|
1104
|
+
For **nanoid** route parameters (same alphabet and default length as **`generateNanoid`** / **`zdb.nanoid()`**), use **`z.nanoid()`** on the `z` passed into your API schema (Webspresso extends it; no extra import). Optional: **`z.nanoid(12)`** or **`z.nanoid({ maxLength: 12 })`** (matches **`zdb.nanoid({ maxLength })`**). For scripts or tests outside `schema: ({ z })`, **`zodNanoid(z, size?)`** and **`extendZ(z)`** are exported from `webspresso`.
|
|
1105
|
+
|
|
1106
|
+
```javascript
|
|
1107
|
+
module.exports = {
|
|
1108
|
+
schema: ({ z }) => ({
|
|
1109
|
+
params: z.object({
|
|
1110
|
+
id: z.nanoid(), // default 21 chars
|
|
1111
|
+
shortId: z.nanoid(12), // numeric length
|
|
1112
|
+
other: z.nanoid({ maxLength: 8 }), // zdb-style options
|
|
1113
|
+
}),
|
|
1114
|
+
}),
|
|
1115
|
+
async handler(req, res) {
|
|
1116
|
+
const { id } = req.input.params;
|
|
1117
|
+
// ...
|
|
1118
|
+
},
|
|
1119
|
+
};
|
|
1120
|
+
```
|
|
1070
1121
|
|
|
1071
1122
|
**Dynamic Route with Params Validation:**
|
|
1072
1123
|
|
package/core/compileSchema.js
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const z = require('zod');
|
|
7
|
+
const { extendZ } = require('./orm/utils/nanoid');
|
|
7
8
|
const schemaCache = require('../utils/schemaCache');
|
|
8
9
|
|
|
10
|
+
/** Zod plus `z.nanoid()` for API schemas (does not mutate the global `z`). */
|
|
11
|
+
const zForApi = extendZ(z);
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Compile schema from an API module
|
|
11
15
|
* @param {string} filePath - Absolute file path to API module
|
|
@@ -32,8 +36,8 @@ function compileSchema(filePath, apiModule) {
|
|
|
32
36
|
throw new Error(`Schema in ${filePath} must be a function`);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
// Call schema function with { z }
|
|
36
|
-
const compiled = schemaFn({ z });
|
|
39
|
+
// Call schema function with { z } (extended with z.nanoid, same as generateNanoid / zdb.nanoid)
|
|
40
|
+
const compiled = schemaFn({ z: zForApi });
|
|
37
41
|
|
|
38
42
|
// Validate compiled schema structure
|
|
39
43
|
if (compiled !== null && typeof compiled !== 'object') {
|
package/core/orm/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const { createSeeder } = require('./seeder');
|
|
|
14
14
|
const { createScopeContext } = require('./scopes');
|
|
15
15
|
const { ModelEvents, Hooks, HookCancellationError, createEventContext } = require('./events');
|
|
16
16
|
const { omitHiddenColumns, sanitizeForOutput } = require('./utils');
|
|
17
|
-
const { generateNanoid } = require('./utils/nanoid');
|
|
17
|
+
const { generateNanoid, zodNanoid, extendZ } = require('./utils/nanoid');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Create a database instance
|
|
@@ -275,6 +275,8 @@ module.exports = {
|
|
|
275
275
|
extractColumnsFromSchema,
|
|
276
276
|
getColumnMeta,
|
|
277
277
|
generateNanoid,
|
|
278
|
+
zodNanoid,
|
|
279
|
+
extendZ,
|
|
278
280
|
// Output sanitization (exclude hidden columns from API/templates)
|
|
279
281
|
omitHiddenColumns,
|
|
280
282
|
sanitizeForOutput,
|
package/core/orm/utils/nanoid.js
CHANGED
|
@@ -24,7 +24,60 @@ function generateNanoid(size = 21) {
|
|
|
24
24
|
return id;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Zod schema for IDs produced by {@link generateNanoid} (same default alphabet & length).
|
|
29
|
+
* Prefer **`z.nanoid()`** in API `schema: ({ z }) => …`; use this when building Zod schemas outside compiled routes.
|
|
30
|
+
* @param {typeof import('zod').z} z - Zod instance (from `schema: ({ z }) => …`)
|
|
31
|
+
* @param {number} [size=21]
|
|
32
|
+
*/
|
|
33
|
+
function zodNanoid(z, size = 21) {
|
|
34
|
+
const n = Math.max(1, Math.floor(size));
|
|
35
|
+
const alphabet = new Set(URL_ALPHABET);
|
|
36
|
+
return z.string().length(n).refine((s) => [...s].every((ch) => alphabet.has(ch)), {
|
|
37
|
+
message: `Expected ${n}-character nanoid (default URL alphabet)`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {unknown} arg - `undefined` → 21; number → length; `{ maxLength }` (zdb-compatible)
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
function resolveNanoidSize(arg) {
|
|
46
|
+
if (arg === undefined) return 21;
|
|
47
|
+
if (arg === null) return 21;
|
|
48
|
+
if (typeof arg === 'number' && !Number.isNaN(arg)) {
|
|
49
|
+
return Math.max(1, Math.floor(arg));
|
|
50
|
+
}
|
|
51
|
+
if (typeof arg === 'object' && arg !== null && 'maxLength' in arg) {
|
|
52
|
+
const n = /** @type {{ maxLength?: unknown }} */ (arg).maxLength;
|
|
53
|
+
if (typeof n === 'number' && !Number.isNaN(n)) {
|
|
54
|
+
return Math.max(1, Math.floor(n));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 21;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wraps Zod's `z` with **`z.nanoid()`** / **`z.nanoid(12)`** / **`z.nanoid({ maxLength: 12 })`**
|
|
62
|
+
* without mutating the global `z` object. Used by API `schema: ({ z }) => …`.
|
|
63
|
+
* @param {typeof import('zod').z} zod
|
|
64
|
+
*/
|
|
65
|
+
function extendZ(zod) {
|
|
66
|
+
return new Proxy(zod, {
|
|
67
|
+
get(target, prop, receiver) {
|
|
68
|
+
if (prop === 'nanoid') {
|
|
69
|
+
return function nanoid(arg) {
|
|
70
|
+
return zodNanoid(zod, resolveNanoidSize(arg));
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return Reflect.get(target, prop, receiver);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
module.exports = {
|
|
28
79
|
generateNanoid,
|
|
80
|
+
zodNanoid,
|
|
81
|
+
extendZ,
|
|
29
82
|
URL_ALPHABET,
|
|
30
83
|
};
|
package/package.json
CHANGED
package/src/file-router.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { ZodError } = require('zod');
|
|
9
|
+
const { compileSchema, invalidateSchema } = require('../core/compileSchema');
|
|
10
|
+
const { applySchema } = require('../core/applySchema');
|
|
8
11
|
const { createHelpers } = require('./helpers');
|
|
9
12
|
|
|
10
13
|
// Cache for i18n files (key: filePath, value: { mtime, data })
|
|
@@ -412,6 +415,30 @@ function mountPages(app, options) {
|
|
|
412
415
|
const fn = typeof currentHandler === 'function'
|
|
413
416
|
? currentHandler
|
|
414
417
|
: currentHandler.default || currentHandler.handler;
|
|
418
|
+
|
|
419
|
+
if (isDev) {
|
|
420
|
+
invalidateSchema(route.fullPath);
|
|
421
|
+
}
|
|
422
|
+
let compiledSchema;
|
|
423
|
+
try {
|
|
424
|
+
compiledSchema = compileSchema(route.fullPath, currentHandler);
|
|
425
|
+
} catch (schemaErr) {
|
|
426
|
+
console.error(`API schema compile error ${route.routePath}:`, schemaErr);
|
|
427
|
+
res.status(500).json({ error: 'Internal Server Error', message: schemaErr.message });
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
applySchema(req, compiledSchema);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
if (err instanceof ZodError) {
|
|
435
|
+
return res.status(400).json({
|
|
436
|
+
error: 'Validation Error',
|
|
437
|
+
issues: err.issues,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
415
442
|
|
|
416
443
|
// Run middleware if defined
|
|
417
444
|
const mwConfig = isDev ? currentHandler.middleware : routeMiddleware;
|
|
@@ -102,9 +102,11 @@ project/
|
|
|
102
102
|
**Shapes**
|
|
103
103
|
|
|
104
104
|
1. **Function** — `module.exports = async (req, res) => { ... }`
|
|
105
|
-
2. **Object** —
|
|
105
|
+
2. **Object** — **`handler`**, optional **`middleware`** (names from **`createApp({ middlewares })`**), optional **`schema`**
|
|
106
106
|
|
|
107
|
-
**
|
|
107
|
+
**Order:** `req.db` (if any) → **Zod** `schema` → **`middleware`** → **`handler`**.
|
|
108
|
+
|
|
109
|
+
**Zod** — `schema: ({ z }) => ({ body, query, params, response })` → **`req.input`**; invalid → **400** `{ error: 'Validation Error', issues }`.
|
|
108
110
|
|
|
109
111
|
---
|
|
110
112
|
|