@felloh-org/lambda-wrapper 1.11.214 → 1.11.215

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/README.md CHANGED
@@ -33,140 +33,588 @@ export const handler = LambdaWrapper(configuration, async (di, request) => {
33
33
  });
34
34
  ```
35
35
 
36
- ## Features
36
+ ## Lambda Wrapper
37
37
 
38
- ### Lambda Wrapper
38
+ The core wrapper function takes a configuration object and a handler function, returning a Lambda-compatible function with built-in dependency injection, error handling, logging, and metrics.
39
39
 
40
- The core wrapper provides:
40
+ ```javascript
41
+ import { LambdaWrapper } from '@felloh-org/lambda-wrapper';
41
42
 
42
- - **Dependency Injection** - Access services via `di.get(DEFINITIONS.SERVICE_NAME)`
43
- - **Automatic Error Handling** - Catches and formats errors consistently
44
- - **Request Parsing** - Parses body, query params, path params, and headers
45
- - **Logging & Metrics** - Built-in logging with automatic metric collection
46
- - **Warm-up Support** - Handles `serverless-plugin-warmup` events automatically
43
+ const handler = LambdaWrapper(configuration, async (di, request, callback) => {
44
+ // di - DependencyInjection container with all services
45
+ // request - RequestService instance with parsed request data
46
+ // callback - Lambda callback (for non-async handlers)
47
+ });
48
+ ```
47
49
 
48
- ### Built-in Services
50
+ ### Handler Patterns
51
+
52
+ **Async handler (recommended):**
53
+
54
+ ```javascript
55
+ export const handler = LambdaWrapper(config, async (di, request) => {
56
+ const response = new ResponseModel({ result: 'ok' }, 200);
57
+ return response.generate();
58
+ });
59
+ ```
60
+
61
+ **Callback handler:**
62
+
63
+ ```javascript
64
+ export const handler = LambdaWrapper(config, (di, request, callback) => {
65
+ const response = new ResponseModel({ result: 'ok' }, 200);
66
+ callback(null, response.generate());
67
+ });
68
+ ```
69
+
70
+ **Throw errors as responses:**
71
+
72
+ ```javascript
73
+ export const handler = LambdaWrapper(config, async (di, request) => {
74
+ const params = request.getAll();
75
+
76
+ if (!params.email) {
77
+ const error = new ResponseModel({}, 422);
78
+ error.addError({
79
+ title: 'Validation Error',
80
+ message: 'Email is required',
81
+ });
82
+ throw error; // Returns a 422 response, not a 500
83
+ }
84
+
85
+ // continue processing...
86
+ });
87
+ ```
88
+
89
+ ### Error Handling
90
+
91
+ The wrapper automatically catches errors and returns appropriate responses:
92
+
93
+ - **ResponseModel thrown** - returns the response as-is (e.g. 400, 404, 422)
94
+ - **LambdaTermination thrown** - returns a response using the error's `code` property
95
+ - **Unhandled errors** - returns a 500 response and logs the error
96
+ - **Database size limit errors** - returns a 413 response
97
+
98
+ 5xx errors are logged automatically; 4xx errors are not.
99
+
100
+ ### Warm-up Support
101
+
102
+ The wrapper handles [serverless-plugin-warmup](https://github.com/juanjoDiaz/serverless-plugin-warmup) events automatically. When a warmup event is detected, the handler returns immediately without executing your code.
49
103
 
50
- Access these via `di.get(DEFINITIONS.SERVICE_NAME)`:
104
+ ## Dependency Injection
105
+
106
+ Every handler receives a `DependencyInjection` container as its first argument. The container provides access to all built-in services and any custom services you register.
107
+
108
+ ### Built-in Services
51
109
 
52
110
  | Service | Definition | Description |
53
111
  |---------|------------|-------------|
54
- | Logger | `DEFINITIONS.LOGGER` | Structured logging with metric support |
55
- | Request | `DEFINITIONS.REQUEST` | Parsed request data (body, params, headers) |
112
+ | Logger | `DEFINITIONS.LOGGER` | Structured logging with Winston and Baselime |
113
+ | Request | `DEFINITIONS.REQUEST` | Parsed HTTP request data |
56
114
  | Warehouse | `DEFINITIONS.WAREHOUSE` | TypeORM database connection manager |
57
- | Authentication | `DEFINITIONS.AUTHENTICATION` | JWT authentication and user context |
115
+ | Authentication | `DEFINITIONS.AUTHENTICATION` | User role and organisation checks |
116
+ | User | `DEFINITIONS.USER` | Current user from Cognito JWT claims |
58
117
  | EventBridge | `DEFINITIONS.EVENT_BRIDGE` | AWS EventBridge event publishing |
59
- | Secrets | `DEFINITIONS.SECRETS` | AWS Secrets Manager integration |
60
- | HTTP | `DEFINITIONS.HTTP` | HTTP client for external requests |
61
- | Webhook | `DEFINITIONS.WEBHOOK` | Webhook dispatch service |
62
- | User | `DEFINITIONS.USER` | User service utilities |
63
- | AuditLogger | `DEFINITIONS.AUDIT_LOGGER` | Audit trail logging |
118
+ | Secrets | `DEFINITIONS.SECRETS` | AWS Secrets Manager with caching |
119
+ | HTTP | `DEFINITIONS.HTTP` | Axios-based HTTP client |
120
+ | Webhook | `DEFINITIONS.WEBHOOK` | HMAC-signed webhook dispatch |
121
+ | AuditLogger | `DEFINITIONS.AUDIT_LOGGER` | Audit event creation via EventBridge |
122
+ | PaymentInitiationObject | `DEFINITIONS.PAYMENT_INITIATION_OBJECT` | Payment link and ecommerce lookup |
123
+
124
+ ### Custom Services
125
+
126
+ Create custom services by extending `DependencyAwareClass`:
127
+
128
+ ```javascript
129
+ import { DependencyAwareClass, DEFINITIONS } from '@felloh-org/lambda-wrapper';
130
+
131
+ class PaymentProcessor extends DependencyAwareClass {
132
+ async processPayment(transactionId) {
133
+ const logger = this.getContainer().get(DEFINITIONS.LOGGER);
134
+ const warehouse = this.getContainer().get(DEFINITIONS.WAREHOUSE);
135
+
136
+ const connection = await warehouse.connect();
137
+ const repo = connection.getRepository(TransactionEntity);
138
+ const transaction = await repo.findOne({ id: transactionId });
139
+
140
+ logger.info(`Processing payment ${transactionId}`);
141
+
142
+ return transaction;
143
+ }
144
+ }
145
+ ```
146
+
147
+ Register custom services in the configuration:
64
148
 
65
- ### Response Model
149
+ ```javascript
150
+ const configuration = {
151
+ DEPENDENCIES: {
152
+ PAYMENT_PROCESSOR: PaymentProcessor,
153
+ },
154
+ };
155
+
156
+ export const handler = LambdaWrapper(configuration, async (di, request) => {
157
+ const processor = di.get('PAYMENT_PROCESSOR');
158
+ await processor.processPayment('txn_123');
159
+ });
160
+ ```
66
161
 
67
- All Lambda responses are standardised:
162
+ ### Container API
163
+
164
+ ```javascript
165
+ // Get a service
166
+ const logger = di.get(DEFINITIONS.LOGGER);
167
+
168
+ // Get the raw Lambda event
169
+ const event = di.getEvent();
170
+
171
+ // Get the Lambda context
172
+ const context = di.getContext();
173
+
174
+ // Get configuration values
175
+ const config = di.getConfiguration(); // full config
176
+ const dbHost = di.getConfiguration('DB_HOST'); // specific key, returns null if missing
177
+
178
+ // Check if running in serverless-offline
179
+ if (di.isOffline) {
180
+ // local development mode
181
+ }
182
+ ```
183
+
184
+ ## Services
185
+
186
+ ### RequestService
187
+
188
+ Parses and provides access to all HTTP request data.
189
+
190
+ ```javascript
191
+ const request = di.get(DEFINITIONS.REQUEST);
192
+
193
+ // Get a single parameter (checks query params for GET, body for POST)
194
+ const email = request.get('email');
195
+ const page = request.get('page', '1'); // with default value
196
+
197
+ // Get all parameters
198
+ const allParams = request.getAll();
199
+
200
+ // Force reading from query string regardless of HTTP method
201
+ const queryParams = request.getAll('GET');
202
+
203
+ // Path parameters
204
+ const id = request.getPathParameter('id');
205
+ const allPath = request.getPathParameter(); // all path params
206
+
207
+ // Headers (case-insensitive)
208
+ const contentType = request.getHeader('Content-Type');
209
+ const allHeaders = request.getAllHeaders();
210
+
211
+ // Authorization
212
+ const token = request.getAuthorizationToken(); // extracts Bearer token
213
+
214
+ // Client info
215
+ const ip = request.getIp();
216
+ const browser = request.getUserBrowserAndDevice(); // parsed user-agent
217
+
218
+ // AWS event records (SQS, SNS, etc.)
219
+ const record = request.getAWSRecords();
220
+ ```
221
+
222
+ **Content type support:**
223
+
224
+ The request service automatically parses the body based on the `Content-Type` header:
225
+
226
+ - `application/json` - JSON parsing
227
+ - `application/x-www-form-urlencoded` - form data parsing
228
+ - `text/xml` - XML parsing via xml2js
229
+ - `multipart/form-data` - file upload parsing with Buffer support
230
+
231
+ **Request validation:**
232
+
233
+ ```javascript
234
+ const constraints = {
235
+ email: { presence: true, email: true },
236
+ amount: { presence: true, numericality: { greaterThan: 0 } },
237
+ start_date: { datetime: { dateOnly: true } },
238
+ };
239
+
240
+ await request.validateAgainstConstraints(constraints);
241
+ // Resolves on success, rejects with a 422 ResponseModel on failure
242
+ ```
243
+
244
+ Validation uses [validate.js](https://validatejs.org) with datetime support via moment.
245
+
246
+ ### LoggerService
247
+
248
+ Structured logging with Winston and Baselime integration. Pretty-prints output in serverless-offline mode.
249
+
250
+ ```javascript
251
+ const logger = di.get(DEFINITIONS.LOGGER);
252
+
253
+ // Standard logging
254
+ logger.info('Payment processed successfully');
255
+ logger.error(new Error('Connection failed'));
256
+ logger.warning(error); // routes to error() or info() based on LOGGER_SOFT_WARNING env
257
+
258
+ // Labels (lightweight markers for tracing)
259
+ logger.label('payment-initiated');
260
+ logger.label('silent-label', true); // silent = not logged to console
261
+
262
+ // Metrics
263
+ logger.metric('response_time', 250);
264
+ logger.metric('ip_address', '10.0.0.1', true); // silent = not logged to console
265
+
266
+ // Object inspection
267
+ logger.object('Processing payload', { id: 1, amount: 500 });
268
+ logger.object('Failed response', errorObj, 'error'); // supports 'info', 'warning', 'error'
269
+ ```
270
+
271
+ Axios errors are automatically trimmed to `config`, `message`, `response.status`, and `response.data` - stripping verbose request/header data from logs.
272
+
273
+ ### WarehouseService
274
+
275
+ Manages TypeORM database connections with connection caching.
276
+
277
+ ```javascript
278
+ const warehouse = di.get(DEFINITIONS.WAREHOUSE);
279
+
280
+ // Get a connection (cached by default)
281
+ const connection = await warehouse.connect();
282
+
283
+ // Force a new connection (bypass cache)
284
+ const fresh = await warehouse.connect(null, false);
285
+
286
+ // Connect to a specific database
287
+ const analytics = await warehouse.connect('analytics_db');
288
+
289
+ // Health check
290
+ await warehouse.status();
291
+ ```
292
+
293
+ **Connection types:**
294
+
295
+ Set via the `WAREHOUSE_TYPE` environment variable:
296
+
297
+ - `postgres` - direct PostgreSQL connection using `WAREHOUSE_HOST`, `WAREHOUSE_USERNAME`, `WAREHOUSE_PASSWORD`, `WAREHOUSE_DATABASE`
298
+ - `postgres-ssm` - credentials fetched from AWS Secrets Manager using `DB_CREDS_ARN`
299
+
300
+ ### AuthenticationService
301
+
302
+ Checks user roles and organisation access. Lazy-initialises a database connection on first use.
303
+
304
+ ```javascript
305
+ const auth = di.get(DEFINITIONS.AUTHENTICATION);
306
+ await auth.init();
307
+
308
+ // Check organisation access and roles in one call
309
+ // Throws a 401 ResponseModel if checks fail
310
+ await auth.checkUserRoles('org-123', ['admin', 'finance']);
311
+
312
+ // Check organisation access only
313
+ await auth.checkUserRoles('org-123');
314
+
315
+ // Check roles only
316
+ await auth.checkUserRoles(null, ['admin']);
317
+
318
+ // Individual checks
319
+ const roles = await auth.fetchUserRoles(); // ['admin', 'viewer']
320
+ const hasAdmin = await auth.hasRole('admin'); // true/false
321
+ const orgs = await auth.fetchUserOrganisations(user); // includes descendants
322
+ ```
323
+
324
+ ### HTTPService
325
+
326
+ Axios-based HTTP client with configurable timeout.
327
+
328
+ ```javascript
329
+ const http = di.get(DEFINITIONS.HTTP);
330
+
331
+ // Default timeout is 10 seconds
332
+ http.setDefaultTimeout(30000);
333
+
334
+ const response = await http.request({
335
+ method: 'POST',
336
+ url: 'https://api.example.com/payments',
337
+ headers: { Authorization: 'Bearer token' },
338
+ data: { amount: 1000 },
339
+ });
340
+ ```
341
+
342
+ ### SecretsService
343
+
344
+ Fetches and caches secrets from AWS Secrets Manager.
345
+
346
+ ```javascript
347
+ const secrets = di.get(DEFINITIONS.SECRETS);
348
+
349
+ // Fetched once, then cached for the Lambda execution
350
+ const creds = await secrets.get('arn:aws:secretsmanager:eu-west-1:123:secret:db-creds');
351
+ // { username: 'admin', password: '...', host: '...', ... }
352
+ ```
353
+
354
+ ### WebhookService
355
+
356
+ Dispatches HMAC-SHA256 signed webhooks.
357
+
358
+ ```javascript
359
+ const webhook = di.get(DEFINITIONS.WEBHOOK);
360
+
361
+ await webhook.send(
362
+ 'https://example.com/webhook',
363
+ { event: 'payment.completed', transaction_id: 'txn_123' },
364
+ 'webhook-signing-secret'
365
+ );
366
+ // POST with X-Signature header containing HMAC-SHA256 hex digest
367
+ ```
368
+
369
+ ### AuditLoggerService
370
+
371
+ Creates audit trail events via EventBridge.
372
+
373
+ ```javascript
374
+ const auditLogger = di.get(DEFINITIONS.AUDIT_LOGGER);
375
+
376
+ await auditLogger.createEvent('payment.refunded', 'entity-123', 'org-456');
377
+ ```
378
+
379
+ ## Response Model
380
+
381
+ All Lambda responses use a standardised format with CORS headers.
68
382
 
69
383
  ```javascript
