Skip to content

Commit bcadddf

Browse files
feat(api-interception): add FHIR API interception example
1 parent 90d702c commit bcadddf

13 files changed

Lines changed: 1662 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
BOX_SETTINGS_MODE=read-write
2+
3+
BOX_BOOTSTRAP_FHIR_PACKAGES="hl7.fhir.r4.core#4.0.1"
4+
BOX_FHIR_TERMINOLOGY_SERVICE_BASE_URL=https://tx.health-samurai.io/fhir
5+
BOX_FHIR_SCHEMA_VALIDATION=true
6+
BOX_FHIR_CREATEDAT_URL=https://aidbox.app/ex/createdAt
7+
BOX_FHIR_CORRECT_AIDBOX_FORMAT=true
8+
BOX_FHIR_COMPLIANT_MODE=true
9+
BOX_FHIR_SEARCH_COMPARISONS=true
10+
BOX_FHIR_JSON_SCHEMA_DATETIME_REGEX='#{:fhir-datetime}'
11+
BOX_FHIR_SEARCH_INCLUDE_CONFORMANT=true
12+
BOX_FHIR_SEARCH_AUTHORIZE_INLINE_REQUESTS=true
13+
BOX_SECURITY_AUDIT_LOG_ENABLED=true
14+
15+
BOX_INIT_BUNDLE=file:///tmp/init-bundle.json
16+
17+
POSTGRES_PORT="5432"
18+
POSTGRES_USER=postgres
19+
POSTGRES_PASSWORD=postgres
20+
POSTGRES_DB=aidbox
21+
22+
BOX_DB_HOST=aidbox-db
23+
BOX_DB_PORT="5432"
24+
BOX_DB_USER=postgres
25+
BOX_DB_PASSWORD=postgres
26+
BOX_DB_DATABASE=aidbox
27+
BOX_DB_POOL_MAXIMUM_POOL_SIZE=8
28+
29+
BOX_WEB_THREAD=8
30+
BOX_WEB_PORT=8888
31+
BOX_WEB_BASE_URL=http://aidbox:8888
32+
33+
BOX_ROOT_CLIENT_SECRET=secret
34+
BOX_ROOT_CLIENT_ID=root
35+
36+
BOX_ADMIN_ID=admin
37+
BOX_ADMIN_PASSWORD=password
38+
39+
APP_PORT=4000
40+
APP_URL=http://node-app:4000
41+
APP_SECRET=secret
42+
APP_CALLBACK_URL=/aidbox
43+
APP_ID=my-app
44+
45+
BOX_SECURITY_AUTH_KEYS_SECRET=auth-key-secret
46+
BOX_SECURITY_AUTH_KEYS_PRIVATE="-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEApbUYGNmCz1P8G0j/FFOjx1d5GNssJ/jj6xasSwTIbjjt6FtY\nCDw8o7hayOc/u8aUqXCGhK3JD2T9gtKv9/rV30w4YzmHhA8OOuLJE7tfh/PJA4Hn\n4i2JJ30BuoZ7rPTlTRGdc1FS3XFdmBQtnplEkJ7y8qbdrVme3Kbtn+BR1BdtgwSy\nbpNH2yqh3bb6PwpgNSMH7BIkBWL4A6QDpaFf1/9jSNE1vO25ssLC+bhFQNWLYriu\n+HogzEf9NWIrR2W29mI1QiA7wqvEuhg1yx38ylWD8GhCGL6+2QLKBYgp7DIGv6Uo\nTnqcVISatdQ51lVcCPmU6L1BhmcXVti6dWBI+wIDAQABAoIBAFKMOcJbTKpKvLq8\n7PErz1lFDpreyArrlmKsy0ydx9j8vCt1oY+MrmqisnsFk/7PaIxV9XUP+6qTFSUA\nHtAKYVOZLTfk10jmlSCpjCCrxWW9AISiSKkoJPyKbfuE9gRNhRMU9NoXB5Av4r+Z\nQbaRxJHE1OMjVCgAjr592786qJjd+shhY8ZLchrxctpBj6/4T2Rd4Q8ltyEV3hiy\noYaFVp9g332bFw7jZSuxgedZojNO6xPvbparTAgVDDwKB+CVUhuZ5EXWwemRvwoc\nYZM1UKPgtCqBZwm2GRv7s6XzJKBAZEMxcL7hS0RfijCe4MJcZlUCoM43Tf5XqDlT\nMmoXnPECgYEA4dkY/uqDLjJep5+4imRbceotxV2CZoJRQ0D85Ewu3tm9zdXhqL4p\n3XAOcNnqj7xBP3qkb/cXZumwdAIZns4kO1kw5hVQLX+xwMAJuravxp8sYJkx3CLO\noaOPNnlhGRv35fg4ZnoHHMO2C0wUmtSqsi6vE1EObYsIIFil58pI0NECgYEAu9SL\ne6AUCI/sdDlrTXQ8fdW8XSSJYPhZHqAvOAZfkeG4uuA2Qzxe8yUSES7z5V29futl\nWU7x+FWfqzkjh8qerviydAEFxVOpZ99ih9VB9dAwz3nX3OCoz3EUFmQGtTMxQmbo\nfW9sT4E6R7Hpa5jKnYvixk6u4p3aoEaZI4KeUAsCgYEA2OC3hiQBcN1h1Com9o7E\n2bF93qebT4EZNDI2J62Y3NvPztfy6S4j2cd/tpMtEnY/WgwV2Ic5a9RBZEWYAM4I\nMQ3HTUtuQSL8uRIwxaIlTeEQpnq2TKUINGRyZGdO/OPEvIwO7SmFpvOx30tiBgTv\nHkiCS1RtPHhkh1tZhirUneECgYAxNmARVQDKuYLXdM/jbEgJJD4FHXSNHqSi/I9C\nm5DgtQZkmCg/d4rdI+JW9Dlc6DGlFmHog2GskiqSfxcLFhB7gZeoAziS2fexynqT\nYlG06QZQ5fij24z/RP5hW3XSdgY7AqF5c/8p2Y7+h+PDmDXGD4esM6NoprlIcxbe\nkfOOvwKBgQCoOpkW+OWnxPLawmG/gv8+s5CsfOPUpURwAjltSXz9LXvsJmWQPQVG\np4sKEOJidYyt24YrIHi9/UEqRi+uuRQ4zCuXS6UjXftjAarPIPGkL/1S6B1Z91zg\nE5C0rXOvAlrvK09p4HGXLrwQxjrWt8R7rPvaD2yqVKLP4liFj8RMdg==\n-----END RSA PRIVATE KEY-----\n"
47+
BOX_SECURITY_AUTH_KEYS_PUBLIC="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApbUYGNmCz1P8G0j/FFOj\nx1d5GNssJ/jj6xasSwTIbjjt6FtYCDw8o7hayOc/u8aUqXCGhK3JD2T9gtKv9/rV\n30w4YzmHhA8OOuLJE7tfh/PJA4Hn4i2JJ30BuoZ7rPTlTRGdc1FS3XFdmBQtnplE\nkJ7y8qbdrVme3Kbtn+BR1BdtgwSybpNH2yqh3bb6PwpgNSMH7BIkBWL4A6QDpaFf\n1/9jSNE1vO25ssLC+bhFQNWLYriu+HogzEf9NWIrR2W29mI1QiA7wqvEuhg1yx38\nylWD8GhCGL6+2QLKBYgp7DIGv6UoTnqcVISatdQ51lVcCPmU6L1BhmcXVti6dWBI\n+wIDAQAB\n-----END PUBLIC KEY-----\n"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM node:18-alpine
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
RUN npm ci
7+
8+
COPY . .
9+
10+
EXPOSE 4000
11+
12+
CMD ["npm", "run", "dev"]
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
features: [API interception, Custom operations, Fastify, App framework, FHIR extensions]
3+
languages: [TypeScript]
4+
---
5+
# Aidbox: Intercept a FHIR endpoint
6+
7+
This example demonstrates how to intercept a standard Aidbox FHIR endpoint with a custom application, apply extra logic, and then continue the normal persistence flow through Aidbox.
8+
9+
Aidbox provides the ["App" functionality](https://docs.aidbox.app/app-development/aidbox-sdk/apps), which allows you to create an App entity and define operations for FHIR requests. In this example, Aidbox delegates `POST /fhir/Encounter` to a Node.js service. The service updates the incoming Encounter resource, sends the modified resource back to Aidbox, and returns Aidbox's response to the original caller.
10+
11+
## How it works?
12+
13+
You define an App entity in Aidbox that points to your custom HTTP server. Then, you register an Operation entity that tells Aidbox to delegate matching FHIR requests to your App.
14+
15+
We use the [init-bundle logic](https://docs.aidbox.app/configuration/init-bundle), which loads the App and Operation resources into Aidbox as a FHIR transactional bundle on startup (`./init-bundle.json`).
16+
Aidbox expects the Node.js service to be available at `http://host.docker.internal:4000`, as defined in the App resource.
17+
18+
```JSON
19+
{
20+
"type": "transaction",
21+
"entry": [
22+
{
23+
"resource": {
24+
"id": "node-service-1",
25+
"resourceType": "App",
26+
"apiVersion": 1,
27+
"type": "app",
28+
"endpoint": { "url": "http://host.docker.internal:4000", "type": "http-rpc", "secret": "secret" }
29+
},
30+
"request": { "method": "PUT", "url": "/App/node-service-1" }
31+
}, {
32+
"resource": {
33+
"id": "encounter-fhir-api",
34+
"resourceType": "Operation",
35+
"app": { "reference": "App/node-service-1" },
36+
"module": "node-service-1",
37+
"action": "proto.app/endpoint",
38+
"request": ["post", "fhir", "Encounter"]
39+
},
40+
"request": { "method": "PUT", "url": "/Operation/encounter-fhir-api" }
41+
}
42+
]
43+
}
44+
```
45+
46+
The interception logic is implemented in `src/index.ts`:
47+
48+
1. Aidbox receives `POST /fhir/Encounter`.
49+
2. Aidbox forwards the request to the Node.js service.
50+
3. The service adds an `identifier` to the Encounter:
51+
52+
```JSON
53+
{
54+
"system": "organization-1",
55+
"value": "00001"
56+
}
57+
```
58+
59+
4. The service calls Aidbox directly with `POST /Encounter` and the modified resource.
60+
5. The service returns Aidbox's response status and response body to the original client.
61+
62+
This pattern is useful when you need to enrich, validate, audit, route, or call external systems before allowing the original FHIR operation to continue.
63+
64+
## Prerequisites
65+
66+
- Node.js >= 18.0
67+
- Docker (optional)
68+
69+
70+
## STEP 1: Environment and Aidbox license
71+
72+
Copy `.env.tpl` file into `.env` file:
73+
74+
```shell
75+
cp .env.tpl .env
76+
```
77+
78+
If you are hosting Aidbox on your local computer, obtain the self-hosted license as described in the [documentation](https://docs.aidbox.app/getting-started/run-aidbox-locally-with-docker).
79+
80+
Add the license (`AIDBOX_LICENSE`) int the .env file.
81+
82+
## STEP 2: Run aidbox and node-app in Docker
83+
84+
```shell
85+
npm install
86+
docker compose up
87+
```
88+
89+
## Step 3: Open and log in into Aidbox instance
90+
91+
Open in browser http://localhost:8888
92+
93+
And log in with username: `admin` and password: `password`
94+
95+
## Step 4: Request `POST /fhir/Encounter` using REST Console
96+
97+
Aidbox delegates the request to the Node.js service using the App feature. The service intercepts the request, adds an Encounter identifier, persists the modified Encounter through Aidbox, and returns Aidbox's response.
98+
99+
### You can test the endpoint using curl:
100+
101+
```shell
102+
curl -H "Authorization: Basic cm9vdDpzZWNyZXQ=" \
103+
-H "Content-Type: application/json" \
104+
-X POST http://localhost:8888/fhir/Encounter \
105+
-d '{
106+
"resourceType": "Encounter",
107+
"status": "planned",
108+
"class": {
109+
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
110+
"code": "AMB",
111+
"display": "ambulatory"
112+
}
113+
}'
114+
```
115+
116+
The persisted Encounter contains the identifier added by the interception service:
117+
118+
```JSON
119+
{
120+
"identifier": [
121+
{
122+
"system": "organization-1",
123+
"value": "00001"
124+
}
125+
]
126+
}
127+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import Fastify from 'fastify';
2+
import healthcheck from 'fastify-healthcheck';
3+
import dotenv from 'dotenv';
4+
const requestToString = (request) => {
5+
return "/" + request.map((item) => {
6+
if (typeof item === "string")
7+
return item;
8+
return `:${item.name}`;
9+
}).join("/");
10+
};
11+
dotenv.config();
12+
if (!process.env.BOX_WEB_BASE_URL)
13+
throw "BOX_WEB_BASE_URL is not provided!";
14+
if (!process.env.BOX_ROOT_CLIENT_ID)
15+
throw "BOX_ROOT_CLIENT_ID is not provided!";
16+
if (!process.env.BOX_ROOT_CLIENT_SECRET)
17+
throw "BOX_ROOT_CLIENT_SECRET is not provided!";
18+
console.log(process.env.BOX_WEB_BASE_URL);
19+
const aidbox = async (path, options = {}) => {
20+
const url = `${process.env.BOX_WEB_BASE_URL}${path}`;
21+
const username = process.env.BOX_ROOT_CLIENT_ID;
22+
const password = process.env.BOX_ROOT_CLIENT_SECRET;
23+
const basic = Buffer.from(`${username}:${password}`).toString('base64');
24+
const headers = {
25+
...(options.headers || {}),
26+
'Authorization': `Basic ${basic}`,
27+
'Content-Type': 'application/json',
28+
};
29+
return fetch(url, { ...options, headers });
30+
};
31+
const fastify = Fastify({
32+
logger: true
33+
});
34+
fastify.register(healthcheck);
35+
fastify.post('/', async (req, res) => {
36+
const { operation, request } = req.body;
37+
const [method, ...rest] = operation.request;
38+
console.log("Incoming request:", requestToString(rest));
39+
console.log("Method:", method);
40+
console.log("Data:", request.resource);
41+
if ("/fhir/Encounter" === requestToString(rest) && method === "post") {
42+
const encounter = { ...request.resource, identifier: [{ system: "organization-1", value: "00001" }] };
43+
// Here you can do any kind of interception logic: run functions, make async http calls etc.
44+
// In this case we update Encounter resource with identifier, save it and response with original OperationOutcome:
45+
const response = await aidbox('/Encounter', { method: "POST", body: JSON.stringify(encounter) });
46+
const operationOutcome = await response.json();
47+
console.dir(operationOutcome);
48+
return res.status(response.status).send(operationOutcome);
49+
}
50+
return res.status(404).send({ error: "Not Supported" });
51+
});
52+
const start = async () => {
53+
try {
54+
const port = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : 4000;
55+
const host = '0.0.0.0';
56+
await fastify.listen({ port, host });
57+
fastify.log.info(`Server listening on ${host}:${port}`);
58+
}
59+
catch (err) {
60+
fastify.log.error(err);
61+
process.exit(1);
62+
}
63+
};
64+
start();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"root":["../src/index.ts"],"version":"5.8.3"}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
volumes:
2+
pg_data:
3+
name: pg_data
4+
services:
5+
aidbox-db:
6+
image: healthsamurai/aidboxdb:16.1
7+
pull_policy: always
8+
ports:
9+
- "${PGHOSTPORT}:5432"
10+
volumes:
11+
- "pg_data:/data:delegated"
12+
environment:
13+
POSTGRES_USER: "${PGUSER}"
14+
POSTGRES_PASSWORD: "${PGPASSWORD}"
15+
POSTGRES_DB: "${PGDATABASE}"
16+
17+
aidbox:
18+
image: healthsamurai/aidboxone:edge
19+
# pull_policy: always
20+
depends_on: ["aidbox-db"]
21+
volumes:
22+
- ./init-bundle.json:/tmp/init-bundle.json
23+
ports:
24+
- "${BOX_WEB_PORT}:${BOX_WEB_PORT}"
25+
env_file:
26+
- .env
27+
28+
app:
29+
build: .
30+
ports:
31+
- "${APP_PORT:-4000}:4000"
32+
env_file:
33+
- .env
34+
volumes:
35+
- ./src:/app/src
36+
- ./nodemon.json:/app/nodemon.json
37+
depends_on: ["aidbox"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"type": "transaction",
3+
"entry": [
4+
{
5+
"resource": {
6+
"id": "node-service-1",
7+
"resourceType": "App",
8+
"apiVersion": 1,
9+
"type": "app",
10+
"endpoint": { "url": "http://host.docker.internal:4000", "type": "http-rpc", "secret": "secret" }
11+
},
12+
"request": { "method": "PUT", "url": "/App/node-service-1" }
13+
}, {
14+
"resource": {
15+
"id": "encounter-fhir-api",
16+
"resourceType": "Operation",
17+
"app": { "reference": "App/node-service-1" },
18+
"module": "node-service-1",
19+
"action": "proto.app/endpoint",
20+
"request": ["post", "fhir", "Encounter"]
21+
},
22+
"request": { "method": "PUT", "url": "/Operation/encounter-fhir-api" }
23+
}
24+
]
25+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"watch": ["src"],
3+
"ext": "ts",
4+
"ignore": ["src/**/*.spec.ts"],
5+
"exec": "node --loader ts-node/esm src/index.ts"
6+
}

0 commit comments

Comments
 (0)