webspresso 0.0.95 → 0.0.97
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/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +209 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/new.js +13 -2
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +75 -89
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/auth/hash.js +10 -5
- 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 +36 -14
- package/plugins/admin-panel/app.js +10 -9
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/admin-panel/core/api-extensions.js +4 -0
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- 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 +217 -0
- package/plugins/index.js +45 -79
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/site-analytics/admin-component.js +7 -8
- 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 +9 -1
- 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/discovery.js +6 -1
- package/src/services/index.js +6 -11
- 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/ORM.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Webspresso ORM & Database Layer Guide
|
|
2
|
+
|
|
3
|
+
Webspresso features an intuitive, zero-sprawl ORM (`core/orm`) supporting schema definitions with `zdb`, repository pattern, query caching, soft deletes, multi-tenant scopes, and relation eager loading.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ⚠️ STRICT RULE: ALWAYS Use Repositories (NO Raw Knex `db('table')`)
|
|
8
|
+
|
|
9
|
+
In application code (API handlers, SSR loaders, services, plugins), **NEVER** write raw Knex table queries like `db('table').where(...)` or `req.db('table').insert(...)`.
|
|
10
|
+
Raw Knex queries **completely bypass** schema validation, lifecycle hooks, soft-delete scopes, multi-tenant filters, hidden field stripping, query caching, and ambient transactions.
|
|
11
|
+
|
|
12
|
+
### ❌ BAD vs ✅ GOOD Code Comparison
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// ❌ WRONG (Raw Knex — bypasses hooks, schemas, hidden fields, caching)
|
|
16
|
+
const user = await req.db('users').where({ id: req.params.id }).first();
|
|
17
|
+
await req.db('users').where({ id }).update({ balance: 100 });
|
|
18
|
+
|
|
19
|
+
// ✅ CORRECT (Webspresso Repository Pattern)
|
|
20
|
+
const userRepo = req.db.getRepository('User');
|
|
21
|
+
const user = await userRepo.findById(req.params.id);
|
|
22
|
+
await userRepo.update(id, { balance: 100 });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Comparison: Raw Knex vs Webspresso Repository
|
|
26
|
+
|
|
27
|
+
| Operation | ❌ NEVER Use Raw Knex | ✅ ALWAYS Use Repository (`db.getRepository`) |
|
|
28
|
+
| :--- | :--- | :--- |
|
|
29
|
+
| **Get by ID** | `await db('notes').where({ id }).first()` | `await noteRepo.findById(id)` |
|
|
30
|
+
| **Find List** | `await db('notes').where({ active: true })` | `await noteRepo.find({ active: true })` |
|
|
31
|
+
| **Find One** | `await db('notes').where({ slug }).first()` | `await noteRepo.findOne({ slug })` |
|
|
32
|
+
| **Create Record** | `await db('notes').insert(data)` | `await noteRepo.create(data)` *(validates schema & runs hooks)* |
|
|
33
|
+
| **Update Record** | `await db('notes').where({ id }).update(data)`| `await noteRepo.update(id, data)` |
|
|
34
|
+
| **Delete Record** | `await db('notes').where({ id }).del()` | `await noteRepo.delete(id)` *(respects soft-delete)* |
|
|
35
|
+
| **Custom Query** | `db('notes').where('price', '>', 100)` | `noteRepo.query().where('price', '>', 100)` |
|
|
36
|
+
| **Eager Load** | *Manual SQL joins and manual mapping* | `noteRepo.query().include('user', 'tags').find()` |
|
|
37
|
+
| **Pagination** | *Manual count + offset/limit calculation* | `noteRepo.query().paginate({ page, limit })` |
|
|
38
|
+
|
|
39
|
+
> [!NOTE]
|
|
40
|
+
> `noteRepo.query()` returns a fully-featured, model-aware query builder that supports all Knex chaining (`where`, `whereIn`, `orderBy`, etc.) while preserving model scopes, hooks, and transactions!
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. Model Definitions & `zdb` Schemas
|
|
45
|
+
|
|
46
|
+
Models are defined in `models/*.js` using `defineModel`:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { defineModel, zdb } from 'webspresso';
|
|
50
|
+
|
|
51
|
+
export default defineModel({
|
|
52
|
+
name: 'Product',
|
|
53
|
+
table: 'products',
|
|
54
|
+
schema: zdb.schema({
|
|
55
|
+
id: zdb.id(),
|
|
56
|
+
name: zdb.string().min(2),
|
|
57
|
+
slug: zdb.string().unique(),
|
|
58
|
+
price: zdb.number().positive(),
|
|
59
|
+
description: zdb.text({ optional: true }),
|
|
60
|
+
image: zdb.file({ optional: true }),
|
|
61
|
+
metadata: zdb.json({ optional: true }),
|
|
62
|
+
category_id: zdb.integer(),
|
|
63
|
+
active: zdb.boolean({ default: true }),
|
|
64
|
+
created_at: zdb.timestamp({ auto: 'create' }),
|
|
65
|
+
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
66
|
+
}),
|
|
67
|
+
relations: {
|
|
68
|
+
category: { type: 'belongsTo', model: 'Category', foreignKey: 'category_id' },
|
|
69
|
+
reviews: { type: 'hasMany', model: 'Review', foreignKey: 'product_id' },
|
|
70
|
+
},
|
|
71
|
+
scopes: {
|
|
72
|
+
active: (qb) => qb.where('active', true),
|
|
73
|
+
},
|
|
74
|
+
hidden: ['metadata'],
|
|
75
|
+
admin: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
label: 'Products',
|
|
78
|
+
icon: '📦',
|
|
79
|
+
},
|
|
80
|
+
cache: 'auto', // 'auto' | 'smart' | true | false
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Supported `zdb` Data Types
|
|
85
|
+
- `zdb.id()` — Primary key (BigInt / Auto-increment)
|
|
86
|
+
- `zdb.string({ max, unique, optional, sortable })` — String / VarChar
|
|
87
|
+
- `zdb.text({ sortable })` — Text blob
|
|
88
|
+
- `zdb.integer()`, `zdb.number({ sortable })` — Numeric types
|
|
89
|
+
- `zdb.boolean({ default, sortable })` — Boolean flag
|
|
90
|
+
- `zdb.file()` — File asset URL / upload path (non-sortable by default)
|
|
91
|
+
- `zdb.json()` — Serialized JSON column (non-sortable by default)
|
|
92
|
+
- `zdb.timestamp({ auto, sortable })` — Timestamp (`auto: 'create'` / `auto: 'update'`)
|
|
93
|
+
|
|
94
|
+
*Note: All `zdb` types support chainable `.config({ sortable: boolean })` or options object `{ sortable: boolean }`.*
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 2. Repository API
|
|
99
|
+
|
|
100
|
+
Repositories are accessed via `db.getRepository(modelName)` or `req.db.getRepository(modelName)`:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const productRepo = db.getRepository('Product');
|
|
104
|
+
|
|
105
|
+
// 1. Basic Read & Find
|
|
106
|
+
const product = await productRepo.findById(1);
|
|
107
|
+
const activeProducts = await productRepo.find({ active: true });
|
|
108
|
+
const singleProduct = await productRepo.findOne({ slug: 'laptop' });
|
|
109
|
+
|
|
110
|
+
// 2. Insert, Update, Delete
|
|
111
|
+
const newProduct = await productRepo.create({ name: 'Laptop', price: 999, category_id: 1 });
|
|
112
|
+
const updated = await productRepo.update(newProduct.id, { price: 899 });
|
|
113
|
+
await productRepo.delete(newProduct.id); // Soft or hard delete based on model config
|
|
114
|
+
|
|
115
|
+
// 3. Query Builder & Pagination
|
|
116
|
+
const result = await productRepo.query()
|
|
117
|
+
.where('price', '>', 500)
|
|
118
|
+
.orderBy('created_at', 'desc')
|
|
119
|
+
.paginate({ page: 1, limit: 10 });
|
|
120
|
+
// Returns: { data: [...], pagination: { page: 1, limit: 10, total: 42, totalPages: 5 } }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 3. Relations & Eager Loading
|
|
126
|
+
|
|
127
|
+
Support for `belongsTo`, `hasMany`, and `hasOne`:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// Eager-load relations using include
|
|
131
|
+
const productsWithCategory = await productRepo.query()
|
|
132
|
+
.include('category', 'reviews')
|
|
133
|
+
.find();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Ambient Transactions & AsyncLocalStorage
|
|
139
|
+
|
|
140
|
+
Webspresso features transparent **Ambient Transactions** (`core/orm/transaction.js`). When code executes inside `db.transaction()` or a service with `transaction: true`, all standard repository operations and query builders (`db.getRepository(...)`, `repo.create()`, `repo.update()`, `repo.query()`) automatically bind to the active transaction context without requiring manual `trx` passing:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// 1. Database Ambient Transaction
|
|
144
|
+
await db.transaction(async () => {
|
|
145
|
+
// All repositories automatically bind to the current transaction!
|
|
146
|
+
const user = await db.getRepository('User').create({ name: 'Alice', email: 'alice@example.com' });
|
|
147
|
+
await db.getRepository('Wallet').create({ user_id: user.id, balance: 100 });
|
|
148
|
+
// If an error is thrown anywhere in this block, all operations roll back automatically!
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 2. Checking active transaction state
|
|
152
|
+
if (db.hasActiveTransaction()) {
|
|
153
|
+
const currentTrx = db.getAmbientTransaction();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Low-level ambient runner
|
|
157
|
+
import { runWithAmbientTransaction } from 'webspresso';
|
|
158
|
+
await runWithAmbientTransaction(trx, async () => {
|
|
159
|
+
await db.getRepository('Order').update(orderId, { status: 'paid' });
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 5. Query Caching (`cache`)
|
|
166
|
+
|
|
167
|
+
- **`cache: 'auto'`**: Caches primary key lookups (`findById`) and invalidates automatically on `create`, `update`, `delete`.
|
|
168
|
+
- **`cache: 'smart'`**: Selective invalidation per updated record ID.
|
|
169
|
+
- **Cache Provider**: In-memory cache by default (`createMemoryCacheProvider`). Supports custom Redis / Memcached providers.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. Schema Migrations
|
|
174
|
+
|
|
175
|
+
Database migrations are managed via Knex in `migrations/`:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Run migrations
|
|
179
|
+
npx webspresso db:migrate
|
|
180
|
+
|
|
181
|
+
# Seed database
|
|
182
|
+
npx webspresso db:seed
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 6. REST API Exposure (`restResourcePlugin`)
|
|
188
|
+
|
|
189
|
+
Models can be exposed as RESTful CRUD endpoints automatically via `restResourcePlugin`:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
defineModel({
|
|
193
|
+
name: 'Product',
|
|
194
|
+
table: 'products',
|
|
195
|
+
schema: zdb.schema({ /* ... */ }),
|
|
196
|
+
hidden: ['secret_token'],
|
|
197
|
+
rest: {
|
|
198
|
+
enabled: true, // Expose REST routes /api/rest/products
|
|
199
|
+
path: 'items', // Custom path segment (default: pluralized model name)
|
|
200
|
+
allowInclude: ['category'], // Whitelist relations for ?include=
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
See [Plugin Ecosystem Guide](.agents/PLUGINS.md#216-restresourceplugin-pluginsrest-resources) for full details on querying, filtering, pagination, and soft-delete scoping.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 7. Query Complexity & DoS Protection (`queryLimits`)
|
|
210
|
+
|
|
211
|
+
Webspresso protects your database against unconstrained queries, heavy relation eager loading, and connection pool exhaustion:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
defineModel({
|
|
215
|
+
name: 'Product',
|
|
216
|
+
table: 'products',
|
|
217
|
+
schema: zdb.schema({ /* ... */ }),
|
|
218
|
+
queryLimits: {
|
|
219
|
+
maxLimit: 100, // Max items allowed in a single page (default: 100)
|
|
220
|
+
defaultLimit: 20, // Default perPage limit (default: 15)
|
|
221
|
+
maxIncludes: 5, // Max relations eager loaded in one query (default: 5)
|
|
222
|
+
maxFilterConditions: 25, // Max WHERE filters permitted (default: 25)
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- **QueryBuilder Enforcement**: `query().paginate()` and `query().limit()` clamp requested limits to `maxLimit`.
|
|
228
|
+
- **Include Guards**: Excessive `query().with(...)` or `?include=...` relations beyond `maxIncludes` throw `QueryComplexityError` or safely cap relations.
|
|
229
|
+
- **REST Resources Protection**: Automatically validates client parameters (`?perPage=5000` or `?include=...`) against model complexity boundaries.
|
|
230
|
+
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Webspresso Plugin Ecosystem Guide
|
|
2
|
+
|
|
3
|
+
Webspresso applications are extended through a modular plugin architecture. Plugins hook into framework lifecycle events (`register(ctx)`, `onRoutesReady(ctx)`), mount Express routes, register Nunjucks template helpers, and supply Content-Security-Policy (CSP) headers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Plugin Lifecycle & Anatomy
|
|
8
|
+
|
|
9
|
+
A Webspresso plugin is an object or factory function returning a plugin contract:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export function myCustomPlugin(options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
name: 'my-custom-plugin',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
dependencies: [], // Optional dependent plugins
|
|
17
|
+
csp: {
|
|
18
|
+
scriptSrc: ["'self'", 'https://cdn.example.com'],
|
|
19
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// 1. Register phase (before route mounting)
|
|
23
|
+
register(ctx) {
|
|
24
|
+
const { app, nunjucksEnv, options: appOptions, middlewares } = ctx;
|
|
25
|
+
// Add Nunjucks filters or global middleware
|
|
26
|
+
nunjucksEnv.addFilter('customFilter', (str) => str.toUpperCase());
|
|
27
|
+
|
|
28
|
+
// Register named middleware for file routes: export const config = { middleware: ['myCustom'] }
|
|
29
|
+
if (middlewares) {
|
|
30
|
+
middlewares.myCustom = (routeOptions = {}) => (req, res, next) => {
|
|
31
|
+
// Middleware logic...
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// 2. On routes ready phase (after file-based routes mounted)
|
|
38
|
+
onRoutesReady(ctx) {
|
|
39
|
+
const { app, routes, db } = ctx;
|
|
40
|
+
// Mount plugin-level Express routes
|
|
41
|
+
app.get('/_my-plugin/status', (req, res) => res.json({ status: 'ok' }));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 1.1 Named Middleware Registration via Plugins
|
|
48
|
+
Plugins can register route middlewares in `ctx.middlewares`:
|
|
49
|
+
- **Factory Registration**: `ctx.middlewares.basicAuth = (routeOpts) => createBasicAuthMiddleware(routeOpts)`
|
|
50
|
+
- **File Route Consumption**:
|
|
51
|
+
```js
|
|
52
|
+
// pages/admin-only.njk or pages/api/secret.js
|
|
53
|
+
export const config = {
|
|
54
|
+
middleware: [
|
|
55
|
+
'basicAuth', // Uses plugin defaults
|
|
56
|
+
// or with options:
|
|
57
|
+
['basicAuth', { realm: 'Internal Area', users: { boss: 'secret' } }]
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 2. Core & Official Plugins Reference
|
|
65
|
+
|
|
66
|
+
### 2.1 `basicAuthPlugin` (`plugins/basic-auth`)
|
|
67
|
+
Zero-dependency HTTP Basic Authentication (RFC 7617) plugin with timing-safe equality checks (`crypto.timingSafeEqual`).
|
|
68
|
+
- **Options**:
|
|
69
|
+
- `global`: Boolean, whether to protect the entire application.
|
|
70
|
+
- `users`: Key-value map `{ username: 'password' }`.
|
|
71
|
+
- `verify`: Async/sync custom verification function `(username, password, req) => boolean | userObject`.
|
|
72
|
+
- `realm`: String for `WWW-Authenticate` header (default `'Restricted Area'`).
|
|
73
|
+
- `challenge`: Boolean (default `true`), whether to send 401 challenge header.
|
|
74
|
+
- `routes`: Array of route prefixes to protect when in global mode.
|
|
75
|
+
- `skipPaths` / `skip`: Exclusion paths or predicate `(req) => boolean`.
|
|
76
|
+
- `unauthorizedResponse`: Custom handler `(req, res) => ...` or message string/JSON.
|
|
77
|
+
- `onAuthenticated`: Callback hook `(req, user) => ...`.
|
|
78
|
+
- **Named Middleware**: Registers `ctx.middlewares.basicAuth` for route files.
|
|
79
|
+
|
|
80
|
+
### 2.2 `adminPanelPlugin` (`plugins/admin-panel`)
|
|
81
|
+
Mithril.js SPA Admin Panel mounted at `/_admin`.
|
|
82
|
+
- **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
|
|
83
|
+
- **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
|
|
84
|
+
|
|
85
|
+
### 2.3 `contentPlugin` (`plugins/content`)
|
|
86
|
+
Schema-driven headless CMS & inline content editing.
|
|
87
|
+
- **Options**: `{ db, adminPath: '/_admin', inlineEdit: true }`
|
|
88
|
+
- **Features**: Dynamic content types & entries, public API (`/api/content/:type/:slug`), visual inline editor widget.
|
|
89
|
+
|
|
90
|
+
### 2.4 `uploadPlugin` (`plugins/upload`)
|
|
91
|
+
Multipart file upload manager with local / cloud storage providers.
|
|
92
|
+
- **Options**: `{ path: '/api/upload', local: { destDir, publicBasePath }, maxBytes: 10000000, mimeAllowlist: ['image/png', 'image/jpeg'] }`
|
|
93
|
+
- **Features**: Single & multi-file uploads, file size & extension validation, storage drivers.
|
|
94
|
+
|
|
95
|
+
### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
|
|
96
|
+
Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
|
|
97
|
+
- **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'Dosya Yöneticisi', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
|
|
98
|
+
- **Features**:
|
|
99
|
+
- Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
|
|
100
|
+
- Path traversal attack protection (`resolveSafePath`).
|
|
101
|
+
- Grid (thumbnail gallery) & List (table) view modes.
|
|
102
|
+
- Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
|
|
103
|
+
- Media preview modal with copy URL and direct playback/preview.
|
|
104
|
+
- "Kütüphaneden Seç" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
|
|
105
|
+
- Built-in services: `fileManager.list`, `fileManager.mkdir`, `fileManager.rename`, `fileManager.delete`, `fileManager.move`, `fileManager.upload`.
|
|
106
|
+
|
|
107
|
+
### 2.6 `corsPlugin` (`plugins/cors`)
|
|
108
|
+
Zero-dependency Cross-Origin Resource Sharing (CORS) handler.
|
|
109
|
+
- **Options**: `{ origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE'], credentials: true, maxAge: 86400 }`
|
|
110
|
+
- **Features**: Native header management, preflight OPTIONS handling.
|
|
111
|
+
|
|
112
|
+
### 2.6 `csrfPlugin` (`plugins/csrf`)
|
|
113
|
+
Cross-Site Request Forgery token validation.
|
|
114
|
+
- **Options**: `{ mode: 'session', secret: 'csrf-secret', ignorePaths: ['/api/*'] }`
|
|
115
|
+
|
|
116
|
+
### 2.7 `redirectPlugin` (`plugins/redirect`)
|
|
117
|
+
HTTP 301–308 URL redirect manager.
|
|
118
|
+
- **Options**: `{ rules: [{ from: '/old-path', to: '/new-path', status: 301 }] }`
|
|
119
|
+
|
|
120
|
+
### 2.8 `rateLimitPlugin` (`plugins/rate-limit`)
|
|
121
|
+
Request rate limiting using express-rate-limit logic.
|
|
122
|
+
- **Options**: `{ windowMs: 15 * 60 * 1000, max: 100 }`
|
|
123
|
+
|
|
124
|
+
### 2.9 `siteAnalyticsPlugin` (`plugins/site-analytics`)
|
|
125
|
+
Self-hosted privacy-focused page view analytics and client error tracking.
|
|
126
|
+
- **Options**: `{ path: '/_analytics', db }`
|
|
127
|
+
|
|
128
|
+
### 2.10 `emailPlugin` (`plugins/email`)
|
|
129
|
+
Transactional email compiler (MJML) & Nodemailer transport integration.
|
|
130
|
+
- **Options**: `{ transport, from: 'noreply@example.com', templatesDir }`
|
|
131
|
+
|
|
132
|
+
### 2.11 `polarPlugin` (`plugins/polar`)
|
|
133
|
+
Polar.sh subscription billing — checkout, customer portal, webhooks, Customer State sync, tier columns. Zero extra deps (`crypto` + `fetch`).
|
|
134
|
+
|
|
135
|
+
- **Import paths** (via `package.json` exports — no `/index.js` suffix):
|
|
136
|
+
```js
|
|
137
|
+
const { polarPlugin, rateLimitPlugin } = require('webspresso/plugins');
|
|
138
|
+
const { quickAuth } = require('webspresso/core/auth');
|
|
139
|
+
const { createWebhookRawBodyMiddleware } = require('webspresso/plugins/polar/src/webhooks');
|
|
140
|
+
const { mergePolarCspDirectives } = require('webspresso/plugins/polar/src/urls');
|
|
141
|
+
```
|
|
142
|
+
- **Wiring** (rate limit **before** polar):
|
|
143
|
+
```js
|
|
144
|
+
plugins: [
|
|
145
|
+
rateLimitPlugin(),
|
|
146
|
+
polarPlugin({ db, plans, tierMapping, rateLimit: true }),
|
|
147
|
+
]
|
|
148
|
+
```
|
|
149
|
+
- **Options**: `{ db, userModel, userTable, plans, tierMapping, routes, urls, fields, hooks, syncMiddleware, syncBeforeCheckout, rateLimit, requireAuth }`
|
|
150
|
+
- **Rate limit**: Per-route limiters when `rateLimitPlugin` is loaded first. Uses `express-rate-limit` v8 `ipKeyGenerator` (never raw `req.ip`). Defaults: webhook 120/min (IP), checkout 5/min, portal 10/min, status 60/min (user id or IP).
|
|
151
|
+
- **Auto routes**: webhook (`POST`), checkout (`GET`/`POST`), portal (`GET`), status (`GET`)
|
|
152
|
+
- **POST checkout**: HTML `<form method="post">` — needs CSP `form-action 'self'` (included in `polarCspDirectives()`)
|
|
153
|
+
- **GET checkout**: simple `<a href="…">` link — fewer CSP constraints
|
|
154
|
+
- **External tier tables** (when paid status is not on `users.tier`):
|
|
155
|
+
```js
|
|
156
|
+
polarPlugin({
|
|
157
|
+
db,
|
|
158
|
+
syncBeforeCheckout: false,
|
|
159
|
+
hooks: {
|
|
160
|
+
async isPaidUser({ user, knex }) {
|
|
161
|
+
const sub = await knex('subscriptions').where('user_id', user.id).first();
|
|
162
|
+
return sub?.plan_tier === 'pro';
|
|
163
|
+
},
|
|
164
|
+
async onSubscriptionChange({ user, tier, knex, update }) {
|
|
165
|
+
await knex('subscriptions').where('user_id', user.id).update({ plan_tier: tier });
|
|
166
|
+
return knex('users').where('id', user.id).update(update);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
Legacy hook signature `(user, knex, config)` still supported when declared with 2+ parameters.
|
|
172
|
+
- **Webhook user lookup**: `metadata.user_id` is sent as a **string** (nanoid/UUID safe; numeric ids also work).
|
|
173
|
+
- **CSP**: Plugin `csp` export is **union-merged** by plugin manager (does not replace Helmet). `polarCspDirectives()` includes `'self'` + Polar domains. Use `mergePolarCspDirectives(existing)` for manual Helmet config.
|
|
174
|
+
- **CLI**: `webspresso polar:migrate` — idempotent billing columns migration
|
|
175
|
+
- **SDK** (`plugin.api`): `verifyPolarWebhook`, `syncPolarBillingForUser`, `syncBillingForAppUser`, `getPolarCheckoutUrl`, `getPolarPortalUrl`, `handlePolarWebhookEvent`, `resolveUserFromPolarData`, `generateMigration`, `parseEnv`, `polarCspDirectives`, `mergePolarCspDirectives`, `createWebhookRawBodyMiddleware`
|
|
176
|
+
- **Guides**:
|
|
177
|
+
- [docs/guides/polar-billing.md](../docs/guides/polar-billing.md)
|
|
178
|
+
- [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md)
|
|
179
|
+
|
|
180
|
+
### 2.12 `auditLogPlugin` (`plugins/audit-log`)
|
|
181
|
+
Admin mutation tracking and audit log audit history.
|
|
182
|
+
- **Options**: `{ db, retainDays: 90 }`
|
|
183
|
+
|
|
184
|
+
### 2.13 `ormCacheAdminPlugin` (`plugins/orm-cache-admin`)
|
|
185
|
+
ORM Cache inspection and invalidation dashboard.
|
|
186
|
+
|
|
187
|
+
### 2.14 `schemaExplorerPlugin` (`plugins/schema-explorer`)
|
|
188
|
+
Interactive JSON schema & OpenAPI route specification generator.
|
|
189
|
+
|
|
190
|
+
### 2.15 `swaggerPlugin` (`plugins/swagger`)
|
|
191
|
+
Interactive OpenAPI 3.0 Swagger UI documentation.
|
|
192
|
+
|
|
193
|
+
### 2.16 `realtimePlugin` (`plugins/realtime`, `core/realtime`)
|
|
194
|
+
Framework-agnostic, plugin-based, and adapter-driven Realtime layer supporting WebSocket, SSE, Socket.IO, and distributed Redis Pub/Sub transports with zero SSR execution hazards.
|
|
195
|
+
|
|
196
|
+
- **Architecture**:
|
|
197
|
+
- `core/realtime`: Connection lifecycle, subscription registry (`identifier::params` composite keys), auth strategies, exponential backoff reconnect with jitter, and capability assertions.
|
|
198
|
+
- `core/realtime/adapters`: `websocket`, `sse`, `socketIo`, `redis`.
|
|
199
|
+
- `plugins/realtime`: Webspresso app integration and shutdown lifecycle management.
|
|
200
|
+
|
|
201
|
+
- **Examples**:
|
|
202
|
+
|
|
203
|
+
#### 1. Basic (WebSocket + Cookie Auth)
|
|
204
|
+
```js
|
|
205
|
+
const { createApp } = require('webspresso');
|
|
206
|
+
const { realtimePlugin, websocket } = require('webspresso/plugins');
|
|
207
|
+
|
|
208
|
+
const { app } = createApp({
|
|
209
|
+
plugins: [
|
|
210
|
+
realtimePlugin({
|
|
211
|
+
adapter: websocket({ url: '/realtime' }),
|
|
212
|
+
auth: { strategy: 'cookie' },
|
|
213
|
+
autoConnect: true,
|
|
214
|
+
}),
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### 2. Token Auth & Refresh
|
|
220
|
+
```js
|
|
221
|
+
const { realtime, websocket } = require('webspresso');
|
|
222
|
+
|
|
223
|
+
const client = realtime({
|
|
224
|
+
adapter: websocket({ url: 'wss://api.example.com/realtime' }),
|
|
225
|
+
auth: {
|
|
226
|
+
strategy: 'token',
|
|
227
|
+
getToken: async () => localStorage.getItem('token'),
|
|
228
|
+
refreshToken: async () => {
|
|
229
|
+
const res = await fetch('/api/auth/refresh', { method: 'POST' });
|
|
230
|
+
const { token } = await res.json();
|
|
231
|
+
localStorage.setItem('token', token);
|
|
232
|
+
return token;
|
|
233
|
+
},
|
|
234
|
+
onUnauthorized: (err) => {
|
|
235
|
+
window.location.href = '/login';
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### 3. Subscriptions & Messages
|
|
242
|
+
```js
|
|
243
|
+
const sub = app.realtime.subscribe('project:123', { role: 'editor' }, {
|
|
244
|
+
connected() {
|
|
245
|
+
console.log('Subscribed to project:123');
|
|
246
|
+
},
|
|
247
|
+
disconnected() {
|
|
248
|
+
console.log('Temporarily disconnected');
|
|
249
|
+
},
|
|
250
|
+
received(data) {
|
|
251
|
+
console.log('New update received:', data);
|
|
252
|
+
},
|
|
253
|
+
rejected(err) {
|
|
254
|
+
console.error('Subscription denied:', err);
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### 4. Perform Action & Send Data
|
|
260
|
+
```js
|
|
261
|
+
// Send raw data
|
|
262
|
+
sub.send({ text: 'Hello team!' });
|
|
263
|
+
|
|
264
|
+
// Perform named RPC action
|
|
265
|
+
sub.perform('user_typing', { userId: 42, isTyping: true });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### 5. Cleanup & Unsubscribe
|
|
269
|
+
```js
|
|
270
|
+
// Unsubscribe single topic
|
|
271
|
+
sub.unsubscribe();
|
|
272
|
+
|
|
273
|
+
// Destroy entire realtime client and teardown socket connections
|
|
274
|
+
app.realtime.destroy();
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### 6. Reauthenticate Workflow
|
|
278
|
+
```js
|
|
279
|
+
// Trigger token refresh, disconnect, reconnect, and automatic subscription restore
|
|
280
|
+
await app.realtime.reauthenticate();
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 2.17 `restResourcePlugin` (`plugins/rest-resources`)
|
|
284
|
+
Zero-boilerplate RESTful CRUD endpoint generator directly from Knex ORM models, complete with relationship eager-loading (`?include=...`), pagination, sorting, column filtering, soft-delete scopes, and recursive hidden field sanitization.
|
|
285
|
+
|
|
286
|
+
- **Options**:
|
|
287
|
+
- `path`: Base path prefix for REST routes (default: `'/api/rest'`).
|
|
288
|
+
- `middleware`: Array of Express middleware executed prior to database resolution and endpoint handlers (e.g. `[authMiddleware]`).
|
|
289
|
+
- `models`: Whitelist of model names to expose (bypasses `rest.enabled: true` requirement).
|
|
290
|
+
- `excludeModels`: Array of model names to exclude.
|
|
291
|
+
- `filter`: Custom predicate function `(modelDefinition) => boolean`.
|
|
292
|
+
|
|
293
|
+
- **Model Configuration (`defineModel`)**:
|
|
294
|
+
```js
|
|
295
|
+
defineModel({
|
|
296
|
+
name: 'User',
|
|
297
|
+
table: 'users',
|
|
298
|
+
schema: UserSchema,
|
|
299
|
+
hidden: ['password_hash', 'internal_notes'],
|
|
300
|
+
scopes: { softDelete: true, timestamps: true },
|
|
301
|
+
relations: {
|
|
302
|
+
company: { type: 'belongsTo', model: () => Company, foreignKey: 'company_id' },
|
|
303
|
+
posts: { type: 'hasMany', model: () => Post, foreignKey: 'user_id' }
|
|
304
|
+
},
|
|
305
|
+
rest: {
|
|
306
|
+
enabled: true, // Opt-in flag (or use plugin models whitelist)
|
|
307
|
+
path: 'users', // Optional custom URL segment (default: pluralized name)
|
|
308
|
+
allowInclude: ['company'], // Allowed relations for ?include=
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- **Generated RESTful Endpoints**:
|
|
314
|
+
| Method | Path | Description | Query Parameters / Body |
|
|
315
|
+
|---|---|---|---|
|
|
316
|
+
| `GET` | `/api/rest/:resource` | Paginated list of records | `page`, `perPage`, `sort`, `order`, `include`, `trashed`, column filters |
|
|
317
|
+
| `GET` | `/api/rest/:resource/:id` | Single record by primary key | `include` |
|
|
318
|
+
| `POST` | `/api/rest/:resource` | Create new record (201 Created) | JSON body (writable columns only) |
|
|
319
|
+
| `PATCH` | `/api/rest/:resource/:id` | Update record by ID | JSON body (writable columns only) |
|
|
320
|
+
| `DELETE` | `/api/rest/:resource/:id` | Delete / Soft-delete by ID | N/A (Returns `{ success: true }`) |
|
|
321
|
+
|
|
322
|
+
- **Query Parameters**:
|
|
323
|
+
- `page`: Page number (1-indexed, default `1`).
|
|
324
|
+
- `perPage`: Items per page (default `15`, clamped between `1` and `100`).
|
|
325
|
+
- `sort`: Multi-column sort supported via comma-separated list (`?sort=-price,created_at:asc`), array (`?sort[]=price:desc&sort[]=name:asc`), or single column (`?sort=price&order=desc`). Prepending `-` indicates descending, `+` indicates ascending.
|
|
326
|
+
- `order`: `'asc'` or `'desc'` (default `'desc'`).
|
|
327
|
+
- `include`: Comma-separated allowed relation names (e.g. `?include=company,profile`). Eager loads without N+1 query overhead. Disallowed or nested relations are safely ignored.
|
|
328
|
+
- `trashed`: For soft-delete models, `'include'` queries active + deleted records; `'only'` queries exclusively soft-deleted records.
|
|
329
|
+
- **Rich Column Filtering**: Direct query parameters or dedicated `filter` object with comprehensive SQL operators:
|
|
330
|
+
- **Equality & Inequality**: `?status=active`, `?status[eq]=active`, `?status[ne]=banned`
|
|
331
|
+
- **Comparison Ranges**: `?price[gte]=100&price[lte]=500`, `?age[gt]=18`, `?age[lt]=65`
|
|
332
|
+
- **Set Membership**: `?status[in]=active,pending` (or `?status[in][]=active`), `?status[nin]=banned,deleted`
|
|
333
|
+
- **String Pattern Matching**: `?name[contains]=phone`, `?sku[startsWith]=APL-`, `?email[endsWith]=@gmail.com`, `?name[like]=app%`
|
|
334
|
+
- **Null Checks**: `?deleted_at[isNull]=true`, `?avatar[isNotNull]=true`
|
|
335
|
+
- **Between Range**: `?price[between]=100,500`
|
|
336
|
+
- **Dedicated Filter Object**: `?filter[name][contains]=phone&filter[price][gte]=100`
|
|
337
|
+
|
|
338
|
+
- **Security & Data Sanitization**:
|
|
339
|
+
- **Hidden Fields**: Columns in `model.hidden` are stripped recursively across the primary record and all eager-loaded relations.
|
|
340
|
+
- **Writable Columns**: `POST` and `PATCH` payloads are stripped of primary auto-increment keys, non-existent columns, and hidden fields before hitting the repository.
|
|
341
|
+
- **Include Protection**: Only relations declared in `model.rest.allowInclude` (or all relations if omitted) can be eager-loaded. Dotted nested includes (`posts.comments`) are ignored to prevent denial-of-service / memory leaks.
|
|
342
|
+
- **Parameterized Queries**: All filters and search operators are parameterized through Knex to prevent SQL injection. Non-existent model columns are safely ignored.
|
|
343
|
+
|
|
344
|
+
- **Example Usage**:
|
|
345
|
+
```js
|
|
346
|
+
const { createApp, restResourcePlugin } = require('webspresso');
|
|
347
|
+
const db = require('./models');
|
|
348
|
+
|
|
349
|
+
const { app } = createApp({
|
|
350
|
+
db,
|
|
351
|
+
plugins: [
|
|
352
|
+
restResourcePlugin({
|
|
353
|
+
path: '/api/v1',
|
|
354
|
+
middleware: [authenticateApiToken],
|
|
355
|
+
}),
|
|
356
|
+
],
|
|
357
|
+
});
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 2.16 `mcpPlugin` (`plugins/mcp`)
|
|
361
|
+
Model Context Protocol (MCP) server for connecting Webspresso applications directly to AI agents (Antigravity, Claude Desktop, Cursor, Zed, Windsurf).
|
|
362
|
+
- **Options**:
|
|
363
|
+
- `path`: URL mount path (default: `'/_mcp'`).
|
|
364
|
+
- `auth`: `{ token, localhostOnly, verify }`.
|
|
365
|
+
- `services`: Boolean or `{ include, exclude, readOnly, role }` auto-bridging `services/` to MCP Tools.
|
|
366
|
+
- `orm`: Boolean or `{ readOnly, models }` exposing models as Tools (`orm_find`, `orm_create`, etc.) and Resources (`webspresso://models`).
|
|
367
|
+
- `system`: Boolean, exposes `webspresso://routes`, `webspresso://services`, and `webspresso://health`.
|
|
368
|
+
- `discovery`: Boolean (default: `true`), scans project `mcp/` directory for custom tools, resources, and prompts.
|
|
369
|
+
- **Dual Transport**: Supports HTTP/SSE inside Webspresso server and Stdio via `webspresso mcp` CLI.
|
|
370
|
+
|
|
371
|
+
### 2.17 `xlsxPlugin` (`plugins/xlsx`)
|
|
372
|
+
High-performance Excel spreadsheet generation, parsing, ORM model export, streaming writer, formula injection security, and Express `res.xlsx()` decorator.
|
|
373
|
+
- **Options**:
|
|
374
|
+
- `sanitizeFormulas`: Boolean (default `true`), escapes formula injection prefixes (`=`, `+`, `-`, `@`, `\t`, `\r`) to prevent spreadsheet macro exploits.
|
|
375
|
+
- `headerStyle`: Custom styling `{ fill: '4F46E5', color: 'FFFFFF', bold: true }`.
|
|
376
|
+
- `db`: Database instance for ORM model export service.
|
|
377
|
+
- **Response Helper (`res.xlsx`)**:
|
|
378
|
+
```javascript
|
|
379
|
+
// pages/api/export-users.get.js
|
|
380
|
+
module.exports = {
|
|
381
|
+
handler: async (req, res) => {
|
|
382
|
+
const users = await req.db.getRepository('User').find({ active: true });
|
|
383
|
+
return res.xlsx(users, 'active_users.xlsx');
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
```
|
|
387
|
+
- **Toolkit API (`xlsx`)**:
|
|
388
|
+
- `xlsx.generate({ sheets: [...], title })` → `Promise<Buffer>`
|
|
389
|
+
- `xlsx.parse(bufferOrStream, { sheet, transformRow })` → `Promise<Array|Object>`
|
|
390
|
+
- `xlsx.fromModel(repository, filterOrQuery, options)` → `Promise<Buffer>`
|
|
391
|
+
- `xlsx.createStreamWriter(stream, options)` → `ExcelJS.stream.xlsx.WorkbookWriter`
|
|
392
|
+
- **Built-in Services**: `xlsx.generate`, `xlsx.parse`, `xlsx.exportModel` accessible via `ctx.service()` or `req.service()`.
|
|
393
|
+
|
|
394
|
+
### 2.18 `csvPlugin` (`plugins/csv`)
|
|
395
|
+
RFC 4180 compliant CSV generator, parser, ORM model export, streaming chunk writer, Excel UTF-8 BOM compatibility, and Express `res.csv()` decorator.
|
|
396
|
+
- **Options**:
|
|
397
|
+
- `delimiter`: String (default `','`), supports `;`, `\t`, `|`.
|
|
398
|
+
- `bom`: Boolean (default `true`), adds UTF-8 Byte Order Mark (`\uFEFF`) ensuring Microsoft Excel opens Turkish and international characters properly.
|
|
399
|
+
- `sanitizeFormulas`: Boolean (default `true`), escapes formula injection prefixes (`=`, `+`, `-`, `@`, `\t`, `\r`).
|
|
400
|
+
- `db`: Database instance for ORM model export service.
|
|
401
|
+
- **Response Helper (`res.csv`)**:
|
|
402
|
+
```javascript
|
|
403
|
+
// pages/api/export-orders.get.js
|
|
404
|
+
module.exports = {
|
|
405
|
+
handler: async (req, res) => {
|
|
406
|
+
const orders = await req.db.getRepository('Order').find({ status: 'completed' });
|
|
407
|
+
return res.csv(orders, 'orders_export.csv');
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
```
|
|
411
|
+
- **Toolkit API (`csv`)**:
|
|
412
|
+
- `csv.generate({ rows, columns, delimiter, bom, asBuffer })` → `Buffer|string`
|
|
413
|
+
- `csv.parse(bufferOrStreamOrString, { delimiter, transformRow })` → `Promise<Array<Object>>`
|
|
414
|
+
- `csv.fromModel(repository, filterOrQuery, options)` → `Promise<Buffer>`
|
|
415
|
+
- `csv.createStreamWriter(stream, options)` → `{ writeHeader, writeRow, writeRows, end }`
|
|
416
|
+
- **Built-in Services**: `csv.generate`, `csv.parse`, `csv.exportModel` accessible via `ctx.service()` or `req.service()`.
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
|