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,281 @@
1
+ # Utils
2
+
3
+ ## Role
4
+
5
+ Utils are **pure utility functions** — small, reusable helpers that don't contain business logic. They handle validation, formatting, date operations, and common transformations.
6
+
7
+ **What utils do:**
8
+ - Validate input (required fields, field formats)
9
+ - Format data (dates, currencies, strings)
10
+ - Calculate values (taxes, discounts)
11
+ - Provide shared helpers across controllers/services
12
+
13
+ **What utils do NOT do:**
14
+ - Access the database
15
+ - Call external APIs
16
+ - Contain business logic workflows
17
+
18
+ ---
19
+
20
+ ## Location & Naming
21
+
22
+ ```
23
+ src/utils/
24
+ ├── index.js # Re-exports common packages + local utils
25
+ ├── sleep.js # sleep(ms) utility
26
+ ├── validate-*.js # Validation utilities
27
+ ├── format-*.js # Formatting utilities
28
+ ├── calculate-*.js # Calculation utilities
29
+ ├── determine-*.js # Decision logic
30
+ ├── get-*.js # Data retrieval helpers
31
+ ├── parse-*.js # Parsing utilities
32
+ └── factory.js # Factory patterns
33
+ ```
34
+
35
+ **Naming convention:** Kebab-case with verb prefix describing the function category.
36
+
37
+ ---
38
+
39
+ ## The Utils Index
40
+
41
+ `src/utils/index.js` re-exports commonly used packages:
42
+
43
+ ```javascript
44
+ export * from 'validate-required-fields';
45
+ export * from 'trigger-api-error';
46
+ export * from 'http-status-codes';
47
+ export * from 'forbid-fields-sent';
48
+ export * from 'validate-fields-format';
49
+ export * from './sleep';
50
+ ```
51
+
52
+ This allows single-line imports across the codebase:
53
+
54
+ ```javascript
55
+ const {
56
+ validateRequiredFields,
57
+ ApiError,
58
+ StatusCodes,
59
+ validateFieldsFormat,
60
+ forbidFieldsSent,
61
+ } = require('@/utils');
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Core Validation Utilities
67
+
68
+ ### validateRequiredFields
69
+
70
+ Validates that required fields are present in the data object. Throws if missing.
71
+
72
+ ```javascript
73
+ const { validateRequiredFields } = require('@/utils');
74
+
75
+ // Throws if name or email is missing/empty
76
+ validateRequiredFields(data, ['name', 'email', 'company_id']);
77
+ ```
78
+
79
+ From package: `validate-required-fields`
80
+
81
+ ### validateFieldsFormat
82
+
83
+ Validates field formats (email, phone, array, etc.). Throws if invalid.
84
+
85
+ ```javascript
86
+ const { validateFieldsFormat } = require('@/utils');
87
+
88
+ validateFieldsFormat([
89
+ ['email', data.email, 'email field'],
90
+ ['phone', data.phone, 'phone field'],
91
+ ['array', data.items, 'items field'],
92
+ ['date', data.start_date, 'start date field'],
93
+ ['number', data.amount, 'amount field'],
94
+ ]);
95
+ ```
96
+
97
+ From package: `validate-fields-format`
98
+
99
+ ### ApiError
100
+
101
+ Creates a "safe" error that returns a specific HTTP status and message to the client:
102
+
103
+ ```javascript
104
+ const { ApiError, StatusCodes } = require('@/utils');
105
+
106
+ // 404 Not Found
107
+ ApiError(StatusCodes.NOT_FOUND, 'User not found');
108
+
109
+ // 400 Bad Request
110
+ ApiError(StatusCodes.BAD_REQUEST, 'Invalid email format');
111
+
112
+ // 409 Conflict
113
+ ApiError(StatusCodes.CONFLICT, 'Email already registered');
114
+
115
+ // 422 Unprocessable Entity
116
+ ApiError(StatusCodes.UNPROCESSABLE_ENTITY, 'Cannot cancel a completed invoice');
117
+
118
+ // With error code
119
+ ApiError(StatusCodes.FORBIDDEN, 'Insufficient permissions', 'PERM_DENIED');
120
+
121
+ // Passing to next() in middleware
122
+ ApiError(StatusCodes.UNAUTHORIZED, 'Token expired', undefined, next);
123
+ ```
124
+
125
+ From package: `trigger-api-error`
126
+
127
+ ### forbidFieldsSent
128
+
129
+ Validates that forbidden fields are not present in the request:
130
+
131
+ ```javascript
132
+ const { forbidFieldsSent } = require('@/utils');
133
+
134
+ // Throws if any of these fields are in data
135
+ forbidFieldsSent(data, ['role', 'api_key', 'password_hash']);
136
+ ```
137
+
138
+ From package: `forbid-fields-sent`
139
+
140
+ ---
141
+
142
+ ## Common Utility Patterns
143
+
144
+ ### Filter Parsing
145
+
146
+ Utility for parsing query string filters into andWhere conditions:
147
+
148
+ ```javascript
149
+ // src/utils/parse-and-validate-filters.js
150
+ function parseAndValidateFilters(query, filterOptions) {
151
+ const andWhere = [];
152
+
153
+ if (!filterOptions) return andWhere;
154
+
155
+ filterOptions.forEach(filter => {
156
+ const value = query[filter.name];
157
+ if (value === undefined) return;
158
+
159
+ const column = filter.column || filter.name;
160
+
161
+ switch (filter.type) {
162
+ case 'select_multiple':
163
+ if (Array.isArray(value)) {
164
+ andWhere.push([column, 'in', value]);
165
+ } else {
166
+ andWhere.push([column, value]);
167
+ }
168
+ break;
169
+ case 'date':
170
+ if (query[`${filter.name}_start`] && query[`${filter.name}_end`]) {
171
+ andWhere.push([column, 'between', [
172
+ query[`${filter.name}_start`],
173
+ query[`${filter.name}_end`],
174
+ ]]);
175
+ }
176
+ break;
177
+ case 'integer':
178
+ if (query[`${filter.name}_min`]) {
179
+ andWhere.push([column, '>=', query[`${filter.name}_min`]]);
180
+ }
181
+ if (query[`${filter.name}_max`]) {
182
+ andWhere.push([column, '<=', query[`${filter.name}_max`]]);
183
+ }
184
+ break;
185
+ case 'is_not_null':
186
+ if (value === 'true') andWhere.push([column, '<>', null]);
187
+ if (value === 'false') andWhere.push([column, null]);
188
+ break;
189
+ }
190
+ });
191
+
192
+ return andWhere;
193
+ }
194
+ ```
195
+
196
+ ### Security Details Extraction
197
+
198
+ ```javascript
199
+ // src/utils/get-request-security-details.js
200
+ function getRequestSecurityDetails(req) {
201
+ return {
202
+ ip_address: req.clientIp,
203
+ device: req.headers['user-agent'],
204
+ os: parseOS(req.headers['user-agent']),
205
+ browser: parseBrowser(req.headers['user-agent']),
206
+ // Additional geolocation from IP if available
207
+ };
208
+ }
209
+ ```
210
+
211
+ ### Date Utilities
212
+
213
+ ```javascript
214
+ // src/utils/date-fns.js
215
+ const { format, addMinutes, differenceInMilliseconds, parseISO } = require('date-fns');
216
+ const { toZonedTime } = require('date-fns-tz');
217
+
218
+ module.exports = {
219
+ format,
220
+ addMinutes,
221
+ differenceInMilliseconds,
222
+ parseISO,
223
+ toZonedTime,
224
+ // Re-export for single import point
225
+ };
226
+ ```
227
+
228
+ ### Sleep Utility
229
+
230
+ ```javascript
231
+ // src/utils/sleep.js
232
+ exports.sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Creating from Scratch
238
+
239
+ 1. **Create the file:** `src/utils/{verb-noun}.js` (e.g., `validate-document.js`, `format-currency.js`)
240
+ 2. **Export the function:**
241
+ ```javascript
242
+ function validateDocument(document, type) {
243
+ // Validation logic
244
+ if (!isValid) {
245
+ const { ApiError, StatusCodes } = require('@/utils');
246
+ ApiError(StatusCodes.BAD_REQUEST, `Invalid ${type}`);
247
+ }
248
+ return true;
249
+ }
250
+
251
+ module.exports = { validateDocument };
252
+ ```
253
+ 3. **Import where needed:**
254
+ ```javascript
255
+ const { validateDocument } = require('@/utils/validate-document');
256
+ ```
257
+ 4. **Optionally re-export from `utils/index.js`** if the utility is used across many files
258
+
259
+ ---
260
+
261
+ ## Anti-patterns
262
+
263
+ - **Do NOT put database access in utils.** Utils are pure functions.
264
+ - **Do NOT create utils that depend on `req` or `res`.** Extract data first, then pass to the util.
265
+ - **Do NOT create God utils** (one file with 50+ unrelated functions). Split by category.
266
+ - **Do NOT duplicate validation** that the CRUD layer already does. Use model schemas for field validation.
267
+ - **Do NOT create utils for one-off operations.** Inline the logic if it's only used once.
268
+
269
+ ---
270
+
271
+ ## Checklist
272
+
273
+ When creating a utility:
274
+
275
+ - [ ] File is in `src/utils/`
276
+ - [ ] Filename uses kebab-case with verb prefix
277
+ - [ ] Function is pure (no side effects, no database access)
278
+ - [ ] Throws `ApiError` for validation failures (not generic Error)
279
+ - [ ] Exports named functions (not default export)
280
+ - [ ] Added to `utils/index.js` if widely reused
281
+ - [ ] No HTTP-specific logic (`req`, `res`)
@@ -0,0 +1,315 @@
1
+ # Testing
2
+
3
+ ## Role
4
+
5
+ Tests validate that services, controllers, and business logic work correctly. The framework provides Jest configuration, test database isolation, and queue mocking out of the box.
6
+
7
+ ---
8
+
9
+ ## Configuration
10
+
11
+ ### Jest Config
12
+
13
+ ```
14
+ Location: jest.config.js
15
+ ```
16
+
17
+ ```javascript
18
+ module.exports = {
19
+ clearMocks: true,
20
+ coverageDirectory: 'coverage',
21
+ coverageProvider: 'v8',
22
+ forceExit: true,
23
+ globalSetup: '<rootDir>/src/config/tests/global-setup-tests.js',
24
+ maxWorkers: 4,
25
+ moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' },
26
+ setupFilesAfterEnv: ['<rootDir>/src/config/tests/setup-tests.js'],
27
+ slowTestThreshold: 30,
28
+ testEnvironment: 'node',
29
+ testMatch: ['**/src/**/*.spec.js'],
30
+ transform: { '^.+\\.js$': 'babel-jest' },
31
+ verbose: true,
32
+ };
33
+ ```
34
+
35
+ **Key settings:**
36
+ - `moduleNameMapper`: Supports `@/` path alias in tests
37
+ - `testMatch`: Test files must be `*.spec.js` inside `src/`
38
+ - `globalSetup`: Runs once before all test suites
39
+ - `setupFilesAfterEnv`: Runs before each test file
40
+ - `maxWorkers: 4`: Limits parallel test runners
41
+
42
+ ### Global Setup
43
+
44
+ ```
45
+ Location: src/config/tests/global-setup-tests.js
46
+ ```
47
+
48
+ Loads `.env.test` for the test environment:
49
+ ```javascript
50
+ require('dotenv').config({ path: '.env.test' });
51
+ ```
52
+
53
+ ### Test Setup
54
+
55
+ ```
56
+ Location: src/config/tests/setup-tests.js
57
+ ```
58
+
59
+ Adds extended matchers:
60
+ ```javascript
61
+ const matchers = require('jest-extended');
62
+ expect.extend(matchers);
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Test Database
68
+
69
+ ### Configuration
70
+
71
+ Create `.env.test` with a separate test database:
72
+
73
+ ```
74
+ NODE_ENV="test"
75
+ DB_HOST="127.0.0.1"
76
+ DB_USER="root"
77
+ DB_PASSWORD=""
78
+ DB_NAME="my_project_tests" # MUST end with '_tests'
79
+ DB_PORT="3306"
80
+ ```
81
+
82
+ **Critical:** The database name MUST contain `_tests`. The connection factory throws if the test environment tries to use a non-test database.
83
+
84
+ ### Setup
85
+
86
+ 1. Create the test database in MySQL
87
+ 2. Run migrations against the test database
88
+ 3. Tests use the same schema as production
89
+
90
+ ---
91
+
92
+ ## Queue Mocking
93
+
94
+ In the test environment (`NODE_ENV=test`), the queue system automatically returns a mock:
95
+
96
+ ```javascript
97
+ // From queues.js initializeQueues:
98
+ if (isTestEnv) {
99
+ return { add: jest.fn() };
100
+ }
101
+ ```
102
+
103
+ You can assert jobs were queued without actually processing them:
104
+
105
+ ```javascript
106
+ expect(Queue.add).toHaveBeenCalledWith('send-mail', expect.objectContaining({
107
+ templateName: 'welcome',
108
+ }));
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Test File Naming
114
+
115
+ Test files live next to their source:
116
+
117
+ ```
118
+ src/database/services/
119
+ ├── users.services.js
120
+ ├── users.services.spec.js # Tests for users service
121
+ src/controllers/users/
122
+ ├── users.controller.js
123
+ ├── users.controller.spec.js # Tests for users controller
124
+ ```
125
+
126
+ **Convention:** `{filename}.spec.js`
127
+
128
+ ---
129
+
130
+ ## Testing Services
131
+
132
+ ```javascript
133
+ // src/database/services/users.services.spec.js
134
+ const { getAppInstance, getQueue } = require('@/config/app');
135
+ const knex = require('@/database/connection')('test');
136
+
137
+ let services;
138
+ let Queue;
139
+
140
+ beforeAll(async () => {
141
+ const app = getAppInstance(knex);
142
+ await app.readyPromise;
143
+ Queue = await getQueue(knex);
144
+
145
+ // Access services from the app
146
+ // (exact access pattern depends on project setup)
147
+ });
148
+
149
+ afterAll(async () => {
150
+ await knex.destroy();
151
+ });
152
+
153
+ describe('UsersServices', () => {
154
+ it('should create a user with permissions', async () => {
155
+ const result = await services.UsersServices.newUserRegistered({
156
+ name: 'Test User',
157
+ email: 'test@example.com',
158
+ password: 'password123',
159
+ });
160
+
161
+ expect(result.success).toBe(true);
162
+ expect(result.data.id).toBeDefined();
163
+
164
+ // Verify permissions were created
165
+ const { data: permissions } = await services.UsersPermissionsServices.getWhere(
166
+ 'user_id', result.data.id
167
+ );
168
+ expect(permissions.length).toBeGreaterThan(0);
169
+ });
170
+ });
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Testing Controllers / Routes
176
+
177
+ Using supertest:
178
+
179
+ ```javascript
180
+ const request = require('supertest');
181
+ const { getAppInstance } = require('@/config/app');
182
+ const knex = require('@/database/connection')('test');
183
+
184
+ let app;
185
+ let token;
186
+
187
+ beforeAll(async () => {
188
+ const appInstance = getAppInstance(knex);
189
+ await appInstance.readyPromise;
190
+ app = appInstance.express;
191
+
192
+ // Get auth token
193
+ const loginResponse = await request(app)
194
+ .post('/auth/login')
195
+ .send({ email: 'test@example.com', password: 'password123' });
196
+ token = loginResponse.body.data.token;
197
+ });
198
+
199
+ afterAll(async () => {
200
+ await knex.destroy();
201
+ });
202
+
203
+ describe('GET /users', () => {
204
+ it('should return user profile', async () => {
205
+ const response = await request(app)
206
+ .get('/users')
207
+ .set('Authorization', `Bearer ${token}`)
208
+ .expect(200);
209
+
210
+ expect(response.body.data.email).toBe('test@example.com');
211
+ });
212
+
213
+ it('should reject unauthenticated requests', async () => {
214
+ await request(app)
215
+ .get('/users')
216
+ .expect(403);
217
+ });
218
+ });
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Test Data Management
224
+
225
+ ### Transaction Rollback Pattern
226
+
227
+ Wrap each test in a transaction that rolls back:
228
+
229
+ ```javascript
230
+ let trx;
231
+
232
+ beforeEach(async () => {
233
+ trx = await knex.transaction();
234
+ });
235
+
236
+ afterEach(async () => {
237
+ await trx.rollback();
238
+ });
239
+
240
+ it('should create something', async () => {
241
+ // Use trx for all operations — rolled back after test
242
+ });
243
+ ```
244
+
245
+ ### Truncation Pattern
246
+
247
+ Clean specific tables between tests:
248
+
249
+ ```javascript
250
+ afterEach(async () => {
251
+ await knex('users').truncate();
252
+ await knex('companies').truncate();
253
+ });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Scripts
259
+
260
+ ```bash
261
+ # Run all tests
262
+ npm test
263
+
264
+ # Run specific test file
265
+ npx jest src/database/services/users.services.spec.js
266
+
267
+ # Watch mode (re-run on file changes)
268
+ npm run test:watch
269
+
270
+ # With coverage report
271
+ npm run test:coverage
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Creating from Scratch
277
+
278
+ 1. **Create the test file:** `{component}.spec.js` next to the component
279
+ 2. **Use the template:**
280
+ ```javascript
281
+ describe('ComponentName', () => {
282
+ it('should do something', async () => {
283
+ // Arrange
284
+ // Act
285
+ // Assert
286
+ });
287
+ });
288
+ ```
289
+ 3. **Set up test fixtures** (database connection, auth tokens)
290
+ 4. **Clean up** in `afterAll` / `afterEach`
291
+
292
+ ---
293
+
294
+ ## Anti-patterns
295
+
296
+ - **Do NOT test against the production database.** Always use `.env.test` with a `_tests` database.
297
+ - **Do NOT forget `forceExit: true`** — without it, Jest may hang due to open connections.
298
+ - **Do NOT import services directly.** Use the app bootstrap to get properly initialized instances.
299
+ - **Do NOT test CRUD methods directly** (they're tested by knex-extended-crud). Test your custom service methods and controller logic.
300
+ - **Do NOT skip cleanup.** Leftover test data causes flaky tests.
301
+
302
+ ---
303
+
304
+ ## Checklist
305
+
306
+ When writing tests:
307
+
308
+ - [ ] Test file is named `{component}.spec.js`
309
+ - [ ] Test file is next to the source file
310
+ - [ ] Uses `.env.test` database (name contains `_tests`)
311
+ - [ ] Database connections destroyed in `afterAll`
312
+ - [ ] Test data cleaned up between tests
313
+ - [ ] Auth tokens obtained via login (not hardcoded)
314
+ - [ ] Queue interactions verified via `jest.fn()` assertions
315
+ - [ ] Tests are independent (don't depend on execution order)