@felloh-org/lambda-wrapper 1.11.214 → 1.11.216
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/.concourse/pipeline.yml +49 -3
- package/.concourse/private.yml +0 -0
- package/.github/workflows/release.yml +5 -8
- package/.github/workflows/sast.yml +20 -0
- package/README.md +573 -97
- package/SECURITY.md +58 -0
- package/{ormconfig.js → data-source.js} +2 -4
- package/dist/index.js +1 -1
- package/jest.config.js +8 -0
- package/package.json +36 -13
- package/webpack.config.js +1 -1
package/README.md
CHANGED
|
@@ -33,140 +33,588 @@ export const handler = LambdaWrapper(configuration, async (di, request) => {
|
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Lambda Wrapper
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
|
|
40
|
+
```javascript
|
|
41
|
+
import { LambdaWrapper } from '@felloh-org/lambda-wrapper';
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
55
|
-
| Request | `DEFINITIONS.REQUEST` | Parsed request data
|
|
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` |
|
|
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
|
|
60
|
-
| HTTP | `DEFINITIONS.HTTP` | HTTP client
|
|
61
|
-
| Webhook | `DEFINITIONS.WEBHOOK` |
|
|
62
|
-
|
|
|
63
|
-
|
|
|
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({ where: { 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
73
|
-
|
|
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
|
-
//
|
|
76
|
-
return
|
|
77
|
-
.setErrors([{ field: 'email', message: 'Invalid email' }])
|
|
78
|
-
.generate();
|
|
409
|
+
// Static shorthand
|
|
410
|
+
return ResponseModel.generate({ result: 'ok' }, 200);
|
|
79
411
|
```
|
|
80
412
|
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
| `nuapay` | Nuapay
|
|
116
|
-
| `nuvei` | Nuvei
|
|
117
|
-
| `trust-payments` | Trust Payments
|
|
118
|
-
| `total-processing` | Total Processing
|
|
119
|
-
| `planet` | Planet
|
|
120
|
-
| `saltedge` | Saltedge
|
|
121
|
-
| `basis-theory` | Tokenisation
|
|
122
|
-
| `go-cardless` | GoCardless
|
|
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
|
-
###
|
|
561
|
+
### Migrations
|
|
126
562
|
|
|
127
|
-
|
|
563
|
+
Run migrations against the database:
|
|
128
564
|
|
|
129
|
-
```
|
|
130
|
-
|
|
565
|
+
```bash
|
|
566
|
+
# Create database schemas
|
|
567
|
+
yarn orm:schema:create
|
|
131
568
|
|
|
132
|
-
|
|
133
|
-
|
|
569
|
+
# Run pending migrations
|
|
570
|
+
yarn orm:migration:run
|
|
134
571
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
143
|
-
|
|
575
|
+
# Revert last migration
|
|
576
|
+
yarn orm:revert
|
|
144
577
|
```
|
|
145
578
|
|
|
146
|
-
|
|
579
|
+
Migrations require the following environment variables:
|
|
147
580
|
|
|
148
|
-
|
|
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
|
-
|
|
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 {
|
|
602
|
+
import { PromisifiedDelay } from '@felloh-org/lambda-wrapper';
|
|
154
603
|
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
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
|
-
├──
|
|
219
|
-
├──
|
|
220
|
-
├──
|
|
221
|
-
├──
|
|
222
|
-
├──
|
|
223
|
-
├──
|
|
224
|
-
├──
|
|
225
|
-
├──
|
|
226
|
-
|
|
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
|