70
384
  import { ResponseModel } from '@felloh-org/lambda-wrapper';
71
385
 
72
- // Success response
73
- return new ResponseModel({ users: [...] }, 200).generate();
386
+ // Basic response
387
+ const response = new ResponseModel({ users: [] }, 200);
388
+ return response.generate();
389
+
390
+ // Building a response
391
+ const response = new ResponseModel();
392
+ response.setData({ id: 1, name: 'Test' });
393
+ response.setCode(201);
394
+ response.setMetaVariable('pagination', { page: 1, total: 10 });
395
+ response.setBodyVariable('warnings', ['Deprecated field used']);
396
+ return response.generate();
397
+
398
+ // Adding errors
399
+ const error = new ResponseModel({}, 422);
400
+ error.addError({
401
+ title: 'Validation Error',
402
+ message: 'Email is not valid',
403
+ documentation_url: 'https://developers.felloh.com/errors#error-responses',
404
+ type: 'validation',
405
+ code: 'validation.email',
406
+ });
407
+ throw error; // or return error.generate()
74
408
 
75
- // Error response
76
- return new ResponseModel({}, 400)
77
- .setErrors([{ field: 'email', message: 'Invalid email' }])
78
- .generate();
409
+ // Static shorthand
410
+ return ResponseModel.generate({ result: 'ok' }, 200);
79
411
  ```
80
412
 
81
- Response format:
413
+ **Generated response format:**
82
414
 
83
415
  ```json
