webspresso 0.0.5 → 0.0.6
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 +70 -0
- package/core/applySchema.js +48 -0
- package/core/compileSchema.js +68 -0
- package/package.json +6 -3
- package/src/plugin-manager.js +1 -0
- package/utils/schemaCache.js +59 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
7
7
|
- **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
|
|
8
8
|
- **Dynamic Routes**: Use `[param]` for dynamic params and `[...rest]` for catch-all routes
|
|
9
9
|
- **API Endpoints**: Add `.js` files to `pages/api/` with method suffixes (e.g., `health.get.js`)
|
|
10
|
+
- **Schema Validation**: Zod-based request validation for body, params, and query
|
|
10
11
|
- **Built-in i18n**: JSON-based translations with automatic locale detection
|
|
11
12
|
- **Lifecycle Hooks**: Global and route-level hooks for request processing
|
|
12
13
|
- **Template Helpers**: Laravel-inspired helper functions available in templates
|
|
@@ -494,6 +495,75 @@ Create `.js` files in `pages/api/` with optional method suffixes:
|
|
|
494
495
|
| `pages/api/echo.post.js` | `POST /api/echo` |
|
|
495
496
|
| `pages/api/users/[id].get.js` | `GET /api/users/:id` |
|
|
496
497
|
|
|
498
|
+
**Basic API Handler:**
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
// pages/api/health.get.js
|
|
502
|
+
module.exports = async function handler(req, res) {
|
|
503
|
+
res.json({ status: 'ok' });
|
|
504
|
+
};
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**With Schema Validation:**
|
|
508
|
+
|
|
509
|
+
```javascript
|
|
510
|
+
// pages/api/posts.post.js
|
|
511
|
+
module.exports = {
|
|
512
|
+
schema: ({ z }) => ({
|
|
513
|
+
body: z.object({
|
|
514
|
+
title: z.string().min(3).max(100),
|
|
515
|
+
content: z.string(),
|
|
516
|
+
tags: z.array(z.string()).optional()
|
|
517
|
+
}),
|
|
518
|
+
query: z.object({
|
|
519
|
+
draft: z.coerce.boolean().default(false)
|
|
520
|
+
})
|
|
521
|
+
}),
|
|
522
|
+
|
|
523
|
+
async handler(req, res) {
|
|
524
|
+
// Validated & parsed data available in req.input
|
|
525
|
+
const { title, content, tags } = req.input.body;
|
|
526
|
+
const { draft } = req.input.query;
|
|
527
|
+
|
|
528
|
+
// Original req.body, req.query remain untouched
|
|
529
|
+
res.json({ success: true, title, draft });
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Schema Options:**
|
|
535
|
+
|
|
536
|
+
| Key | Description |
|
|
537
|
+
|-----|-------------|
|
|
538
|
+
| `body` | Validates `req.body` (POST/PUT/PATCH) |
|
|
539
|
+
| `params` | Validates route parameters (e.g., `:id`) |
|
|
540
|
+
| `query` | Validates query string parameters |
|
|
541
|
+
| `response` | Response schema (for documentation, not enforced) |
|
|
542
|
+
|
|
543
|
+
All schemas use [Zod](https://zod.dev) for validation. Invalid requests throw a `ZodError` which can be caught by error handlers.
|
|
544
|
+
|
|
545
|
+
**Dynamic Route with Params Validation:**
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
// pages/api/users/[id].get.js
|
|
549
|
+
module.exports = {
|
|
550
|
+
schema: ({ z }) => ({
|
|
551
|
+
params: z.object({
|
|
552
|
+
id: z.string().uuid()
|
|
553
|
+
}),
|
|
554
|
+
query: z.object({
|
|
555
|
+
fields: z.string().optional()
|
|
556
|
+
})
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
async handler(req, res) {
|
|
560
|
+
const { id } = req.input.params; // Validated UUID
|
|
561
|
+
const user = await getUser(id);
|
|
562
|
+
res.json(user);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
```
|
|
566
|
+
|
|
497
567
|
### Route Config
|
|
498
568
|
|
|
499
569
|
Add a `.js` file alongside your `.njk` file to configure the route:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Applicator
|
|
3
|
+
* Parses and validates request input against compiled schemas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Apply compiled schema to request
|
|
8
|
+
* Parses body, params, and query against their respective schemas
|
|
9
|
+
* Stores validated results in req.input
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} req - Express request object
|
|
12
|
+
* @param {Object|null} compiledSchema - Compiled schema from compileSchema
|
|
13
|
+
* @returns {void}
|
|
14
|
+
* @throws {ZodError} If validation fails
|
|
15
|
+
*/
|
|
16
|
+
function applySchema(req, compiledSchema) {
|
|
17
|
+
// Initialize req.input
|
|
18
|
+
req.input = {
|
|
19
|
+
body: undefined,
|
|
20
|
+
params: undefined,
|
|
21
|
+
query: undefined
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// No schema means no validation
|
|
25
|
+
if (!compiledSchema) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse body if schema exists
|
|
30
|
+
if (compiledSchema.body) {
|
|
31
|
+
req.input.body = compiledSchema.body.parse(req.body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse params if schema exists
|
|
35
|
+
if (compiledSchema.params) {
|
|
36
|
+
req.input.params = compiledSchema.params.parse(req.params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse query if schema exists
|
|
40
|
+
if (compiledSchema.query) {
|
|
41
|
+
req.input.query = compiledSchema.query.parse(req.query);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
applySchema
|
|
47
|
+
};
|
|
48
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Compiler
|
|
3
|
+
* Compiles schema definitions from API files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const z = require('zod');
|
|
7
|
+
const schemaCache = require('../utils/schemaCache');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compile schema from an API module
|
|
11
|
+
* @param {string} filePath - Absolute file path to API module
|
|
12
|
+
* @param {Object} apiModule - The loaded API module
|
|
13
|
+
* @returns {Object|null} Compiled schema object or null if no schema
|
|
14
|
+
*/
|
|
15
|
+
function compileSchema(filePath, apiModule) {
|
|
16
|
+
// Return cached schema if exists
|
|
17
|
+
if (schemaCache.has(filePath)) {
|
|
18
|
+
return schemaCache.get(filePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if module exports schema
|
|
22
|
+
const schemaFn = apiModule.schema;
|
|
23
|
+
|
|
24
|
+
// Schema is optional
|
|
25
|
+
if (schemaFn === undefined) {
|
|
26
|
+
schemaCache.set(filePath, null);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Schema must be a function
|
|
31
|
+
if (typeof schemaFn !== 'function') {
|
|
32
|
+
throw new Error(`Schema in ${filePath} must be a function`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Call schema function with { z }
|
|
36
|
+
const compiled = schemaFn({ z });
|
|
37
|
+
|
|
38
|
+
// Validate compiled schema structure
|
|
39
|
+
if (compiled !== null && typeof compiled !== 'object') {
|
|
40
|
+
throw new Error(`Schema function in ${filePath} must return an object or null`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Cache and return
|
|
44
|
+
schemaCache.set(filePath, compiled);
|
|
45
|
+
return compiled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clear schema cache for a file (for hot-reload)
|
|
50
|
+
* @param {string} filePath - Absolute file path
|
|
51
|
+
*/
|
|
52
|
+
function invalidateSchema(filePath) {
|
|
53
|
+
schemaCache.del(filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Clear all cached schemas
|
|
58
|
+
*/
|
|
59
|
+
function clearAllSchemas() {
|
|
60
|
+
schemaCache.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
compileSchema,
|
|
65
|
+
invalidateSchema,
|
|
66
|
+
clearAllSchemas
|
|
67
|
+
};
|
|
68
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,14 +29,17 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"index.js",
|
|
31
31
|
"bin/",
|
|
32
|
-
"src/"
|
|
32
|
+
"src/",
|
|
33
|
+
"utils/",
|
|
34
|
+
"core/"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"commander": "^11.1.0",
|
|
36
38
|
"express": "^4.18.2",
|
|
37
39
|
"helmet": "^7.2.0",
|
|
38
40
|
"inquirer": "^8.2.6",
|
|
39
|
-
"nunjucks": "^3.2.4"
|
|
41
|
+
"nunjucks": "^3.2.4",
|
|
42
|
+
"zod": "^3.23.0"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"dotenv": "^16.0.0"
|
package/src/plugin-manager.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Cache
|
|
3
|
+
* Caches compiled Zod schemas per file path
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get cached schema for a file path
|
|
10
|
+
* @param {string} filePath - Absolute file path
|
|
11
|
+
* @returns {Object|undefined} Compiled schema or undefined
|
|
12
|
+
*/
|
|
13
|
+
function get(filePath) {
|
|
14
|
+
return cache.get(filePath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set compiled schema for a file path
|
|
19
|
+
* @param {string} filePath - Absolute file path
|
|
20
|
+
* @param {Object} schema - Compiled schema object
|
|
21
|
+
*/
|
|
22
|
+
function set(filePath, schema) {
|
|
23
|
+
cache.set(filePath, schema);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if schema exists in cache
|
|
28
|
+
* @param {string} filePath - Absolute file path
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function has(filePath) {
|
|
32
|
+
return cache.has(filePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear all cached schemas
|
|
37
|
+
* Useful for development hot-reload
|
|
38
|
+
*/
|
|
39
|
+
function clear() {
|
|
40
|
+
cache.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete a specific schema from cache
|
|
45
|
+
* @param {string} filePath - Absolute file path
|
|
46
|
+
* @returns {boolean} True if deleted
|
|
47
|
+
*/
|
|
48
|
+
function del(filePath) {
|
|
49
|
+
return cache.delete(filePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
get,
|
|
54
|
+
set,
|
|
55
|
+
has,
|
|
56
|
+
clear,
|
|
57
|
+
del
|
|
58
|
+
};
|
|
59
|
+
|