langaro-api 1.2.3 → 1.2.5

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.
@@ -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