84
416
  {
85
417
  "statusCode": 200,
86
418
  "headers": {
87
419
  "Content-Type": "application/json",
88
- "Access-Control-Allow-Origin": "*"
420
+ "Access-Control-Allow-Origin": "*",
421
+ "Access-Control-Allow-Credentials": true
89
422
  },
90
- "body": {
91
- "data": { ... },
92
- "errors": [],
93
- "meta": {
94
- "code": 200,
95
- "reason": "OK",
96
- "message": "Success",
97
- "request_id": "uuid"
98
- }
99
- }
423
+ "body": "{\"data\":{},\"errors\":[],\"meta\":{\"code\":200,\"reason\":\"OK\",\"message\":\"The request was successful\",\"request_id\":\"uuid-v4\"}}"
100
424
  }
101
425
  ```
102
426
 
103
- ### Database Entities (TypeORM)
427
+ **Supported status codes:**
428
+
429
+ | Code | Reason |
430
+ |------|--------|
431
+ | 200 | OK |
432
+ | 201 | Created |
433
+ | 204 | No Content |
434
+ | 400 | Bad Request |
435
+ | 401 | Unauthorized |
436
+ | 403 | Forbidden |
437
+ | 404 | Not Found |
438
+ | 406 | Not Acceptable |
439
+ | 410 | Gone |
440
+ | 413 | Request Entity Too Large |
441
+ | 422 | Unprocessable Entity |
442
+ | 429 | Too Many Requests |
443
+ | 500 | Internal Server Error |
444
+ | 502 | Bad Gateway |
445
+ | 503 | Service Unavailable |
446
+ | 504 | Gateway Timeout |
447
+
448
+ ## Status Model
449
+
450
+ Used for health checks and service status reporting.
451
+
452
+ ```javascript
453
+ import { StatusModel, STATUS_TYPES } from '@felloh-org/lambda-wrapper';
454
+
455
+ const status = new StatusModel('database', STATUS_TYPES.OK);
456
+ // STATUS_TYPES: OK, ACCEPTABLE_FAILURE, APPLICATION_FAILURE
457
+ ```
458
+
459
+ ## Lambda Termination
460
+
461
+ A custom error class for terminating Lambda execution with a specific status code and consumer-facing message.
462
+
463
+ ```javascript
464
+ import { LambdaTermination } from '@felloh-org/lambda-wrapper';
465
+
466
+ // Terminates with a 503 response
467
+ throw new LambdaTermination(
468
+ 'Payment provider timeout after 30s', // internal (logged)
469
+ 503, // status code
470
+ 'Service temporarily unavailable', // body (returned to consumer)
471
+ 'Provider X timed out' // details
472
+ );
473
+ ```
474
+
475
+ ## EventBridge Events
476
+
477
+ Publish typed events to AWS EventBridge.
478
+
479
+ ```javascript
480
+ import {
481
+ LambdaWrapper,
482
+ DEFINITIONS,
483
+ TransactionCompleteEvent,
484
+ TransactionCompleteEmailEvent,
485
+ UserRegistrationEvent,
486
+ CSVEmailEvent,
487
+ RefundRequestEmailEvent,
488
+ BaseEvent,
489
+ } from '@felloh-org/lambda-wrapper';
490
+
491
+ export const handler = LambdaWrapper(config, async (di) => {
492
+ const eventBridge = di.get(DEFINITIONS.EVENT_BRIDGE);
493
+
494
+ // Use a pre-built event
495
+ const event = new TransactionCompleteEvent();
496
+ event.setDetailParam('transaction_id', 'txn_123');
497
+ await eventBridge.put(event);
498
+
499
+ // Or build a custom event
500
+ const custom = new BaseEvent('order.shipped');
501
+ custom.setDetailParam('order_id', 'ord_456');
502
+ custom.setDetail({ order_id: 'ord_456', carrier: 'DHL' });
503
+ await eventBridge.put(custom);
504
+ });
505
+ ```
506
+
507
+ **Available event types:**
508
+
509
+ | Event | Description |
510
+ |-------|-------------|
511
+ | `BaseEvent` | Generic event, set your own detail type |
512
+ | `TransactionCompleteEvent` | Transaction completed |
513
+ | `TransactionCompleteEmailEvent` | Transaction receipt email |
514
+ | `UserRegistrationEvent` | New user registration |
515
+ | `UserPasswordChangedEvent` | Password changed |
516
+ | `UserPasswordResetEvent` | Password reset requested |
517
+ | `SandboxRegistrationEvent` | Sandbox account registration |
518
+ | `CSVEmailEvent` | CSV export email |
519
+ | `POSCRequestEmailEvent` | POSC request email |
520
+ | `RefundRequestEmailEvent` | Refund request notification |
521
+
522
+ ## Webhook Models
523
+
524
+ Format transaction and refund data for webhook payloads.
525
+
526
+ ```javascript
527
+ import { TransactionWebhookModel, RefundWebhookModel } from '@felloh-org/lambda-wrapper';
528
+
529
+ // Transaction webhook
530
+ const payload = new TransactionWebhookModel(transaction, 'Payment received');
531
+ const data = payload.get();
532
+ // { transaction: { id, narrative }, amount, booking, status, currency, surcharge, ... }
533
+ ```
534
+
535
+ ## Database (TypeORM)
536
+
537
+ ### Entities
104
538
 
105
539
  The library includes TypeORM entities organised by domain:
106
540
 
107
541
  | Domain | Description |
108
542
  |--------|-------------|
109
- | `payment` | Transactions, refunds, chargebacks, payment links, providers |
110
- | `user` | Users, organisations, roles, features, webhooks |
111
- | `bank` | Accounts, ledgers, settlements, disbursals |
112
- | `agent-data` | Bookings, components, suppliers |
113
- | `acquirer` | BIN data, batches, adjustments |
114
- | `aisp` | Open banking transactions and connections |
115
- | `nuapay` | Nuapay integration entities |
116
- | `nuvei` | Nuvei payment processor entities |
117
- | `trust-payments` | Trust Payments integration |
118
- | `total-processing` | Total Processing integration |
119
- | `planet` | Planet payment processor entities |
120
- | `saltedge` | Saltedge banking integration |
121
- | `basis-theory` | Tokenisation entities |
122
- | `go-cardless` | GoCardless Direct Debit |
543
+ | `payment` | Transactions, refunds, chargebacks, payment links, ecommerce, providers, sessions |
544
+ | `user` | Users, organisations, roles, features, webhooks, billing, partners, portals |
545
+ | `bank` | Accounts, ledgers, settlements, disbursals, adjustments, beneficiaries |
546
+ | `agent-data` | Bookings, components, suppliers, package types |
547
+ | `acquirer` | BIN data, batches, adjustments, band rates, transaction settlements |
548
+ | `aisp` | Open banking transactions, GoCardless accounts/agreements/requisitions |
549
+ | `nuapay` | Nuapay accounts, balances, beneficiaries, transactions |
550
+ | `nuvei` | Nuvei batches, merchants, transactions, metadata |
551
+ | `trust-payments` | Trust Payments batches, merchants, transactions, chargebacks, fees |
552
+ | `total-processing` | Total Processing transactions and metadata |
553
+ | `planet` | Planet transactions, metadata, batches |
554
+ | `saltedge` | Saltedge accounts, connections, customers, transactions, leads |
555
+ | `basis-theory` | Tokenisation tokens |
556
+ | `go-cardless` | GoCardless organisation configuration |
123
557
  | `marketing` | Marketing contacts and ESUs |
558
+ | `reference` | Countries |
559
+ | `playbook` | Playbooks, requests, secrets, versions |
124
560
 
125
- ### EventBridge Events
561
+ ### Migrations
126
562
 
127
- Publish events to AWS EventBridge:
563
+ Run migrations against the database:
128
564
 
129
- ```javascript
130
- import { LambdaWrapper, DEFINITIONS, TransactionCustomerReceipt } from '@felloh-org/lambda-wrapper';
565
+ ```bash
566
+ # Create database schemas
567
+ yarn orm:schema:create
131
568
 
132
- export const handler = LambdaWrapper(configuration, async (di, request) => {
133
- const eventBridge = di.get(DEFINITIONS.EVENT_BRIDGE);
569
+ # Run pending migrations
570
+ yarn orm:migration:run
134
571
 
135
- const event = new TransactionCustomerReceipt();
136
- event.setCustomerEmail('customer@example.com');
137
- event.setOrgName('Acme Travel');
138
- event.setAmount(150000);
139
- event.setCurrencyMajorUnit('GBP');
140
- event.setId('txn_123');
572
+ # Generate a new migration
573
+ yarn orm:migration:generate <name>
141
574
 
142
- await eventBridge.send(event);
143
- });
575
+ # Revert last migration
576
+ yarn orm:revert
144
577
  ```
