webspresso 0.0.94 → 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 +19 -6
- 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/polar/CHANGELOG.md +18 -0
- package/plugins/polar/README.md +30 -7
- package/plugins/polar/index.js +6 -1
- package/plugins/polar/src/billing-hooks.js +51 -0
- package/plugins/polar/src/config.js +2 -0
- package/plugins/polar/src/rate-limit.js +96 -28
- package/plugins/polar/src/routes.js +3 -2
- package/plugins/polar/src/urls.js +26 -2
- package/plugins/polar/src/user-resolver.js +5 -1
- package/plugins/rate-limit/index.js +5 -1
- 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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# Webspresso Services Layer (`services/`, `src/services/`)
|
|
2
|
+
|
|
3
|
+
The **Services Layer** is a lightweight, framework-agnostic architectural abstraction designed to encapsulate business logic, multi-step domain mutations, and data queries into isolated, reusable, testable functions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ STRICT RULE: Encapsulate in `services/` (NO `controllers/`)
|
|
8
|
+
|
|
9
|
+
### ❌ BAD vs ✅ GOOD Code Comparison
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
// ❌ WRONG (Controller pattern / manual transactions — NEVER DO THIS)
|
|
13
|
+
// File: controllers/UserController.js
|
|
14
|
+
class UserController {
|
|
15
|
+
async register(req, res) {
|
|
16
|
+
const trx = await knex.transaction(); // manual trx prone to leak
|
|
17
|
+
try {
|
|
18
|
+
const user = await trx('users').insert(...);
|
|
19
|
+
await trx.commit();
|
|
20
|
+
res.json(user);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
await trx.rollback();
|
|
23
|
+
res.status(500).json({ error: e.message });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ✅ CORRECT (Webspresso Service with declarative validation & automatic transaction)
|
|
29
|
+
// File: services/user/register.js
|
|
30
|
+
'use strict';
|
|
31
|
+
const { z } = require('zod');
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
schema: z.object({
|
|
35
|
+
email: z.string().email(),
|
|
36
|
+
password: z.string().min(8),
|
|
37
|
+
}),
|
|
38
|
+
transaction: true, // Automatically wraps in ACID transaction
|
|
39
|
+
handler: async (input, { db, service }) => {
|
|
40
|
+
const user = await db.getRepository('User').create(input);
|
|
41
|
+
return { success: true, user };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **NEVER** create a `controllers/` or `src/controllers/` directory, and never write class-based controller singletons.
|
|
47
|
+
- **ALWAYS** place business logic, domain mutations, and reusable workflows in `services/domain/action.js`.
|
|
48
|
+
- Services automatically provide Zod input validation (`schema`), role-based authorization (`auth`), automatic ACID transaction propagation (`transaction: true`), and caching (`cache`).
|
|
49
|
+
- Invoke services from API routes (`req.service('name', input)`), SSR loaders (`ctx.service('name', input)`), or other services (`ctx.service(...)`).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 1. Directory Structure & Auto-Discovery
|
|
54
|
+
|
|
55
|
+
Services are located in the `services/` directory at the project root. Files automatically map to dot-separated service names and camelCase aliases.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
services/
|
|
59
|
+
├── user/
|
|
60
|
+
│ ├── get.js → 'user.get'
|
|
61
|
+
│ ├── create.js → 'user.create'
|
|
62
|
+
│ └── reset-password.js → 'user.reset-password' & 'user.resetPassword'
|
|
63
|
+
├── order-items/
|
|
64
|
+
│ └── get-by-id.js → 'order-items.get-by-id' & 'orderItems.getById'
|
|
65
|
+
└── billing/
|
|
66
|
+
└── invoice/
|
|
67
|
+
└── generate.js → 'billing.invoice.generate' & 'billing.invoice.generate'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Discovery Mechanics
|
|
71
|
+
- **Cross-Platform Path Normalization**: Works seamlessly on Windows (`\`) and Unix (`/`).
|
|
72
|
+
- **Kebab & Snake to camelCase Aliases**: Files named with dashes or underscores automatically register both their raw dot-name and camelCase alias (e.g. `order-items/calculate_tax.js` is callable as both `'order-items.calculate_tax'` and `'orderItems.calculateTax'`).
|
|
73
|
+
- **File Extensions**: Discovers `.js` and `.cjs` files. Ignores files starting with `_` or `.` (e.g., `_helpers.js`).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 2. Service Definition Contract
|
|
78
|
+
|
|
79
|
+
A service file exports a `handler` function and optional configuration properties:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// services/user/create.js
|
|
83
|
+
const { z } = require('zod');
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
// Optional input validation schema (Zod)
|
|
87
|
+
schema: z.object({
|
|
88
|
+
email: z.string().email(),
|
|
89
|
+
name: z.string().min(2),
|
|
90
|
+
role: z.enum(['user', 'admin']).default('user'),
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
// Optional authentication & role requirements
|
|
94
|
+
auth: 'admin', // or true, ['admin', 'manager'], or (user, ctx) => boolean
|
|
95
|
+
|
|
96
|
+
// Optional timeout in milliseconds (throws ServiceTimeoutError if exceeded)
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
|
|
99
|
+
// Optional automatic ACID transaction wrapper
|
|
100
|
+
transaction: true,
|
|
101
|
+
|
|
102
|
+
// Optional response memoization / caching
|
|
103
|
+
cache: {
|
|
104
|
+
ttl: 60000, // TTL in milliseconds
|
|
105
|
+
key: (input) => `user:create:${input.email}`, // Custom key generator (defaults to SHA-256 of input)
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Service handler
|
|
109
|
+
async handler(input, ctx) {
|
|
110
|
+
const { email, name, role } = input;
|
|
111
|
+
const { db, trx } = ctx;
|
|
112
|
+
|
|
113
|
+
const user = await db.getRepository('User').create(
|
|
114
|
+
{ email, name, role },
|
|
115
|
+
{ trx } // Reuses the automatic service transaction
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return user;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 3. Invocation & Composition (`ctx.service()`)
|
|
126
|
+
|
|
127
|
+
### 3.1 From SSR Page Loaders
|
|
128
|
+
```javascript
|
|
129
|
+
// pages/users/[id].js
|
|
130
|
+
module.exports = {
|
|
131
|
+
async load({ req, res, ctx }) {
|
|
132
|
+
const user = await ctx.service('user.get', { id: req.params.id });
|
|
133
|
+
return { user };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3.2 From Express API Handlers
|
|
139
|
+
```javascript
|
|
140
|
+
// pages/api/users.js
|
|
141
|
+
module.exports = async function (req, res) {
|
|
142
|
+
const newUser = await req.service('user.create', req.body);
|
|
143
|
+
res.status(201).json({ data: newUser });
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 3.3 Nested Service Composition
|
|
148
|
+
Services can invoke other services via `ctx.service()`. The child service automatically inherits `ctx.db`, `ctx.auth`, `ctx.user`, `ctx.session`, and active transaction (`ctx.trx`):
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// services/order/checkout.js
|
|
152
|
+
module.exports = {
|
|
153
|
+
transaction: true,
|
|
154
|
+
async handler({ orderId, items }, ctx) {
|
|
155
|
+
// 1. Process payment
|
|
156
|
+
await ctx.service('payment.charge', { orderId });
|
|
157
|
+
|
|
158
|
+
// 2. Deduct inventory (runs inside the exact same database transaction!)
|
|
159
|
+
await ctx.service('inventory.deduct', { items });
|
|
160
|
+
|
|
161
|
+
// 3. Send confirmation email (async)
|
|
162
|
+
await ctx.service('email.sendOrderConfirmation', { orderId });
|
|
163
|
+
|
|
164
|
+
return { status: 'completed' };
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Advanced Execution Features
|
|
172
|
+
|
|
173
|
+
### 4.1 Authentication & RBAC (`auth`)
|
|
174
|
+
- `auth: true` — Requires `ctx.user` or `ctx.auth.user` to be present. Throws `UnauthorizedError` (HTTP 401) if unauthenticated.
|
|
175
|
+
- `auth: 'admin'` — Checks `user.role === 'admin'` or `user.roles.includes('admin')`. Throws `ForbiddenError` (HTTP 403) if unauthorized.
|
|
176
|
+
- `auth: ['admin', 'manager']` — Allows any of the specified roles.
|
|
177
|
+
- `auth: (user, ctx) => boolean` — Custom predicate function for dynamic authorization rules.
|
|
178
|
+
- `skipAuth: true` — Option passed in `ctx.service('service.name', input, { skipAuth: true })` to allow internal background crons or system tasks to bypass auth checks.
|
|
179
|
+
|
|
180
|
+
### 4.2 ACID Transactions (`transaction: true`) & Ambient Propagation
|
|
181
|
+
- Wraps the service execution inside a Knex transaction `db.knex.transaction()` and registers it with `AsyncLocalStorage` ambient transaction storage.
|
|
182
|
+
- Automatically binds all ORM repository calls (`db.getRepository(...)`, `ctx.db.getRepository(...)`, query builders) executed in the async tree to the active transaction without manual `trx` argument passing.
|
|
183
|
+
- Exposes `ctx.trx` to the handler and repositories for explicit operations.
|
|
184
|
+
- Automatically commits when handler returns.
|
|
185
|
+
- Automatically rolls back on thrown errors or rejection.
|
|
186
|
+
- Child service calls (`ctx.service(...)`) automatically reuse the parent ambient `trx` without spawning duplicate root transactions or suffering deadlocks.
|
|
187
|
+
|
|
188
|
+
### 4.3 Execution Timeout (`timeout: ms`)
|
|
189
|
+
- Protects against hanging database queries, unresponsive 3rd-party APIs, or deadlocks.
|
|
190
|
+
- Cleans up timers upon completion.
|
|
191
|
+
- Rejects with `ServiceTimeoutError` with descriptive duration message.
|
|
192
|
+
|
|
193
|
+
### 4.4 In-Memory Memoization & Cache (`cache: { ttl, key }`)
|
|
194
|
+
- Prevents redundant computation or queries within TTL window.
|
|
195
|
+
- Generates deterministic SHA-256 cache keys from sorted JSON input representations.
|
|
196
|
+
- Supports custom `key(input)` serializer functions.
|
|
197
|
+
- Provides `services.clearCache(serviceName)` and `services.clearAllCaches()`.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 5. Programmatic & Standalone Usage (CLI / Cron / Tests)
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const { createServiceRegistry } = require('webspresso/services');
|
|
205
|
+
|
|
206
|
+
// Initialize registry directly
|
|
207
|
+
const services = createServiceRegistry({
|
|
208
|
+
servicesDir: path.join(__dirname, 'services'),
|
|
209
|
+
db,
|
|
210
|
+
context: { config: appConfig },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Discover all services
|
|
214
|
+
await services.discover();
|
|
215
|
+
|
|
216
|
+
// Execute service directly
|
|
217
|
+
const result = await services.call('order.getById', { id: 123 });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 6. Built-in `auth.*` Services
|
|
223
|
+
|
|
224
|
+
When a database connection (`options.db`) is configured, Webspresso automatically registers production-ready, standardized authentication services into `serviceRegistry` (unless custom implementations are provided in `services/auth/`):
|
|
225
|
+
|
|
226
|
+
| Service Name | Description | Schema / Input | Guard / ACID |
|
|
227
|
+
|---|---|---|---|
|
|
228
|
+
| `auth.login` | Authenticates user credentials, sets session cookie and returns user data | `{ email, password, remember? }` | Validates hash, sets `req.session.user` |
|
|
229
|
+
| `auth.register` | Validates and hashes password, prevents duplicate emails, creates user record | `{ email, password, name?, role? }` | `transaction: true` |
|
|
230
|
+
| `auth.request-password-reset` | Generates secure password reset token (user enumeration resistant) | `{ email, ttlMs? }` | Returns `{ sent: true }` safely |
|
|
231
|
+
| `auth.reset-password` | Consumes token, validates TTL, updates password hash | `{ token, password }` | `transaction: true` |
|
|
232
|
+
| `auth.change-password` | Verifies current password and updates with new password | `{ currentPassword, newPassword }` | `auth: true`, `transaction: true` |
|
|
233
|
+
| `auth.verify-email` | Consumes verification token and updates `email_verified_at` | `{ token }` | `transaction: true` |
|
|
234
|
+
|
|
235
|
+
### Customizing Built-in Auth Services
|
|
236
|
+
```javascript
|
|
237
|
+
const { createAuthServices } = require('webspresso/services');
|
|
238
|
+
|
|
239
|
+
// Customize model name or column mappings
|
|
240
|
+
const customAuth = createAuthServices({
|
|
241
|
+
userModel: 'Account',
|
|
242
|
+
fields: {
|
|
243
|
+
email: 'email_address',
|
|
244
|
+
password: 'password_hash',
|
|
245
|
+
emailVerifiedAt: 'verified_at',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## 7. Built-in `mail.*` Services
|
|
253
|
+
|
|
254
|
+
When `emailPlugin` is loaded, Webspresso automatically registers standardized transactional email services into `serviceRegistry`:
|
|
255
|
+
|
|
256
|
+
| Service Name | Description | Schema / Input | Features |
|
|
257
|
+
|---|---|---|---|
|
|
258
|
+
| `mail.send` | Sends HTML/plain/MJML transactional email via Nodemailer | `{ to, subject, html?, text?, mjml?, template?, data?, attachments? }` | DB send logging, MJML compilation |
|
|
259
|
+
| `mail.send-templated` | Sends pre-registered MJML template with payload variables | `{ template, to, subject, data?, from?, replyTo? }` | Template caching & variable interpolation |
|
|
260
|
+
| `mail.preview` | Compiles and previews MJML/template without sending | `{ template?, mjml?, data? }` | In-memory HTML & text preview rendering |
|
|
261
|
+
| `mail.query-logs` | Queries email send logs and delivery status | `{ limit?, offset?, status?, to?, template? }` | `auth: 'admin'` |
|
|
262
|
+
|
|
263
|
+
### Customizing or Standalone Mail Services
|
|
264
|
+
```javascript
|
|
265
|
+
const { createMailServices } = require('webspresso/services');
|
|
266
|
+
|
|
267
|
+
// Initialize standalone mail services
|
|
268
|
+
const mail = createMailServices({
|
|
269
|
+
emailService,
|
|
270
|
+
registry: templateRegistry,
|
|
271
|
+
db: appDb,
|
|
272
|
+
tableName: 'email_logs',
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 8. Built-in `media.*` Services
|
|
279
|
+
|
|
280
|
+
When `uploadPlugin` is loaded, Webspresso automatically registers standardized file upload and media handling services into `serviceRegistry`:
|
|
281
|
+
|
|
282
|
+
| Service Name | Description | Schema / Input | Features |
|
|
283
|
+
|---|---|---|---|
|
|
284
|
+
| `media.upload` | Saves Buffer, Base64 data URL, or local file to storage | `{ buffer?, base64?, filePath?, originalName?, mimeType?, destDir?, publicBasePath? }` | Safe filename generation, MIME extraction, storage provider abstraction |
|
|
285
|
+
| `media.delete` | Safely removes stored file from disk/provider | `{ key, destDir? }` | Strict path traversal protection (`..` prevention) |
|
|
286
|
+
| `media.info` | Retrieves file existence, size, and modified timestamp | `{ key, destDir? }` | Fast filesystem stat inspection |
|
|
287
|
+
|
|
288
|
+
### Customizing or Standalone Media Services
|
|
289
|
+
```javascript
|
|
290
|
+
const { createMediaServices } = require('webspresso/services');
|
|
291
|
+
|
|
292
|
+
// Initialize standalone media services
|
|
293
|
+
const media = createMediaServices({
|
|
294
|
+
destDir: '/path/to/custom/uploads',
|
|
295
|
+
publicBasePath: '/static/uploads',
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 9. Built-in `system.*` Services
|
|
302
|
+
|
|
303
|
+
Webspresso automatically registers core system diagnostics, maintenance, and cache invalidation services into `serviceRegistry`:
|
|
304
|
+
|
|
305
|
+
| Service Name | Description | Schema / Input | Guard / Features |
|
|
306
|
+
|---|---|---|---|
|
|
307
|
+
| `system.health` | Live DB ping, latency measurement, memory footprint (`rss`, `heap`), uptime | `{ detailed? }` | Zero downtime monitoring & load balancer health checks |
|
|
308
|
+
| `system.cleanup` | Purges expired auth tokens and old audit logs | `{ olderThanDays?, purgeAuthTokens?, purgeAuditLogs? }` | `auth: 'admin'`, cron/worker ready |
|
|
309
|
+
| `system.cache-flush` | Flushes ORM query cache and service memoization | `{ scope?: 'all'\|'orm'\|'services', model? }` | `auth: 'admin'` |
|
|
310
|
+
| `system.info` | Runtime diagnostics: Node version, platform, registered models, plugins & services | `{}` | `auth: 'admin'` |
|
|
311
|
+
|
|
312
|
+
### Customizing or Standalone System Services
|
|
313
|
+
```javascript
|
|
314
|
+
const { createSystemServices } = require('webspresso/services');
|
|
315
|
+
|
|
316
|
+
// Initialize standalone system services
|
|
317
|
+
const system = createSystemServices({
|
|
318
|
+
db,
|
|
319
|
+
serviceRegistry,
|
|
320
|
+
pluginManager,
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 10. Built-in `exchange.*` Services
|
|
327
|
+
|
|
328
|
+
When `dataExchangePlugin` is loaded, Webspresso automatically registers spreadsheet export and import services into `serviceRegistry`:
|
|
329
|
+
|
|
330
|
+
| Service Name | Description | Schema / Input | Guard / Features |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| `exchange.export` | Exports ORM model records to Excel (.xlsx) buffer | `{ model, ids?, where?, limit? }` | `auth: 'admin'`, frozen header rows, clean output sanitization |
|
|
333
|
+
| `exchange.import` | Imports CSV or XLSX spreadsheets into database models | `{ model, buffer?, base64?, csvText?, mode?: 'insert'\|'upsert', upsertKey? }` | `auth: 'admin'`, `transaction: true` (atomic batch rollback), column type coercion |
|
|
334
|
+
|
|
335
|
+
### Customizing or Standalone Exchange Services
|
|
336
|
+
```javascript
|
|
337
|
+
const { createExchangeServices } = require('webspresso/services');
|
|
338
|
+
|
|
339
|
+
// Initialize standalone exchange services
|
|
340
|
+
const exchange = createExchangeServices({ db });
|
|
341
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Webspresso Graceful Shutdown & Lifecycle Management Guide
|
|
2
|
+
|
|
3
|
+
[← Back to AGENTS.md](.agents/AGENTS.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Overview & Architecture
|
|
8
|
+
|
|
9
|
+
Webspresso includes a framework-level lifecycle coordinator (`ShutdownManager`) and HTTP adapter (`NodeHttpAdapter`) designed to cleanly drain active requests, close keep-alive sockets, and execute reverse-order disposer functions during server termination (`SIGINT`, `SIGTERM`, or `app.close()`).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2. Configuration & Modes
|
|
14
|
+
|
|
15
|
+
Configured via `createApp({ server: { shutdown } })` or `shutdown`:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const { app } = createApp({
|
|
19
|
+
pagesDir: './pages',
|
|
20
|
+
server: {
|
|
21
|
+
shutdown: {
|
|
22
|
+
enabled: true, // Default: true
|
|
23
|
+
mode: 'graceful', // 'graceful' | 'force'
|
|
24
|
+
timeout: 10_000, // Max time in ms to wait for active requests (Default: 10000)
|
|
25
|
+
signals: ['SIGINT', 'SIGTERM'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Shutdown Modes:
|
|
32
|
+
- **`graceful` (Default)**: Rejects new connections, drains active in-flight requests, and closes idle/keep-alive connections.
|
|
33
|
+
- **`force`**: Immediately destroys all active and idle sockets (`forceClose`). Recommended for rapid development/watch server reloads to avoid port lock.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Application Lifecycle Hooks
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// Register cleanup hook
|
|
41
|
+
app.onShutdown(async () => {
|
|
42
|
+
await db.destroy();
|
|
43
|
+
await redis.quit();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Manual close (does not call process.exit())
|
|
47
|
+
await app.close('Maintenance');
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. Plugin Disposer Lifecycle
|
|
53
|
+
|
|
54
|
+
Plugins can return a disposer function from `register(ctx)` or implement `shutdown()` / `dispose()` methods:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const customPlugin = {
|
|
58
|
+
name: 'my-worker',
|
|
59
|
+
register(ctx) {
|
|
60
|
+
const worker = startWorker();
|
|
61
|
+
return async () => {
|
|
62
|
+
await worker.stop();
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
During shutdown, plugin disposers are executed sequentially in **reverse dependency order**.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Webspresso SSR Streaming & Chunked Transfer Guide
|
|
2
|
+
|
|
3
|
+
Webspresso supports zero-dependency, native HTTP chunked response streaming (`Transfer-Encoding: chunked`) for Server-Side Rendered (SSR) Nunjucks templates.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Why SSR Streaming?
|
|
8
|
+
|
|
9
|
+
In traditional SSR, the server must wait for all database queries and API calls to complete before compiling the entire HTML string and sending it with `res.send(html)`.
|
|
10
|
+
|
|
11
|
+
With **SSR Streaming** (`res.renderStream`):
|
|
12
|
+
1. **Instant TTFB**: HTML `<head>`, stylesheets, layout, and loading skeletons are flushed to the client immediately (`res.flushHeaders()`).
|
|
13
|
+
2. **Early Resource Preloading**: Browsers begin downloading CSS, Google Fonts, and JavaScript while backend databases are still executing queries.
|
|
14
|
+
3. **Out-of-Order Slot Resolution**: As asynchronous promises resolve, HTML replacement chunks stream into open slots seamlessly.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 2. Basic Route Streaming (`res.renderStream`)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// In any Express route handler:
|
|
22
|
+
app.get('/dashboard', async (req, res) => {
|
|
23
|
+
const user = await fetchUser(req);
|
|
24
|
+
|
|
25
|
+
const deferredData = {
|
|
26
|
+
analytics: fetchHeavyAnalytics(), // Promise<any>
|
|
27
|
+
recentOrders: fetchOrders(), // Promise<any>
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await res.renderStream('dashboard.njk', { user }, {
|
|
31
|
+
defer: deferredData,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### In Nunjucks Template (`views/dashboard.njk`):
|
|
37
|
+
```html
|
|
38
|
+
{% extends "layout.njk" %}
|
|
39
|
+
|
|
40
|
+
{% block content %}
|
|
41
|
+
<h1>Welcome, {{ user.name }}</h1>
|
|
42
|
+
|
|
43
|
+
<!-- Slot target with fallback skeleton -->
|
|
44
|
+
<div data-stream-target="analytics">
|
|
45
|
+
<div class="skeleton-loader">Loading analytics...</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div data-stream-target="recentOrders">
|
|
49
|
+
<div class="skeleton-loader">Loading orders...</div>
|
|
50
|
+
</div>
|
|
51
|
+
{% endblock %}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Page File Router Streaming (`pages/`)
|
|
57
|
+
|
|
58
|
+
In file-based page loaders (`pages/products/index.js` or `pages/dashboard.js`), simply return `defer` or `{ stream: true }`:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// pages/products/index.js
|
|
62
|
+
export async function load({ req, db }) {
|
|
63
|
+
const categories = await db.getRepository('Category').find();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
stream: true,
|
|
67
|
+
categories,
|
|
68
|
+
defer: {
|
|
69
|
+
products: db.getRepository('Product').query().where('active', true).paginate({ limit: 50 }),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Webspresso Development & Testing Workflow
|
|
2
|
+
|
|
3
|
+
[← Back to AGENTS.md](.agents/AGENTS.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Essential Commands
|
|
8
|
+
|
|
9
|
+
- **Dev Server**: `npm run dev` or `webspresso dev`
|
|
10
|
+
- **Run Unit Tests**: `npm test` or `npx vitest run`
|
|
11
|
+
- **Run Targeted Test File**: `npx vitest run tests/unit/auth/jwt.test.js`
|
|
12
|
+
- **Check TypeScript Types**: `npm run check:types` (`tsc --project tests/ts-smoke/tsconfig.json`)
|
|
13
|
+
- **Run E2E Tests**: `npm run test:e2e` (Playwright)
|
|
14
|
+
- **Run Migrations**: `npx webspresso db:migrate`
|
|
15
|
+
- **Project Sanity Check**: `npx webspresso doctor`
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Testing & Quality Assurance Rules
|
|
20
|
+
|
|
21
|
+
### 2.1 Zero Regression Policy
|
|
22
|
+
- Always run Vitest tests (`npm test` or `npx vitest run`) before declaring any task complete.
|
|
23
|
+
- Verify clean 100% test passes across unit, integration, and E2E test suites.
|
|
24
|
+
|
|
25
|
+
### 2.2 API Backward Compatibility
|
|
26
|
+
- Never break existing public APIs (`registerModule`, `registerPage`, `adminApi`, `createApp`, `defineModel`).
|
|
27
|
+
- Always add new options or parameters as optional or backward-compatible enhancements.
|