# Technical documentation — unity-lambda-api

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/unity-lambda-api.git`, `master` at `fe4b095` (2021-03-17).

Credential values are not in git. This document names environment variables and does not invent their values. Mock files contain identity ids and a client spreadsheet; those values are omitted.

## What this repository is

A Serverless Framework service (`service: unity-lambda-api`) that packages Node.js Lambda functions. HTTP routes use API Gateway with an IAM authorizer. Data access is `serverless-mysql` plus the AWS SDK DynamoDB document client.

The tree started as the Anomaly Innovations Serverless Node.js Starter (`6cebfe4`, 2020-07-24). `README.md`, `LICENSE` (MIT, copyright 2017 Anomaly Innovations), `package.json` name `serverless-nodejs-starter` version `1.1.0`, and the GitHub repository URL in `package.json` were not updated. `handler.js` is still that starter's `hello` function.

## Repository layout

```text
serverless.yml                 service, provider, functions, resources
resources/api-gateway-errors.yml
env.example                    starter sample only
package.json
package-lock.json
create.js                      DynamoDB put (routed)
get.js                         DynamoDB get (routed)
global.js                      generic SQL (routed)
saveimport.js                  log body (routed)
getuser.js                     sysUser by UserID (routed)
getusers.js                    all sysUser rows (routed)
getdetails.js                  sysUser by body id (routed)
getuseruuid.js                 sysUser by strCognitoUuid (routed)
tableall.js                    SELECT * FROM caller table (routed)
addcalendarevent.js            crmCalendar insert (routed)
getcalendarevents.js           crmCalendar select (routed)
updatecalendarevent.js         crmCalendar update (routed)
removecalendarevent.js         crmCalendar delete (routed)
logging.js                     DynamoDB audit put
libs/handler-lib.js            status and CORS wrapper
libs/response-lib.js           success() and failure()
libs/dynamodb-lib.js           DocumentClient promise helpers
libs/images-lib.js             empty
adduser.js                     not in serverless.yml
updateuser.js                  not in serverless.yml
getrole.js                     not in serverless.yml
getuserdetails_example.js      not in serverless.yml
savepermissions.js             not in serverless.yml
updatepermissions.js           not in serverless.yml
updatecustompermissions.js     not in serverless.yml
updateuuid.js                  not in serverless.yml
handler.js                     starter hello, not routed
handler copy.js                duplicate of handler.js
mocks/                         invoke fixtures
tests/handler.test.js          Jest test of hello
.gitignore
LICENSE
README.md
```

No `bitbucket-pipelines.yml`, Dockerfile, `sonar-project.properties`, or infrastructure beyond `serverless.yml`.

## Runtime and deploy template

From `serverless.yml`:

| Setting | Value |
|---|---|
| Framework service | `unity-lambda-api` |
| Provider | `aws` |
| Runtime | `nodejs12.x` |
| Stage | `prod` |
| Region | `eu-west-1` |
| AWS profile | `unity` |
| Memory | 512 |
| Package | `individually: true` |
| Plugins | `serverless-bundle`, `serverless-offline`, `serverless-dotenv-plugin` |

`serverless-bundle` webpack-packages each function and is what `npm test` calls. The README says ESLint runs through that plugin. There is no `.eslintrc.json` in the repo. Linting defaults are the plugin defaults. **Unknown** which ESLint rules that plugin version applied.

The Serverless Framework CLI is not listed in `package.json`. The README says to install it separately.

REST API paths are deployed under the stage name. With `stage: prod` and no `custom.customDomain`, the path prefix is `/prod`. **Requires confirmation** if a domain was added in the AWS console.

Commented and inactive:

```yaml
#vpc:
#  securityGroupIds:
#    - ${env:SECURITYGROUPIDS}
#  subnetIds:
#    - ${env:SUBNETIDS1}
#    - ${env:SUBNETIDS2}
```

## Environment

`serverless-dotenv-plugin` loads `.env` into the Serverless variable resolver. `.env` and `env.yml` are gitignored.

Referenced by the template:

| Name | Used for |
|---|---|
| `DBHOST` | `provider.environment.databaseHost` |
| `DBUSERNAME` | `provider.environment.databaseUser` |
| `DBPASSWORD` | `provider.environment.databasePass` |
| `DBNAME` | `provider.environment.databaseName` |
| `SECURITYGROUPIDS` | commented VPC block |
| `SUBNETIDS1`, `SUBNETIDS2` | commented VPC block |

`tableName` is not read from the environment file. It is the literal `loging` in the template, then exposed to functions as `process.env.tableName`.

`env.example` only documents `SAMPLE_ENV_VAR=i-am-an-environment-variable` and still says to uncomment an `environment` block. The block is already present and does not use `SAMPLE_ENV_VAR`. **Discrepancy** between the example file and the template.

MySQL clients are constructed at module load in each handler:

```javascript
const mysql = require('serverless-mysql')({
  config: {
    host: process.env.databaseHost,
    database: process.env.databaseName,
    user: process.env.databaseUser,
    password: process.env.databasePass
  }
});
```

Each handler calls `mysql.end()` after `mysql.query`. There is no shared pool module. `global.js` and the other MySQL files each create their own client.

## Route table

All methods below are the HTTP method in `serverless.yml`. Authorizer is `aws_iam` and CORS is on for every row.

| Function key | Handler | Method | Path | Input the handler reads |
|---|---|---|---|---|
| `create` | `create.main` | POST | `unity/create` | JSON body `content`, `attachment`; `requestContext.identity.cognitoIdentityId` |
| `global` | `global.main` | POST | `unity` | JSON body `a`, `c`, `t`, and optionally `j`, `w`, `con` |
| `saveimport` | `saveimport.main` | POST | `unity/import` | logs `event.body`; no parse |
| `get` | `get.main` | GET | `unity/{id}` | path `id`; Cognito identity id |
| `getuser` | `getuser.main` | GET | `unity/user/{id}` | path `id` concatenated into `UserID =` |
| `getusers` | `getusers.main` | POST | `unity/getusers/{id}` | path `id` ignored |
| `getdetails` | `getdetails.main` | POST | `unity/getdetails` | JSON body `id` |
| `getuseruuid` | `getuseruuid.main` | GET | `unity/uuid/{id}` | path `id` as `strCognitoUuid` |
| `tableall` | `tableall.main` | POST | `unity/tableall` | JSON body `table`, `where` |
| `addcalendarevent` | `addcalendarevent.main` | POST | `unity/addcalendarevent` | `pathParameters` description, notes, dates, optional user fields |
| `getcalendarevents` | `getcalendarevents.main` | POST | `unity/getcalendarevents` | `pathParameters.userID`, `dtStartDate`, `dtEndDate` |
| `removecalendarevent` | `removecalendarevent.main` | POST | `unity/removecalendarevent` | `pathParameters.CalendarID` |
| `updatecalendarevent` | `updatecalendarevent.main` | POST | `unity/updatecalendarevent` | `pathParameters` description, notes, dates, `CalendarID`, optional user fields |

API Gateway fills `pathParameters` from the path template only. The four calendar routes have no template parameters, so a standard HTTP POST does not provide the fields those handlers require. `adduser.js` and `updateuser.js` have the same pattern and are not routed. Mocks for add/update user are shaped as Lambda invoke events (`pathParameters` plus `requestContext`), which is what `serverless invoke` supplies, not what API Gateway maps from a JSON body.

History of `POST unity`: `create` until `19d769f`, then `global`. Create has been `POST unity/create` since that commit.

## Handler wrapper

`libs/handler-lib.js` returns a function `(event, context) => Promise`. It runs the inner function, maps a resolved value to status 200, and maps a throw to status 500 `{ error: e.message }`. Both paths set:

- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Credentials: true`
- `body` = `JSON.stringify` of the resolved value