145
578
 
146
- See [Event Documentation](./docs/events/README.md) for all available events.
579
+ Migrations require the following environment variables:
147
580
 
148
- ### Dependency Injection
581
+ | Variable | Description |
582
+ |----------|-------------|
583
+ | `WAREHOUSE_TYPE` | `postgres` or `postgres-ssm` |
584
+ | `WAREHOUSE_HOST` | Database host (for `postgres` type) |
585
+ | `WAREHOUSE_USERNAME` | Database username (for `postgres` type) |
586
+ | `WAREHOUSE_PASSWORD` | Database password (for `postgres` type) |
587
+ | `WAREHOUSE_DATABASE` | Database name |
588
+ | `WAREHOUSE_SCHEMA` | Database schema |
589
+ | `DB_CREDS_ARN` | Secrets Manager ARN (for `postgres-ssm` type) |
149
590
 
150
- Create custom services by extending `DependencyAwareClass`:
591
+ ### Database Schemas
592
+
593
+ The database is organised into the following schemas: `user`, `payment`, `bank`, `agent_data`, `nuapay`, `trust_payments`, `total_processing`, `token`, `nuvei`, `reference`, `saltedge`, `acquirer`, `operations`, `basis_theory`, `aisp`, `go_cardless`, `planet`, `marketing`, `amex`, `crm`, `shield`.
594
+
595
+ ## Utilities
596
+
597
+ ### PromisifiedDelay
598
+
599
+ Provides randomised delays for retry logic, with configurable latency profiles.
151
600
 
