@vulkano/core 1.20.1 → 1.22.0

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.
Files changed (31) hide show
  1. package/README.md +380 -70
  2. package/controllers/controllers.js +36 -8
  3. package/database/mongodb.js +11 -4
  4. package/examples/config/express/cookies.js +15 -0
  5. package/examples/config/express/cors.js +23 -0
  6. package/examples/config/express/csp.js +74 -0
  7. package/examples/config/express/helmet.js +19 -0
  8. package/examples/config/express/json.js +14 -0
  9. package/examples/config/express/jwt.js +39 -0
  10. package/examples/config/express/permissionPolicy.js +24 -0
  11. package/examples/config/express/settings.js +25 -0
  12. package/examples/config/middlewares/auth.js +38 -0
  13. package/examples/config/sockets/adapters/mongodb.js +12 -0
  14. package/examples/config/sockets/adapters/redis.js +9 -0
  15. package/examples/config/sockets/config.js +50 -0
  16. package/examples/config/sockets/cors.js +32 -0
  17. package/examples/config/sockets/events.js +12 -0
  18. package/examples/config/sockets/middlewares/auth.js +20 -0
  19. package/examples/config/views/config.js +8 -0
  20. package/examples/config/views/filters/example.js +6 -0
  21. package/examples/config/views/helpers/strpad.js +51 -0
  22. package/examples/controllers/ExampleController.js +22 -0
  23. package/examples/controllers/RestExampleController.js +78 -0
  24. package/examples/controllers/RestScaffoldController.js +18 -0
  25. package/examples/models/Example.js +238 -0
  26. package/examples/models/ExampleWithScaffold.js +51 -0
  27. package/examples/routes.js +55 -0
  28. package/examples/views/_shared/templates/default.html +20 -0
  29. package/examples/views/index.html +7 -0
  30. package/package.json +1 -1
  31. package/pnpm-workspace.yaml +7 -1
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/me`. Don't write `'get me'`; 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/me — no verb prefix needed, GET is the default
138
+ me(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,87 @@ 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.
178
281
 
179
- Models live in `vulkano/models/` and are auto-loaded as globals (e.g., `Product`).
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:
284
+
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
+ ---
308
+
309
+ ## Key conventions
310
+
311
+ ### Naming: controllers in plural (recommended but not mandatory), models in singular
312
+ `@vulkano/core` pairs each model with a controller by name, so the naming convention is what makes the auto-routing work:
313
+ - **Model** → singular PascalCase (e.g., `Product.js` → `global.Product`)
314
+ - **Controller** → plural PascalCase + `Controller` suffix (e.g., `ProductsController.js`)
200
315
 
201
316
  ---
202
317
 
203
318
  ## Built-in Global Libs
204
319
 
205
- All files in `vulkano/services/` are auto-loaded as globals. The framework also exposes:
320
+ All files in `app/services/` are auto-loaded as globals. The framework also exposes:
206
321
 
207
322
  | Global | Description |
208
323
  |-------------|-----------------------------------------------------------------|
@@ -240,33 +355,75 @@ Vulkano uses [Multer](https://github.com/expressjs/multer) v2. Files are availab
240
355
 
241
356
  ## JWT Authentication
242
357
 
243
- Configure in `vulkano/config/express/jwt.js`:
358
+ Vulkano integrates JWT internally to validate protected routes, using
359
+ [`express-jwt`](https://www.npmjs.com/package/express-jwt) (route middleware) and
360
+ [`jwt-simple`](https://www.npmjs.com/package/jwt-simple) (encode/decode) under the hood. Configured
361
+ in `app/config/express/jwt.js` — see [`examples/config/express/jwt.js`](examples/config/express/jwt.js)
362
+ for a full example configuration.
363
+
364
+ When `enabled: true`, every request under `path` is checked by an `express-jwt` middleware, which
365
+ calls `Jwt.getToken(req)` to pull the token from the configured header (`x-token-auth` by default),
366
+ cookie, or query parameter, then validates it with `Jwt.decode()`.
367
+
368
+ ### Signing a token
369
+
370
+ `Jwt.encode(data)` requires an `expiration` field in the payload (a millisecond timestamp) —
371
+ without it, `Jwt.decode()` rejects the token by default:
244
372
 
245
373
  ```js
374
+ // app/controllers/api/AuthController.js
246
375
  module.exports = {
247
- secret: process.env.JWT_SECRET,
248
- unless: ['/auth/login', '/health'] // Public paths (no token required)
376
+ 'post login'(req, res) {
377
+ res.vsr(User.login(req.body).then((user) => ({
378
+ user,
379
+ token: Jwt.encode({
380
+ _id: user._id,
381
+ expiration: String(Date.now() + 24 * 60 * 60 * 1000) // 24h from now
382
+ })
383
+ })));
384
+ }
249
385
  };
