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.
@@ -0,0 +1,352 @@
1
+ # Configuration & Bootstrap
2
+
3
+ ## Role
4
+
5
+ The config layer handles application startup, middleware wiring, database connections, Redis, queue initialization, and error handling. These files are generated by `langaro-api init` and rarely need modification.
6
+
7
+ ---
8
+
9
+ ## server.js
10
+
11
+ Entry point for the application. Handles environment setup, server creation, and graceful shutdown.
12
+
13
+ ```
14
+ Location: src/config/server.js
15
+ ```
16
+
17
+ **Responsibilities:**
18
+ 1. Load environment variables (`.env` or `.env.test`)
19
+ 2. Create Knex database connection
20
+ 3. Call `getAppInstance(knex)` to initialize the App
21
+ 4. Listen on `PORT`
22
+ 5. Handle `SIGINT`/`SIGTERM` for graceful shutdown
23
+
24
+ **Graceful shutdown sequence:**
25
+ 1. Stop accepting new connections
26
+ 2. Shut down BullMQ queues (wait for active jobs to complete)
27
+ 3. Close HTTP server
28
+ 4. Destroy Knex connection
29
+ 5. Exit process
30
+
31
+ **When to modify:** Almost never. Only if you need custom shutdown logic or additional initialization before the app starts.
32
+
33
+ ---
34
+
35
+ ## app.js
36
+
37
+ The core application class that wires everything together.
38
+
39
+ ```
40
+ Location: src/config/app.js
41
+ ```
42
+
43
+ ### App Class Structure
44
+
45
+ ```javascript
46
+ class App {
47
+ constructor(knexInstance, servicesInstance) {
48
+ // Initialize Express, HTTP server, Socket.io
49
+ // Set up readiness promise (blocks requests until boot completes)
50
+ // Call init()
51
+ }
52
+
53
+ async init() {
54
+ // 1. Set query parser
55
+ // 2. Apply readiness middleware
56
+ // 3. Apply standard middlewares
57
+ // 4. Wire routes (models → services → queues → controllers → routes → tasks)
58
+ // 5. Apply error handling
59
+ // 6. Mark as ready
60
+ }
61
+
62
+ middlewares() {
63
+ // requestIp, CORS, morgan, bodyParser (50MB limit), multer
64
+ }
65
+
66
+ async routes() {
67
+ // Socket.io setup
68
+ // loadModels → loadServices → initializeQueues → loadControllers → attachRouters → loadTasks
69
+ }
70
+
71
+ handleErrors() {
72
+ // Sentry error handler
73
+ // Axios network errors → 422
74
+ // Safe errors → err.code + err.data
75
+ // Unsafe errors → 500 + Sentry
76
+ }
77
+
78
+ bullBoard() {
79
+ // Queue dashboard at BULLBOARD_PATH
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Readiness Middleware
85
+
86
+ The app blocks all incoming requests until initialization completes:
87
+ ```javascript
88
+ async readinessMiddleware(req, res, next) {
89
+ if (this.isReady) return next();
90
+ try {
91
+ await this.readyPromise; // Wait for init() to resolve
92
+ next();
93
+ } catch (error) {
94
+ res.status(503).json({ error: { message: 'Server is starting up.' } });
95
+ }
96
+ }
97
+ ```
98
+
99
+ ### Error Handling
100
+
101
+ ```javascript
102
+ // Safe errors (created via ApiError) — returned to client
103
+ if (err.safe) {
104
+ res.status(err.code).json({ error: err.data });
105
+ }
106
+
107
+ // Unsafe errors (uncaught) — generic response + Sentry
108
+ res.status(500).json({
109
+ error: { message: 'Unable to process request. Please try again later.' },
110
+ });
111
+ ```
112
+
113
+ **Creating safe errors:**
114
+ ```javascript
115
+ const { ApiError, StatusCodes } = require('@/utils');
116
+
117
+ // In controllers or services:
118
+ ApiError(StatusCodes.NOT_FOUND, 'User not found');
119
+ ApiError(StatusCodes.BAD_REQUEST, 'Invalid email format', 'INVALID_EMAIL');
120
+ ApiError(StatusCodes.CONFLICT, 'Email already registered');
121
+ ```
122
+
123
+ **When to modify:** When adding global middleware, changing body size limits, adding new middleware to the chain, or customizing error handling.
124
+
125
+ ---
126
+
127
+ ## Database Connection
128
+
129
+ ```
130
+ Location: src/database/connection.js
131
+ ```
132
+
133
+ Creates and configures the Knex instance:
134
+
135
+ ```javascript
136
+ module.exports = (env = NODE_ENV, dbName = DB_NAME) => {
137
+ const knex = require('knex')({
138
+ client: 'mysql2',
139
+ connection: {
140
+ host: DB_HOST,
141
+ user: DB_USER,
142
+ port: DB_PORT,
143
+ password: DB_PASSWORD,
144
+ database: dbName,
145
+ typeCast(field, next) {
146
+ // TINY(1) → boolean conversion
147
+ if (field.type === 'TINY' && field.length === 1) {
148
+ const value = field.string();
149
+ if (value === null) return null;
150
+ return value === '1';
151
+ }
152
+ return next();
153
+ },
154
+ },
155
+ pool: { min: 10, max: 100 },
156
+ });
157
+
158
+ // Attach pagination plugin
159
+ const { attachPaginate } = require('knex-paginate');
160
+ if (!knex.paginate) attachPaginate();
161
+
162
+ // Attach schema inspector
163
+ const { SchemaInspector } = require('knex-schema-inspector');
164
+ knex.inspector = SchemaInspector(knex);
165
+
166
+ return knex;
167
+ };
168
+ ```
169
+
170
+ **Key features:**
171
+ - MySQL TINY(1) → JavaScript boolean conversion
172
+ - Connection pooling (min 10, max 100)
173
+ - `knex-paginate` for built-in pagination
174
+ - `knex-schema-inspector` for runtime schema introspection
175
+
176
+ **Test safety:** Throws if test environment tries to use non-test database (name must contain `_tests`).
177
+
178
+ ---
179
+
180
+ ## Redis
181
+
182
+ ### Redis Config
183
+
184
+ ```
185
+ Location: src/database/redis-config.js
186
+ ```
187
+
188
+ ```javascript
189
+ module.exports = {
190
+ connection: {
191
+ host: process.env.REDIS_HOST || '127.0.0.1',
192
+ port: process.env.REDIS_PORT || 6379,
193
+ password: process.env.REDIS_PASSWORD,
194
+ username: process.env.REDIS_USERNAME,
195
+ tls: process.env.REDIS_TLS || false,
196
+ maxRetriesPerRequest: null,
197
+ },
198
+ };
199
+ ```
200
+
201
+ ### Redis Cache
202
+
203
+ ```
204
+ Location: src/database/redis-cache.js
205
+ ```
206
+
207
+ Singleton class providing simple key-value caching:
208
+
209
+ ```javascript
210
+ class RedisCache {
211
+ async get(key) // Returns parsed JSON or null
212
+ async set(key, value, expiresInSeconds) // Stores as JSON with TTL
213
+ async delete(key) // Removes key
214
+ }
215
+
216
+ module.exports = new RedisCache();
217
+ ```
218
+
219
+ **Key format:** `redis-cache:{key}`
220
+
221
+ **Usage:**
222
+ ```javascript
223
+ const RedisCache = require('@/database/redis-cache');
224
+
225
+ // Cache a value for 1 hour
226
+ await RedisCache.set('exchange-rates', rates, 3600);
227
+
228
+ // Read from cache
229
+ const cached = await RedisCache.get('exchange-rates');
230
+ if (cached) return cached;
231
+
232
+ // Invalidate
233
+ await RedisCache.delete('exchange-rates');
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Queue System
239
+
240
+ ```
241
+ Location: src/config/queues.js
242
+ ```
243
+
244
+ See `12-queues.md` for detailed queue documentation.
245
+
246
+ ---
247
+
248
+ ## Sentry (Error Tracking)
249
+
250
+ ```
251
+ Location: src/config/instrument.js
252
+ ```
253
+
254
+ ```javascript
255
+ const Sentry = require('@sentry/node');
256
+ Sentry.init({
257
+ dsn: process.env.SENTRY_APP,
258
+ tracesSampleRate: 0,
259
+ profilesSampleRate: 0,
260
+ environment: process.env.NODE_ENV,
261
+ });
262
+ module.exports = Sentry;
263
+ ```
264
+
265
+ **When to modify:** To enable performance monitoring (`tracesSampleRate > 0`) or add custom Sentry configuration.
266
+
267
+ ---
268
+
269
+ ## Environment Variables Reference
270
+
271
+ ### Required
272
+
273
+ | Variable | Description | Example |
274
+ |----------|-------------|---------|
275
+ | `NODE_ENV` | Environment | `development`, `production`, `test` |
276
+ | `PORT` | Server port | `8282` |
277
+ | `DB_HOST` | MySQL host | `127.0.0.1` |
278
+ | `DB_USER` | MySQL user | `root` |
279
+ | `DB_PASSWORD` | MySQL password | `password` |
280
+ | `DB_NAME` | MySQL database | `my_project` |
281
+ | `JWT_SECRET` | JWT signing key | `auto-generated-uuid` |
282
+
283
+ ### Optional
284
+
285
+ | Variable | Description | Example |
286
+ |----------|-------------|---------|
287
+ | `DB_PORT` | MySQL port | `3306` |
288
+ | `REDIS_HOST` | Redis host | `127.0.0.1` |
289
+ | `REDIS_PORT` | Redis port | `6379` |
290
+ | `REDIS_PASSWORD` | Redis password | `password` |
291
+ | `REDIS_USERNAME` | Redis username | — |
292
+ | `REDIS_TLS` | Redis TLS | `true` |
293
+ | `MASTER_PASS` | Dev master password | `auto-generated` |
294
+ | `BULLBOARD_PATH` | Queue dashboard URL | `/admin/queues` |
295
+ | `SENTRY_APP` | Sentry DSN | `https://...@sentry.io/...` |
296
+ | `POSTMARK_API_KEY` | Email service | `key-...` |
297
+ | `AWS_ACCESS_KEY_ID` | AWS credentials | `AKIA...` |
298
+ | `AWS_SECRET_ACCESS_KEY` | AWS credentials | `...` |
299
+ | `AWS_REGION` | AWS region | `us-east-1` |
300
+ | `GOOGLE_CLIENT_ID` | OAuth | `...apps.googleusercontent.com` |
301
+ | `GOOGLE_CLIENT_SECRET` | OAuth | `...` |
302
+
303
+ ---
304
+
305
+ ## Babel Configuration
306
+
307
+ ```
308
+ Location: .babelrc
309
+ ```
310
+
311
+ Enables ES6+ features and the `@` path alias:
312
+
313
+ ```json
314
+ {
315
+ "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]],
316
+ "plugins": [
317
+ "@babel/transform-runtime",
318
+ "@babel/plugin-proposal-class-properties",
319
+ ["module-resolver", { "root": ["./"], "alias": { "@": "./src/" } }]
320
+ ]
321
+ }
322
+ ```
323
+
324
+ The `@` alias lets you use `require('@/database/connection')` instead of relative paths.
325
+
326
+ ---
327
+
328
+ ## When to Modify Bootstrap Files
329
+
330
+ | Need | File to modify |
331
+ |------|---------------|
332
+ | Add global middleware | `app.js` → `middlewares()` |
333
+ | Change body size limit | `app.js` → `middlewares()` bodyParser config |
334
+ | Add custom error handling | `app.js` → `handleErrors()` |
335
+ | Change DB pool size | `connection.js` → `pool` config |
336
+ | Add new env variable | `.env` + `.env.example` |
337
+ | Change graceful shutdown | `server.js` → SIGTERM handler |
338
+ | Configure Sentry | `instrument.js` |
339
+ | Change Redis config | `redis-config.js` |
340
+
341
+ ---
342
+
343
+ ## Checklist
344
+
345
+ When modifying configuration:
346
+
347
+ - [ ] Changes work in both development and production
348
+ - [ ] New env variables added to `.env.example`
349
+ - [ ] No hardcoded credentials or secrets
350
+ - [ ] Error handling maintains safe/unsafe distinction
351
+ - [ ] Middleware order is correct (readiness → CORS → logging → parsing → routes → errors)
352
+ - [ ] Pool sizes are appropriate for the deployment environment
@@ -0,0 +1,205 @@
1
+ # Queue System (BullMQ)
2
+
3
+ ## Role
4
+
5
+ The queue system provides **reliable asynchronous job processing** using BullMQ backed by Redis. It handles job creation, worker management, retry logic, rate limiting, and monitoring.
6
+
7
+ ---
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
13
+ │ Controllers │────▶│ Queue.add │────▶│ Redis (BullMQ) │
14
+ │ Tasks │ │ │ │ │
15
+ │ Other Jobs │ └──────────────┘ └────────┬────────┘
16
+ │ │ │
17
+ └──────────────────┘ ▼
18
+ ┌─────────────────┐
19
+ │ Worker (per job)│
20
+ │ job.handle() │
21
+ └─────────────────┘
22
+ ```
23
+
24
+ - **Single Redis connection** shared by all queues
25
+ - **Lazy creation**: Queues and workers are created on first `add()` call
26
+ - **Bull Board dashboard** at `BULLBOARD_PATH` for monitoring
27
+ - **Automatic cleanup**: Inactive queues close after 10 minutes
28
+
29
+ ---
30
+
31
+ ## Queue Lifecycle
32
+
33
+ ### Initialization (at boot)
34
+
35
+ ```
36
+ 1. initializeQueues(services, io, bullBoardSetQueues)
37
+ 2. Create Redis connection
38
+ 3. loadJobs(services) → scan src/jobs/ → register all job configs in Map
39
+ 4. Scan Redis for existing queues with pending jobs
40
+ 5. Start workers for any queues that have waiting/active/delayed jobs
41
+ 6. Start inactivity cleanup interval (checks every 1 minute)
42
+ 7. Return { add } function
43
+ ```
44
+
45
+ ### Adding a Job
46
+
47
+ ```
48
+ 1. Queue.add('job-name', data, options)
49
+ 2. Lookup base config from jobConfigs Map
50
+ 3. If grouped: extract groupId, create unique queue name
51
+ 4. getOrCreateQueueAndWorker():
52
+ a. If queue exists → update lastActivity, return
53
+ b. If new → create Queue + Worker with merged options
54
+ c. Register worker event handlers (failed → Sentry)
55
+ d. Worker.run()
56
+ 5. queue.add(name, data, options) → Redis
57
+ 6. Update Bull Board
58
+ ```
59
+
60
+ ### Processing a Job
61
+
62
+ ```
63
+ 1. Worker picks up job from Redis
64
+ 2. Update lastActivity timestamp
65
+ 3. Call job.handle(data, job, { add }, io)
66
+ 4. If success → job complete → removed per retention policy
67
+ 5. If error → retry according to backoff settings
68
+ 6. If error + discard → permanent failure
69
+ ```
70
+
71
+ ### Inactivity Cleanup
72
+
73
+ Every 1 minute, the system checks all active queues:
74
+ - If a queue has been inactive for 10+ minutes
75
+ - AND has 0 active, 0 waiting, 0 delayed jobs
76
+ - → Close worker and queue, free resources
77
+
78
+ ### Graceful Shutdown
79
+
80
+ ```
81
+ 1. Pause all workers (stop accepting new jobs)
82
+ 2. Wait for active jobs to complete (up to ~20 minutes / 400 retries × 3s)
83
+ 3. Close all workers and queues
84
+ 4. Clear Redis connection
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Default Options
90
+
91
+ ### Job Options (applied to all jobs)
92
+
93
+ ```javascript
94
+ {
95
+ attempts: 100, // Max retry attempts
96
+ backoff: { type: 'fixed', delay: 60000 }, // 1 minute between retries
97
+ removeOnComplete: { age: 2592000, count: 1000 }, // Keep 30 days / max 1000
98
+ removeOnFail: { age: 7776000, count: 5000 }, // Keep 90 days / max 5000
99
+ }
100
+ ```
101
+
102
+ ### Worker Options (applied to all workers)
103
+
104
+ ```javascript
105
+ {
106
+ autorun: false, // Workers started manually
107
+ limiter: { max: 2, duration: 1000 }, // 2 jobs per second
108
+ maxStalledCount: 5, // Fail after 5 stalls
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Grouped Queues
115
+
116
+ When a job config has `group: true`, each unique `groupId` creates a separate queue + worker:
117
+
118
+ ```
119
+ Queue.add('sync-data', data, { groupId: 'company-abc' })
120
+ → Creates queue: sync-data-company-abc
121
+ → Creates worker for: sync-data-company-abc
122
+
123
+ Queue.add('sync-data', data, { groupId: 'company-xyz' })
124
+ → Creates queue: sync-data-company-xyz
125
+ → Creates worker for: sync-data-company-xyz
126
+ ```
127
+
128
+ **Use case:** Per-company processing isolation. Each company's jobs run independently without blocking other companies.
129
+
130
+ **Missing groupId:** If `group: true` but no `groupId` in options → throws error.
131
+
132
+ ---
133
+
134
+ ## Test Environment
135
+
136
+ When `NODE_ENV === 'test'`, `initializeQueues` returns a mock:
137
+
138
+ ```javascript
139
+ return { add: jest.fn() };
140
+ ```
141
+
142
+ No Redis connection, no workers, no queues. Tests can verify `Queue.add` was called without side effects.
143
+
144
+ ---
145
+
146
+ ## Monitoring
147
+
148
+ ### Bull Board Dashboard
149
+
150
+ Available at `process.env.BULLBOARD_PATH` (default: `/admin/queues`).
151
+
152
+ Shows:
153
+ - All active queues
154
+ - Job counts (waiting, active, completed, failed, delayed)
155
+ - Individual job details and retry history
156
+
157
+ ### Sentry Integration
158
+
159
+ All worker failures are automatically captured:
160
+ ```javascript
161
+ worker.on('failed', (job, error) => {
162
+ Sentry.captureException(error, {
163
+ tags: { queue: queueName },
164
+ contexts: { data: { 'Job data': job?.data, queue: queueName } },
165
+ });
166
+ });
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Queue API Reference
172
+
173
+ ### `Queue.add(name, data, options)`
174
+
175
+ | Parameter | Type | Description |
176
+ |-----------|------|-------------|
177
+ | `name` | string | Job name (must match a registered job) |
178
+ | `data` | object | Payload passed to `handle(data, ...)` |
179
+ | `options` | object | Optional: `{ delay, groupId, priority, ... }` |
180
+
181
+ **Options:**
182
+ - `delay: 5000` — Delay execution by 5 seconds
183
+ - `groupId: 'abc'` — Route to grouped queue (requires `group: true` in job config)
184
+ - Any BullMQ job option
185
+
186
+ ---
187
+
188
+ ## Startup Queue Scanning
189
+
190
+ On boot, the queue system scans Redis for existing `bull:*:meta` keys to find queues that have pending work from before the restart. It matches queue names against registered job configs and starts workers for any that have waiting/active/delayed jobs.
191
+
192
+ This ensures jobs added before a deploy or restart are still processed.
193
+
194
+ ---
195
+
196
+ ## When to Modify
197
+
198
+ The queue configuration (`src/config/queues.js`) rarely needs changes. Common modifications:
199
+
200
+ | Need | What to change |
201
+ |------|---------------|
202
+ | Different retry defaults | `getDefaultJobOptions()` |
203
+ | Different rate limits | `getDefaultWorkerOptions()` |
204
+ | Longer inactivity timeout | `inactivityTimeLimitMs` |
205
+ | Disable cleanup interval | Set `inactivityCheckIntervalMs` to 0 |