# PRD — DoctorX_lambda_api_Josh

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/doctorx_lambda_api_josh.git`, `master` at `40da799` (2021-04-30).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `serverless.yml`, the handler files, `libs/`, `logging.js`, `mocks/`, `tests/adduser.test.js`, and the commits on that revision. Provenance is labelled. Mock identity values are omitted.

`README.md` is the upstream Serverless Node.js Starter after a one-line title change. It is not a requirements document for this service. Where it disagrees with the code, the code is the implemented behaviour and the README is stale starter text.

## Product purpose

**Documented.** The README heading is "DoctorX lambda api (josh)".

**Inferred.** An AWS API, service name `josh-lambda-api`, that lets an IAM-authenticated caller insert, list, read, and update doctor rows in MySQL table `crmDoctors`, and that stores an audit item in DynamoDB.

**Implemented.** Six HTTP functions in `serverless.yml`. `global.js` is not one of them. `POST /josh/diagnose` does not read or write MySQL.

**Unknown / Requires Confirmation:** the product owner, the caller, and whether this API is still in use.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| HTTP caller holding AWS IAM credentials | Every route sets `authorizer: aws_iam` | **Implemented**. The caller application is not in this repo |
| Cognito identity on the request | `logging.js` reads `event.requestContext.identity.cognitoIdentityId`. Comments on `getuseruuid` and `diagnose` say "cognito must be logged in" | **Implemented** as a field read and as comments. No Cognito resource is in the template. The YAML authorizer is `aws_iam`, not a Cognito authorizer |
| Doctor row in `crmDoctors` | Insert, update, and three selects | **Implemented** as SQL. No screen is in this repo |
| Person running Serverless locally | README `serverless offline start` and `serverless invoke local`. Mocks match a pathParameters invoke shape | **Documented** in the starter README. Whether this team used it is **Unknown** |

No role check is applied on any routed handler. `strRole` is a column value the caller sends. It is not compared to a permission table.

## Systems the product talks to

1. AWS API Gateway and Lambda. Service `josh-lambda-api`, runtime `nodejs12.x`, stage `prod`, region `eu-west-1`, memory 512 MB, profile `unity`. Each function is packaged alone (`package.individually: true`).
2. DynamoDB in `eu-west-1`. Table name is the literal `loging`. IAM allows describe, query, scan, get, put, update, and delete on `arn:aws:dynamodb:eu-west-1:*:*`. The table itself is not created by this template.
3. MySQL through `serverless-mysql`. Host, user, password, and database come from `DBHOST`, `DBUSERNAME`, `DBPASSWORD`, `DBNAME` via `serverless-dotenv-plugin`. The plugin loads `.env`. The handlers read `process.env.databaseHost`, `databaseName`, `databaseUser`, and `databasePass`, which `serverless.yml` maps from those names. `.env` is gitignored. There is no `env.example` at the tip.
4. Optional VPC, commented out, using `SECURITYGROUPIDS`, `SUBNETIDS1`, `SUBNETIDS2`. Not active in the template.

No queue, mailer, cache, or monitoring vendor is configured. There is no Bitbucket Pipelines file.

## Major workflows

Routes below are the path in `serverless.yml`. Serverless Framework REST APIs also prefix the stage, so the deployed base path is `/prod` unless a custom domain exists. No custom domain is in the template. **Requires confirmation** of the public URL.

Every routed handler is passed through `libs/handler-lib.js` and also calls `success()` or `failure()` from `libs/response-lib.js`. Those helpers already return an HTTP response object. The wrapper then places that object in a new response with status 200, unless the handler throws. The caller therefore receives two encodings. A `failure()` result is HTTP 200 whose parsed body contains `statusCode: 500`. A thrown error is HTTP 500 and body `{"error":"<message>"}`, encoded once. This wrapping is **Implemented** and is shared by WF-1 through WF-6.

Parameter loading in `adduser.js`, `updateuser.js`, `getdetails.js`, and `getuseruuid.js` is the same: if `event.body` is present, `JSON.parse` it and copy every key; then copy every `event.pathParameters` key, which overwrites the same key from the body. If the resulting object has no keys, return `failure({"message":"Valid input not found!"})`. The current routes do not declare path parameters, so API Gateway will not fill `pathParameters` from the URL. The mocks put the inputs in `pathParameters` and have no `body`. A local invoke of a mock can succeed where a live call that sends only a JSON body, or only a path, will not, depending on the route. That split is **Implemented**.

### WF-1 — Create a doctor

**Implemented** in `adduser.js`, `POST /josh/create`. YAML comment still says path `/josh/adduser` and method GET.

1. Load parameters as above.
2. Build `INSERT INTO crmDoctors`.
3. Always include `strName`, `strSurname`, `strEmail`, `strRole`, and `dtCreated`. Missing name, surname, or email is concatenated as the text `undefined`. Missing `strRole` throws when `.replace` is called, which the wrapper turns into HTTP 500.
4. `strRole` single quotes are replaced with double quotes. Other fields are not escaped.
5. `dtCreated` is `new Date().toISOString()` with `T` replaced by a space and the fractional seconds and `Z` removed. That is UTC, not a caller-supplied time.
6. If present and not `""`, also include `DoctorID`, `strUserCreatedName`, `strCity`, and `refUserID`, in that column order, each wrapped in single quotes.
7. Run the string with `mysql.query`, call `mysql.end()`, call `log(...)`.
8. If `results` is truthy, return `success(results)`, else `failure(results)`.

There is no duplicate check on email or `DoctorID`.

### WF-2 — List doctors

**Implemented** in `getusers.js`, `POST /josh/getusers`. YAML comment still says path `/josh/getusers` and method GET. The comment matches the path and not the method.

1. Ignore the event body and path.
2. `SELECT * FROM crmDoctors` (backticks around the table name).
3. Log and return `success` when `results` is truthy.

An empty array is truthy in JavaScript, so an empty table is still the success payload. The mock supplies path parameter `id`. The handler does not read it.

### WF-3 — Read one doctor by id

**Implemented** in `getdetails.js`, `POST /josh/get`. YAML comment says path `/josh/getdetails` and method POST.

1. Load parameters. Empty input returns the "Valid input not found!" failure payload.
2. `SELECT * FROM crmDoctors WHERE DoctorID = ` plus `parameters.id`, with no quotes around the id.
3. Any other parameter is ignored for the filter.
4. Log and return success or failure on the truthiness of `results`.

If the caller sent some other key and omitted `id`, the SQL text contains `undefined` and the MySQL call throws. The route template has no `{id}`. From `1e5f62e` until `bc2666e` the path was `josh/get/{id}`. The mock still sends `id` as a path parameter.

### WF-4 — Read doctors by email

**Implemented** in `getuseruuid.js`, `POST /josh/getuuid`. The function name, the YAML comment (`/josh/getuseruuid`, method GET), and the comment "System user's basic details" do not match the SQL.

1. Load parameters. Empty input returns the "Valid input not found!" failure payload.
2. `SELECT * FROM crmDoctors WHERE strEmail = "` plus `parameters.strEmail` plus `"`.
3. The handler does not read `cognitoIdentityId` and does not select a uuid column.
4. Log and return success or failure on the truthiness of `results`.

`e6d1255` used path `josh/getuuid/{id}`. `bc2666e` removed `{id}`. `414737d` changed the filter to `strEmail`. The mock still sends `strEmail` as a path parameter.

### WF-5 — Update a doctor

**Implemented** in `updateuser.js`, `POST /josh/update`. YAML comment says path `/josh/updateuser` and method GET.

1. Load parameters. Empty input returns the "Valid input not found!" failure payload.
2. If `parameters.id` is set, copy it onto `parameters.DoctorID`.
3. For each of `DoctorID`, `strName`, `strSurname`, `strEmail`, `strCity`, `strRole`, `dtLastLoggedIn`, and `refUserID`, if the value is present and not `""`, append `column = "` + value + `"`. `dtLastLoggedIn` is wrapped as `CAST("..." AS DATETIME)`.
4. If that list is empty, set a local result `{"response":"Nothing to update"}` and return it through `success`.
5. If the list is not empty, build `UPDATE crmDoctors SET ` + the assignments joined by commas + `WHERE `. There is no space before `WHERE`.
6. If `strEmail` is present and not `""`, append `strEmail=` plus the raw email (no quotes). Else if `DoctorID` is present and not `""`, append `DoctorID=` plus the raw id (no quotes). Else return `failure({"response":"Must Supply a DoctorID!"})`.
7. The query result is stored with an inner `let results` that does not assign the outer `results`. The outer variable stays `false`. After the query, the function therefore takes the `failure(false)` branch.

**Implemented consequence:** a call that has fields and an email or `DoctorID` still returns the failure payload with body `false`, after the double wrap, unless `mysql.query` throws. The SQL may already have been sent. The HTTP body is not evidence that the row was unchanged, and it is not evidence that the row was changed.

The mock sends `strName` and `DoctorID` as path parameters. The live route has no path parameters.

### WF-6 — Diagnose

**Implemented** in `postxray.js`, `POST /josh/diagnose`. YAML comment says path `/josh/postxray` and method GET. The Serverless function name is `diagnose`.

1. Do not parse the body. The MySQL client is commented out.
2. Set `results` to an object whose `result` property is the fixed sentence in `postxray.js` (a joke about injury and biting). The request fields are not used.
3. Call `log(event, results)` with no query argument.
4. Return `success(results)`.

The mock path parameters are `type`, `model`, and `xray`. The handler does not read them. The file comment says "evaluate upload here". No upload is evaluated.

This route does not perform a medical assessment. The sentence is a constant.

### WF-7 — Generic SQL (not deployed)

**Implemented** in `global.js` only. It is not a function in `serverless.yml` at `40da799`. It was routed at `POST /josh` in `e6d1255` and removed from the template in `bc2666e`.

The file comment describes `data.a` as Select, Update, Insert, or Delete, nested under `data`, with join, group, having, and order. The function reads top-level `data.a`, `data.c`, `data.t`, `data.j`, `data.w`, and `data.con`.

1. `data.a === 's'` builds `SELECT {data.c} FROM {data.t}`.
2. `data.a === 'u'` builds `UPDATE {data.t} SET {data.c} `.
3. `data.a === 'i'` builds the string `INSERT INTO ` and stops.
4. Any other `data.a` returns `failure("No Action")`.
5. If `data.j.length() > 6`, append ` LEFT JOIN ` plus `data.con`. `length` is a number, so calling it throws unless `j` is not a string or array.
6. If `data.w.length() > 3`, append ` WHERE ` plus `data.con`.
7. Run the string, end MySQL, and log.

Do not treat WF-7 as a live route.

### WF-8 — Audit log

**Implemented** in `logging.js`, called at the end of WF-1 through WF-6.

1. Put an item: `userID` = Cognito identity id, `logID` = `uuid.v1()`, `event` = `JSON.stringify(event)`, `query` = `JSON.stringify(query)`, `result` = `JSON.stringify(result)`, `createdAt` = `Date.now()`.
2. Table is `process.env.tableName`, which the template sets to `loging`.
3. The put callback returns into the SDK, not into the handler. The HTTP response does not wait for DynamoDB and does not surface a put error.
4. WF-6 omits `query`. `JSON.stringify(undefined)` is `undefined`, which the document client does not accept as an attribute value. The put can fail without changing the HTTP response.

`libs/dynamodb-lib.js` is a promise wrapper around the same client and is not imported by any file in the tree.

## Functional requirements

### FR-001 — IAM on every route

The API Gateway event for each function in `serverless.yml` requires `authorizer: aws_iam`.

Evidence: `serverless.yml`. Implementation: **Implemented**. Confidence: high.

### FR-002 — Create a `crmDoctors` row

`POST /josh/create` inserts the columns in WF-1.

Evidence: `adduser.js`, `serverless.yml`. Implementation: **Implemented**. Confidence: high.

### FR-003 — List every `crmDoctors` row

`POST /josh/getusers` returns `SELECT *` with no filter and no paging.

Evidence: `getusers.js`. Implementation: **Implemented**. Confidence: high.

### FR-004 — Read by `DoctorID`

`POST /josh/get` filters `DoctorID` with `parameters.id`.

Evidence: `getdetails.js`. Implementation: **Implemented**. Confidence: high.

### FR-005 — Read by `strEmail`

`POST /josh/getuuid` filters `strEmail`.

Evidence: `getuseruuid.js`, commit `414737d`. Implementation: **Implemented**. The route name says uuid. Confidence: high.

### FR-006 — Update a `crmDoctors` row

`POST /josh/update` builds the UPDATE in WF-5. The identifier preference is email, then `DoctorID`.

Evidence: `updateuser.js`. Implementation: **Implemented**, including the shadowed `results` variable. Confidence: high.

### FR-007 — Diagnose returns a constant

`POST /josh/diagnose` returns the fixed object in `postxray.js` and does not query MySQL.

Evidence: `postxray.js`. Implementation: **Implemented** as a stub. Confidence: high.

### FR-008 — Audit each routed call

Each routed handler calls `log`. The item shape is WF-8.

Evidence: `logging.js` and the six handlers. Implementation: **Implemented**. The put is not part of the HTTP success condition. Confidence: high.

### FR-009 — CORS on handler and gateway errors

Success, failure, and the wrapper set `Access-Control-Allow-Origin` to `*` and `Access-Control-Allow-Credentials` to `true`. Gateway default 4XX and 5XX set origin `*` and allow every header.

Evidence: `libs/response-lib.js`, `libs/handler-lib.js`, `resources/api-gateway-errors.yml`. Implementation: **Implemented**. Confidence: high.

### FR-010 — Reject an empty parameter object

Create, update, get, and get-by-email return the failure payload `{"message":"Valid input not found!"}` when no body keys and no path keys were copied.

Evidence: the four handlers. Implementation: **Implemented**. List and diagnose do not apply this check. Confidence: high.

## Validation and errors

| Condition | Behaviour | Provenance |
|---|---|---|
| No parameter keys on create, update, get, or get-by-email | Inner failure payload `Valid input not found!`, then wrapped as HTTP 200 | **Implemented** |
| `JSON.parse` throws on a body | HTTP 500, `{error: message}` | **Implemented** via `handler-lib.js` |
| `strRole` missing on create | HTTP 500 from `.replace` of `undefined` | **Implemented** |
| Update with fields but neither email nor `DoctorID` | Inner failure `Must Supply a DoctorID!` | **Implemented** |
| Update with fields and an identifier | SQL is attempted; outer `results` stays `false`; inner failure payload `false` | **Implemented** |
| Update with no updatable fields | Inner success `Nothing to update` | **Implemented** |
| MySQL error | Thrown, HTTP 500 `{error: message}` | **Implemented** |
| DynamoDB put error | Not returned to the caller | **Implemented** |
| Missing item on get | Not a 404. `SELECT` of no rows is an empty array, which is truthy, so the success payload | **Implemented** |
| Diagnose | Always the success payload with the constant sentence, unless `log` or the wrapper throws | **Implemented** |

`failure()` uses status 500 inside the inner object. It does not throw, so the outer status stays 200.

## Constraints

- Runtime declared as `nodejs12.x`. Whether AWS still accepts that runtime for a new deploy is **Unknown / Requires Confirmation**.
- Stage is `prod`. There is no other stage in the template.
- Region is `eu-west-1`.
- SQL is string concatenation. The handlers do not use placeholders. That is **Implemented** behaviour, not an approved security design.
- IAM on DynamoDB is the whole region (`arn:aws:dynamodb:eu-west-1:*:*`), not one table ARN.
- `SELECT *` returns every column the table has. Columns other than those named in INSERT and UPDATE are **Unknown**.
- Memory is 512 MB. Timeout is the Serverless default. The template does not set `timeout`.

## Out of scope for the current template

- `global.js` (WF-7).
- `libs/images-lib.js` (empty file).
- `libs/dynamodb-lib.js` (not imported).
- The Unity handlers removed in `df3b05d` (calendar, permissions, `sysUser`, notes, `saveimport`, `tableall`).
- The starter `hello` function described in the README. `handler.js` was deleted in `df3b05d`.

## Human confirmation

1. Who is the caller, and is the API still deployed?
2. Is the service name `josh-lambda-api` or the README title "DoctorX" the name operators use?
3. Is filtering `strEmail` on `POST /josh/getuuid` the intended contract after `414737d`?
4. Is the update response (`failure(false)` after a query) a known defect?
5. Should diagnose remain a constant, or was an image pipeline planned and never added?
6. Which MySQL schema owns `crmDoctors`, and which columns exist beyond the names in the SQL?