250
386
  ```
251
387
 
252
- Sign a token anywhere in your app:
388
+ > To issue tokens that never expire, set `expiration: false` in `app/config/express/jwt.js` — this
389
+ > disables the expiration check on `Jwt.decode()`, not just for tokens missing the field.
390
+
391
+ ### Reading the current user
392
+
393
+ `req.auth` isn't set automatically — decode the token in your own middleware
394
+ (`app/config/middlewares/`) or per-controller. See
395
+ [`examples/config/middlewares/auth.js`](examples/config/middlewares/auth.js) for a full example:
253
396
 
254
397
  ```js
255
- const token = Jwt.encode({ userId: user._id });
398
+ const { _id } = Jwt.decode(Jwt.getToken(req)) || {};
256
399
  ```
257
400
 
258
401
  ---
259
402
 
260
403
  ## Cron Jobs
261
404
 
405
+ When Vulkano starts, you can configure your own tasks to run at a given time.
406
+
262
407
  ```js
263
- // vulkano/services/Jobs.js
264
- module.exports = {
265
- init() {
266
- Crontab.add('cleanup', '0 3 * * *', async () => {
267
- await Report.deleteMany({ active: false });
408
+ // app/config/bootstrap.js
409
+ module.exports = (start) => {
410
+
411
+ start(() => {
412
+
413
+ Crontab.schedule({
414
+ time: '0 0 11 * * 5',
415
+ timeZone: 'America/New_York',
416
+ task: () => {
417
+ console.log('Crontab every Friday (5) at 11');
418
+ Weekly.report().catch( () => {});
419
+ },
420
+ onComplete: () => {
421
+ console.log(`Weekly report job completed at ${new Date()}`);
422
+ }
268
423
  });
269
- }
424
+
425
+ });
426
+
270
427
  };
271
428
  ```
272
429
 
@@ -274,17 +431,153 @@ module.exports = {
274
431
 
275
432
  ## i18n
276
433
 
277
- Translation files go in `vulkano/config/locales/`. Use `i18n.t('key')` anywhere in your app.
434
+ Vulkano wires up [i18next](https://www.i18next.com/) automatically. One file per locale in
435
+ `app/config/locales/`, keyed by filename — no manual registration needed:
436
+
437
+ ```js
438
+ // app/config/locales/en.js
439
+ module.exports = {
440
+ welcome: 'Welcome',
441
+ goodbye: 'Goodbye'
442
+ };
443
+
444
+ // app/config/locales/es.js
445
+ module.exports = {
446
+ welcome: 'Bienvenido',
447
+ goodbye: 'Adiós'
448
+ };
449
+ ```
450
+
451
+ The global `i18n` is the configured i18next instance — use `i18n.t('key')` anywhere in your app
452
+ (controllers, models, services):
453
+
454
+ ```js
455
+ // app/controllers/HomeController.js
456
+ module.exports = {
457
+ get(req, res) {
458
+ res.vsr(Promise.resolve({ message: i18n.t('welcome') })); // "Welcome"
459
+ }
460
+ };
461
+ ```
462
+
463
+ Default language is `en`, with `en` as the fallback if a key or locale is missing. To switch the
464
+ active language at runtime, call `i18n.changeLanguage('es')`.
278
465
 
279
466
  ---
280
467
 
281
468
  ## Socket.io
282
469
 
283
- Enabled via `vulkano/config/settings.js`. Adapters for MongoDB and Redis are included out of the box.
470
+ Enabled via `app/config/sockets/config.js`. Adapters for Redis and MongoDB are included out of the box (default is in-memory).
471
+
472
+ ```js
473
+ // app/config/sockets/config.js
474
+ module.exports = {
475
+
476
+ // Enable sockets
477
+ enabled: true,
478
+
479
+ // Socket IO Adapter (redis|mongodb|memory)
480
+ adapter: 'memory',
481
+
482
+ // Socket configuration
483
+ config: {
484
+ transports: ['websocket', 'polling'],
485
+ timeout: 4000,
486
+ interval: 2000,
487
+ },
488
+
489
+ // Connections
490
+ connections: {
491
+ users: 0,
492
+ clients: {}
493
+ }
494
+
495
+ };
496
+ ```
497
+
498
+ Events map socket event names to a controller action, the same `folder.<Name>Controller.method` convention used by `routes.js`:
499
+
500
+ ```js
501
+ // app/config/sockets/events.js
502
+ module.exports = {
503
+ 'echo': 'sockets.EchoController.echo'
504
+ };
505
+ ```
506
+
507
+ ```js
508
+ // app/controllers/sockets/EchoController.js
509
+ module.exports = {
510
+ echo({ socket, body }, callback) {
511
+ callback({
512
+ echo: body,
513
+ userId: (socket.request.user || {})._id || null
514
+ });
515
+ }
516
+ };
517
+ ```
518
+
519
+ Handler signature is always `({ socket, body }, callback)`.
520
+
521
+ Optional CORS check (`app/config/sockets/cors.js`):
522
+
523
+ ```js
524
+ module.exports = (req, callback) => {
525
+ const { origin, host } = req.headers || {};
526
+ const realOrigin = origin || host;
527
+ const allowedOrigin = ['localhost', 'yourdomain.com'];
528
+
529
+ const found = allowedOrigin.some((o) => (realOrigin || '').indexOf(o) !== -1);
530
+
531
+ if (found) {
532
+ callback(null, true);
533
+ } else {
534
+ callback(new Error(`Invalid origin ${realOrigin} - Socket CORS`));
535
+ }
536
+ };
537
+ ```
538
+
539
+ Optional auth middleware, run before a socket connection is accepted (`app/config/sockets/middlewares/auth.js`):
540
+
541
+ ```js
542
+ module.exports = (socket, next) => {
543
+ const user = Jwt.socket(socket);
544
+ const { _id } = user || {};
545
+
546
+ if (!_id) {
547
+ next(new Error(`Invalid user ${_id || 'or token'}`));
548
+ }
549
+
550
+ socket.request.user = user || {};
551
+ next();
552
+ };
553
+ ```
554
+
555
+ Redis/MongoDB adapter settings live in `app/config/sockets/adapters/redis.js` and `app/config/sockets/adapters/mongodb.js`:
556
+
557
+ ```js
558
+ // app/config/sockets/adapters/redis.js
559
+ module.exports = {
560
+ host: process.env.REDIS_HOST || 'localhost',
561
+ port: process.env.REDIS_PORT || '6379',
562
+ password: process.env.REDIS_PASSWORD || ''
563
+ };
564
+ ```
565
+
566
+ ```js
567
+ // app/config/sockets/adapters/mongodb.js
568
+ module.exports = {
569
+ connection: process.env.SOCKETS_MONGO_URI || null,
570
+ collection: process.env.SOCKETS_MONGO_COLLECTION || 'socket.io-adapter-events'
571
+ };
572
+ ```
573
+
574
+ Available globally as `io` (the Socket.io server instance) and `app.socket` (the current socket).
575
+
576
+ See `test/fixtures/app/config/sockets` and `test/fixtures/app/controllers/sockets` for the full working example used by the integration tests.
284
577
 
285
578
  ---
286
579
 
287
- ## Configuration — `vulkano/config/settings.js`
580
+ ## Configuration — `app/config/settings.js`
288
581
 
289
582
  ```js
290
583
  module.exports = {
@@ -292,9 +585,6 @@ module.exports = {
292
585
  // PORT to listen on
293
586
  port: process.env.PORT || 3000,
294
587
 
295
- // Salt for password
296
- salt: process.env.SALT_KEY || '',
297
-
298
588
  // Database configuration
299
589
  database: {
300
590
 
@@ -322,6 +612,26 @@ module.exports = {
322
612
 
323
613
  ---
324
614
 
615
+ ## Express Configuration
616
+
617
+ Each file in `app/config/express/` configures one Express middleware. All are optional — omitted
618
+ files fall back to sane defaults — and every file is auto-merged into the final config used by
619
+ `bootstrap/server.js`. Full working examples for every file live in
620
+ [`examples/config/express/`](examples/config/express).
621
+
622
+ | File | Configures | Package used |
623
+ |-------------------------|--------------------------------------|-------------------------------------------------------------|
624
+ | [`settings.js`](examples/config/express/settings.js) | Core server behavior (`poweredBy`, `timeout`, `uploadPath`, `trustProxy`) | — (native Express) |
625
+ | [`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) |
626
+ | [`cors.js`](examples/config/express/cors.js) | Cross-Origin Resource Sharing | — (handled with a custom middleware, no `cors` package) |
627
+ | [`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) |
628
+ | [`csp.js`](examples/config/express/csp.js) | Content Security Policy rules | — (custom header builder) |
629
+ | [`helmet.js`](examples/config/express/helmet.js) | Security headers | [`helmet`](https://helmetjs.github.io/) |
630
+ | [`permissionPolicy.js`](examples/config/express/permissionPolicy.js) | `Permission-Policy` header | — (custom header builder) |
631
+ | [`json.js`](examples/config/express/json.js) | JSON body parser MIME types | — (native `express.json()`) |
632
+
633
+ ---
634
+
325
635
  ## Running Tests
326
636
 
327
637
  ```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
  }