152
601
  ```javascript
153
- import { DependencyAwareClass, DEFINITIONS } from '@felloh-org/lambda-wrapper';
602
+ import { PromisifiedDelay } from '@felloh-org/lambda-wrapper';
154
603
 
155
- class MyCustomService extends DependencyAwareClass {
156
- async doSomething() {
157
- const logger = this.di.get(DEFINITIONS.LOGGER);
158
- const warehouse = this.di.get(DEFINITIONS.WAREHOUSE);
604
+ // High latency profile (2-20s delays, weighted towards shorter)
605
+ const delay = new PromisifiedDelay(true);
159
606
 
160
- // Your service logic
607
+ // Standard latency profile (2-5s delays)
608
+ const delay = new PromisifiedDelay(false);
609
+
610
+ // Use in retry loop
611
+ for (let attempt = 0; attempt < 3; attempt++) {
612
+ try {
613
+ return await makeRequest();
614
+ } catch (error) {
615
+ await delay.get();
161
616
  }
162
617
  }
163
-
164
- // Register in configuration
165
- const configuration = {
166
- DEPENDENCIES: {
167
- MY_SERVICE: MyCustomService,
168
- },
169
- };
170
618
  ```
171
619
 
172
620
  ## Development
@@ -186,6 +634,9 @@ yarn lint
186
634
  # Run tests with coverage
