@vulkano/core 1.20.2 → 1.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,6 +21,8 @@ Vulkano is a lightweight MVC framework for building web applications and APIs wi
21
21
 
22
22
  Inspired by [KumbiaPHP](https://www.kumbiaphp.com).
23
23
 
24
+ For the full project generator/scaffolding (frontend + backend structure), see the framework: [https://github.com/vulkanojs/vulkano](https://github.com/vulkanojs/vulkano)
25
+
24
26
  ---
25
27
 
26
28
  ## Requirements
@@ -39,6 +41,15 @@ npm install @vulkano/core
39
41
 
40
42
  ---
41
43
 
44
+ ## Environment variables
45
+
46
+ ```
47
+ PORT=8000
48
+ MONGO_URI=mongodb://localhost:27017/myapp
49
+ SALT_KEY=random-string
50
+ JWT_SECRET=supersecret
51
+ ```
52
+
42
53
  ## Quick Start
43
54
 
44
55
  ### 1. Entry point — `app.js`
@@ -53,23 +64,23 @@ vulkano();
53
64
  ```
54
65
  your-app/
55
66
  ├── app.js # Entry point
56
- ├── public/ # Static files served over HTTP
67
+ ├── app/ # Your application
68
+ │ ├── config/
69
+ │ │ ├── settings.js # App-wide settings (port, database, JWT…)
70
+ │ │ ├── routes.js # Explicit route overrides (optional)
71
+ │ │ ├── env/ # Per-environment config overrides
72
+ │ │ ├── express/ # Express middleware customization
73
+ │ │ ├── settings.js # App-wide settings (port, database, JWT…)
74
+ │ │ ├── routes.js # Explicit route overrides (optional)
75
+ │ │ └── locales/ # i18n translation files (en.js, es.js, etc.)
76
+ │ ├── controllers/ # Request handlers
77
+ │ ├── models/ # Mongoose model definitions
78
+ │ └── services/ # Shared services & libs (auto-loaded as globals)
79
+ └── public/ # Static files served over HTTP
57
80
  │ ├── css/
58
81
  │ ├── js/
59
82
  │ ├── img/
60
83
  │ └── files/ # Uploaded files
61
- └── vulkano/ # Your application
62
- ├── config/
63
- │ ├── settings.js # App-wide settings (port, database, JWT…)
64
- │ ├── routes.js # Explicit route overrides (optional)
65
- │ ├── env/ # Per-environment config overrides
66
- │ ├── express/ # Express middleware customization
67
- │ ├── settings.js # App-wide settings (port, database, JWT…)
68
- │ ├── routes.js # Explicit route overrides (optional)
69
- │ └── locales/ # i18n translation files (en.js, es.js, etc.)
70
- ├── controllers/ # Request handlers
71
- ├── models/ # Mongoose model definitions
72
- └── services/ # Shared services & libs (auto-loaded as globals)
73
84
  ```
74
85
 
75
86
  ---
@@ -78,17 +89,66 @@ your-app/
78
89
 
79
90
  Vulkano resolves routes **by convention** — no route file required for standard CRUD.
80
91
 
92
+ The URL segments map to `/:resource/:method?/:param?`, resolving to `<Resource>Controller.<method>(param)`.
93
+ The **resource segment is always the requesting controller's own filename** (`UsersController` → `users`).
94
+
95
+ ```
96
+ GET /users/edit/1
97
+ │ │ │
98
+ │ │ └── param → passed as the method argument
99
+ │ └─────── method → UsersController.edit
100
+ └───────────-─ resource ("users") → UsersController
101
+ ```
102
+
81
103
  ### Convention-based (automatic)
82
104
 
83
- | HTTP method | URL | Resolves to |
84
- |-------------|------------------|-------------------------------------|
85
- | `GET` | `/user` | `UserController.get` |
86
- | `POST` | `/user` | `UserController.post` |
87
- | `PUT` | `/user/42` | `UserController['put :id']` |
88
- | `PATCH` | `/user/42` | `UserController['patch :id']` |
89
- | `DELETE` | `/user/42` | `UserController['delete :id']` |
90
- | `POST` | `/user/save` | `UserController['post save']` |
91
- | `GET` | `/user/42/info` | `UserController['get :id/info']` |
105
+ | HTTP method | URL | Resolves to |
106
+ |-------------|-------------------|--------------------------------------|
107
+ | `GET` | `/users` | `UsersController.get` |
108
+ | `POST` | `/users` | `UsersController.post` |
109
+ | `PUT` | `/users/42` | `UsersController['put :id']` |
110
+ | `PATCH` | `/users/42` | `UsersController['patch :id']` |
111
+ | `DELETE` | `/users/42` | `UsersController['delete :id']` |
112
+ | `POST` | `/users/save` | `UsersController['post save']` |
113
+ | `GET` | `/users/42/info` | `UsersController['get :id/info']` |
114
+
115
+ ### Method key convention: `'<verb>? <path tail>'`
116
+
117
+ A controller method key is `<path tail>` on its own, or `'<verb> <path tail>'` when the verb isn't `GET`. The auto-router only reassigns the HTTP method when the key has a space-separated verb prefix — otherwise it defaults to **GET**.
118
+
119
+ - A **custom action name with no verb prefix** (no space in the key) is still `GET`, e.g. `me(req, res)` on `AuthController` → `GET /auth/current`. Don't write `'get current'`; it's redundant.
120
+ - A **custom action that isn't `GET`** needs the verb spelled out, e.g. `'post login'` → `POST /auth/login`.
121
+ - The path tail can carry arbitrary nested segments and multiple params:
122
+
123
+ ```js
124
+ // controllers/api/UsersController.js
125
+ module.exports = {
126
+
127
+ // GET /api/users/123/orders/988
128
+ 'get :id/orders/:orderId': (req, res) => {
129
+ // req.params → { id: '123', orderId: '988' }
130
+ }
131
+
132
+ };
133
+
134
+ // controllers/api/AuthController.js
135
+ module.exports = {
136
+
137
+ // GET /api/auth/current — no verb prefix needed, GET is the default
138
+ current(req, res) { },
139
+
140
+ // POST /api/auth/login
141
+ 'post login': (req, res) => { },
142
+
143
+ // POST /api/auth/logout
144
+ 'post logout': (req, res) => { }
145
+
146
+ };
147
+ ```
148
+
149
+ ### Controllers stay thin — business logic lives in the model
150
+
151
+ Controllers only orchestrate the HTTP request/response cycle: read params, call the model, send the response with `res.vsr(...)` (REST API) or `res.render(...)` (server-side rendering). They should **not** contain business logic, validation rules, or data manipulation — that belongs on the model (instance/static methods, hooks, or virtuals), so it stays reusable outside the HTTP layer (crontabs, sockets, other models, tests).
92
152
 
93
153
  ### Controller example
94
154
 
@@ -100,12 +160,20 @@ module.exports = {
100
160
  res.vsr(Promise.resolve({ users: [] }));
101
161
  },
102
162
 
103
- 'get :id': function (req, res) {
163
+ 'get :id': (req, res) => {
104
164
  res.vsr(Promise.resolve({ id: req.params.id }));
105
165
  },
106
166
 
107
- 'post save': function (req, res) {
108
- res.vsr(Promise.resolve({ saved: true }));
167
+ post(req, res) {
168
+ res.vsr(Promise.resolve({ data: req.body }));
169
+ },
170
+
171
+ 'put :id': (req, res) => {
172
+ res.vsr(Promise.resolve({ updated: req.params.id, data: req.body }));
173
+ },
174
+
175
+ 'delete :id': (req, res) => {
176
+ res.vsr(Promise.resolve({ deleted: req.params.id }));
109
177
  }
110
178
 
111
179
  };
@@ -113,14 +181,31 @@ module.exports = {
113
181
 
114
182
  ### Explicit routes — `config/routes.js`
115
183
 
184
+ `routes.js` exists for whatever the convention can't resolve on its own — an absolute path, a catch-all for a frontend router, or breaking the "resource segment = controller filename" rule entirely. For everything else, don't add entries here; a redundant explicit entry just gives the route two sources of truth that can drift apart.
185
+
116
186
  ```js
117
- module.exports = [
118
- { method: 'GET', path: '/health', controller: 'StatusController', action: 'ping' },
119
- { method: 'POST', path: '/auth/login', controller: 'AuthController', action: 'login' },
120
- ];
121
- ```
187
+ module.exports = {
188
+
189
+ // Routes as string simple and easy to use
190
+ '/about-me': 'AboutController.get',
122
191
 
123
- You can also register routes with inline handlers or using `app.vulkano.get/post/…`.
192
+ // Catch-all for a frontend router (SPA)
193
+ '/admin*': 'AdminController.get',
194
+
195
+ // Routes as definition — most flexible
196
+ '/test': (req, res) => {
197
+ res.json({ message: 'Hello, world!' });
198
+ },
199
+
200
+ // Routes as method — more advanced (`app.vulkano` is the Express instance)
201
+ custom() {
202
+ app.vulkano.get('/test2', (req, res) => {
203
+ res.json({ hello: 'world2' });
204
+ });
205
+ }
206
+
207
+ };
208
+ ```
124
209
 
125
210
  ---
126
211
 
@@ -152,57 +237,91 @@ res.vsr(Promise.reject(new Error('Something went wrong')));
152
237
  Point a controller at a model and get a full REST API for free:
153
238
 
154
239
  ```js
155
- // vulkano/controllers/api/ProductController.js
240
+ // app/controllers/api/ProductsController.js
156
241
  module.exports = {
157
- scaffold: 'Product', // Mongoose model name
242
+ scaffold: 'Product', // Mongoose model name — must exist as global.Product
158
243
  allowedMethods: ['get', 'post', 'put', 'patch', 'delete']
159
244
  };
160
245
  ```
161
246
 
247
+ `scaffold` is the model name directly, so no separate `model` field is needed. If the string
248
+ doesn't match a loaded model (`global.Product` in this example), Vulkano throws at startup instead
249
+ of silently registering an empty controller.
250
+
251
+ > You may also see `scaffold: true` paired with a separate `model: 'Product'` field — both forms are
252
+ > supported and behave identically, but `scaffold: 'Product'` is the recommended, shorter form.
253
+
162
254
  This automatically exposes:
163
255
 
164
- | Method | Path | Action |
165
- |----------|--------------------|------------------|
166
- | `GET` | `/api/product` | List (paginated) |
167
- | `GET` | `/api/product/:id` | Get by ID |
168
- | `POST` | `/api/product` | Create |
169
- | `PUT` | `/api/product/:id` | Replace |
170
- | `PATCH` | `/api/product/:id` | Partial update |
171
- | `DELETE` | `/api/product/:id` | Soft-delete |
256
+ | Method | Path | Action |
257
+ |----------|---------------------|------------------|
258
+ | `GET` | `/api/products` | List (paginated) |
259
+ | `GET` | `/api/products/:id` | Get by ID |
260
+ | `POST` | `/api/products` | Create |
261
+ | `PUT` | `/api/products/:id` | Replace |
262
+ | `PATCH` | `/api/products/:id` | Partial update |
263
+ | `DELETE` | `/api/products/:id` | Soft-delete |
172
264
 
173
265
  Query string params supported on list: `page`, `per_page`, `sort`, `search`, `fields`.
174
266
 
267
+ A scaffold controller wires each allowed HTTP method to the matching standard CRUD method on the model
268
+ (`getAll`, `get<ModelName>`, `create`, `update`, `delete` — see [Models](#models-business-logic-lives-here) below), so the model
269
+ still needs those methods implemented or auto-generated.
270
+
271
+ NOTE: To find examples with the best practices, look in `examples/controllers` to find a well-structured controller for server side rendering, like `ExampleController.js`, REST API like `RestExampleController.js` and Scaffold REST API like `RestScaffoldController.js`.
272
+
175
273
  ---
176
274
 
177
- ## Models
275
+ ## Models: business logic lives here
276
+
277
+ Models live in `app/models/` are auto-loaded as globals. A file `Project.js` becomes `global.Project` (singular).
278
+ Every model gets `attributes` (Mongoose schema fields), plus `active`, `createdAt`, `updatedAt` automatically.
279
+
280
+ Models are where validation, data manipulation, and business rules belong — not just the raw Mongoose schema. Controllers should only ever call methods on the model; they shouldn't reach into `Model.find(...)` or manipulate documents directly.
281
+
282
+ ### Standard CRUD methods
283
+ Every model is expected to expose this same set of methods, so controllers can call them the same way regardless of the resource:
178
284
 
179
- Models live in `vulkano/models/` and are auto-loaded as globals (e.g., `Product`).
285
+ | Method | Purpose |
286
+ |------------------------|--------------------------------------------------------------------|
287
+ | `getAll(props)` | List/paginate records. `props` = `{ page, perPage, search, sort }` |
288
+ | `get<ModelName>(id)` | Get a single record by id (e.g. `getProduct(id)`) |
289
+ | `create(data)` | Create a new record |
290
+ | `update(id, data)` | Update a record by id |
291
+ | `delete(id)` | Soft-delete a record (sets `active: false`) |
180
292
 
181
293
  ```js
182
- // vulkano/models/Product.js
294
+
295
+ // app/models/Product.js
183
296
  module.exports = {
184
297
  attributes: {
185
298
  name: { type: String, required: true },
186
299
  price: { type: Number, default: 0 },
187
300
  tags: { type: [String] }
188
301
  },
189
-
190
- // Lifecycle hooks
191
- beforeSave(next) {
192
- this.updatedAt = new Date();
193
- next();
194
- }
195
302
  };
196
303
  ```
197
304
 
198
- Every model automatically gets `active`, `createdAt`, and `updatedAt` fields.
199
- Models use [`mongoose-paginate-v2`](https://github.com/aravindnc/mongoose-paginate-v2) for pagination.
305
+ NOTE: To find examples with the best practices for available methods ahd hooks, look in `examples/models` and read the file `Example.js`, and Scaffold Model API `ExampleWithScaffold.js`.
306
+
307
+ ### Vulkano models — don't hand-roll `createdAt` or `updatedAt`
308
+
309
+ `@vulkano/core`'s `database/mongodb.js` auto-injects `createdAt: Date` and `updatedAt: Date` attributes into every model schema if the model doesn't already define them (`if (!attributes.createdAt) { ... }`, same for `updatedAt`). Never add a manual timestamp field (`at`, `date`, `timestamp`, `createdAt`, `updatedAt`, etc.) to a model's `attributes` — they're already automatic in Vulkano, so a hand-rolled one is redundant, and if named anything other than `createdAt`/`updatedAt` it also fights the framework's own sort/index defaults (`database/scaffold.js` defaults `sort: 'createdAt|DESC'`). Use `createdAt` and `updatedAt` directly in indexes, sort strings, and business logic.
310
+
311
+ ---
312
+
313
+ ## Key conventions
314
+
315
+ ### Naming: controllers in plural (recommended but not mandatory), models in singular
316
+ `@vulkano/core` pairs each model with a controller by name, so the naming convention is what makes the auto-routing work:
317
+ - **Model** → singular PascalCase (e.g., `Product.js` → `global.Product`)
318
+ - **Controller** → plural PascalCase + `Controller` suffix (e.g., `ProductsController.js`)
200
319
 
201
320
  ---
202
321
 
203
322
  ## Built-in Global Libs
204
323
 
205
- All files in `vulkano/services/` are auto-loaded as globals. The framework also exposes:
324
+ All files in `app/services/` are auto-loaded as globals. The framework also exposes:
206
325
 
207
326
  | Global | Description |
208
327
  |-------------|-----------------------------------------------------------------|
@@ -240,33 +359,75 @@ Vulkano uses [Multer](https://github.com/expressjs/multer) v2. Files are availab
240
359
 
241
360
  ## JWT Authentication
242
361
 
243
- Configure in `vulkano/config/express/jwt.js`:
362
+ Vulkano integrates JWT internally to validate protected routes, using
363
+ [`express-jwt`](https://www.npmjs.com/package/express-jwt) (route middleware) and
364
+ [`jwt-simple`](https://www.npmjs.com/package/jwt-simple) (encode/decode) under the hood. Configured
365
+ in `app/config/express/jwt.js` — see [`examples/config/express/jwt.js`](examples/config/express/jwt.js)
366
+ for a full example configuration.
367
+
368
+ When `enabled: true`, every request under `path` is checked by an `express-jwt` middleware, which
369
+ calls `Jwt.getToken(req)` to pull the token from the configured header (`x-token-auth` by default),
370
+ cookie, or query parameter, then validates it with `Jwt.decode()`.
371
+
372
+ ### Signing a token
373
+
374
+ `Jwt.encode(data)` requires an `expiration` field in the payload (a millisecond timestamp) —
375
+ without it, `Jwt.decode()` rejects the token by default:
244
376
 
245
377
  ```js
378
+ // app/controllers/api/AuthController.js
246
379
  module.exports = {
247
- secret: process.env.JWT_SECRET,
248
- unless: ['/auth/login', '/health'] // Public paths (no token required)
380
+ 'post login'(req, res) {
381
+ res.vsr(User.login(req.body).then((user) => ({
382
+ user,
383
+ token: Jwt.encode({
384
+ _id: user._id,
385
+ expiration: String(Date.now() + 24 * 60 * 60 * 1000) // 24h from now
386
+ })
387
+ })));
388
+ }
249
389
  };
250
390
  ```
251
391
 
252
- Sign a token anywhere in your app:
392
+ > To issue tokens that never expire, set `expiration: false` in `app/config/express/jwt.js` — this
393
+ > disables the expiration check on `Jwt.decode()`, not just for tokens missing the field.
394
+
395
+ ### Reading the current user
396
+
397
+ `req.auth` isn't set automatically — decode the token in your own middleware
398
+ (`app/config/middlewares/`) or per-controller. See
399
+ [`examples/config/middlewares/auth.js`](examples/config/middlewares/auth.js) for a full example:
253
400
 
254
401
  ```js
255
- const token = Jwt.encode({ userId: user._id });
402
+ const { _id } = Jwt.decode(Jwt.getToken(req)) || {};
256
403
  ```
257
404
 
258
405
  ---
259
406
 
260
407
  ## Cron Jobs
261
408
 
409
+ When Vulkano starts, you can configure your own tasks to run at a given time.
410
+
262
411
  ```js
263
- // vulkano/services/Jobs.js
264
- module.exports = {
265
- init() {
266
- Crontab.add('cleanup', '0 3 * * *', async () => {
267
- await Report.deleteMany({ active: false });
412
+ // app/config/bootstrap.js
413
+ module.exports = (start) => {
414
+
415
+ start(() => {
416
+
417
+ Crontab.schedule({
418
+ time: '0 0 11 * * 5',
419
+ timeZone: 'America/New_York',
420
+ task: () => {
421
+ console.log('Crontab every Friday (5) at 11');
422
+ Weekly.report().catch( () => {});
423
+ },
424
+ onComplete: () => {
425
+ console.log(`Weekly report job completed at ${new Date()}`);
426
+ }
268
427
  });
269
- }
428
+
429
+ });
430
+
270
431
  };
271
432
  ```
272
433
 
@@ -274,17 +435,153 @@ module.exports = {
274
435
 
275
436
  ## i18n
276
437
 
277
- Translation files go in `vulkano/config/locales/`. Use `i18n.t('key')` anywhere in your app.
438
+ Vulkano wires up [i18next](https://www.i18next.com/) automatically. One file per locale in
439
+ `app/config/locales/`, keyed by filename — no manual registration needed:
440
+
441
+ ```js
442
+ // app/config/locales/en.js
443
+ module.exports = {
444
+ welcome: 'Welcome',
445
+ goodbye: 'Goodbye'
446
+ };
447
+
448
+ // app/config/locales/es.js
449
+ module.exports = {
450
+ welcome: 'Bienvenido',
451
+ goodbye: 'Adiós'
452
+ };
453
+ ```
454
+
455
+ The global `i18n` is the configured i18next instance — use `i18n.t('key')` anywhere in your app
456
+ (controllers, models, services):
457
+
458
+ ```js
459
+ // app/controllers/HomeController.js
460
+ module.exports = {
461
+ get(req, res) {
462
+ res.vsr(Promise.resolve({ message: i18n.t('welcome') })); // "Welcome"
463
+ }
464
+ };
465
+ ```
466
+
467
+ Default language is `en`, with `en` as the fallback if a key or locale is missing. To switch the
468
+ active language at runtime, call `i18n.changeLanguage('es')`.
278
469
 
279
470
  ---
280
471
 
281
472
  ## Socket.io
282
473
 
283
- Enabled via `vulkano/config/settings.js`. Adapters for MongoDB and Redis are included out of the box.
474
+ Enabled via `app/config/sockets/config.js`. Adapters for Redis and MongoDB are included out of the box (default is in-memory).
475
+
476
+ ```js
477
+ // app/config/sockets/config.js
478
+ module.exports = {
479
+
480
+ // Enable sockets
481
+ enabled: true,
482
+
483
+ // Socket IO Adapter (redis|mongodb|memory)
484
+ adapter: 'memory',
485
+
486
+ // Socket configuration
487
+ config: {
488
+ transports: ['websocket', 'polling'],
489
+ timeout: 4000,
490
+ interval: 2000,
491
+ },
492
+
493
+ // Connections
494
+ connections: {
495
+ users: 0,
496
+ clients: {}
497
+ }
498
+
499
+ };
500
+ ```
501
+
502
+ Events map socket event names to a controller action, the same `folder.<Name>Controller.method` convention used by `routes.js`:
503
+
504
+ ```js
505
+ // app/config/sockets/events.js
506
+ module.exports = {
507
+ 'echo': 'sockets.EchoController.echo'
508
+ };
509
+ ```
510
+
511
+ ```js
512
+ // app/controllers/sockets/EchoController.js
513
+ module.exports = {
514
+ echo({ socket, body }, callback) {
515
+ callback({
516
+ echo: body,
517
+ userId: (socket.request.user || {})._id || null
518
+ });
519
+ }
520
+ };
521
+ ```
522
+
523
+ Handler signature is always `({ socket, body }, callback)`.
524
+
525
+ Optional CORS check (`app/config/sockets/cors.js`):
526
+
527
+ ```js
528
+ module.exports = (req, callback) => {
529
+ const { origin, host } = req.headers || {};
530
+ const realOrigin = origin || host;
531
+ const allowedOrigin = ['localhost', 'yourdomain.com'];
532
+
533
+ const found = allowedOrigin.some((o) => (realOrigin || '').indexOf(o) !== -1);
534
+
535
+ if (found) {
536
+ callback(null, true);
537
+ } else {
538
+ callback(new Error(`Invalid origin ${realOrigin} - Socket CORS`));
539
+ }
540
+ };
541
+ ```
542
+
543
+ Optional auth middleware, run before a socket connection is accepted (`app/config/sockets/middlewares/auth.js`):
544
+
545
+ ```js
546
+ module.exports = (socket, next) => {
547
+ const user = Jwt.socket(socket);
548
+ const { _id } = user || {};
549
+
550
+ if (!_id) {
551
+ next(new Error(`Invalid user ${_id || 'or token'}`));
552
+ }
553
+
554
+ socket.request.user = user || {};
555
+ next();
556
+ };
557
+ ```
558
+
559
+ Redis/MongoDB adapter settings live in `app/config/sockets/adapters/redis.js` and `app/config/sockets/adapters/mongodb.js`:
560
+
561
+ ```js
562
+ // app/config/sockets/adapters/redis.js
563
+ module.exports = {
564
+ host: process.env.REDIS_HOST || 'localhost',
565
+ port: process.env.REDIS_PORT || '6379',
566
+ password: process.env.REDIS_PASSWORD || ''
567
+ };
568
+ ```
569
+
570
+ ```js
571
+ // app/config/sockets/adapters/mongodb.js
572
+ module.exports = {
573
+ connection: process.env.SOCKETS_MONGO_URI || null,
574
+ collection: process.env.SOCKETS_MONGO_COLLECTION || 'socket.io-adapter-events'
575
+ };
576
+ ```
577
+
578
+ Available globally as `io` (the Socket.io server instance) and `app.socket` (the current socket).
579
+
580
+ See `test/fixtures/app/config/sockets` and `test/fixtures/app/controllers/sockets` for the full working example used by the integration tests.
284
581
 
285
582
  ---
286
583
 
287
- ## Configuration — `vulkano/config/settings.js`
584
+ ## Configuration — `app/config/settings.js`
288
585
 
289
586
  ```js
290
587
  module.exports = {
@@ -292,9 +589,6 @@ module.exports = {
292
589
  // PORT to listen on
293
590
  port: process.env.PORT || 3000,
294
591
 
295
- // Salt for password
296
- salt: process.env.SALT_KEY || '',
297
-
298
592
  // Database configuration
299
593
  database: {
300
594
 
@@ -322,6 +616,26 @@ module.exports = {
322
616
 
323
617
  ---
324
618
 
619
+ ## Express Configuration
620
+
621
+ Each file in `app/config/express/` configures one Express middleware. All are optional — omitted
622
+ files fall back to sane defaults — and every file is auto-merged into the final config used by
623
+ `bootstrap/server.js`. Full working examples for every file live in
624
+ [`examples/config/express/`](examples/config/express).
625
+
626
+ | File | Configures | Package used |
627
+ |-------------------------|--------------------------------------|-------------------------------------------------------------|
628
+ | [`settings.js`](examples/config/express/settings.js) | Core server behavior (`poweredBy`, `timeout`, `uploadPath`, `trustProxy`) | — (native Express) |
629
+ | [`cookies.js`](examples/config/express/cookies.js) | Cookie parsing + session secret | [`cookie-parser`](https://www.npmjs.com/package/cookie-parser), [`express-session`](https://www.npmjs.com/package/express-session) |
630
+ | [`cors.js`](examples/config/express/cors.js) | Cross-Origin Resource Sharing | — (handled with a custom middleware, no `cors` package) |
631
+ | [`jwt.js`](examples/config/express/jwt.js) | JWT authentication middleware | [`express-jwt`](https://www.npmjs.com/package/express-jwt), [`jwt-simple`](https://www.npmjs.com/package/jwt-simple) |
632
+ | [`csp.js`](examples/config/express/csp.js) | Content Security Policy rules | — (custom header builder) |
633
+ | [`helmet.js`](examples/config/express/helmet.js) | Security headers | [`helmet`](https://helmetjs.github.io/) |
634
+ | [`permissionPolicy.js`](examples/config/express/permissionPolicy.js) | `Permission-Policy` header | — (custom header builder) |
635
+ | [`json.js`](examples/config/express/json.js) | JSON body parser MIME types | — (native `express.json()`) |
636
+
637
+ ---
638
+
325
639
  ## Running Tests
326
640
 
327
641
  ```bash
@@ -26,9 +26,17 @@ module.exports = function loadControllersApplication() {
26
26
  model
27
27
  } = current;
28
28
 
29
- if (scaffold && model) {
29
+ // `scaffold` can be `true` + a separate `model` field, or the model
30
+ // name given directly as the `scaffold` string (no `model` needed).
31
+ const scaffoldModel = typeof scaffold === 'string' ? scaffold : model;
30
32
 
31
- const scaffoldingCurrent = scaffoldController(model, allowedMethods);
33
+ if (scaffold && scaffoldModel) {
34
+
35
+ if (!global[scaffoldModel]) {
36
+ throw new Error(`Scaffold model "${scaffoldModel}" not found in global scope for controller "${controller}". Make sure the model exists in app/models.`);
37
+ }
38
+
39
+ const scaffoldingCurrent = scaffoldController(scaffoldModel, allowedMethods);
32
40
 
33
41
  Object.keys(scaffoldingCurrent).forEach( (m) => {
34
42
 
@@ -66,9 +74,15 @@ module.exports = function loadControllersApplication() {
66
74
  model: subcurrentModel
67
75
  } = subcurrent || {};
68
76
 
69
- if (subcurrentScaffold && subcurrentModel) {
77
+ const subScaffoldModel = typeof subcurrentScaffold === 'string' ? subcurrentScaffold : subcurrentModel;
78
+
79
+ if (subcurrentScaffold && subScaffoldModel) {
80
+
81
+ if (!global[subScaffoldModel]) {
82
+ throw new Error(`Scaffold model "${subScaffoldModel}" not found in global scope for controller "${subcontroller}". Make sure the model exists in app/models.`);
83
+ }
70
84
 
71
- const scaffoldingSubcurrent = scaffoldController(subcurrentModel, subAllowedMethods);
85
+ const scaffoldingSubcurrent = scaffoldController(subScaffoldModel, subAllowedMethods);
72
86
 
73
87
  Object.keys(scaffoldingSubcurrent).forEach( (m) => {
74
88
 
@@ -87,8 +101,15 @@ module.exports = function loadControllersApplication() {
87
101
  const [tmpMethod, tmpPath] = parts;
88
102
 
89
103
  if (tmpPath) {
90
- method = tmpMethod.toLowerCase();
91
- pathToRun = tmpPath;
104
+ if (methods.indexOf(tmpMethod.toLowerCase()) >= 0) {
105
+ method = tmpMethod.toLowerCase();
106
+ pathToRun = tmpPath;
107
+ } else {
108
+ // First token isn't a real HTTP method — default to GET,
109
+ // treating the whole key as the path (e.g. 'edit :id' → GET .../edit/:id)
110
+ method = 'get';
111
+ pathToRun = parts.join('/');
112
+ }
92
113
  } else {
93
114
  pathToRun = tmpMethod;
94
115
  }
@@ -119,8 +140,15 @@ module.exports = function loadControllersApplication() {
119
140
  const [tmpMethod, tmpPath] = parts;
120
141
 
121
142
  if (tmpPath) {
122
- method = tmpMethod.toLowerCase();
123
- pathToRun = tmpPath;
143
+ if (methods.indexOf(tmpMethod.toLowerCase()) >= 0) {
144
+ method = tmpMethod.toLowerCase();
145
+ pathToRun = tmpPath;
146
+ } else {
147
+ // First token isn't a real HTTP method — default to GET,
148
+ // treating the whole key as the path (e.g. 'edit :id' → GET .../edit/:id)
149
+ method = 'get';
150
+ pathToRun = parts.join('/');
151
+ }
124
152
  } else {
125
153
  pathToRun = tmpMethod;
126
154
  }