webspresso 0.0.62 → 0.0.64
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 +95 -3
- package/core/compileSchema.js +6 -2
- package/core/orm/index.js +3 -1
- package/core/orm/model.js +6 -0
- package/core/orm/types.js +9 -0
- package/core/orm/utils/nanoid.js +53 -0
- package/index.d.ts +482 -0
- package/index.js +2 -1
- package/package.json +7 -1
- package/plugins/admin-panel/components.js +120 -120
- package/plugins/admin-panel/field-renderers/array.js +2 -2
- package/plugins/admin-panel/field-renderers/basic.js +6 -6
- package/plugins/admin-panel/field-renderers/file-upload.js +2 -2
- package/plugins/admin-panel/field-renderers/json.js +1 -1
- package/plugins/admin-panel/field-renderers/relations.js +2 -2
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -3
- package/plugins/admin-panel/index.js +35 -4
- package/plugins/admin-panel/modules/bulk-actions.js +6 -6
- package/plugins/admin-panel/modules/custom-pages.js +7 -7
- package/plugins/admin-panel/modules/dashboard.js +14 -14
- package/plugins/admin-panel/modules/menu.js +71 -26
- package/plugins/index.js +2 -0
- package/plugins/rest-resources/index.js +350 -0
- package/src/file-router.js +27 -0
- package/src/server.js +128 -36
- package/templates/skills/webspresso-usage/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -15,7 +15,8 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
15
15
|
- **Lifecycle Hooks**: Global and route-level hooks for request processing
|
|
16
16
|
- **Template Helpers**: Laravel-inspired helper functions available in templates
|
|
17
17
|
- **Plugin System**: Extensible architecture with version control and inter-plugin communication
|
|
18
|
-
- **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint
|
|
18
|
+
- **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint, optional REST CRUD routes from ORM models
|
|
19
|
+
- **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -25,6 +26,18 @@ npm install -g webspresso
|
|
|
25
26
|
npm install webspresso
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
## TypeScript
|
|
30
|
+
|
|
31
|
+
The npm package ships with **[`index.d.ts`](index.d.ts)** so consumers get typings for the public API (`createApp`, `defineModel`, `createDatabase`, `zdb`, plugins, etc.). No extra `@types/webspresso` package is required.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { createApp, defineModel, zdb } from 'webspresso';
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Install **`@types/express`** in your app if you want full **`Express.Application`** / **`Request`** / **`Response`** inference when you touch `createApp().app` or write middleware. **`knex`** and **`zod`** bring their own types.
|
|
38
|
+
|
|
39
|
+
Framework development (this repo): run **`npm run check:types`** to typecheck the declarations against a small smoke file (`tests/ts-smoke/`).
|
|
40
|
+
|
|
28
41
|
## Quick Start
|
|
29
42
|
|
|
30
43
|
### Using CLI (Recommended)
|
|
@@ -1030,7 +1043,40 @@ module.exports = async function handler(req, res) {
|
|
|
1030
1043
|
};
|
|
1031
1044
|
```
|
|
1032
1045
|
|
|
1033
|
-
**
|
|
1046
|
+
**Object export (`handler`, `middleware`, `schema`):**
|
|
1047
|
+
|
|
1048
|
+
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**.
|
|
1049
|
+
|
|
1050
|
+
```javascript
|
|
1051
|
+
// pages/api/search.post.js
|
|
1052
|
+
module.exports = {
|
|
1053
|
+
middleware: ['requireAuth'],
|
|
1054
|
+
schema: ({ z }) => ({
|
|
1055
|
+
body: z.object({ q: z.string() }),
|
|
1056
|
+
}),
|
|
1057
|
+
handler: async (req, res) => {
|
|
1058
|
+
const { q } = req.input.body;
|
|
1059
|
+
const results = [];
|
|
1060
|
+
return res.json({ results, q });
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
Register `requireAuth` (and any other names) on the app:
|
|
1066
|
+
|
|
1067
|
+
```javascript
|
|
1068
|
+
createApp({
|
|
1069
|
+
pagesDir: './pages',
|
|
1070
|
+
middlewares: {
|
|
1071
|
+
requireAuth: (req, res, next) => {
|
|
1072
|
+
if (!req.session?.user) return res.status(401).json({ error: 'Unauthorized' });
|
|
1073
|
+
next();
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
**With Schema Validation (same object shape):**
|
|
1034
1080
|
|
|
1035
1081
|
```javascript
|
|
1036
1082
|
// pages/api/posts.post.js
|
|
@@ -1066,7 +1112,25 @@ module.exports = {
|
|
|
1066
1112
|
| `query` | Validates query string parameters |
|
|
1067
1113
|
| `response` | Response schema (for documentation, not enforced) |
|
|
1068
1114
|
|
|
1069
|
-
All schemas use [Zod](https://zod.dev) for validation. Invalid requests
|
|
1115
|
+
All schemas use [Zod](https://zod.dev) for validation. Invalid requests receive **`400 JSON`**: `{ error: 'Validation Error', issues: [...] }` (no `handler` / `middleware` run).
|
|
1116
|
+
|
|
1117
|
+
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`.
|
|
1118
|
+
|
|
1119
|
+
```javascript
|
|
1120
|
+
module.exports = {
|
|
1121
|
+
schema: ({ z }) => ({
|
|
1122
|
+
params: z.object({
|
|
1123
|
+
id: z.nanoid(), // default 21 chars
|
|
1124
|
+
shortId: z.nanoid(12), // numeric length
|
|
1125
|
+
other: z.nanoid({ maxLength: 8 }), // zdb-style options
|
|
1126
|
+
}),
|
|
1127
|
+
}),
|
|
1128
|
+
async handler(req, res) {
|
|
1129
|
+
const { id } = req.input.params;
|
|
1130
|
+
// ...
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
```
|
|
1070
1134
|
|
|
1071
1135
|
**Dynamic Route with Params Validation:**
|
|
1072
1136
|
|
|
@@ -1831,6 +1895,34 @@ const user = plugin.api.getModel('User'); // Single model
|
|
|
1831
1895
|
const names = plugin.api.getModelNames(); // Model names
|
|
1832
1896
|
```
|
|
1833
1897
|
|
|
1898
|
+
### REST resources plugin
|
|
1899
|
+
|
|
1900
|
+
Registers **REST-style CRUD** routes for ORM models (`GET` list, `GET /:id`, `POST`, `PATCH /:id`, `DELETE /:id`). Relations are eager-loaded only when the client passes **`?include=relation1,relation2`**; loading uses the ORM batch eager loader (no classic N+1 from relation queries). **Nested includes** (e.g. `posts.comments`) are **not** supported—only top-level relation names defined on the model.
|
|
1901
|
+
|
|
1902
|
+
**Model opt-in** via `defineModel({ ..., rest: { enabled: true, path: 'optional-segment', allowInclude: ['company'] } })`. If you pass **`models: ['User', 'Post']`** to the plugin, those names are exposed even when `rest.enabled` is false.
|
|
1903
|
+
|
|
1904
|
+
**Setup:**
|
|
1905
|
+
|
|
1906
|
+
```javascript
|
|
1907
|
+
const { createApp, restResourcePlugin } = require('webspresso');
|
|
1908
|
+
|
|
1909
|
+
const { app } = createApp({
|
|
1910
|
+
pagesDir: './pages',
|
|
1911
|
+
db,
|
|
1912
|
+
plugins: [
|
|
1913
|
+
restResourcePlugin({
|
|
1914
|
+
path: '/api/rest',
|
|
1915
|
+
middleware: [], // optional Express handlers (e.g. auth) — `attachDbMiddleware` is applied after these
|
|
1916
|
+
models: null, // optional whitelist of model names; when omitted, only `rest.enabled` models are exposed
|
|
1917
|
+
excludeModels: [],
|
|
1918
|
+
filter: null, // optional (model) => boolean
|
|
1919
|
+
}),
|
|
1920
|
+
],
|
|
1921
|
+
});
|
|
1922
|
+
```
|
|
1923
|
+
|
|
1924
|
+
List query parameters: `page`, `perPage`, `sort`, `order`, `include`, `trashed` (`only` / `include` when the model uses soft delete), plus **equality filters** on real columns (unknown keys are ignored).
|
|
1925
|
+
|
|
1834
1926
|
### Health check plugin
|
|
1835
1927
|
|
|
1836
1928
|
Exposes a lightweight **GET** endpoint for load balancers and orchestrators (Kubernetes, Docker healthcheck, etc.). **Enabled by default** in all environments; set `enabled: false` to turn it off.
|
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/model.js
CHANGED
|
@@ -27,6 +27,7 @@ function defineModel(options) {
|
|
|
27
27
|
relations = {},
|
|
28
28
|
scopes = {},
|
|
29
29
|
admin = {},
|
|
30
|
+
rest = {},
|
|
30
31
|
hooks = {},
|
|
31
32
|
hidden = [],
|
|
32
33
|
} = options;
|
|
@@ -89,6 +90,11 @@ function defineModel(options) {
|
|
|
89
90
|
customFields: admin.customFields || {},
|
|
90
91
|
queries: admin.queries || {},
|
|
91
92
|
},
|
|
93
|
+
rest: {
|
|
94
|
+
enabled: rest.enabled === true,
|
|
95
|
+
path: typeof rest.path === 'string' && rest.path.length > 0 ? rest.path.replace(/^\/+|\/+$/g, '') : null,
|
|
96
|
+
allowInclude: Array.isArray(rest.allowInclude) ? rest.allowInclude.filter((x) => typeof x === 'string') : null,
|
|
97
|
+
},
|
|
92
98
|
hidden: Array.isArray(hidden) ? hidden : [],
|
|
93
99
|
hooks: {},
|
|
94
100
|
};
|
package/core/orm/types.js
CHANGED
|
@@ -134,6 +134,13 @@
|
|
|
134
134
|
* @property {Object.<string, QueryConfig>} [queries={}] - Custom query functions
|
|
135
135
|
*/
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @typedef {Object} RestMetadata
|
|
139
|
+
* @property {boolean} [enabled=false] - Expose this model via restResourcePlugin (when not using plugin `models` whitelist)
|
|
140
|
+
* @property {string} [path] - URL segment override (no slashes); default: pluralized model name
|
|
141
|
+
* @property {string[]} [allowInclude] - Allowed relation names for `?include=`; default: all `model.relations` keys
|
|
142
|
+
*/
|
|
143
|
+
|
|
137
144
|
// ============================================================================
|
|
138
145
|
// Model Types
|
|
139
146
|
// ============================================================================
|
|
@@ -147,6 +154,7 @@
|
|
|
147
154
|
* @property {RelationsMap} [relations={}] - Relation definitions
|
|
148
155
|
* @property {ScopeOptions} [scopes={}] - Scope options
|
|
149
156
|
* @property {AdminMetadata} [admin] - Admin panel metadata
|
|
157
|
+
* @property {RestMetadata} [rest] - REST resource plugin metadata
|
|
150
158
|
* @property {string[]} [hidden=[]] - Column names to never expose in API/templates (e.g. password_hash, api_token)
|
|
151
159
|
*/
|
|
152
160
|
|
|
@@ -160,6 +168,7 @@
|
|
|
160
168
|
* @property {ScopeOptions} scopes - Scope options
|
|
161
169
|
* @property {Map<string, ColumnMeta>} columns - Parsed column metadata
|
|
162
170
|
* @property {AdminMetadata} [admin] - Admin panel metadata
|
|
171
|
+
* @property {RestMetadata} rest - REST resource plugin metadata
|
|
163
172
|
* @property {string[]} hidden - Column names never exposed in API/templates
|
|
164
173
|
*/
|
|
165
174
|
|
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
|
};
|