@vulkano/core 1.20.2 → 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.
- package/README.md +380 -70
- package/controllers/controllers.js +36 -8
- package/database/mongodb.js +11 -4
- package/examples/config/express/cookies.js +15 -0
- package/examples/config/express/cors.js +23 -0
- package/examples/config/express/csp.js +74 -0
- package/examples/config/express/helmet.js +19 -0
- package/examples/config/express/json.js +14 -0
- package/examples/config/express/jwt.js +39 -0
- package/examples/config/express/permissionPolicy.js +24 -0
- package/examples/config/express/settings.js +25 -0
- package/examples/config/middlewares/auth.js +38 -0
- package/examples/config/sockets/adapters/mongodb.js +12 -0
- package/examples/config/sockets/adapters/redis.js +9 -0
- package/examples/config/sockets/config.js +50 -0
- package/examples/config/sockets/cors.js +32 -0
- package/examples/config/sockets/events.js +12 -0
- package/examples/config/sockets/middlewares/auth.js +20 -0
- package/examples/config/views/config.js +8 -0
- package/examples/config/views/filters/example.js +6 -0
- package/examples/config/views/helpers/strpad.js +51 -0
- package/examples/controllers/RestScaffoldController.js +3 -6
- package/examples/models/Example.js +64 -4
- package/examples/routes.js +55 -0
- package/package.json +2 -2
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
|
-
├──
|
|
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
|
|
84
|
-
|
|
85
|
-
| `GET` | `/
|
|
86
|
-
| `POST` | `/
|
|
87
|
-
| `PUT` | `/
|
|
88
|
-
| `PATCH` | `/
|
|
89
|
-
| `DELETE` | `/
|
|
90
|
-
| `POST` | `/
|
|
91
|
-
| `GET` | `/
|
|
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':
|
|
163
|
+
'get :id': (req, res) => {
|
|
104
164
|
res.vsr(Promise.resolve({ id: req.params.id }));
|
|
105
165
|
},
|
|
106
166
|
|
|
107
|
-
|
|
108
|
-
res.vsr(Promise.resolve({
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```
|
|
187
|
+
module.exports = {
|
|
188
|
+
|
|
189
|
+
// Routes as string — simple and easy to use
|
|
190
|
+
'/about-me': 'AboutController.get',
|
|
122
191
|
|
|
123
|
-
|
|
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
|
-
//
|
|
240
|
+
// app/controllers/api/ProductsController.js
|
|
156
241
|
module.exports = {
|
|
157
|
-
scaffold: 'Product',
|
|
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
|
|
165
|
-
|
|
166
|
-
| `GET` | `/api/
|
|
167
|
-
| `GET` | `/api/
|
|
168
|
-
| `POST` | `/api/
|
|
169
|
-
| `PUT` | `/api/
|
|
170
|
-
| `PATCH` | `/api/
|
|
171
|
-
| `DELETE` | `/api/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
264
|
-
module.exports = {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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 `
|
|
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 — `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
}
|