langaro-api 1.2.3 → 1.2.4
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/bin/langaro-api.js +12 -2
- package/lib/cli/documentation-templates/01-architecture-overview.md +240 -0
- package/lib/cli/documentation-templates/02-crud-layer.md +504 -0
- package/lib/cli/documentation-templates/03-models.md +362 -0
- package/lib/cli/documentation-templates/04-services.md +355 -0
- package/lib/cli/documentation-templates/05-controllers.md +395 -0
- package/lib/cli/documentation-templates/06-routes.md +268 -0
- package/lib/cli/documentation-templates/07-jobs.md +361 -0
- package/lib/cli/documentation-templates/08-tasks.md +265 -0
- package/lib/cli/documentation-templates/09-middlewares.md +238 -0
- package/lib/cli/documentation-templates/10-integrations.md +332 -0
- package/lib/cli/documentation-templates/11-config-and-bootstrap.md +352 -0
- package/lib/cli/documentation-templates/12-queues.md +205 -0
- package/lib/cli/documentation-templates/13-utils.md +281 -0
- package/lib/cli/documentation-templates/14-testing.md +315 -0
- package/lib/cli/documentation-templates/15-cli-and-scaffolding.md +344 -0
- package/lib/cli/documentation-templates/SUMMARY.md +116 -0
- package/lib/cli/init.js +30 -2
- package/package.json +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# Controllers
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
|
|
5
|
+
Controllers are **request handlers**. They receive HTTP requests, validate input, delegate business logic to services, trigger background jobs, and return responses.
|
|
6
|
+
|
|
7
|
+
**What controllers do:**
|
|
8
|
+
- Extract and validate data from `req.body`, `req.query`, `req.params`
|
|
9
|
+
- Call service methods for business logic
|
|
10
|
+
- Trigger background jobs via `this.Queue`
|
|
11
|
+
- Emit real-time events via `this.io`
|
|
12
|
+
- Return JSON responses
|
|
13
|
+
|
|
14
|
+
**What controllers do NOT do:**
|
|
15
|
+
- Contain business logic (that belongs in services)
|
|
16
|
+
- Execute database queries directly (use services/CRUD methods)
|
|
17
|
+
- Define routes (that's in routers)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Location & Naming
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/controllers/
|
|
25
|
+
├── index.js # loadControllers loader (do not modify)
|
|
26
|
+
├── users/
|
|
27
|
+
│ └── users.controller.js # Controller for 'users'
|
|
28
|
+
├── auth/
|
|
29
|
+
│ └── auth.controller.js # Controller for 'auth' (no DB table)
|
|
30
|
+
├── products_invoices/
|
|
31
|
+
│ └── products_invoices.controller.js # Controller for 'products_invoices'
|
|
32
|
+
└── ...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Naming convention:** `{name}/{name}.controller.js` (subdirectory pattern, preferred)
|
|
36
|
+
|
|
37
|
+
The loader also supports flat files: `{name}.controller.js` (without subdirectory).
|
|
38
|
+
|
|
39
|
+
**Controller instance key:** `PascalCase(name) + 'Controller'`
|
|
40
|
+
- `users.controller.js` → `UsersController`
|
|
41
|
+
- `products_invoices.controller.js` → `ProductsInvoicesController`
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Standard Structure
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
/** @param {typeof UsersControllerBase} ServicesClass @generated-types */
|
|
49
|
+
module.exports = (ServicesClass) => class extends ServicesClass {
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GET / — List all records
|
|
53
|
+
*/
|
|
54
|
+
async getAll(req, res) {
|
|
55
|
+
const {
|
|
56
|
+
perPage, currentPage, search, searchExactMatch, searchFields,
|
|
57
|
+
sortBy, sort, append,
|
|
58
|
+
} = req.query;
|
|
59
|
+
|
|
60
|
+
const data = await this.service.get({
|
|
61
|
+
perPage,
|
|
62
|
+
currentPage,
|
|
63
|
+
search,
|
|
64
|
+
searchExactMatch,
|
|
65
|
+
searchFields,
|
|
66
|
+
sortBy,
|
|
67
|
+
sort,
|
|
68
|
+
append: append ? append.split(',') : undefined,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
res.status(200).json(data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* GET /:id — Get single record
|
|
76
|
+
*/
|
|
77
|
+
async getById(req, res) {
|
|
78
|
+
const { id } = req.params;
|
|
79
|
+
|
|
80
|
+
const { data: item } = await this.service.getWhere('id', id, {
|
|
81
|
+
firstOnly: true,
|
|
82
|
+
append: ['related_table'],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!item) {
|
|
86
|
+
ApiError(StatusCodes.NOT_FOUND, 'Item not found');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
res.status(200).json({ success: true, data: item });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* POST / — Create new record
|
|
94
|
+
*/
|
|
95
|
+
async create(req, res) {
|
|
96
|
+
const data = req.body;
|
|
97
|
+
validateRequiredFields(data, ['name', 'email']);
|
|
98
|
+
|
|
99
|
+
const result = await this.service.create({
|
|
100
|
+
...data,
|
|
101
|
+
company_id: req.company_id,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
res.status(201).json(result);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* PUT /:id — Update record
|
|
109
|
+
*/
|
|
110
|
+
async update(req, res) {
|
|
111
|
+
const { id } = req.params;
|
|
112
|
+
const data = req.body;
|
|
113
|
+
|
|
114
|
+
const result = await this.service.updateWhere('id', id, data, {
|
|
115
|
+
andWhere: [['company_id', req.company_id]],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
res.status(200).json(result);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* DELETE /:id — Delete record
|
|
123
|
+
*/
|
|
124
|
+
async remove(req, res) {
|
|
125
|
+
const { id } = req.params;
|
|
126
|
+
|
|
127
|
+
await this.service.deleteWhere('id', id, {
|
|
128
|
+
andWhere: [['company_id', req.company_id]],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
res.status(200).json({ success: true });
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Available Properties via ServicesClass
|
|
139
|
+
|
|
140
|
+
Every controller method has access to these via `this`:
|
|
141
|
+
|
|
142
|
+
| Property | Type | Description |
|
|
143
|
+
|----------|------|-------------|
|
|
144
|
+
| `this.service` | Service instance | The service matching this controller's table. Has all CRUD methods + custom service methods. |
|
|
145
|
+
| `this.services` | ServicesMap | Map of ALL service instances. Access via `this.services.UsersServices`, `this.services.CompaniesServices`, etc. |
|
|
146
|
+
| `this.Queue` | `{ add(name, data, options) }` | BullMQ queue interface for adding background jobs. |
|
|
147
|
+
| `this.io` | Socket.io Server | Socket.io instance for real-time events. |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Request Properties (set by auth middleware)
|
|
152
|
+
|
|
153
|
+
After the auth middleware runs, these are available on `req`:
|
|
154
|
+
|
|
155
|
+
| Property | Description |
|
|
156
|
+
|----------|-------------|
|
|
157
|
+
| `req.user_id` | Authenticated user's ID |
|
|
158
|
+
| `req.email` | User's email |
|
|
159
|
+
| `req.role` | User's role (`'user'` or `'admin'`) |
|
|
160
|
+
| `req.company_id` | Company ID from headers (if `companyIdRequired`) |
|
|
161
|
+
| `req.company` | Full company object (in some projects) |
|
|
162
|
+
| `req.user_permissions` | Array of `{ subject, action }` permission objects |
|
|
163
|
+
| `req.token` | Raw JWT token string |
|
|
164
|
+
| `req.files` | Uploaded files (multer) |
|
|
165
|
+
| `req.rawBody` | Raw request body (for webhook signature verification) |
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Input Validation Pattern
|
|
170
|
+
|
|
171
|
+
Controllers validate input before passing to services:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
const { validateRequiredFields } = require('@/utils');
|
|
175
|
+
const { validateFieldsFormat } = require('@/utils');
|
|
176
|
+
const { ApiError, StatusCodes } = require('@/utils');
|
|
177
|
+
|
|
178
|
+
async create(req, res) {
|
|
179
|
+
const data = req.body;
|
|
180
|
+
|
|
181
|
+
// 1. Required fields
|
|
182
|
+
validateRequiredFields(data, ['name', 'email', 'company_id']);
|
|
183
|
+
|
|
184
|
+
// 2. Field format validation
|
|
185
|
+
validateFieldsFormat([
|
|
186
|
+
['email', data.email, 'email field'],
|
|
187
|
+
['phone', data.phone, 'phone field'],
|
|
188
|
+
['array', data.items, 'items field'],
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
// 3. Business validation
|
|
192
|
+
if (data.amount < 0) {
|
|
193
|
+
ApiError(StatusCodes.BAD_REQUEST, 'Amount cannot be negative');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. Delegate to service
|
|
197
|
+
const result = await this.service.create(data);
|
|
198
|
+
res.status(201).json(result);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Queue Integration
|
|
205
|
+
|
|
206
|
+
Trigger background jobs from controllers:
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
async createInvoice(req, res) {
|
|
210
|
+
const invoice = await this.service.create(req.body);
|
|
211
|
+
|
|
212
|
+
// Fire-and-forget job
|
|
213
|
+
await this.Queue.add('send-mail', {
|
|
214
|
+
templateName: 'invoice-created',
|
|
215
|
+
recipientsList: [{ Email: req.email, TemplateModel: { invoiceId: invoice.data.id } }],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Job with delay
|
|
219
|
+
await this.Queue.add('process-invoice', {
|
|
220
|
+
invoiceId: invoice.data.id,
|
|
221
|
+
}, { delay: 5000 }); // Wait 5 seconds
|
|
222
|
+
|
|
223
|
+
// Job with group (per-company isolation)
|
|
224
|
+
await this.Queue.add('sync-data', {
|
|
225
|
+
companyId: req.company_id,
|
|
226
|
+
}, { groupId: req.company_id });
|
|
227
|
+
|
|
228
|
+
res.status(201).json(invoice);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Socket.io Integration
|
|
235
|
+
|
|
236
|
+
Emit real-time events from controllers:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
async updateStatus(req, res) {
|
|
240
|
+
const { id } = req.params;
|
|
241
|
+
const { status } = req.body;
|
|
242
|
+
|
|
243
|
+
const result = await this.service.updateWhere('id', id, { status });
|
|
244
|
+
|
|
245
|
+
// Notify the specific user
|
|
246
|
+
this.io.sockets.in(req.user_id).emit('item:updated', { id, status });
|
|
247
|
+
|
|
248
|
+
// Notify all users in the company
|
|
249
|
+
this.io.sockets.in(req.company_id).emit('item:updated', { id, status });
|
|
250
|
+
|
|
251
|
+
res.status(200).json(result);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Controllers Without DB Tables
|
|
258
|
+
|
|
259
|
+
Valid pattern for orchestration endpoints (auth, checkout, webhooks):
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
// src/controllers/auth/auth.controller.js
|
|
263
|
+
module.exports = (ServicesClass) => class extends ServicesClass {
|
|
264
|
+
// this.service will be a pseudo-service (no CRUD methods)
|
|
265
|
+
// Use this.services to access other services
|
|
266
|
+
|
|
267
|
+
async login(req, res) {
|
|
268
|
+
const { email, password } = req.body;
|
|
269
|
+
const result = await this.services.UsersServices.authenticate(email, password);
|
|
270
|
+
res.status(200).json(result);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The loader creates a minimal `ServicesClass` with `this.services`, `this.Queue`, and `this.io` even when there's no matching database table.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## List Endpoint with Filters Pattern
|
|
280
|
+
|
|
281
|
+
The most common controller pattern for list endpoints:
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
async getAll(req, res) {
|
|
285
|
+
const {
|
|
286
|
+
perPage, currentPage, search, searchExactMatch, searchFields,
|
|
287
|
+
sortBy, sort, append,
|
|
288
|
+
} = req.query;
|
|
289
|
+
|
|
290
|
+
// Parse and validate filter parameters from query string
|
|
291
|
+
const andWhereFilters = parseAndValidateFilters(
|
|
292
|
+
req.query,
|
|
293
|
+
this.service.fields.filterOptions // From model config
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const data = await this.service.get({
|
|
297
|
+
perPage,
|
|
298
|
+
currentPage,
|
|
299
|
+
search,
|
|
300
|
+
searchExactMatch,
|
|
301
|
+
searchFields,
|
|
302
|
+
sortBy,
|
|
303
|
+
sort,
|
|
304
|
+
append: append ? append.split(',') : undefined,
|
|
305
|
+
andWhere: [
|
|
306
|
+
['company_id', req.company_id], // Always scope by company
|
|
307
|
+
...andWhereFilters, // Dynamic filters
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
res.status(200).json(data);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## File Upload Pattern
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
async uploadFile(req, res) {
|
|
321
|
+
const file = req.files?.[0];
|
|
322
|
+
|
|
323
|
+
if (!file) {
|
|
324
|
+
ApiError(StatusCodes.BAD_REQUEST, 'File is required');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Validate file
|
|
328
|
+
validateFiles(file, {
|
|
329
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
330
|
+
allowedTypes: ['image/jpeg', 'image/png'],
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Upload to S3
|
|
334
|
+
const aws = require('@/integrations/aws');
|
|
335
|
+
const location = await aws.uploadToS3(process.env.S3_BUCKET_DOCUMENTS, {
|
|
336
|
+
filename: `${req.user_id}/${Date.now()}-${file.originalname}`,
|
|
337
|
+
file: file.buffer,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Update record with file URL
|
|
341
|
+
const result = await this.service.updateWhere('id', req.params.id, {
|
|
342
|
+
file_url: location,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
res.status(200).json(result);
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Creating from Scratch
|
|
352
|
+
|
|
353
|
+
1. **Create the directory:** `src/controllers/{table_name}/`
|
|
354
|
+
2. **Create the file:** `src/controllers/{table_name}/{table_name}.controller.js`
|
|
355
|
+
3. **Use the template:**
|
|
356
|
+
```javascript
|
|
357
|
+
/** @param {typeof TableControllerBase} ServicesClass @generated-types */
|
|
358
|
+
module.exports = (ServicesClass) => class extends ServicesClass {
|
|
359
|
+
async getAll(req, res) {
|
|
360
|
+
const data = await this.service.get();
|
|
361
|
+
res.status(200).json(data);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
```
|
|
365
|
+
4. **Add methods** for each endpoint the resource needs
|
|
366
|
+
5. **Create corresponding route** in `src/routes/{table_name}.router.js`
|
|
367
|
+
6. **Regenerate types:** `npx langaro-api generate`
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Anti-patterns
|
|
372
|
+
|
|
373
|
+
- **Do NOT put business logic in controllers.** If it involves more than validation and delegation, move it to the service.
|
|
374
|
+
- **Do NOT access `knex` directly in controllers.** Use `this.service` or `this.services`.
|
|
375
|
+
- **Do NOT forget `.bind(controller)` in routes.** Without it, `this` is undefined inside the controller method.
|
|
376
|
+
- **Do NOT return raw error messages** from external services. Use `ApiError()` with safe, user-facing messages.
|
|
377
|
+
- **Do NOT mix async patterns.** Use `async/await` consistently, never mix callbacks.
|
|
378
|
+
- **Do NOT forget to scope queries by `company_id`** when the endpoint is company-scoped.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Checklist
|
|
383
|
+
|
|
384
|
+
When creating or modifying a controller:
|
|
385
|
+
|
|
386
|
+
- [ ] File is in `src/controllers/{name}/{name}.controller.js`
|
|
387
|
+
- [ ] Exports a factory function `(ServicesClass) => class extends ServicesClass`
|
|
388
|
+
- [ ] Has correct `@generated-types` JSDoc annotation
|
|
389
|
+
- [ ] Each method receives `(req, res)` — standard Express handler signature
|
|
390
|
+
- [ ] Input validation happens before service delegation
|
|
391
|
+
- [ ] Business logic is in the service, not the controller
|
|
392
|
+
- [ ] Company-scoped queries include `company_id` in filters
|
|
393
|
+
- [ ] Background jobs use `this.Queue.add()`, not inline processing
|
|
394
|
+
- [ ] Error responses use `ApiError()` with appropriate status codes
|
|
395
|
+
- [ ] Corresponding route file exists in `src/routes/`
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Routes
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
|
|
5
|
+
Routes define the **HTTP API surface**. They map URL paths to controller methods and apply middleware (authentication, permissions).
|
|
6
|
+
|
|
7
|
+
**What routes do:**
|
|
8
|
+
- Map HTTP methods + paths to controller methods
|
|
9
|
+
- Apply auth middleware with permissions
|
|
10
|
+
- Define the public API contract
|
|
11
|
+
|
|
12
|
+
**What routes do NOT do:**
|
|
13
|
+
- Contain business logic
|
|
14
|
+
- Execute database queries
|
|
15
|
+
- Validate request bodies (that's in controllers)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Location & Naming
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
src/routes/
|
|
23
|
+
├── index.js # attachRouters loader (do not modify)
|
|
24
|
+
├── users.router.js # Routes for /users
|
|
25
|
+
├── auth.router.js # Routes for /auth
|
|
26
|
+
├── products_invoices.router.js # Routes for /products-invoices
|
|
27
|
+
└── ...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Naming convention:** `{table_name}.router.js`
|
|
31
|
+
|
|
32
|
+
**URL path conversion:** The loader converts `snake_case` filenames to `kebab-case` URL paths:
|
|
33
|
+
- `users.router.js` → `/users`
|
|
34
|
+
- `products_invoices.router.js` → `/products-invoices`
|
|
35
|
+
- `auth.router.js` → `/auth`
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Standard Structure
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
const createAuthMiddleware = require('@/middlewares/auth.middleware');
|
|
43
|
+
|
|
44
|
+
/** @param {ControllersMap} controllers @param {ServicesMap} services @generated-types */
|
|
45
|
+
module.exports = (controllers, services) => {
|
|
46
|
+
const auth = createAuthMiddleware(services);
|
|
47
|
+
const router = require('express').Router();
|
|
48
|
+
const controller = controllers.UsersController;
|
|
49
|
+
const subject = 'users';
|
|
50
|
+
|
|
51
|
+
// List all
|
|
52
|
+
router.get('/',
|
|
53
|
+
auth([{ subject }]),
|
|
54
|
+
controller.getAll.bind(controller)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Get by ID
|
|
58
|
+
router.get('/:id',
|
|
59
|
+
auth([{ subject }]),
|
|
60
|
+
controller.getById.bind(controller)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Create
|
|
64
|
+
router.post('/',
|
|
65
|
+
auth([{ subject, action: 'create' }]),
|
|
66
|
+
controller.create.bind(controller)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Update
|
|
70
|
+
router.put('/:id',
|
|
71
|
+
auth([{ subject, action: 'update' }]),
|
|
72
|
+
controller.update.bind(controller)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Delete
|
|
76
|
+
router.delete('/:id',
|
|
77
|
+
auth([{ subject, action: 'delete' }]),
|
|
78
|
+
controller.remove.bind(controller)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return router;
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Auth Middleware Usage
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
const auth = createAuthMiddleware(services);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Signature
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
auth(permissionsRequired, authorizedRoles, companyIdRequired)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Parameters
|
|
100
|
+
|
|
101
|
+
**`permissionsRequired`** (array) — Required permissions. Each entry is `{ subject, action? }`:
|
|
102
|
+
```javascript
|
|
103
|
+
// Require any permission on 'users' subject
|
|
104
|
+
auth([{ subject: 'users' }])
|
|
105
|
+
|
|
106
|
+
// Require specific action
|
|
107
|
+
auth([{ subject: 'users', action: 'delete' }])
|
|
108
|
+
|
|
109
|
+
// Require one of multiple permissions (OR logic)
|
|
110
|
+
auth([{ subject: 'users' }, { subject: 'admin' }])
|
|
111
|
+
|
|
112
|
+
// No permission check (just authentication)
|
|
113
|
+
auth([])
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**`authorizedRoles`** (array, default: `['user', 'admin']`) — Allowed roles:
|
|
117
|
+
```javascript
|
|
118
|
+
// Default: both users and admins
|
|
119
|
+
auth([{ subject: 'users' }])
|
|
120
|
+
|
|
121
|
+
// Admin only
|
|
122
|
+
auth([{ subject: 'users' }], ['admin'])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**`companyIdRequired`** (boolean, default: varies by project) — Whether `company_id` header is required:
|
|
126
|
+
```javascript
|
|
127
|
+
// Route works without company context
|
|
128
|
+
auth([{ subject: 'users' }], undefined, false)
|
|
129
|
+
|
|
130
|
+
// Route requires company context (company_id header)
|
|
131
|
+
auth([{ subject: 'users' }], undefined, true)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Public Routes (no auth)
|
|
135
|
+
|
|
136
|
+
Simply omit the auth middleware:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
router.post('/register', controller.register.bind(controller));
|
|
140
|
+
router.post('/login', controller.login.bind(controller));
|
|
141
|
+
router.get('/health', (req, res) => res.status(200).json({ status: 'ok' }));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Binding Convention
|
|
147
|
+
|
|
148
|
+
**Always use `.bind(controller)`** when passing controller methods to routes:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// ✅ CORRECT
|
|
152
|
+
router.get('/', auth([{ subject }]), controller.getAll.bind(controller));
|
|
153
|
+
|
|
154
|
+
// ❌ WRONG — 'this' will be undefined in the controller method
|
|
155
|
+
router.get('/', auth([{ subject }]), controller.getAll);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Why:** Express calls the handler without a `this` context. `.bind()` ensures the controller instance is preserved.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Common Route Patterns
|
|
163
|
+
|
|
164
|
+
### Standard CRUD Routes
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
router.get('/', auth([{ subject }]), controller.getAll.bind(controller));
|
|
168
|
+
router.get('/:id', auth([{ subject }]), controller.getById.bind(controller));
|
|
169
|
+
router.post('/', auth([{ subject }]), controller.create.bind(controller));
|
|
170
|
+
router.put('/:id', auth([{ subject }]), controller.update.bind(controller));
|
|
171
|
+
router.delete('/:id', auth([{ subject }]), controller.remove.bind(controller));
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Action Routes
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Export data
|
|
178
|
+
router.get('/export',
|
|
179
|
+
auth([{ subject, action: 'export' }]),
|
|
180
|
+
controller.exportData.bind(controller)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Batch operations
|
|
184
|
+
router.post('/batch-update',
|
|
185
|
+
auth([{ subject, action: 'update' }]),
|
|
186
|
+
controller.batchUpdate.bind(controller)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Status changes
|
|
190
|
+
router.put('/:id/cancel',
|
|
191
|
+
auth([{ subject, action: 'cancel' }]),
|
|
192
|
+
controller.cancel.bind(controller)
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Mixed Auth Routes
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
// Public route
|
|
200
|
+
router.post('/register', controller.register.bind(controller));
|
|
201
|
+
|
|
202
|
+
// Authenticated but no permission check
|
|
203
|
+
router.get('/me', auth([]), controller.getProfile.bind(controller));
|
|
204
|
+
|
|
205
|
+
// Full permission check
|
|
206
|
+
router.put('/settings',
|
|
207
|
+
auth([{ subject: 'settings' }]),
|
|
208
|
+
controller.updateSettings.bind(controller)
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Admin only
|
|
212
|
+
router.get('/admin/stats',
|
|
213
|
+
auth([{ subject: 'admin' }], ['admin']),
|
|
214
|
+
controller.getStats.bind(controller)
|
|
215
|
+
);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Creating from Scratch
|
|
221
|
+
|
|
222
|
+
1. **Create the file:** `src/routes/{table_name}.router.js`
|
|
223
|
+
2. **Use the template:**
|
|
224
|
+
```javascript
|
|
225
|
+
const createAuthMiddleware = require('@/middlewares/auth.middleware');
|
|
226
|
+
|
|
227
|
+
/** @param {ControllersMap} controllers @param {ServicesMap} services @generated-types */
|
|
228
|
+
module.exports = (controllers, services) => {
|
|
229
|
+
const auth = createAuthMiddleware(services);
|
|
230
|
+
const router = require('express').Router();
|
|
231
|
+
const controller = controllers.TableNameController;
|
|
232
|
+
const subject = 'table_name';
|
|
233
|
+
|
|
234
|
+
router.get('/', auth([{ subject }]), controller.getAll.bind(controller));
|
|
235
|
+
|
|
236
|
+
return router;
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
3. **Add routes** for each controller method
|
|
240
|
+
4. **Apply auth** with appropriate permissions and roles
|
|
241
|
+
5. **Regenerate types:** `npx langaro-api generate`
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Anti-patterns
|
|
246
|
+
|
|
247
|
+
- **Do NOT put logic in route files.** Routes are pure wiring — no validation, no database calls.
|
|
248
|
+
- **Do NOT forget `.bind(controller)`.** This is the most common bug.
|
|
249
|
+
- **Do NOT use inline middleware functions** for complex logic. Create separate middleware files.
|
|
250
|
+
- **Do NOT hardcode URL paths.** The loader auto-generates them from the filename.
|
|
251
|
+
- **Do NOT create multiple router files for the same resource.** One file per resource.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Checklist
|
|
256
|
+
|
|
257
|
+
When creating or modifying a route:
|
|
258
|
+
|
|
259
|
+
- [ ] File is named `{table_name}.router.js`
|
|
260
|
+
- [ ] File is in `src/routes/`
|
|
261
|
+
- [ ] Exports a factory function `(controllers, services) => Router`
|
|
262
|
+
- [ ] Has correct `@generated-types` JSDoc annotation
|
|
263
|
+
- [ ] Controller accessed via `controllers.PascalCaseController`
|
|
264
|
+
- [ ] Auth middleware applied to all protected routes
|
|
265
|
+
- [ ] All controller methods use `.bind(controller)`
|
|
266
|
+
- [ ] Returns the Express router
|
|
267
|
+
- [ ] Corresponding controller exists in `src/controllers/`
|
|
268
|
+
- [ ] Permission subjects match the resource name
|