@vulkano/core 1.25.0 → 1.26.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/AGENTS.md +648 -0
- package/README.md +17 -0
- package/controllers/controllers.js +53 -109
- package/package.json +1 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
# Vulkano Core — CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@vulkano/core` (v1.26.0) is the engine of the Vulkano MVC framework. It bootstraps the environment, connects to the database, and auto-loads all models, controllers, services, and responses before starting the Express server. The user app only calls `require('@vulkano/core')`.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/**
|
|
9
|
+
* app.js
|
|
10
|
+
*
|
|
11
|
+
* To start the server, run: `node app.js`.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const vulkano = require('@vulkano/core');
|
|
16
|
+
|
|
17
|
+
vulkano();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## File structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
core/
|
|
26
|
+
├── app.js ← Entry point: full bootstrap sequence
|
|
27
|
+
├── bootstrap/
|
|
28
|
+
│ ├── express.js ← Merges all Express configuration sources
|
|
29
|
+
│ ├── logger.js ← Console helpers (colors, column formatting)
|
|
30
|
+
│ ├── responses.js ← Auto-loads and injects response methods into res
|
|
31
|
+
│ ├── server.js ← Starts Express, registers middleware, routes, sockets
|
|
32
|
+
│ ├── services.js ← Auto-loads libs/services and injects them as globals
|
|
33
|
+
│ └── views.js ← Nunjucks base config (path, filters, helpers)
|
|
34
|
+
├── controllers/
|
|
35
|
+
│ ├── controllers.js ← Auto-discovers *Controller.js files, builds route table
|
|
36
|
+
│ └── ScaffoldController.js ← Auto-generates CRUD methods for scaffold:true controllers
|
|
37
|
+
├── database/
|
|
38
|
+
│ ├── mongodb.js ← Connects Mongoose, compiles models, registers them as globals
|
|
39
|
+
│ ├── models.js ← Loads user models, merges lifecycle callbacks and scaffold methods
|
|
40
|
+
│ └── scaffold.js ← Base CRUD methods: getAll, getByField, create, update, delete, subdocs
|
|
41
|
+
├── libs/
|
|
42
|
+
│ ├── ApiClient.js ← Axios wrapper for outbound HTTP requests
|
|
43
|
+
│ ├── Crontab.js ← node-cron wrapper for scheduled tasks
|
|
44
|
+
│ ├── Download.js ← File download helper
|
|
45
|
+
│ ├── Encrypter.js ← AES-256-CBC encrypt/decrypt
|
|
46
|
+
│ ├── Filter.js ← String filter system (trim, prefix, suffix, etc.)
|
|
47
|
+
│ ├── Jwt.js ← JWT encode/decode + Express middleware
|
|
48
|
+
│ ├── Paginate.js ← Mongoose pagination, search, and filtering
|
|
49
|
+
│ ├── VSError.js ← Standard error class with statusCode
|
|
50
|
+
│ ├── i18n.js ← i18next wrapper + moment locale sync
|
|
51
|
+
│ └── filters/ ← Individual filter modules (trim, ltrim, rtrim, prefix, suffix, number, objectId, saveinteger)
|
|
52
|
+
├── responses/
|
|
53
|
+
│ └── vsr.js ← Vulkano Standard Response: resolves a Promise → JSON
|
|
54
|
+
└── views/
|
|
55
|
+
├── filters/ ← Core Nunjucks filters (vCamelCase, vLowercase)
|
|
56
|
+
└── errors/ ← Dev-mode HTML error templates (no_controller, no_action, no_view, exception)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Bootstrap sequence (`app.js`)
|
|
62
|
+
|
|
63
|
+
1. Sets path globals: `START_TIME`, `ABS_PATH`, `APP_PATH`, `PUBLIC_PATH`, `CORE_PATH`, `app`, `_`
|
|
64
|
+
2. Loads `.env` via dotenv
|
|
65
|
+
3. Auto-discovers all user config via `include-all` from `app/config/`
|
|
66
|
+
4. Deep-merges config in order: general → settings → `env/{NODE_ENV}` → `local.js`
|
|
67
|
+
5. Stores result in `app.config`
|
|
68
|
+
6. Runs in sequence: `loadServices()` → `loadDatabase()` → `loadControllers()` → `loadServer()`
|
|
69
|
+
7. Registers `app.routes` and `app.server`
|
|
70
|
+
8. Calls the user's `bootstrap.js`, which in turn calls `app.server.start(cb)`
|
|
71
|
+
9. Inside `start()`: registers middleware, routes, sockets, then calls `cb()`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Auto-loading system
|
|
76
|
+
|
|
77
|
+
Vulkano automatically discovers, loads, and registers all application components at startup —
|
|
78
|
+
**no manual `require()` or imports are needed** inside controllers, models, or services.
|
|
79
|
+
|
|
80
|
+
This is the core design principle of the framework: models, controllers, libraries, and services are
|
|
81
|
+
all loaded and made globally accessible at boot time. This reduces boilerplate and means any file in
|
|
82
|
+
the app can use `User`, `Paginate`, `Jwt`, or any other component directly without importing it.
|
|
83
|
+
|
|
84
|
+
Every component type is scanned from **two locations**: the framework core and the user's `app/`
|
|
85
|
+
project folder. Both are merged, with the project's files taking precedence over core files when
|
|
86
|
+
names collide.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### 1. Libraries and services → global scope
|
|
91
|
+
|
|
92
|
+
**Loader:** `bootstrap/services.js`
|
|
93
|
+
|
|
94
|
+
Scanned directories (all merged into a single global namespace):
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
core/libs/*.js ← built-in framework libraries
|
|
98
|
+
app/services/*.js ← project-level services
|
|
99
|
+
app/libs/*.js ← project-level utility libraries
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Every exported module is injected as `global[filename]`. The global name is the **filename without
|
|
103
|
+
extension**, PascalCase by convention:
|
|
104
|
+
|
|
105
|
+
| File | Global |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `core/libs/Paginate.js` | `global.Paginate` |
|
|
108
|
+
| `core/libs/VSError.js` | `global.VSError` |
|
|
109
|
+
| `core/libs/Jwt.js` | `global.Jwt` |
|
|
110
|
+
| `core/libs/Encrypter.js` | `global.Encrypter` |
|
|
111
|
+
| `core/libs/Filter.js` | `global.Filter` |
|
|
112
|
+
| `core/libs/ApiClient.js` | `global.ApiClient` |
|
|
113
|
+
| `core/libs/Crontab.js` | `global.Crontab` |
|
|
114
|
+
| `core/libs/i18n.js` | `global.i18n` |
|
|
115
|
+
|
|
116
|
+
> `ActiveRecord` and `AppController` are explicitly excluded from globals even if present.
|
|
117
|
+
|
|
118
|
+
If a project file has the same name as a core lib (e.g. `app/services/Paginate.js`), the project
|
|
119
|
+
version wins and replaces the core one globally.
|
|
120
|
+
|
|
121
|
+
**Available in every controller, model, service, or config file — no import needed:**
|
|
122
|
+
```js
|
|
123
|
+
VSError.reject('Not found', 404);
|
|
124
|
+
Paginate.get(User, query);
|
|
125
|
+
Jwt.encode({ userId: '123' });
|
|
126
|
+
Filter.get(' hello ', 'trim');
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 2. Models → compiled Mongoose models in global scope
|
|
132
|
+
|
|
133
|
+
**Loader:** `database/models.js` + `database/mongodb.js`
|
|
134
|
+
|
|
135
|
+
Scanned directory:
|
|
136
|
+
```
|
|
137
|
+
app/models/*.js ← all project models (one file per model)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For each model file the loader:
|
|
141
|
+
1. Reads `attributes` and separates virtual fields
|
|
142
|
+
2. Adds automatic fields: `active` (Boolean, default `true`), `createdAt` (Date), `updatedAt` (Date)
|
|
143
|
+
3. Sets `trim: true` on all non-Boolean attributes (disable with `trim: false`)
|
|
144
|
+
4. Merges default lifecycle callbacks (`beforeSave`, `afterSave`, `beforeUpdate`, etc.)
|
|
145
|
+
5. Merges base scaffold CRUD methods from `database/scaffold.js`
|
|
146
|
+
6. Compiles the Mongoose schema and registers it as `global[ModelName]`
|
|
147
|
+
|
|
148
|
+
Collection name is always `modelName.toLowerCase()`:
|
|
149
|
+
|
|
150
|
+
| File | Global | MongoDB collection |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `app/models/User.js` | `global.User` | `user` |
|
|
153
|
+
| `app/models/Product.js` | `global.Product` | `product` |
|
|
154
|
+
| `app/models/BlogPost.js` | `global.BlogPost` | `blogpost` |
|
|
155
|
+
|
|
156
|
+
**Available in any controller, service, or other model — no import needed:**
|
|
157
|
+
```js
|
|
158
|
+
User.create(req.body);
|
|
159
|
+
Product.getAll(req.query);
|
|
160
|
+
BlogPost.getByField(req.params.id);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 3. Controllers → route table
|
|
166
|
+
|
|
167
|
+
**Loader:** `controllers/controllers.js`
|
|
168
|
+
|
|
169
|
+
Scanned directory:
|
|
170
|
+
```
|
|
171
|
+
app/controllers/**/*Controller.js ← all controllers, including subfolders
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For each controller file the loader:
|
|
175
|
+
- Reads the exported object's method names as route definitions
|
|
176
|
+
- Builds a flat route map: `{ "get /user/": fn, "post /user/save": fn, ... }`
|
|
177
|
+
- If the controller has `scaffold: 'ModelName'` (or `scaffold: true` + `model`), injects CRUD methods before building routes — throws if the model isn't found in global scope
|
|
178
|
+
|
|
179
|
+
Subfolder = URL namespace, **any nesting depth** — ideal for grouping controllers by module/domain:
|
|
180
|
+
```
|
|
181
|
+
app/controllers/UserController.js → /user/...
|
|
182
|
+
app/controllers/api/ProductController.js → /api/product/...
|
|
183
|
+
app/controllers/admin/ReportController.js → /admin/report/...
|
|
184
|
+
app/controllers/api/config/VatTypesController.js → /api/config/vat-types/...
|
|
185
|
+
app/controllers/api/config/billing/InvoiceController.js → /api/config/billing/invoice/...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The loader walks the folder tree recursively (`processNode` in `controllers.js`): a key ending in
|
|
189
|
+
`Controller` is a controller file, anything else is a module/namespace folder that gets kebab-cased
|
|
190
|
+
and appended to the path, then recursion continues into it. Same `XxxController.js` filename at
|
|
191
|
+
different depths produces fully independent routes — e.g. `VatTypesController.js` can exist at the
|
|
192
|
+
root, under `api/`, and under `api/config/` at once, each resolving to its own namespace
|
|
193
|
+
(`/vat-types/`, `/api/vat-types/`, `/api/config/vat-types/`) with no collision.
|
|
194
|
+
|
|
195
|
+
**Multi-word controller names → kebab-case URL segment:**
|
|
196
|
+
The controller name (filename minus `Controller`) is converted with `toKebabCase()`
|
|
197
|
+
before becoming a URL segment — a hyphen is inserted at each PascalCase word boundary,
|
|
198
|
+
then the whole thing is lowercased. A single-word name is unaffected.
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
app/controllers/MyaccountController.js → /myaccount/...
|
|
202
|
+
app/controllers/MyAccountController.js → /my-account/...
|
|
203
|
+
app/controllers/inventory/MaterialReceptionsController.js → /inventory/material-receptions/...
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Route naming convention:**
|
|
207
|
+
```js
|
|
208
|
+
// app/controllers/UserController.js
|
|
209
|
+
module.exports = {
|
|
210
|
+
get(req, res) { ... }, // GET /user/
|
|
211
|
+
'get :id'(req, res) { ... }, // GET /user/:id
|
|
212
|
+
'post save'(req, res) { ... }, // POST /user/save
|
|
213
|
+
'delete :id'(req, res) { ... }, // DELETE /user/:id
|
|
214
|
+
'/absolute/path'(req, res) { ... }, // GET /absolute/path (no namespace)
|
|
215
|
+
'edit :id'(req, res) { ... } // GET /user/edit/:id (no method prefix → defaults to GET)
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> **Important:** Declare specific routes **before** parameterized routes (`:id`) in the object.
|
|
220
|
+
> Express matches in registration order — a wildcard declared first will shadow specific paths.
|
|
221
|
+
|
|
222
|
+
> **Method-prefix fallback:** the first space-separated token is only treated as the HTTP method
|
|
223
|
+
> if it's one of `get`, `post`, `put`, `patch`, `delete`. Otherwise the loader defaults to `GET` and
|
|
224
|
+
> joins the whole key with `/` to build the path — e.g. `'edit :id'` → `GET /user/edit/:id`. This
|
|
225
|
+
> lets you write multi-segment action names (`'edit :id'`, `'archive :id/:reason'`) without an
|
|
226
|
+
> explicit method prefix.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### 4. Responses → injected into `res`
|
|
231
|
+
|
|
232
|
+
**Loader:** `bootstrap/responses.js`
|
|
233
|
+
|
|
234
|
+
Scanned directories (merged, project files override core):
|
|
235
|
+
```
|
|
236
|
+
core/responses/*.js ← built-in response helpers
|
|
237
|
+
app/responses/*.js ← project-level custom responses
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Every exported function is attached to the Express `res` object. The method name is the
|
|
241
|
+
**filename without extension**:
|
|
242
|
+
|
|
243
|
+
| File | Available as |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `core/responses/vsr.js` | `res.vsr(promise, statusCode?)` |
|
|
246
|
+
| `app/responses/render.js` | `res.render(...)` *(example)* |
|
|
247
|
+
| `app/responses/ok.js` | `res.ok(data)` *(example)* |
|
|
248
|
+
|
|
249
|
+
**Available in any controller handler — no import needed:**
|
|
250
|
+
```js
|
|
251
|
+
get(req, res) {
|
|
252
|
+
res.vsr(User.getAll(req.query)); // standard VSR JSON response
|
|
253
|
+
res.vsr(User.create(req.body), 201); // with custom HTTP status
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Custom response example:
|
|
258
|
+
```js
|
|
259
|
+
// app/responses/paginated.js
|
|
260
|
+
module.exports = function paginated(data) {
|
|
261
|
+
const { res } = this.req;
|
|
262
|
+
res.status(200).json({ success: true, ...data });
|
|
263
|
+
};
|
|
264
|
+
// Usage: res.paginated({ items, total })
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## All globals available across the app
|
|
270
|
+
|
|
271
|
+
These are set automatically — never import them manually:
|
|
272
|
+
|
|
273
|
+
| Global | Set by | What it is |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| `app` | `app.js` | Config, server, routes, pkg |
|
|
276
|
+
| `app.config` | `app.js` | Full merged configuration |
|
|
277
|
+
| `app.vulkano` | `server.js` | Express app instance |
|
|
278
|
+
| `app.server` | `server.js` | Node.js HTTP server |
|
|
279
|
+
| `app.redisClient` | `server.js` | Redis client (if enabled) |
|
|
280
|
+
| `app.nunjucks` | `server.js` | Nunjucks environment instance |
|
|
281
|
+
| `app.socket` | `server.js` | Current Socket.io socket |
|
|
282
|
+
| `io` | `server.js` | Global Socket.io server instance |
|
|
283
|
+
| `mongoose` | `mongodb.js` | Mongoose instance |
|
|
284
|
+
| `Virtual` | `mongodb.js` | `'Virtual'` marker string for virtual fields |
|
|
285
|
+
| `Mixed` | `mongodb.js` | `mongoose.Schema.Types.Mixed` |
|
|
286
|
+
| `[ModelName]` | `mongodb.js` | Each compiled model (e.g. `User`, `Product`) |
|
|
287
|
+
| `Filter` | `services.js` | String filter library |
|
|
288
|
+
| `Paginate` | `services.js` | Pagination and search library |
|
|
289
|
+
| `VSError` | `services.js` | Standard error class |
|
|
290
|
+
| `Jwt` | `services.js` | JWT encode/decode library |
|
|
291
|
+
| `Encrypter` | `services.js` | AES-256-CBC encrypt/decrypt |
|
|
292
|
+
| `ApiClient` | `services.js` | Outbound HTTP client |
|
|
293
|
+
| `Crontab` | `services.js` | Cron job scheduler |
|
|
294
|
+
| `i18n` | `services.js` | i18next instance |
|
|
295
|
+
| `_` | `app.js` | Underscore.js |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Route system
|
|
300
|
+
|
|
301
|
+
### Convention-based auto-routing
|
|
302
|
+
|
|
303
|
+
`controllers/controllers.js` reads all `*Controller.js` files and generates routes from method names:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
// HomeController.js
|
|
307
|
+
module.exports = {
|
|
308
|
+
get(req, res) { ... }, // → GET /home/
|
|
309
|
+
'post save'(req, res) { ... }, // → POST /home/save
|
|
310
|
+
'get :id'(req, res) { ... }, // → GET /home/:id
|
|
311
|
+
'delete :id'(req, res) { ... }, // → DELETE /home/:id
|
|
312
|
+
'/absolute/path'(req, res) { ... } // → GET /absolute/path
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Parsing rules:**
|
|
317
|
+
- Key with space: `'METHOD action'` → `[method, pathSegment]`
|
|
318
|
+
- Key is only an HTTP method (`get`, `post`, `put`, `delete`) → controller root path
|
|
319
|
+
- Key starts with `/` → used as an absolute route, not namespaced
|
|
320
|
+
- Controller in subfolder (`api/UserController.js`) → path is `/{folder}/{controller}/{action}`
|
|
321
|
+
|
|
322
|
+
### Explicit routes (`app/config/routes.js`)
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
module.exports = {
|
|
326
|
+
'GET /': 'HomeController.get',
|
|
327
|
+
'POST /api/users': 'api.UserController.create',
|
|
328
|
+
'/about': 'HomeController.about', // defaults to GET
|
|
329
|
+
'/handler': (req, res) => res.send('ok') // inline function
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Known routing limitations
|
|
334
|
+
- Only supports: `get`, `post`, `put`, `patch`, `delete`. No `head`, `options`.
|
|
335
|
+
- Parsing uses `split(' ')` — fragile with extra whitespace (only the first two tokens are ever considered when the first token IS a valid method; the fallback-to-GET path joins all tokens with `/`).
|
|
336
|
+
- Does not validate that the controller/action exist before registering (fails at runtime with `console.error`).
|
|
337
|
+
- Route conflicts resolved by registration order (first registered wins).
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Models
|
|
342
|
+
|
|
343
|
+
### Definition
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
// app/models/User.js
|
|
347
|
+
module.exports = {
|
|
348
|
+
attributes: {
|
|
349
|
+
name: { type: String, required: true },
|
|
350
|
+
email: { type: String },
|
|
351
|
+
age: { type: Number },
|
|
352
|
+
// Virtual field — computed, not stored in MongoDB
|
|
353
|
+
fullLabel: { type: 'Virtual', get() { return this.name; } }
|
|
354
|
+
},
|
|
355
|
+
indexes: [{ email: 1 }],
|
|
356
|
+
plugins: [],
|
|
357
|
+
beforeSave(next) { next(); },
|
|
358
|
+
afterSave() {}
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Auto-added fields** (always present, no need to declare):
|
|
363
|
+
- `active: Boolean` (default: `true`) — soft-delete flag
|
|
364
|
+
- `createdAt: Date` (default: `Date.now`)
|
|
365
|
+
- `updatedAt: Date`
|
|
366
|
+
|
|
367
|
+
**Auto-trim:** All non-Boolean attributes get `trim: true` by default. Disable with `{ type: String, trim: false }`.
|
|
368
|
+
|
|
369
|
+
### Scaffold methods available on every model
|
|
370
|
+
|
|
371
|
+
> **Model scaffold vs ScaffoldController are two different things.**
|
|
372
|
+
> Model scaffold provides database-level CRUD methods on the model class.
|
|
373
|
+
> ScaffoldController generates HTTP routes on a controller. They are independent.
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
Model.getAll(props) // paginated list
|
|
377
|
+
Model.getByField(value, field?) // by _id or custom field
|
|
378
|
+
Model.create(data) // insert new record
|
|
379
|
+
Model.update(id, data) // merge-update existing record
|
|
380
|
+
Model.delete(id) // soft delete — sets active: false (no hard delete)
|
|
381
|
+
Model.createSubdoc(key, parentId, data) // push to subdocument array
|
|
382
|
+
Model.updateSubdoc(key, parentId, subdocId, data)
|
|
383
|
+
Model.removeSubdoc(key, parentId, subdocId)
|
|
384
|
+
Model.deleteSubdoc(...) // alias for removeSubdoc
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Auto-generated aliases per model (e.g. for `User`):
|
|
388
|
+
```js
|
|
389
|
+
User.getAllUser(props) // → User.getAll(props)
|
|
390
|
+
User.getUser(id) // → User.getByField(id)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Overriding scaffold methods
|
|
394
|
+
|
|
395
|
+
Any method defined in the model file overrides the scaffold default:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
// app/models/Product.js
|
|
399
|
+
module.exports = {
|
|
400
|
+
attributes: { ... },
|
|
401
|
+
|
|
402
|
+
// Override getAll to enable search and custom sort
|
|
403
|
+
getAll(props) {
|
|
404
|
+
const defaultProps = {
|
|
405
|
+
sort: 'name|ASC',
|
|
406
|
+
searchBy: ['name', 'sku'], // fields to search on
|
|
407
|
+
filter: { active: true }
|
|
408
|
+
};
|
|
409
|
+
const query = Paginate.serializeQuery(defaultProps, props);
|
|
410
|
+
return Paginate.get(this, query);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
> **Note:** `searchBy` is `[]` in the scaffold default, so text search via `?search=` query param
|
|
416
|
+
> does nothing unless you override `getAll` and configure `searchBy`.
|
|
417
|
+
|
|
418
|
+
### Auto-populate (relations)
|
|
419
|
+
|
|
420
|
+
`Model.getAll(props)` and `Model.getByField(value, field, props)` — the scaffold defaults — expand
|
|
421
|
+
`ref` relations on request via `?populate=`, using two helpers from `database/scaffold.js`:
|
|
422
|
+
`this._getSanitizedPopulate(props)` and `this._buildPopulate(props, extra?)`.
|
|
423
|
+
|
|
424
|
+
**Security gate — a relation is never populated by default.** A ref field only becomes reachable
|
|
425
|
+
through `?populate=` when its own attribute definition opts in:
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
// app/models/Product.js
|
|
429
|
+
module.exports = {
|
|
430
|
+
attributes: {
|
|
431
|
+
name: { type: String, required: true },
|
|
432
|
+
supplier: {
|
|
433
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
434
|
+
ref: 'Supplier',
|
|
435
|
+
autopopulate: true // ← without this, ?populate=supplier is silently ignored
|
|
436
|
+
},
|
|
437
|
+
category: {
|
|
438
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
439
|
+
ref: 'Category',
|
|
440
|
+
autopopulate: true
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// getAll/getByField are inherited from the scaffold as-is — no override needed
|
|
444
|
+
};
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Add `autopopulate: true` to every `ref` field you want populatable; a model with several relations
|
|
448
|
+
(supplier, category, unit of measure, ...) needs no separate list to maintain — it's declared right
|
|
449
|
+
on the field. A `ref` field left without it stays a raw id no matter what the caller requests.
|
|
450
|
+
|
|
451
|
+
**One-off override:** a model that overrides `getAll`/`getByField` can allow a relation for that
|
|
452
|
+
call only, without adding `autopopulate: true` to the schema, via the second argument:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
const populate = this._buildPopulate(props, ['supplier']);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Query syntax** (`?populate=...`):
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
?populate=supplier → full Supplier doc
|
|
462
|
+
?populate=supplier,category → both, full docs
|
|
463
|
+
?populate=supplier:name → only { _id, name } (Mongoose always keeps _id)
|
|
464
|
+
?populate=supplier:name|address → several fields, pipe-separated
|
|
465
|
+
?populate=supplier:name,category → per-relation fields only affect that relation
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Relation names and field lists are trimmed and lower-cased; unknown/misspelled relation names are
|
|
469
|
+
silently ignored (no error, no populate). The field-select syntax lives entirely inside the
|
|
470
|
+
`populate` value — deliberately not a separate `?supplier=name` param, since that would collide
|
|
471
|
+
with `supplier` used as an actual filter/query param on the same route.
|
|
472
|
+
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
## ScaffoldController
|
|
476
|
+
|
|
477
|
+
> **ScaffoldController vs model scaffold are two different things.**
|
|
478
|
+
> ScaffoldController generates HTTP routes on a controller.
|
|
479
|
+
> Model scaffold provides database-level CRUD methods on the model class. They are independent.
|
|
480
|
+
|
|
481
|
+
When a controller sets `scaffold` to a model name, the framework automatically generates all 5 HTTP routes:
|
|
482
|
+
|
|
483
|
+
```js
|
|
484
|
+
// app/controllers/api/ProductController.js
|
|
485
|
+
module.exports = {
|
|
486
|
+
scaffold: 'Product', // must exist as global.Product — throws at startup otherwise
|
|
487
|
+
allowedMethods: ['get', 'post', 'put'] // optional: restrict to these methods only
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
> The older `scaffold: true` + separate `model: 'Product'` form is still supported and behaves
|
|
492
|
+
> identically — `scaffold: 'Product'` is just the shorter, recommended way to write it.
|
|
493
|
+
|
|
494
|
+
Generates las siguientes rutas, cada una delegando al método correspondiente del modelo:
|
|
495
|
+
|
|
496
|
+
| Ruta | Método del modelo | Status |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| `GET /api/product/` | `Product.getAll(req.query)` | 200 |
|
|
499
|
+
| `GET /api/product/:id` | `Product.getByField(req.params.id)`| 200 |
|
|
500
|
+
| `POST /api/product/` | `Product.create(req.body)` | 201 |
|
|
501
|
+
| `PUT /api/product/:id` | `Product.update(id, req.body)` | 202 |
|
|
502
|
+
| `DELETE /api/product/:id` | `Product.delete(id)` — soft delete | 204 |
|
|
503
|
+
|
|
504
|
+
> **Important:** Scaffold endpoints have no authentication middleware by default.
|
|
505
|
+
> Protect them via JWT config or custom middleware.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## VSR — Vulkano Standard Response
|
|
510
|
+
|
|
511
|
+
`res.vsr(promise, statusCode?)` is the standard way to respond in all controllers.
|
|
512
|
+
|
|
513
|
+
```js
|
|
514
|
+
get(req, res) {
|
|
515
|
+
res.vsr(User.getAll(req.query)); // 200
|
|
516
|
+
res.vsr(User.create(req.body), 201); // 201
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- Expects a **Promise** (returns 500 with descriptive error if not)
|
|
521
|
+
- On `.then()`: responds `{ success: true, statusCode, data: result }`
|
|
522
|
+
- On `.catch()`: responds `{ success: false, statusCode, error: { detail, errorCode, errorName } }`
|
|
523
|
+
- `.finally()`: always calls `res.status(code).jsonp(output)`
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## VSError
|
|
528
|
+
|
|
529
|
+
Available globally — no import needed:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
VSError.reject('Not allowed', 403) // Promise.reject with VSError
|
|
533
|
+
VSError.notFound('User') // Promise.reject with 404
|
|
534
|
+
new VSError('message', 500, props) // direct instantiation
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Paginate
|
|
540
|
+
|
|
541
|
+
Available globally — no import needed:
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
Paginate.get(Model, query, populate?)
|
|
545
|
+
Paginate.serializeQuery(defaultProps, requestQuery)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Supported query params:** `page`, `per_page`, `search`, `searchType` (contains/startwith/endwith), `sort` (e.g. `createdAt|DESC`), `fields`
|
|
549
|
+
|
|
550
|
+
**Response shape from `_set()`:**
|
|
551
|
+
```js
|
|
552
|
+
{ items, cursor, page, perPage, next, prev, totalPages, totalItems }
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
- `next` is `false` when `(page * perPage) >= totalItems`
|
|
556
|
+
- `prev` is `false` on page 1, or when current page exceeds `totalPages`
|
|
557
|
+
- `cursor` is the index of the first item on the current page (1-based)
|
|
558
|
+
- `page=all` skips pagination and returns a plain array via `Model.find()`
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## JWT (`Jwt.js`)
|
|
563
|
+
|
|
564
|
+
Available globally — no import needed:
|
|
565
|
+
|
|
566
|
+
```js
|
|
567
|
+
Jwt.encode(data) // AES-encrypts payload, then encodes as JWT
|
|
568
|
+
Jwt.decode(token) // decodes and validates expiration
|
|
569
|
+
Jwt.getToken(req) // extracts token from header / cookie / query param
|
|
570
|
+
Jwt.socket(socket) // extracts token from Socket.io handshake
|
|
571
|
+
Jwt.init(opts) // returns an express-jwt middleware instance
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Tokens require an `expiration` field. Tokens without it are rejected unless `config.jwt.expiration === false`.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Configuration hierarchy (merge order)
|
|
579
|
+
|
|
580
|
+
```
|
|
581
|
+
app/config/*.js ← general config
|
|
582
|
+
app/config/env/{NODE_ENV}/*.js ← environment overrides
|
|
583
|
+
app/config/local.js ← local overrides (gitignored)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
All sources are deep-merged with `deepmerge`. Final result is in `app.config`.
|
|
587
|
+
|
|
588
|
+
**Key config files:**
|
|
589
|
+
- `settings.js` — port, database connection, paths
|
|
590
|
+
- `express/cors.js`, `express/jwt.js`, `express/csp.js`, `express/cookies.js`
|
|
591
|
+
- `routes.js` — explicit route mappings
|
|
592
|
+
- `bootstrap.js` — startup hook (**required**)
|
|
593
|
+
- `sockets/` — Socket.io config and adapters
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Sockets (Socket.io)
|
|
598
|
+
|
|
599
|
+
Enable with `config.sockets.enabled: true`. Adapters: `memory` (default), `redis`, `mongodb`.
|
|
600
|
+
|
|
601
|
+
```js
|
|
602
|
+
// app/config/sockets/events.js
|
|
603
|
+
module.exports = {
|
|
604
|
+
'message': 'ChatController.message',
|
|
605
|
+
'join': (socket, body, callback) => { ... }
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Handler signature: `({ socket, body }, callback)`.
|
|
610
|
+
|
|
611
|
+
Available globally as `io` and `app.socket`.
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Known issues / tech debt
|
|
616
|
+
|
|
617
|
+
- **`services.js`** — All libs/services are injected into `global`. Makes unit testing hard without mocking globals.
|
|
618
|
+
- **`bluebird`** — Still imported in a few places. Not needed in Node 18+ where `Promise` is native.
|
|
619
|
+
- **`ApiClient`** — `rejectUnauthorized: false` disables SSL verification by default for all outbound requests.
|
|
620
|
+
- **`Crontab`** — Default timezone is `America/New_York` instead of UTC.
|
|
621
|
+
- **`path` and `fs` npm packages** — These are Node.js built-ins and should not be in `package.json` dependencies.
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Project conventions
|
|
626
|
+
|
|
627
|
+
- Controller files: `{Name}Controller.js` (PascalCase)
|
|
628
|
+
- Model files: `{Name}.js` (PascalCase) — becomes `global[Name]`
|
|
629
|
+
- Service/lib files: `{Name}.js` (PascalCase) — becomes `global[Name]`
|
|
630
|
+
- Response files: `{name}.js` (camelCase) — becomes `res.name()`
|
|
631
|
+
- Config: camelCase, one concern per file, organized in subfolders
|
|
632
|
+
- All API responses go through `res.vsr(promise)`
|
|
633
|
+
- Deletion is always soft (`active: false`); hard delete is not available by default — **this applies only to models**, not to controllers directly
|
|
634
|
+
- No TypeScript; tests live in `test/integration/` and run with Jest
|
|
635
|
+
|
|
636
|
+
## Tests
|
|
637
|
+
|
|
638
|
+
Run with:
|
|
639
|
+
```bash
|
|
640
|
+
npm test # run all integration tests
|
|
641
|
+
npm run test:watch # watch mode
|
|
642
|
+
npm run test:coverage
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Requires `core/.env.test` with `TEST_DB_URI`, `TEST_PORT`, and `JWT_SECRET_KEY` (used to sign the test JWTs for socket auth). The test suite:
|
|
646
|
+
- Starts a full Vulkano fixture server as a child process
|
|
647
|
+
- Drops and rebuilds the test database on every run
|
|
648
|
+
- Covers: VSR response format, routing (params + query strings), scaffold CRUD, pagination, model validation, ReDoS protection, file uploads, sockets (handshake auth + event routing)
|
package/README.md
CHANGED
|
@@ -112,6 +112,23 @@ GET /users/edit/1
|
|
|
112
112
|
| `POST` | `/users/save` | `UsersController['post save']` |
|
|
113
113
|
| `GET` | `/users/42/info` | `UsersController['get :id/info']` |
|
|
114
114
|
|
|
115
|
+
### Nested module folders — group controllers by domain
|
|
116
|
+
|
|
117
|
+
Controllers can live in subfolders, at **any nesting depth**, to group them by module. Each folder
|
|
118
|
+
segment becomes a URL namespace, in order, before the controller's own resource segment:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
controllers/UsersController.js → /users/...
|
|
122
|
+
controllers/api/ProductsController.js → /api/products/...
|
|
123
|
+
controllers/api/config/VatTypesController.js → /api/config/vat-types/...
|
|
124
|
+
controllers/api/config/billing/InvoiceController.js → /api/config/billing/invoice/...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The same controller filename can exist at several depths at once — `VatTypesController.js` at the
|
|
128
|
+
root, under `api/`, and under `api/config/` resolve to `/vat-types/`, `/api/vat-types/`, and
|
|
129
|
+
`/api/config/vat-types/` respectively, fully independent of each other. No config needed; it's
|
|
130
|
+
purely the folder path on disk.
|
|
131
|
+
|
|
115
132
|
### Method key convention: `'<verb>? <path tail>'`
|
|
116
133
|
|
|
117
134
|
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**.
|
|
@@ -19,20 +19,21 @@ function toKebabCase(str) {
|
|
|
19
19
|
.toLowerCase();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
|
23
|
+
|
|
22
24
|
module.exports = function loadControllersApplication() {
|
|
23
25
|
|
|
24
26
|
const routes = {};
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const current = AllControllers[controller];
|
|
28
|
+
// Registers all routes found in a single controller definition object,
|
|
29
|
+
// namespaced under the given module path segments (possibly empty).
|
|
30
|
+
function processController(controllerFileName, current, modulePathSegments) {
|
|
30
31
|
|
|
31
32
|
const {
|
|
32
33
|
scaffold,
|
|
33
34
|
allowedMethods,
|
|
34
35
|
model
|
|
35
|
-
} = current;
|
|
36
|
+
} = current || {};
|
|
36
37
|
|
|
37
38
|
// `scaffold` can be `true` + a separate `model` field, or the model
|
|
38
39
|
// name given directly as the `scaffold` string (no `model` needed).
|
|
@@ -41,7 +42,7 @@ module.exports = function loadControllersApplication() {
|
|
|
41
42
|
if (scaffold && scaffoldModel) {
|
|
42
43
|
|
|
43
44
|
if (!global[scaffoldModel]) {
|
|
44
|
-
throw new Error(`Scaffold model "${scaffoldModel}" not found in global scope for controller "${
|
|
45
|
+
throw new Error(`Scaffold model "${scaffoldModel}" not found in global scope for controller "${controllerFileName}". Make sure the model exists in app/models.`);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
const scaffoldingCurrent = scaffoldController(scaffoldModel, allowedMethods);
|
|
@@ -56,131 +57,74 @@ module.exports = function loadControllersApplication() {
|
|
|
56
57
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let parts = [];
|
|
62
|
-
let method = 'get';
|
|
63
|
-
let pathToRun = '';
|
|
64
|
-
let moduleName = '';
|
|
60
|
+
const controllerName = toKebabCase(controllerFileName.replace('Controller', ''));
|
|
61
|
+
const namespace = modulePathSegments.join('/');
|
|
65
62
|
|
|
66
63
|
Object.keys(current || []).forEach( (route) => {
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
moduleName = controllerName;
|
|
72
|
-
const submodules = AllControllers[moduleName];
|
|
73
|
-
|
|
74
|
-
Object.keys(submodules || []).forEach( (subcontroller) => {
|
|
75
|
-
|
|
76
|
-
controllerName = toKebabCase(subcontroller.replace('Controller', ''));
|
|
77
|
-
const subcurrent = submodules[subcontroller];
|
|
78
|
-
|
|
79
|
-
const {
|
|
80
|
-
scaffold: subcurrentScaffold,
|
|
81
|
-
allowedMethods: subAllowedMethods,
|
|
82
|
-
model: subcurrentModel
|
|
83
|
-
} = subcurrent || {};
|
|
84
|
-
|
|
85
|
-
const subScaffoldModel = typeof subcurrentScaffold === 'string' ? subcurrentScaffold : subcurrentModel;
|
|
65
|
+
let method = 'get';
|
|
66
|
+
let pathToRun = '';
|
|
86
67
|
|
|
87
|
-
|
|
68
|
+
const parts = route.split(' ');
|
|
69
|
+
const [tmpMethod, tmpPath] = parts;
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
71
|
+
if (tmpPath) {
|
|
72
|
+
if (methods.indexOf(tmpMethod.toLowerCase()) >= 0) {
|
|
73
|
+
method = tmpMethod.toLowerCase();
|
|
74
|
+
pathToRun = tmpPath;
|
|
75
|
+
} else {
|
|
76
|
+
// First token isn't a real HTTP method — default to GET,
|
|
77
|
+
// treating the whole key as the path (e.g. 'edit :id' → GET .../edit/:id)
|
|
78
|
+
method = 'get';
|
|
79
|
+
pathToRun = parts.join('/');
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
pathToRun = tmpMethod;
|
|
83
|
+
}
|
|
102
84
|
|
|
103
|
-
|
|
85
|
+
const isAbsolute = (pathToRun.substring(0, 1) === '/') ? true : false;
|
|
104
86
|
|
|
105
|
-
|
|
87
|
+
if (!isAbsolute) {
|
|
106
88
|
|
|
107
|
-
|
|
89
|
+
const base = namespace ? `/${namespace}/${controllerName}/` : `/${controllerName}/`;
|
|
108
90
|
|
|
109
|
-
|
|
91
|
+
if (methods.indexOf(pathToRun.toLowerCase()) >= 0) {
|
|
92
|
+
method = pathToRun.toLowerCase();
|
|
93
|
+
pathToRun = base;
|
|
94
|
+
} else {
|
|
95
|
+
pathToRun = `${base}${pathToRun.replace(/GET|POST|DELETE|PUT|PATCH/i, '')}`;
|
|
96
|
+
}
|
|
110
97
|
|
|
111
|
-
|
|
112
|
-
if (methods.indexOf(tmpMethod.toLowerCase()) >= 0) {
|
|
113
|
-
method = tmpMethod.toLowerCase();
|
|
114
|
-
pathToRun = tmpPath;
|
|
115
|
-
} else {
|
|
116
|
-
// First token isn't a real HTTP method — default to GET,
|
|
117
|
-
// treating the whole key as the path (e.g. 'edit :id' → GET .../edit/:id)
|
|
118
|
-
method = 'get';
|
|
119
|
-
pathToRun = parts.join('/');
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
pathToRun = tmpMethod;
|
|
123
|
-
}
|
|
98
|
+
}
|
|
124
99
|
|
|
125
|
-
|
|
100
|
+
if (typeof current[route] === 'function') {
|
|
101
|
+
routes[`${method} ${pathToRun}`] = current[route];
|
|
102
|
+
}
|
|
126
103
|
|
|
127
|
-
|
|
104
|
+
});
|
|
128
105
|
|
|
129
|
-
|
|
130
|
-
method = pathToRun.toLowerCase();
|
|
131
|
-
pathToRun = `/${moduleName}/${controllerName}/`;
|
|
132
|
-
} else {
|
|
133
|
-
pathToRun = `/${moduleName}/${controllerName}/${pathToRun.replace(/GET|POST|DELETE|PUT|PATCH/i, '')}`;
|
|
134
|
-
}
|
|
106
|
+
}
|
|
135
107
|
|
|
136
|
-
|
|
108
|
+
// Walks the (possibly nested) AllControllers tree. A key ending in
|
|
109
|
+
// "Controller" is a controller file; any other key is a module/namespace
|
|
110
|
+
// folder whose value is another node to recurse into.
|
|
111
|
+
function processNode(node, modulePathSegments) {
|
|
137
112
|
|
|
138
|
-
|
|
139
|
-
routes[`${method} ${pathToRun}`] = subcurrent[subroute];
|
|
140
|
-
}
|
|
113
|
+
Object.keys(node || []).forEach( (key) => {
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
});
|
|
115
|
+
const value = node[key];
|
|
144
116
|
|
|
117
|
+
if (/Controller$/.test(key)) {
|
|
118
|
+
processController(key, value, modulePathSegments);
|
|
145
119
|
} else {
|
|
146
|
-
|
|
147
|
-
parts = route.split(' ');
|
|
148
|
-
const [tmpMethod, tmpPath] = parts;
|
|
149
|
-
|
|
150
|
-
if (tmpPath) {
|
|
151
|
-
if (methods.indexOf(tmpMethod.toLowerCase()) >= 0) {
|
|
152
|
-
method = tmpMethod.toLowerCase();
|
|
153
|
-
pathToRun = tmpPath;
|
|
154
|
-
} else {
|
|
155
|
-
// First token isn't a real HTTP method — default to GET,
|
|
156
|
-
// treating the whole key as the path (e.g. 'edit :id' → GET .../edit/:id)
|
|
157
|
-
method = 'get';
|
|
158
|
-
pathToRun = parts.join('/');
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
pathToRun = tmpMethod;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const isAbsolute = (pathToRun.substring(0, 1) === '/') ? true : false;
|
|
165
|
-
|
|
166
|
-
if (!isAbsolute) {
|
|
167
|
-
if (methods.indexOf(pathToRun.toLowerCase()) >= 0) {
|
|
168
|
-
method = pathToRun.toLowerCase();
|
|
169
|
-
pathToRun = `/${controllerName}/`;
|
|
170
|
-
} else {
|
|
171
|
-
pathToRun = `/${controllerName}/${pathToRun.replace(/GET|POST|DELETE|PUT|PATCH/i, '')}`;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (typeof current[route] === 'function') {
|
|
176
|
-
routes[`${method} ${pathToRun}`] = current[route];
|
|
177
|
-
}
|
|
178
|
-
|
|
120
|
+
processNode(value, [...modulePathSegments, toKebabCase(key)]);
|
|
179
121
|
}
|
|
180
122
|
|
|
181
123
|
});
|
|
182
124
|
|
|
183
|
-
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
processNode(AllControllers, []);
|
|
184
128
|
|
|
185
129
|
return routes;
|
|
186
130
|
|