`libs/response-lib.js` `success` and `failure` return a full response object (status 200 or 500, the same CORS headers, and a stringified body). MySQL handlers return that object from inside the wrapper. The client therefore receives status 200 and a body that is the JSON of another response. See PRD WF-9.

`create.js` uses the Node callback style and the DynamoDB client callback. It does not use the wrapper. It builds the same CORS headers itself.

`get.js` uses the wrapper and returns the DynamoDB item, or throws. Single wrap.

`libs/dynamodb-lib.js` exports `get`, `put`, `query`, `update`, `delete` as `DocumentClient` promises. Only `get` is used (`get.js`). `create.js` and `logging.js` construct their own `DocumentClient`.

## DynamoDB

No table resource is declared. IAM:

```yaml
Action:
  - dynamodb:DescribeTable
  - dynamodb:Query
  - dynamodb:Scan
  - dynamodb:GetItem
  - dynamodb:PutItem
  - dynamodb:UpdateItem
  - dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:eu-west-1:*:*"
```

That resource is every table in the region, not the `loging` ARN.

Item written by `create.js`: `userId`, `noteId` (`uuid.v1()`), `content`, `attachment`, `createdAt` (epoch milliseconds).

Item written by `logging.js`: `userID`, `logID` (`uuid.v1()`), `event` (full event JSON), `query`, `result`, `createdAt`. The put callback does not check `err` and is not awaited. `context.callbackWaitsForEmptyEventLoop` is not set.