187
635
  yarn test
188
636
 
637
+ # Run dependency vulnerability audit
638
+ yarn audit:check
639
+
189
640
  # Run database migrations
190
641
  yarn orm:migration:run
191
642
 
@@ -205,25 +656,50 @@ yarn orm:schema:create
205
656
  |----------|-------------|
206
657
  | `SERVICE_NAME` | Service identifier for logging and events |
207
658
  | `EVENT_BRIDGE_ARN` | AWS EventBridge bus ARN |
208
- | `DB_HOST` | Database host |
209
- | `DB_PORT` | Database port |
210
- | `DB_USERNAME` | Database username |
211
- | `DB_PASSWORD` | Database password |
212
- | `DB_DATABASE` | Database name |
659
+ | `REGION` | AWS region |
660
+ | `WAREHOUSE_TYPE` | Database connection type (`postgres` or `postgres-ssm`) |
661
+ | `WAREHOUSE_HOST` | Database host |
662
+ | `WAREHOUSE_USERNAME` | Database username |
663
+ | `WAREHOUSE_PASSWORD` | Database password |
664
+ | `WAREHOUSE_DATABASE` | Database name |
665
+ | `WAREHOUSE_SCHEMA` | Database schema |
666
+ | `DB_CREDS_ARN` | Secrets Manager ARN for database credentials |
667
+ | `DB_LOGGING` | Set to `true` to enable TypeORM query logging |
668
+ | `LOGGER_SOFT_WARNING` | Set to `true` to downgrade warnings to info level |
669
+ | `IS_OFFLINE` | Set by serverless-offline for local development |
213
670
 
