webspresso 0.0.95 → 0.0.96
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/.agents/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
package/.agents/QUEUE.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Webspresso Background Job Queue Guide
|
|
2
|
+
|
|
3
|
+
Webspresso features a high-performance, zero-dependency background job queue engine (`core/queue`) and plugin (`plugins/queue`), supporting In-Memory, Knex Database, and Distributed Redis adapters, automatic retries with exponential backoff, priority scheduling, and graceful shutdown draining.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Quick Start
|
|
8
|
+
|
|
9
|
+
### Basic Usage with `core/queue`
|
|
10
|
+
```js
|
|
11
|
+
const { createQueueManager } = require('webspresso/core/queue');
|
|
12
|
+
|
|
13
|
+
const queue = createQueueManager({
|
|
14
|
+
concurrency: 4,
|
|
15
|
+
pollInterval: 200,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// 1. Define job handler
|
|
19
|
+
queue.define('email.send', async (job, ctx) => {
|
|
20
|
+
const { to, subject, body } = job.data;
|
|
21
|
+
job.progress(20, 'Preparing template');
|
|
22
|
+
// ... send email ...
|
|
23
|
+
job.progress(100, 'Sent');
|
|
24
|
+
return { delivered: true };
|
|
25
|
+
}, {
|
|
26
|
+
attempts: 3,
|
|
27
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
28
|
+
timeout: 10000,
|
|
29
|
+
priority: 20,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 2. Dispatch job
|
|
33
|
+
await queue.dispatch('email.send', {
|
|
34
|
+
to: 'user@example.com',
|
|
35
|
+
subject: 'Welcome!',
|
|
36
|
+
}, {
|
|
37
|
+
priority: 50,
|
|
38
|
+
delay: 5000, // delay 5 seconds
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Using the Plugin (`plugins/queue`)
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const { createApp, queuePlugin } = require('webspresso');
|
|
48
|
+
|
|
49
|
+
const app = createApp({
|
|
50
|
+
plugins: [
|
|
51
|
+
queuePlugin({
|
|
52
|
+
adapter: 'memory', // 'memory' | 'database' | 'redis'
|
|
53
|
+
concurrency: 4,
|
|
54
|
+
jobsDir: './jobs', // Auto-discovers jobs/ directory
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// In Express route handlers:
|
|
60
|
+
app.post('/api/export', async (req, res) => {
|
|
61
|
+
const job = await req.queue.dispatch('export.users', { format: 'csv' });
|
|
62
|
+
res.json({ jobId: job.id, status: job.status });
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Adapters
|
|
69
|
+
|
|
70
|
+
### 1. `MemoryQueueAdapter` (Default)
|
|
71
|
+
Zero-dependency in-memory priority queue. Ideal for lightweight background tasks, single-process apps, and development.
|
|
72
|
+
|
|
73
|
+
### 2. `DatabaseQueueAdapter`
|
|
74
|
+
Persistent queue backed by Knex database table (`webspresso_jobs`) supporting SQLite, PostgreSQL, and MySQL with row-level locking.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
queuePlugin({
|
|
78
|
+
adapter: 'database',
|
|
79
|
+
db: appDb,
|
|
80
|
+
tableName: 'webspresso_jobs',
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. `RedisQueueAdapter`
|
|
85
|
+
Distributed queue backed by Redis for multi-instance PM2/Kubernetes cluster deployments.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
queuePlugin({
|
|
89
|
+
adapter: 'redis',
|
|
90
|
+
redisClient: redis,
|
|
91
|
+
prefix: 'app:queue:',
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 4. File-Based Auto-Discovery (`jobs/`)
|
|
98
|
+
|
|
99
|
+
Files placed in `jobs/` are automatically mapped to dot-separated job names:
|
|
100
|
+
- `jobs/email/send.js` → `'email.send'`
|
|
101
|
+
- `jobs/user-report.js` → `'user-report'`
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// jobs/email/send.js
|
|
105
|
+
module.exports = async function(job, ctx) {
|
|
106
|
+
// job.data
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports.options = {
|
|
110
|
+
attempts: 5,
|
|
111
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
112
|
+
};
|
|
113
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Webspresso Realtime Layer & Adapters Guide
|
|
2
|
+
|
|
3
|
+
[← Back to AGENTS.md](.agents/AGENTS.md)
|
|
4
|
+
|
|
5
|
+
Webspresso features a framework-agnostic, plugin-based, adapter-driven Realtime layer (`core/realtime`) designed for universal browser execution with zero SSR hazards.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Architectural Overview
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Webspresso App / Client
|
|
13
|
+
│
|
|
14
|
+
├── AuthManager (Token / Cookie / Custom / None)
|
|
15
|
+
├── SubscriptionRegistry (identifier::params composite identity)
|
|
16
|
+
├── ReconnectManager (exponential backoff + jitter)
|
|
17
|
+
│
|
|
18
|
+
└── Realtime Core
|
|
19
|
+
│
|
|
20
|
+
├── Generic WebSocket Adapter (`core/realtime/adapters/websocket`)
|
|
21
|
+
├── SSE Adapter (`core/realtime/adapters/sse`)
|
|
22
|
+
├── Socket.IO Adapter (`core/realtime/adapters/socket-io`)
|
|
23
|
+
└── Redis Pub/Sub Adapter (`core/realtime/adapters/redis`)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Core Tenets:
|
|
27
|
+
1. **Zero SSR Network Hazards**: Server-side rendering execution never attempts socket connections or touches browser globals.
|
|
28
|
+
2. **Unified Subscription Model**: All realtime technologies share identical public subscription interfaces (`subscribe`, `send`, `perform`, `unsubscribe`).
|
|
29
|
+
3. **Deterministic Subscription Registry**: Subscriptions with identical identifiers and query parameters share channel instances without duplication.
|
|
30
|
+
4. **Secret Sanitization**: Auth tokens and credentials are automatically stripped from error logs (`token=***`).
|
|
31
|
+
5. **Distributed Cluster Sync**: Redis adapter allows multi-process / multi-container instances (PM2 cluster, K8s, Docker) to broadcast across all connected clients.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Core API Reference
|
|
36
|
+
|
|
37
|
+
### 2.1 Factory Functions (`core/realtime`, `plugins/realtime`)
|
|
38
|
+
```js
|
|
39
|
+
const { createRealtime, realtime, realtimePlugin, redis, websocket, sse, socketIo } = require('webspresso');
|
|
40
|
+
// Or via plugins
|
|
41
|
+
const { realtimePlugin, redis, websocket, sse, socketIo } = require('webspresso/plugins');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `createRealtime(options)`: Creates a standalone `RealtimeClient`.
|
|
45
|
+
- `realtime(options)`: Universal helper returning `RealtimeClient` with `.plugin` property attached.
|
|
46
|
+
- `realtimePlugin(options)`: Webspresso plugin mounting `app.realtime` and handling shutdown disposers.
|
|
47
|
+
|
|
48
|
+
### 2.2 Options Configuration
|
|
49
|
+
| Option | Type | Default | Description |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `adapter` | `RealtimeAdapter` | **Required** | Transport adapter (`websocket()`, `sse()`, `socketIo()`, `redis()`). |
|
|
52
|
+
| `auth` | `Object` \| `false` | `false` | Strategy config: `{ strategy: 'token'|'cookie'|'custom' }`. |
|
|
53
|
+
| `autoConnect` | `boolean` | `true` | Automatically connect when browser environment is ready. |
|
|
54
|
+
| `reconnect` | `Object` | `{ enabled: true }` | Backoff settings (`baseDelay`, `maxDelay`, `factor`, `jitter`). |
|
|
55
|
+
| `isBrowser` | `boolean` | `null` | Force browser mode (testing override). |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 3. Adapters
|
|
60
|
+
|
|
61
|
+
### 3.1 Generic WebSocket Adapter (`websocket`)
|
|
62
|
+
```js
|
|
63
|
+
const { websocket } = require('webspresso');
|
|
64
|
+
|
|
65
|
+
const adapter = websocket({
|
|
66
|
+
url: 'wss://api.example.com/realtime', // string or async (ctx) => url
|
|
67
|
+
authTransport: 'query', // 'query' | 'header' | 'protocol' | 'message'
|
|
68
|
+
queryParamName: 'token',
|
|
69
|
+
messages: {
|
|
70
|
+
subscribe: ({ identifier, params }) => ({ type: 'subscribe', identifier, params }),
|
|
71
|
+
send: ({ identifier, data }) => ({ type: 'message', identifier, data }),
|
|
72
|
+
perform: ({ identifier, action, data }) => ({ type: 'perform', identifier, action, data }),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3.2 Server-Sent Events (SSE) Adapter (`sse`)
|
|
78
|
+
```js
|
|
79
|
+
const { sse } = require('webspresso');
|
|
80
|
+
|
|
81
|
+
const adapter = sse({
|
|
82
|
+
url: '/api/realtime/events',
|
|
83
|
+
queryParamName: 'token',
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
*Note: SSE adapter is unidirectional and read-only. Calling `sub.send()` or `sub.perform()` will throw a clear `UNSUPPORTED_CAPABILITY` error.*
|
|
87
|
+
|
|
88
|
+
### 3.3 Socket.IO Adapter (`socketIo`)
|
|
89
|
+
```js
|
|
90
|
+
const { socketIo } = require('webspresso');
|
|
91
|
+
|
|
92
|
+
const adapter = socketIo({
|
|
93
|
+
url: 'https://socket.example.com',
|
|
94
|
+
path: '/socket.io',
|
|
95
|
+
socketOptions: { transports: ['websocket'] },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3.4 Redis Pub/Sub Distributed Adapter (`redis`)
|
|
100
|
+
Enables cross-process distributed event synchronization across PM2 clusters, Docker containers, and Kubernetes pods:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const { createApp, realtimePlugin, redis } = require('webspresso');
|
|
104
|
+
const Redis = require('ioredis');
|
|
105
|
+
|
|
106
|
+
// Option A: Single client instance (adapter will duplicate for subClient)
|
|
107
|
+
const pubsub = new Redis(process.env.REDIS_URL);
|
|
108
|
+
const adapter = redis({
|
|
109
|
+
redis: pubsub,
|
|
110
|
+
channelPrefix: 'myapp:rt:',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Option B: Explicit dual clients
|
|
114
|
+
const pubClient = new Redis(process.env.REDIS_URL);
|
|
115
|
+
const subClient = new Redis(process.env.REDIS_URL);
|
|
116
|
+
const adapter = redis({
|
|
117
|
+
pubClient,
|
|
118
|
+
subClient,
|
|
119
|
+
channelPrefix: 'myapp:rt:',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Option C: In-memory fallback (useful for local development & unit tests)
|
|
123
|
+
const localAdapter = redis({ inMemory: true });
|
|
124
|
+
|
|
125
|
+
const { app } = createApp({
|
|
126
|
+
plugins: [
|
|
127
|
+
realtimePlugin({
|
|
128
|
+
adapter,
|
|
129
|
+
autoConnect: true,
|
|
130
|
+
}),
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Server-side broadcasting to all cluster nodes:
|
|
135
|
+
await app.realtime.adapter.publish('room:general', { message: 'Cluster-wide notification' });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 4. Usage Recipes
|
|
142
|
+
|
|
143
|
+
### 4.1 Webspresso Plugin Integration
|
|
144
|
+
```js
|
|
145
|
+
const { createApp } = require('webspresso');
|
|
146
|
+
const { realtimePlugin, websocket } = require('webspresso/plugins');
|
|
147
|
+
|
|
148
|
+
const { app } = createApp({
|
|
149
|
+
plugins: [
|
|
150
|
+
realtimePlugin({
|
|
151
|
+
adapter: websocket({ url: '/realtime' }),
|
|
152
|
+
auth: { strategy: 'cookie' },
|
|
153
|
+
autoConnect: true,
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 4.2 Token Authentication & Automatic Refresh
|
|
160
|
+
```js
|
|
161
|
+
const { realtime, websocket } = require('webspresso');
|
|
162
|
+
|
|
163
|
+
const client = realtime({
|
|
164
|
+
adapter: websocket({ url: 'wss://api.example.com/realtime' }),
|
|
165
|
+
auth: {
|
|
166
|
+
strategy: 'token',
|
|
167
|
+
getToken: async () => localStorage.getItem('jwt_token'),
|
|
168
|
+
refreshToken: async () => {
|
|
169
|
+
const res = await fetch('/api/auth/refresh', { method: 'POST' });
|
|
170
|
+
const { token } = await res.json();
|
|
171
|
+
localStorage.setItem('jwt_token', token);
|
|
172
|
+
return token;
|
|
173
|
+
},
|
|
174
|
+
onUnauthorized: (err) => {
|
|
175
|
+
window.location.href = '/login';
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 4.3 Subscriptions & Remote Actions
|
|
182
|
+
```js
|
|
183
|
+
// Subscribe to channel
|
|
184
|
+
const sub = app.realtime.subscribe('project:123', { role: 'editor' }, {
|
|
185
|
+
connected() {
|
|
186
|
+
console.log('Connected to project:123 channel');
|
|
187
|
+
},
|
|
188
|
+
disconnected() {
|
|
189
|
+
console.log('Temporarily disconnected');
|
|
190
|
+
},
|
|
191
|
+
received(data) {
|
|
192
|
+
console.log('New event:', data);
|
|
193
|
+
},
|
|
194
|
+
rejected(error) {
|
|
195
|
+
console.error('Subscription rejected:', error);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Send raw message
|
|
200
|
+
sub.send({ text: 'Hello!' });
|
|
201
|
+
|
|
202
|
+
// Perform named action (RPC)
|
|
203
|
+
sub.perform('cursor_move', { x: 120, y: 340 });
|
|
204
|
+
|
|
205
|
+
// Unsubscribe
|
|
206
|
+
sub.unsubscribe();
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 4.4 Re-authentication Workflow
|
|
210
|
+
```js
|
|
211
|
+
// Refresh credentials and restore all active subscriptions seamlessly
|
|
212
|
+
await app.realtime.reauthenticate();
|
|
213
|
+
```
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Webspresso File-Based Routing & API Routes
|
|
2
|
+
|
|
3
|
+
[← Back to AGENTS.md](AGENTS.md)
|
|
4
|
+
|
|
5
|
+
Webspresso is a full-stack Node.js framework powered by **100% File-Based Routing** (similar to Next.js App Router / Nuxt 3).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ⚠️ STRICT ANTI-PATTERNS (DO NOT DO THIS)
|
|
10
|
+
|
|
11
|
+
### ❌ BAD vs ✅ GOOD Code Comparison
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
// ❌ WRONG (Express / Legacy reflex — NEVER DO THIS)
|
|
15
|
+
// File: src/routes/users.js
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
router.get('/users', async (req, res) => {
|
|
19
|
+
const users = await db('users').select('*'); // Raw query bypasses ORM!
|
|
20
|
+
res.json(users);
|
|
21
|
+
});
|
|
22
|
+
module.exports = router;
|
|
23
|
+
|
|
24
|
+
// ✅ CORRECT (Webspresso 100% File-Based Routing)
|
|
25
|
+
// File: pages/api/users.get.js
|
|
26
|
+
'use strict';
|
|
27
|
+
module.exports = {
|
|
28
|
+
schema: ({ z }) => ({
|
|
29
|
+
query: z.object({ limit: z.coerce.number().optional().default(20) }),
|
|
30
|
+
}),
|
|
31
|
+
middleware: ['auth'], // optional
|
|
32
|
+
handler: async (req, res) => {
|
|
33
|
+
const users = await req.db.getRepository('User').find(req.input.query);
|
|
34
|
+
return res.json({ success: true, data: users });
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
1. **NEVER create a `routes/` or `src/routes/` folder.**
|
|
40
|
+
2. **NEVER use `express.Router()` or `require('express').Router()`.**
|
|
41
|
+
3. **NEVER call `app.get()`, `app.post()`, `app.put()`, or `app.delete()` for application endpoints.**
|
|
42
|
+
4. **NEVER use `createApp({ setupRoutes: (app) => ... })` to mount domain/application routes.**
|
|
43
|
+
5. **ALL application routes MUST live inside the `pages/` directory.**
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 1. Directory Structure & URL Mapping
|
|
48
|
+
|
|
49
|
+
All routes are automatically discovered and registered from the `pages/` directory:
|
|
50
|
+
|
|
51
|
+
| File Path | HTTP Method | URL Route | Description |
|
|
52
|
+
| :--- | :--- | :--- | :--- |
|
|
53
|
+
| `pages/index.njk` | `GET` | `/` | Home page (SSR Nunjucks) |
|
|
54
|
+
| `pages/about.njk` | `GET` | `/about` | Static SSR page |
|
|
55
|
+
| `pages/notes/[id].njk` | `GET` | `/notes/:id` | Dynamic SSR route with param |
|
|
56
|
+
| `pages/api/notes.get.js` | `GET` | `/api/notes` | List notes API endpoint |
|
|
57
|
+
| `pages/api/notes.post.js` | `POST` | `/api/notes` | Create note API endpoint |
|
|
58
|
+
| `pages/api/notes/[id].get.js` | `GET` | `/api/notes/:id` | Get single note API endpoint |
|
|
59
|
+
| `pages/api/notes/[id].put.js` | `PUT` | `/api/notes/:id` | Update note API endpoint |
|
|
60
|
+
| `pages/api/notes/[id].delete.js` | `DELETE` | `/api/notes/:id` | Delete note API endpoint |
|
|
61
|
+
|
|
62
|
+
### Method Extension Suffixes
|
|
63
|
+
API route filenames **MUST** declare their HTTP method before the `.js` extension:
|
|
64
|
+
- `.get.js`
|
|
65
|
+
- `.post.js`
|
|
66
|
+
- `.put.js`
|
|
67
|
+
- `.patch.js`
|
|
68
|
+
- `.delete.js`
|
|
69
|
+
|
|
70
|
+
If no method suffix is provided (e.g. `pages/api/notes.js`), it defaults to `GET`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. Standard API Route Contract
|
|
75
|
+
|
|
76
|
+
Every API route module exports an object with optional `schema`, optional `middleware`, and a `handler` function:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// pages/api/notes.post.js
|
|
80
|
+
'use strict';
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
// 1. Declarative Input Validation (Zod)
|
|
84
|
+
schema: ({ z }) => ({
|
|
85
|
+
body: z.object({
|
|
86
|
+
title: z.string().min(1, 'Title is required').max(200),
|
|
87
|
+
content: z.string().min(1, 'Content is required'),
|
|
88
|
+
isPublic: z.boolean().optional().default(false),
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
// 2. Named Middleware Guards (resolved from ctx.middlewares or global plugins)
|
|
93
|
+
middleware: ['auth'], // e.g. 'auth', 'jwt', 'basicAuth', or inline (req, res, next) => ...
|
|
94
|
+
|
|
95
|
+
// 3. Request Handler
|
|
96
|
+
handler: async (req, res) => {
|
|
97
|
+
// req.db: Injected database repository factory
|
|
98
|
+
// req.service(): Injected service registry invocation helper
|
|
99
|
+
// req.input.body: Type-safe, validated request payload
|
|
100
|
+
// req.auth / req.session: Authenticated user session
|
|
101
|
+
|
|
102
|
+
const noteRepo = req.db.getRepository('Note');
|
|
103
|
+
const note = await noteRepo.create({
|
|
104
|
+
...req.input.body,
|
|
105
|
+
userId: req.session?.user?.id || req.auth?.user?.id,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return res.status(201).json({
|
|
109
|
+
success: true,
|
|
110
|
+
data: note,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> [!IMPORTANT]
|
|
117
|
+
> **NEVER use raw Knex queries** like `req.db('notes')` in route handlers. **ALWAYS use repositories**: `req.db.getRepository('Note')`. This guarantees schema validation, lifecycle hooks, soft deletes, and query caching are respected.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 3. Dynamic Route Parameters (`[param]`)
|
|
122
|
+
|
|
123
|
+
Dynamic path segments use square brackets:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// pages/api/notes/[id].get.js
|
|
127
|
+
'use strict';
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
schema: ({ z }) => ({
|
|
131
|
+
params: z.object({
|
|
132
|
+
id: z.string().min(1),
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
handler: async (req, res) => {
|
|
137
|
+
const { id } = req.params;
|
|
138
|
+
const note = await req.db.getRepository('Note').findById(id);
|
|
139
|
+
|
|
140
|
+
if (!note) {
|
|
141
|
+
return res.status(404).json({ error: 'Note not found' });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return res.json({ success: true, data: note });
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 4. SSR Page Routes (`load()` Loader)
|
|
152
|
+
|
|
153
|
+
For server-rendered Nunjucks HTML pages, use `.njk` templates and companion `.js` loader files (or inline `async function load` in a `.js` file):
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// pages/notes/[id].js (Companion data loader for pages/notes/[id].njk)
|
|
157
|
+
'use strict';
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
// Runs server-side before rendering the template
|
|
161
|
+
async load({ req, res, db, ctx }) {
|
|
162
|
+
const note = await db.getRepository('Note').findById(req.params.id);
|
|
163
|
+
if (!note) {
|
|
164
|
+
return { status: 404, notFound: true };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
note,
|
|
169
|
+
title: note.title,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 5. Module & File-Based Discovery (`src/pages`, `src/api`, `modules/{name}`)
|
|
178
|
+
|
|
179
|
+
Webspresso supports clean, domain-driven module discovery alongside root-level pages and APIs:
|
|
180
|
+
|
|
181
|
+
```txt
|
|
182
|
+
src/
|
|
183
|
+
pages/ -> Global UI pages (definePage or Nunjucks)
|
|
184
|
+
index.js -> GET /
|
|
185
|
+
about.js -> GET /about
|
|
186
|
+
docs/[...path].js -> GET /docs/*path
|
|
187
|
+
|
|
188
|
+
api/ -> Global API endpoints (defineApi)
|
|
189
|
+
health.get.js -> GET /api/health
|
|
190
|
+
users/[id].patch.js -> PATCH /api/users/:id
|
|
191
|
+
|
|
192
|
+
modules/ -> Feature-driven modules
|
|
193
|
+
auth/
|
|
194
|
+
auth.module.js -> defineModule({ name: 'auth', ... })
|
|
195
|
+
pages/
|
|
196
|
+
login.js -> GET /auth/login
|
|
197
|
+
register.js -> GET /auth/register
|
|
198
|
+
api/
|
|
199
|
+
login.post.js -> POST /api/auth/login
|
|
200
|
+
me.get.js -> GET /api/auth/me
|
|
201
|
+
services/
|
|
202
|
+
login.js -> ctx.service('auth.login', input)
|
|
203
|
+
middleware/
|
|
204
|
+
auth-guard.js -> module-local named middleware 'authGuard'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 5.1 `definePage()` Contract
|
|
208
|
+
```javascript
|
|
209
|
+
const { definePage } = require('webspresso');
|
|
210
|
+
|
|
211
|
+
module.exports = definePage({
|
|
212
|
+
middleware: ['authGuard'], // module or global middleware
|
|
213
|
+
load: async (ctx) => {
|
|
214
|
+
// ctx: { req, res, params, query, service, db, redirect, fsy, locale, t }
|
|
215
|
+
if (!ctx.req.user) {
|
|
216
|
+
return ctx.redirect('/auth/login', 302);
|
|
217
|
+
}
|
|
218
|
+
const profile = await ctx.service('user.profile', { id: ctx.req.user.id });
|
|
219
|
+
return { profile };
|
|
220
|
+
},
|
|
221
|
+
head: (data) => ({
|
|
222
|
+
title: `${data.profile.name}'s Profile`,
|
|
223
|
+
}),
|
|
224
|
+
render: (data, ctx) => `
|
|
225
|
+
<main>
|
|
226
|
+
<h1>${data.profile.name}</h1>
|
|
227
|
+
<p>Email: ${data.profile.email}</p>
|
|
228
|
+
</main>
|
|
229
|
+
`,
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 5.2 `defineApi()` Contract
|
|
234
|
+
```javascript
|
|
235
|
+
const { defineApi, z } = require('webspresso');
|
|
236
|
+
|
|
237
|
+
module.exports = defineApi({
|
|
238
|
+
description: 'Authenticate user and return session',
|
|
239
|
+
tags: ['Auth'],
|
|
240
|
+
middleware: ['rateLimit'],
|
|
241
|
+
schema: {
|
|
242
|
+
body: z.object({
|
|
243
|
+
email: z.string().email(),
|
|
244
|
+
password: z.string().min(8),
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
handler: async (req, res, ctx) => {
|
|
248
|
+
// req.input.body contains validated payload
|
|
249
|
+
const result = await ctx.service('auth.login', req.input.body);
|
|
250
|
+
return res.json(result);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 5.3 `defineModule()` Contract
|
|
256
|
+
```javascript
|
|
257
|
+
const { defineModule } = require('webspresso');
|
|
258
|
+
|
|
259
|
+
module.exports = defineModule({
|
|
260
|
+
name: 'auth',
|
|
261
|
+
version: '1.0.0',
|
|
262
|
+
pages: { prefix: '/auth' }, // default: /auth
|
|
263
|
+
api: { prefix: '/api/auth' }, // default: /api/auth
|
|
264
|
+
middlewares: {
|
|
265
|
+
authGuard: (req, res, next) => {
|
|
266
|
+
if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
|
|
267
|
+
next();
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 5.4 Route Introspection (`app.routes.list()`)
|
|
274
|
+
Compiled routes are accessible at runtime for tooling, CLI inspection, and documentation:
|
|
275
|
+
```javascript
|
|
276
|
+
const routeList = app.routes.list();
|
|
277
|
+
// [
|
|
278
|
+
// { method: 'GET', path: '/', type: 'page', source: 'src/pages/index.js' },
|
|
279
|
+
// { method: 'POST', path: '/api/auth/login', type: 'api', module: 'auth', description: 'Authenticate user' }
|
|
280
|
+
// ]
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 6. Summary Checklist for Agents
|
|
286
|
+
|
|
287
|
+
When a user asks to add or modify endpoints:
|
|
288
|
+
1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).
|
|
289
|
+
2. Create in `pages/api/...`, `src/api/...`, or `modules/{module}/api/...` with HTTP method extension (`.post.js`).
|
|
290
|
+
3. Use `defineApi()` or `{ schema, middleware, handler }`.
|
|
291
|
+
4. Use `req.db.getRepository(...)` or `ctx.service(...)`.
|
|
292
|
+
5. **NEVER touch `routes/` or create manual Express router files.**
|