npm package: @felloh-org/lambda-wrapper
Description: Lambda wrapper for all Felloh Serverless Projects
Author: Felloh.com
Latest version: 1.11.215

# Lambda Wrapper

[![release](https://github.com/felloh-org/lambda-wrapper/actions/workflows/release.yml/badge.svg)](https://github.com/felloh-org/lambda-wrapper/actions/workflows/release.yml)

A shared library for Felloh serverless projects that provides core functionality including dependency injection, logging, request handling, database entities, and response models for AWS Lambda functions.

## Installation

```bash
npm install @felloh-org/lambda-wrapper
# or
yarn add @felloh-org/lambda-wrapper
```

## Quick Start

```javascript
import { LambdaWrapper, DEFINITIONS, ResponseModel } from '@felloh-org/lambda-wrapper';

const configuration = {
  SERVICE_NAME: 'my-service',
};

export const handler = LambdaWrapper(configuration, async (di, request) => {
  const logger = di.get(DEFINITIONS.LOGGER);
  const warehouse = di.get(DEFINITIONS.WAREHOUSE);

  logger.info('Processing request');

  // Your handler logic here

  return new ResponseModel({ message: 'Success' }, 200).generate();
});
```

## Lambda Wrapper

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.

```javascript
import { LambdaWrapper } from '@felloh-org/lambda-wrapper';

const handler = LambdaWrapper(configuration, async (di, request, callback) => {
  // di      - DependencyInjection container with all services
  // request - RequestService instance with parsed request data
  // callback - Lambda callback (for non-async handlers)
});
```

### Handler Patterns

**Async handler (recommended):**

```javascript
export const handler = LambdaWrapper(config, async (di, request) => {
  const response = new ResponseModel({ result: 'ok' }, 200);
  return response.generate();
});
```

**Callback handler:**

```javascript
export const handler = LambdaWrapper(config, (di, request, callback) => {
  const response = new ResponseModel({ result: 'ok' }, 200);
  callback(null, response.generate());
});
```

**Throw errors as responses:**

```javascript
export const handler = LambdaWrapper(config, async (di, request) => {
  const params = request.getAll();

  if (!params.email) {
    const error = new ResponseModel({}, 422);
    error.addError({
      title: 'Validation Error',
      message: 'Email is required',
    });
    throw error;  // Returns a 422 response, not a 500
  }

  // continue processing...
});
```

### Error Handling

The wrapper automatically catches errors and returns appropriate responses:

- **ResponseModel thrown** - returns the response as-is (e.g. 400, 404, 422)
- **LambdaTermination thrown** - returns a response using the error's `code` property
- **Unhandled errors** - returns a 500 response and logs the error
- **Database size limit errors** - returns a 413 response

5xx errors are logged automatically; 4xx errors are not.

### Warm-up Support

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.

## Dependency Injection

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.

### Built-in Services

| Service | Definition | Description |
|---------|------------|-------------|
| Logger | `DEFINITIONS.LOGGER` | Structured logging with Winston and Baselime |
| Request | `DEFINITIONS.REQUEST` | Parsed HTTP request data |
| Warehouse | `DEFINITIONS.WAREHOUSE` | TypeORM database connection manager |
| Authentication | `DEFINITIONS.AUTHENTICATION` | User role and organisation checks |
| User | `DEFINITIONS.USER` | Current user from Cognito JWT claims |
| EventBridge | `DEFINITIONS.EVENT_BRIDGE` | AWS EventBridge event publishing |
| Secrets | `DEFINITIONS.SECRETS` | AWS Secrets Manager with caching |
| HTTP | `DEFINITIONS.HTTP` | Axios-based HTTP client |
| Webhook | `DEFINITIONS.WEBHOOK` | HMAC-signed webhook dispatch |
| AuditLogger | `DEFINITIONS.AUDIT_LOGGER` | Audit event creation via EventBridge |
| PaymentInitiationObject | `DEFINITIONS.PAYMENT_INITIATION_OBJECT` | Payment link and ecommerce lookup |

### Custom Services

Create custom services by extending `DependencyAwareClass`:

```javascript
import { DependencyAwareClass, DEFINITIONS } from '@felloh-org/lambda-wrapper';

class PaymentProcessor extends DependencyAwareClass {
  async processPayment(transactionId) {
    const logger = this.getContainer().get(DEFINITIONS.LOGGER);
    const warehouse = this.getContainer().get(DEFINITIONS.WAREHOUSE);

    const connection = await warehouse.connect();
    const repo = connection.getRepository(TransactionEntity);
    const transaction = await repo.findOne({ id: transactionId });

    logger.info(`Processing payment ${transactionId}`);

    return transaction;
  }
}
```

Register custom services in the configuration:

```javascript
const configuration = {
  DEPENDENCIES: {
    PAYMENT_PROCESSOR: PaymentProcessor,
  },
};

export const handler = LambdaWrapper(configuration, async (di, request) => {
  const processor = di.get('PAYMENT_PROCESSOR');
  await processor.processPayment('txn_123');
});
```

### Container API

```javascript
// Get a service
const logger = di.get(DEFINITIONS.LOGGER);

// Get the raw Lambda event
const event = di.getEvent();

// Get the Lambda context
const context = di.getContext();

// Get configuration values
const config = di.getConfiguration();           // full config
const dbHost = di.getConfiguration('DB_HOST');   // specific key, returns null if missing

// Check if running in serverless-offline
if (di.isOffline) {
  // local development mode
}
```

## Services

### RequestService

Parses and provides access to all HTTP request data.

```javascript
const request = di.get(DEFINITIONS.REQUEST);

// Get a single parameter (checks query params for GET, body for POST)
const email = request.get('email');
const page = request.get('page', '1');              // with default value

// Get all parameters
const allParams = request.getAll();

// Force reading from query string regardless of HTTP method
const queryParams = request.getAll('GET');

// Path parameters
const id = request.getPathParameter('id');
const allPath = request.getPathParameter();          // all path params

// Headers (case-insensitive)
const contentType = request.getHeader('Content-Type');
const allHeaders = request.getAllHeaders();

// Authorization
const token = request.getAuthorizationToken();       // extracts Bearer token

// Client info
const ip = request.getIp();
const browser = request.getUserBrowserAndDevice();   // parsed user-agent

// AWS event records (SQS, SNS, etc.)
const record = request.getAWSRecords();
```

**Content type support:**

The request service automatically parses the body based on the `Content-Type` header:

- `application/json` - JSON parsing
- `application/x-www-form-urlencoded` - form data parsing
- `text/xml` - XML parsing via xml2js
- `multipart/form-data` - file upload parsing with Buffer support

**Request validation:**

```javascript
const constraints = {
  email: { presence: true, email: true },
  amount: { presence: true, numericality: { greaterThan: 0 } },
  start_date: { datetime: { dateOnly: true } },
};

await request.validateAgainstConstraints(constraints);
// Resolves on success, rejects with a 422 ResponseModel on failure
```

Validation uses [validate.js](https://validatejs.org) with datetime support via moment.

### LoggerService

Structured logging with Winston and Baselime integration. Pretty-prints output in serverless-offline mode.

```javascript
const logger = di.get(DEFINITIONS.LOGGER);

// Standard logging
logger.info('Payment processed successfully');
logger.error(new Error('Connection failed'));
logger.warning(error);                            // routes to error() or info() based on LOGGER_SOFT_WARNING env

// Labels (lightweight markers for tracing)
logger.label('payment-initiated');
logger.label('silent-label', true);               // silent = not logged to console

// Metrics
logger.metric('response_time', 250);
logger.metric('ip_address', '10.0.0.1', true);   // silent = not logged to console

// Object inspection
logger.object('Processing payload', { id: 1, amount: 500 });
logger.object('Failed response', errorObj, 'error');  // supports 'info', 'warning', 'error'
```

Axios errors are automatically trimmed to `config`, `message`, `response.status`, and `response.data` - stripping verbose request/header data from logs.

### WarehouseService

Manages TypeORM database connections with connection caching.

```javascript
const warehouse = di.get(DEFINITIONS.WAREHOUSE);

// Get a connection (cached by default)
const connection = await warehouse.connect();

// Force a new connection (bypass cache)
const fresh = await warehouse.connect(null, false);

// Connect to a specific database
const analytics = await warehouse.connect('analytics_db');

// Health check
await warehouse.status();
```

**Connection types:**

Set via the `WAREHOUSE_TYPE` environment variable:

- `postgres` - direct PostgreSQL connection using `WAREHOUSE_HOST`, `WAREHOUSE_USERNAME`, `WAREHOUSE_PASSWORD`, `WAREHOUSE_DATABASE`
- `postgres-ssm` - credentials fetched from AWS Secrets Manager using `DB_CREDS_ARN`

### AuthenticationService

Checks user roles and organisation access. Lazy-initialises a database connection on first use.

```javascript
const auth = di.get(DEFINITIONS.AUTHENTICATION);
await auth.init();

// Check organisation access and roles in one call
// Throws a 401 ResponseModel if checks fail
await auth.checkUserRoles('org-123', ['admin', 'finance']);

// Check organisation access only
await auth.checkUserRoles('org-123');

// Check roles only
await auth.checkUserRoles(null, ['admin']);

// Individual checks
const roles = await auth.fetchUserRoles();                // ['admin', 'viewer']
const hasAdmin = await auth.hasRole('admin');              // true/false
const orgs = await auth.fetchUserOrganisations(user);     // includes descendants
```

### HTTPService

Axios-based HTTP client with configurable timeout.

```javascript
const http = di.get(DEFINITIONS.HTTP);

// Default timeout is 10 seconds
http.setDefaultTimeout(30000);

const response = await http.request({
  method: 'POST',
  url: 'https://api.example.com/payments',
  headers: { Authorization: 'Bearer token' },
  data: { amount: 1000 },
});
```

### SecretsService

Fetches and caches secrets from AWS Secrets Manager.

```javascript
const secrets = di.get(DEFINITIONS.SECRETS);

// Fetched once, then cached for the Lambda execution
const creds = await secrets.get('arn:aws:secretsmanager:eu-west-1:123:secret:db-creds');
// { username: 'admin', password: '...', host: '...', ... }
```

### WebhookService

Dispatches HMAC-SHA256 signed webhooks.

```javascript
const webhook = di.get(DEFINITIONS.WEBHOOK);

await webhook.send(
  'https://example.com/webhook',
  { event: 'payment.completed', transaction_id: 'txn_123' },
  'webhook-signing-secret'
);
// POST with X-Signature header containing HMAC-SHA256 hex digest
```

### AuditLoggerService

Creates audit trail events via EventBridge.

```javascript
const auditLogger = di.get(DEFINITIONS.AUDIT_LOGGER);

await auditLogger.createEvent('payment.refunded', 'entity-123', 'org-456');
```

## Response Model

All Lambda responses use a standardised format with CORS headers.

```javascript
import { ResponseModel } from '@felloh-org/lambda-wrapper';

// Basic response
const response = new ResponseModel({ users: [] }, 200);
return response.generate();

// Building a response
const response = new ResponseModel();
response.setData({ id: 1, name: 'Test' });
response.setCode(201);
response.setMetaVariable('pagination', { page: 1, total: 10 });
response.setBodyVariable('warnings', ['Deprecated field used']);
return response.generate();

// Adding errors
const error = new ResponseModel({}, 422);
error.addError({
  title: 'Validation Error',
  message: 'Email is not valid',
  documentation_url: 'https://developers.felloh.com/errors#error-responses',
  type: 'validation',
  code: 'validation.email',
});
throw error;  // or return error.generate()

// Static shorthand
return ResponseModel.generate({ result: 'ok' }, 200);
```

**Generated response format:**

```json
{
  "statusCode": 200,
  "headers": {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": true
  },
  "body": "{\"data\":{},\"errors\":[],\"meta\":{\"code\":200,\"reason\":\"OK\",\"message\":\"The request was successful\",\"request_id\":\"uuid-v4\"}}"
}
```

**Supported status codes:**

| Code | Reason |
|------|--------|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 406 | Not Acceptable |
| 410 | Gone |
| 413 | Request Entity Too Large |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
| 504 | Gateway Timeout |

## Status Model

Used for health checks and service status reporting.

```javascript
import { StatusModel, STATUS_TYPES } from '@felloh-org/lambda-wrapper';

const status = new StatusModel('database', STATUS_TYPES.OK);
// STATUS_TYPES: OK, ACCEPTABLE_FAILURE, APPLICATION_FAILURE
```

## Lambda Termination

A custom error class for terminating Lambda execution with a specific status code and consumer-facing message.

```javascript
import { LambdaTermination } from '@felloh-org/lambda-wrapper';

// Terminates with a 503 response
throw new LambdaTermination(
  'Payment provider timeout after 30s',  // internal (logged)
  503,                                    // status code
  'Service temporarily unavailable',      // body (returned to consumer)
  'Provider X timed out'                  // details
);
```

## EventBridge Events

Publish typed events to AWS EventBridge.

```javascript
import {
  LambdaWrapper,
  DEFINITIONS,
  TransactionCompleteEvent,
  TransactionCompleteEmailEvent,
  UserRegistrationEvent,
  CSVEmailEvent,
  RefundRequestEmailEvent,
  BaseEvent,
} from '@felloh-org/lambda-wrapper';

export const handler = LambdaWrapper(config, async (di) => {
  const eventBridge = di.get(DEFINITIONS.EVENT_BRIDGE);

  // Use a pre-built event
  const event = new TransactionCompleteEvent();
  event.setDetailParam('transaction_id', 'txn_123');
  await eventBridge.put(event);

  // Or build a custom event
  const custom = new BaseEvent('order.shipped');
  custom.setDetailParam('order_id', 'ord_456');
  custom.setDetail({ order_id: 'ord_456', carrier: 'DHL' });
  await eventBridge.put(custom);
});
```

**Available event types:**

| Event | Description |
|-------|-------------|
| `BaseEvent` | Generic event, set your own detail type |
| `TransactionCompleteEvent` | Transaction completed |
| `TransactionCompleteEmailEvent` | Transaction receipt email |
| `UserRegistrationEvent` | New user registration |
| `UserPasswordChangedEvent` | Password changed |
| `UserPasswordResetEvent` | Password reset requested |
| `SandboxRegistrationEvent` | Sandbox account registration |
| `CSVEmailEvent` | CSV export email |
| `POSCRequestEmailEvent` | POSC request email |
| `RefundRequestEmailEvent` | Refund request notification |

## Webhook Models

Format transaction and refund data for webhook payloads.

```javascript
import { TransactionWebhookModel, RefundWebhookModel } from '@felloh-org/lambda-wrapper';

// Transaction webhook
const payload = new TransactionWebhookModel(transaction, 'Payment received');
const data = payload.get();
// { transaction: { id, narrative }, amount, booking, status, currency, surcharge, ... }
```

## Database (TypeORM)

### Entities

The library includes TypeORM entities organised by domain:

| Domain | Description |
|--------|-------------|
| `payment` | Transactions, refunds, chargebacks, payment links, ecommerce, providers, sessions |
| `user` | Users, organisations, roles, features, webhooks, billing, partners, portals |
| `bank` | Accounts, ledgers, settlements, disbursals, adjustments, beneficiaries |
| `agent-data` | Bookings, components, suppliers, package types |
| `acquirer` | BIN data, batches, adjustments, band rates, transaction settlements |
| `aisp` | Open banking transactions, GoCardless accounts/agreements/requisitions |
| `nuapay` | Nuapay accounts, balances, beneficiaries, transactions |
| `nuvei` | Nuvei batches, merchants, transactions, metadata |
| `trust-payments` | Trust Payments batches, merchants, transactions, chargebacks, fees |
| `total-processing` | Total Processing transactions and metadata |
| `planet` | Planet transactions, metadata, batches |
| `saltedge` | Saltedge accounts, connections, customers, transactions, leads |
| `basis-theory` | Tokenisation tokens |
| `go-cardless` | GoCardless organisation configuration |
| `marketing` | Marketing contacts and ESUs |
| `reference` | Countries |
| `playbook` | Playbooks, requests, secrets, versions |

### Migrations

Run migrations against the database:

```bash
# Create database schemas
yarn orm:schema:create

# Run pending migrations
yarn orm:migration:run

# Generate a new migration
yarn orm:migration:generate <name>

# Revert last migration
yarn orm:revert
```

Migrations require the following environment variables:

| Variable | Description |
|----------|-------------|
| `WAREHOUSE_TYPE` | `postgres` or `postgres-ssm` |
| `WAREHOUSE_HOST` | Database host (for `postgres` type) |
| `WAREHOUSE_USERNAME` | Database username (for `postgres` type) |
| `WAREHOUSE_PASSWORD` | Database password (for `postgres` type) |
| `WAREHOUSE_DATABASE` | Database name |
| `WAREHOUSE_SCHEMA` | Database schema |
| `DB_CREDS_ARN` | Secrets Manager ARN (for `postgres-ssm` type) |

### Database Schemas

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

## Utilities

### PromisifiedDelay

Provides randomised delays for retry logic, with configurable latency profiles.

```javascript
import { PromisifiedDelay } from '@felloh-org/lambda-wrapper';

// High latency profile (2-20s delays, weighted towards shorter)
const delay = new PromisifiedDelay(true);

// Standard latency profile (2-5s delays)
const delay = new PromisifiedDelay(false);

// Use in retry loop
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await makeRequest();
  } catch (error) {
    await delay.get();
  }
}
```

## Development

### Commands

```bash
# Build for production (webpack)
yarn build

# Build for TypeORM migrations (babel)
yarn build:orm

# Run linter
yarn lint

# Run tests with coverage
yarn test

# Run dependency vulnerability audit
yarn audit:check

# Run database migrations
yarn orm:migration:run

# Generate a new migration
yarn orm:migration:generate <name>

# Revert last migration
yarn orm:revert

# Create database schemas
yarn orm:schema:create
```

### Environment Variables

| Variable | Description |
|----------|-------------|
| `SERVICE_NAME` | Service identifier for logging and events |
| `EVENT_BRIDGE_ARN` | AWS EventBridge bus ARN |
| `REGION` | AWS region |
| `WAREHOUSE_TYPE` | Database connection type (`postgres` or `postgres-ssm`) |
| `WAREHOUSE_HOST` | Database host |
| `WAREHOUSE_USERNAME` | Database username |
| `WAREHOUSE_PASSWORD` | Database password |
| `WAREHOUSE_DATABASE` | Database name |
| `WAREHOUSE_SCHEMA` | Database schema |
| `DB_CREDS_ARN` | Secrets Manager ARN for database credentials |
| `DB_LOGGING` | Set to `true` to enable TypeORM query logging |
| `LOGGER_SOFT_WARNING` | Set to `true` to downgrade warnings to info level |
| `IS_OFFLINE` | Set by serverless-offline for local development |

### Project Structure

```
src/
├── action/              # Action handlers (e.g. unmatched route)
├── config/              # Dependency definitions
├── dependency-injection/  # DI container and base class
├── entity/              # TypeORM entities by domain
├── enums/               # Shared enumerations
├── event/               # EventBridge event classes
├── migration/           # Database migrations by schema
├── model/               # Response, status, and webhook models
├── service/             # Core services (logger, request, warehouse, auth, etc.)
├── util/                # Utilities (LambdaTermination, PromisifiedDelay)
└── wrapper/             # Lambda wrapper implementation
```

### CI/CD

The project uses Concourse CI with the following pipeline:

1. **lint-and-test** - runs lint, tests, and dependency audit in parallel
2. **migrations.staging** - creates schemas and runs migrations on staging
3. **migrations.production** - promotes to production after staging succeeds
4. **migrations.sandbox** - promotes to sandbox after staging succeeds

### Testing

Tests are colocated with source files (e.g. `src/service/logger.test.js` alongside `src/service/logger.js`).

```bash
yarn test
```

## License

MIT