Key schema is **Unknown**. The two writers use different attribute names, so a single-table key of `userId` + `noteId` would not match the log item, and the reverse would not match the note. **Requires confirmation.**

## MySQL

Driver: `serverless-mysql` ^1.5.4. Queries are strings passed to `mysql.query` with no placeholder array.

Tables named in routed SQL:

| Table | Columns named in routed SQL | Writers |
|---|---|---|
| `sysUser` | `UserID`, `strCognitoUuid` | none routed (`adduser.js` / `updateuser.js` also name `strUser`, `refSecurityGroupID` and are not routed) |
| `crmCalendar` | `txtDescription`, `txtNotes`, `dtStartDate`, `dtEndDate`, `strFirstUser`, `refSysUserID`, `CalendarID` | calendar handlers |

Tables named only in unwired files: `tbl_member`, `tbl_member_role`, `tbl_gender`, `tbl_ethnicity`, `tbl_organisation`, `tbl_status`, `tbl_country`, `tbl_city`, `tbl_identitytype`, `tbl_relationship`, `tbl_sportscode`, `tbl_position`, `tbl_suburb`, `tbl_province`, `tbl_socialmedia`, `tbl_additional_member_info`, `tbl_media_hub`, `tbl_medical_aid`, `tbl_permissions_group`, `tbl_permissions_member`.

No migration or `CREATE TABLE` is in the repository. Existence of every table is **Unknown / Requires Confirmation**.

`global.js` will run whatever statement the body builds, including the incomplete `INSERT INTO `. Join/where call `data.j.length()` and `data.w.length()`. If `j` or `w` is an array or string, `.length` is not a function and the call throws before the query.

## Auth

Implemented mechanism: API Gateway `aws_iam` on every route.

Not in this repository: Cognito user pool, identity pool, IAM role for the caller, resource policy, or usage plan.

Code that assumes a Cognito identity id:

- `create.js` and `get.js` use it as the DynamoDB partition key.
- `logging.js` stores it as `userID`.

If `requestContext.identity` is missing, those property reads throw.

## Response and error behaviour

| Case | HTTP status | Body |
|---|---|---|
| `create` DynamoDB success | 200 | the item |
| `create` DynamoDB error | 500 | `{"status":false}` |
| `get` item found | 200 | the item |
| `get` item missing | 500 | `{"error":"Item not found."}` |
| MySQL handler returns `success(...)` | 200 | JSON of the inner response object (results are inside the inner `body` string) |
| MySQL handler returns `failure(...)` | 200 | JSON of the inner 500 response |
| Thrown error inside `handler()` | 500 | `{"error":"<exception message>"}` |
| `saveimport` returns undefined | 200 | empty or the JSON encoding of `undefined` |

Gateway `DEFAULT_4XX` and `DEFAULT_5XX` add CORS headers. They do not change the application body.

## Dependencies

`package.json`:

| Package | Range | Role |
|---|---|---|
| `serverless-mysql` | ^1.5.4 | MySQL |
| `uuid` | ^7.0.3 | `v1()` ids for notes and logs |
| `aws-sdk` | ^2.706.0 (dev) | DynamoDB document client. On Lambda the runtime also provides a v2 SDK; the package is bundled because it is imported |
| `serverless-bundle` | ^1.2.5 (dev) | Webpack package and test runner |
| `serverless-dotenv-plugin` | ^2.1.1 (dev) | `.env` loading |
| `serverless-offline` | ^5.3.3 (dev) | Local API Gateway |