214
671
  ### Project Structure
215
672
 
216
673
  ```
217
674
  src/
218
- ├── config/ # Dependency definitions
219
- ├── dependency-injection/ # DI implementation
220
- ├── entity/ # TypeORM entities by domain
221
- ├── event/ # EventBridge event classes
222
- ├── migration/ # Database migrations by schema
223
- ├── model/ # Response and data models
224
- ├── service/ # Core services
225
- ├── util/ # Utility functions
226
- └── wrapper/ # Lambda wrapper implementation
675
+ ├── action/ # Action handlers (e.g. unmatched route)
676
+ ├── config/ # Dependency definitions
677
+ ├── dependency-injection/ # DI container and base class
678
+ ├── entity/ # TypeORM entities by domain
679
+ ├── enums/ # Shared enumerations
680
+ ├── event/ # EventBridge event classes
681
+ ├── migration/ # Database migrations by schema
682
+ ├── model/ # Response, status, and webhook models
683
+ ├── service/ # Core services (logger, request, warehouse, auth, etc.)
684
+ ├── util/ # Utilities (LambdaTermination, PromisifiedDelay)
685
+ └── wrapper/ # Lambda wrapper implementation
686
+ ```
687
+
688
+ ### CI/CD
689
+
690
+ The project uses Concourse CI with the following pipeline:
691
+
692
+ 1. **lint-and-test** - runs lint, tests, and dependency audit in parallel
693
+ 2. **migrations.staging** - creates schemas and runs migrations on staging
694
+ 3. **migrations.production** - promotes to production after staging succeeds
695
+ 4. **migrations.sandbox** - promotes to sandbox after staging succeeds
696
+
697
+ ### Testing
698
+
699
+ Tests are colocated with source files (e.g. `src/service/logger.test.js` alongside `src/service/logger.js`).
700
+
701
+ ```bash
702
+ yarn test
227
703
  ```
228
704
 
229
705
  ## License