`npm test` → `serverless-bundle test`. Lockfile is `package-lock.json` from the first commit era (the file is the starter lock plus the added dependencies). Node engine is not pinned in `package.json`.

## Security observations from the source

These are facts about the code, not a recommended design.

- SQL is string concatenation of path parameters and JSON fields (`getuser`, `getdetails`, `getuseruuid`, `global`, `tableall`, calendar handlers, and the unwired files).
- `tableall` and `global` let the caller choose table and clause text.
- `getusers` returns every `sysUser` row to any IAM caller of that route.
- Calendar update and delete filter only on `CalendarID`.
- The audit log stores `JSON.stringify(event)`, which includes headers and the body.
- DynamoDB IAM is regional (`eu-west-1:*:*`), not one table.
- CORS allows any origin with credentials.
- `nodejs12.x` is the configured runtime.
- No rate limit, WAF, or authorizer scope beyond `aws_iam` appears in the template.
- `savepermissions.js` and the insert branch of `updatecustompermissions.js` contain SQL that is syntactically invalid as written. They are not routed.

No password, token, or key material is committed. Database secrets are expected in `.env`, which is gitignored. `mocks/` does contain sample identity ids and personal/contact fields in the import spreadsheet. Those files are tracked.

## Logging and monitoring

Application logging:

- `console.log` of the SQL in `getuser.js`, of the import body in `saveimport.js`, and of the member-query result in `getuserdetails_example.js`.
- `logging.js` DynamoDB put, described above.

No CloudWatch metric filter, alarm, X-Ray, or log subscription is in the template. Lambda's default CloudWatch log group is an AWS default, not something this repo configures. Whether anyone reads those logs is **Unknown**.

## Development and testing

Documented in `README.md` (starter text, only partly applicable):

- `serverless invoke local --function hello` — `hello` is not a function in `serverless.yml`, so this command does not match the template.
- `serverless offline start`
- `serverless deploy` and `serverless deploy function --function hello`
- `npm test`

The only test is `tests/handler.test.js`. It imports `handler.hello`, passes the strings `'event'` and `'context'`, and expects the callback form `statusCode === 200` and a string body. `handler.hello` returns a promise and does not call the callback argument. The test's expectations sit inside a callback that `hello` never invokes.

No test covers MySQL, DynamoDB, or the routes.

There is no CI configuration, so nothing in git runs tests on push.

## Local mocks

`mocks/*.json` are Lambda proxy-style events: `pathParameters` and/or string `body`, and `requestContext.identity.cognitoIdentityId`. They are not HTTP requests.

Fixtures that match a file:

| Mock | File it resembles | Match |
|---|---|---|
| `adduser.json` | `adduser.js` | path `name` |
| `updateuser.json` | `updateuser.js` | path `name`, `userUuid` |
| `getuser.json` | `getuser.js` | path `id` |
| `getusers.json` | `getusers.js` | path `id` (handler ignores it) |
| `getuseruuid.json` | `getuseruuid.js` | path `id` |
| `getuserdetails.json` | `getuserdetails_example.js` | does not match: mock body vs handler path id |
| `updatepermissions.json` | `updatepermissions.js` | body flags, `id`, `personId` |
| `updatecustompermissions.json` | `updatecustompermissions.js` | body flags, `id`, `personId`, `memberId` |
| `updateuuid.json` | `updateuuid.js` | body `MemberID`, `uuid` |
| `saveimport.json` | `saveimport.js` | body is a JSON array in the file, not a string. The handler only logs it |

`mocks/Untitled-1.json` duplicates the import spreadsheet as a top-level array. No handler reads that filename.

## Background work, queues, storage, jobs

None. There is no SQS, SNS, EventBridge, scheduled event, or worker. The DynamoDB log write is in-process and not queued.

S3 appears only as a commented client and as URL strings in `getuserdetails_example.js` (bucket `footprint-reboot-qa-zaf`, prefix `sportscode-1/members/`). Not deployed by this template.

## What this document does not establish

- The AWS account behind profile `unity`.
- That a stack named for stage `prod` exists or is healthy.
- The MySQL schema.
- The DynamoDB key schema for `loging`.
- The caller that signs IAM requests.
- A link to other Unity repositories in Silicon Ark. This tree does not name them.
