diff --git a/.env b/.env index 874226e..a964798 100644 --- a/.env +++ b/.env @@ -1,2 +1,5 @@ -SERVER_PORT=33333 -SERVER_HOST=127.0.0.1 \ No newline at end of file +SERVER_PORT=3323 +SERVER_HOST=0.0.0.0 +SPL_ZIP_PATH=/src/spl/TESTDATA_rems_document_and_rems_indexing_spl_files.zip +REMS_ADMIN_1_URL=http://localhost:8090/ +REMS_ADMIN_2_URL=http://localhost:8095/ \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..94b4532 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## Describe your changes + +Please include a summary of the changes and the related issue/task. Please also include relevant motivation and context. List any dependencies that are required for this change, including links to other pull requests/branches in other repositories if applicable. + +## Issue ticket number and Jira link + +Please include the Jira Ticket Number and Link for this issue/task. + +## Checklist before requesting a review +- [ ] I have performed a self-review of my code +- [ ] Ensure the target / base branch for any feature PR is set to `dev` not main (the only exception to this is releases from `dev` and hotfix branches) + +## Checklist for conducting a review +- [ ] Review the code changes and make sure they all make sense and are necessary. +- [ ] Pull the PR branch locally and test by running through workflow and making sure everything works as it is supposed to. + +## Workflow + +Owner of the Pull Request will be responsible for merge after all requirements are met, including approval from at least one reviewer. Additional changes made after a review will dismiss any approvals and require re-review of the additional updates. Auto merging can be enabled below if additional changes are likely not to be needed. The bot will auto assign reviewers to your Pull Request for you. + diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml new file mode 100644 index 0000000..7c45cfe --- /dev/null +++ b/.github/workflows/ci-workflow.yml @@ -0,0 +1,40 @@ +# name: Lint and Test + +# on: [push, pull_request] + +# jobs: +# lint: +# name: Check tsc, lint, and prettier +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v1 +# - name: Checkout Repository +# uses: actions/checkout@v3 +# with: +# submodules: true +# node-version: '18.x' + +# - run: npm install +# - run: npm run lint +# - run: npm run prettier +# env: +# CI: true +# test: +# name: Test on node ${{ matrix.node-version }} and ${{ matrix.os }} +# runs-on: ${{ matrix.os }} +# strategy: +# matrix: +# os: [ubuntu-latest, windows-latest, macos-latest] +# node-version: [18] + +# steps: +# - uses: actions/checkout@v1 +# - name: Use Node.js ${{ matrix.node-version }} +# uses: actions/setup-node@v1 +# with: +# node-version: ${{ matrix.node-version }} +# - run: npm install +# - run: git submodule update --init +# - run: npm test +# env: +# CI: true diff --git a/.github/workflows/docker-cd-dev.yml b/.github/workflows/docker-cd-dev.yml new file mode 100644 index 0000000..1daef2b --- /dev/null +++ b/.github/workflows/docker-cd-dev.yml @@ -0,0 +1,41 @@ +name: Docker Development Image CD + +on: + push: + branches: [ dev ] + workflow_dispatch: + +jobs: + docker-cd: + runs-on: ubuntu-latest + steps: + + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker meta data + id: docker-meta-data + uses: docker/metadata-action@v4 + with: + images: codexrems/rems-directory + flavor: latest=false + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push Server Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: codexrems/rems-directory:experimental + labels: ${{ steps.docker-meta-data.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-cd.yml b/.github/workflows/docker-cd.yml new file mode 100644 index 0000000..ce76ed6 --- /dev/null +++ b/.github/workflows/docker-cd.yml @@ -0,0 +1,41 @@ +name: Docker Image CD + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + docker-cd: + runs-on: ubuntu-latest + steps: + + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker meta data + id: docker-meta-data + uses: docker/metadata-action@v4 + with: + images: codexrems/rems-directory + flavor: latest=false + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push Server Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: codexrems/rems-directory:latest + labels: ${{ steps.docker-meta-data.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..5edae1d --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,20 @@ +name: Docker Image CI + +on: + pull_request: + branches: [ main, dev ] + workflow_dispatch: + + +jobs: + docker-ci: + runs-on: ubuntu-latest + steps: + + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Test Server Docker image Builds + run: docker build . \ No newline at end of file diff --git a/.github/workflows/docker-tag-cd.yml b/.github/workflows/docker-tag-cd.yml new file mode 100644 index 0000000..d86c5fc --- /dev/null +++ b/.github/workflows/docker-tag-cd.yml @@ -0,0 +1,41 @@ +name: Docker Tagged Image CD +on: + workflow_dispatch: + push: + tags: + - '*' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: true + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker meta data + id: docker-meta-data + uses: docker/metadata-action@v4 + with: + images: codexrems/rems-directory + flavor: latest=false + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.docker-meta-data.outputs.tags }} + labels: ${{ steps.docker-meta-data.outputs.labels }} diff --git a/.gitignore b/.gitignore index f06235c..8dbe853 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +logs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9818dc2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:21-alpine +WORKDIR /home/node/app/rems-directory + +COPY --chown=node:node . . +RUN npm install +EXPOSE 3323 + +HEALTHCHECK --interval=30s --start-period=15s --timeout=10m --retries=10 CMD wget --no-verbose --tries=1 --spider http://localhost:33333/health || exit 1 +CMD npm run dev \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..7ce1ce8 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,10 @@ +FROM node:21-alpine +WORKDIR /home/node/app/rems-directory + +COPY --chown=node:node . . +RUN npm install +EXPOSE 3323 +EXPOSE 3324 + +HEALTHCHECK --interval=30s --start-period=15s --timeout=10m --retries=10 CMD wget --no-verbose --tries=1 --spider http://localhost:33333/health || exit 1 +CMD ./dockerRunnerDev.sh \ No newline at end of file diff --git a/README.md b/README.md index 4c2b625..14b4268 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Description -The [REMS](https://www.fda.gov/drugs/drug-safety-and-availability/risk-evaluation-and-mitigation-strategies-rems) directory application is an app that acts as a API endpoint similar to the FDA API. It returns medication information when queried at the /drug/ndc.json endpoint. It adds the rems_endpoint that identifies the CDS Hooks server used by the REMS program. +The [REMS](https://www.fda.gov/drugs/drug-safety-and-availability/risk-evaluation-and-mitigation-strategies-rems) directory application is an app that acts as a API endpoint similar to the FDA API. It returns medication information when queried at the /drug/ndc.json endpoint. It adds the rems_endpoint that identifies the CDS Hooks server used by the REMS program. The /drugs/spl.zip returns spl information for the drugs it has spl info for, providing a zip file similar to the one from the FDA's website. The /health endpoint is a simple health check endpoint to make sure the server is live and online. + +Disclaimer: The SPL Zip file information is test data not to be representative of real world SPL information. While they are based on the actual SPL Zip from FDA, the files have been modified for the purposes of the prototype and may even be out of date. The data in these files are not intended for medical use. # Getting Started with REMS Administrator @@ -49,4 +51,7 @@ Following are a list of modifiable paths: | URI Name | Default | Description | | --------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | SERVER_PORT | 33333 | The port to run the server on. | -| SERVER_HOST | `127.0.0.1` | The hostname of the server. | \ No newline at end of file +| SERVER_HOST | `127.0.0.1` | The hostname of the server. | +| SPL_ZIP_PATH | /src/spl/TESTDATA_rems_document_and_rems_indexing_spl_files.zip | the path to the spl zip | +| REMS_ADMIN_1_URL | http://localhost:8090/ | the base url for the first rems admin | +| REMS_ADMIN_2_URL | http://localhost:8095/ | the base url for the second rems admin | \ No newline at end of file diff --git a/dockerRunnerDev.sh b/dockerRunnerDev.sh new file mode 100755 index 0000000..6615a56 --- /dev/null +++ b/dockerRunnerDev.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +# Handle closing application on signal interrupt (ctrl + c) +trap 'kill $CONTINUOUS_INSTALL_PID $SERVER_PID; gradle --stop; exit' INT + +mkdir logs +# Reset log file content for new application boot +echo "*** Logs for continuous installer ***" > ./logs/installer.log +echo "*** Logs for 'npm run start' ***" > ./logs/runner.log + +# Print that the application is starting in watch mode +echo "starting application in watch mode..." + +# Start the continious build listener process +echo "starting continuous installer..." +npm install + +( package_modify_time=$(stat -c %Y package.json) +package_lock_modify_time=$(stat -c %Y package-lock.json) +while sleep 1 +do + new_package_modify_time=$(stat -c %Y package.json) + new_package_lock_modify_time=$(stat -c %Y package-lock.json) + + if [[ "$package_modify_time" != "$new_package_modify_time" ]] || [[ "$package_lock_modify_time" != "$new_package_lock_modify_time" ]] + then + echo "running npm install..." + npm install | tee ./logs/installer.log + fi + + package_modify_time=$new_package_modify_time + package_lock_modify_time=$new_package_lock_modify_time + +done ) & CONTINUOUS_INSTALL_PID=$! + +# Start server process once initial build finishes +( npm run dev | tee ./logs/runner.log ) & SERVER_PID=$! + +# Handle application background process exiting +wait $CONTINUOUS_INSTALL_PID $SERVER_PID +EXIT_CODE=$? +echo "application exited with exit code $EXIT_CODE..." + + diff --git a/package-lock.json b/package-lock.json index 9b8d97e..231b019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,20 +9,21 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@types/node": "^22.10.0", "dotenv": "^16.4.7", "env-var": "^7.5.0", "runner": "^4.0.1" }, "devDependencies": { + "@types/node": "^22.13.13", "tsc-watch": "^6.2.1", "typescript": "^5.6.3" } }, "node_modules/@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, "dependencies": { "undici-types": "~6.20.0" } @@ -460,7 +461,8 @@ "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true }, "node_modules/which": { "version": "2.0.2", @@ -480,9 +482,10 @@ }, "dependencies": { "@types/node": { - "version": "22.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", - "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, "requires": { "undici-types": "~6.20.0" } @@ -802,7 +805,8 @@ "undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true }, "which": { "version": "2.0.2", diff --git a/package.json b/package.json index a954d5d..b5df06c 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "author": "", "license": "Apache-2.0", "devDependencies": { + "@types/node": "^22.13.13", "tsc-watch": "^6.2.1", "typescript": "^5.6.3" }, "dependencies": { - "@types/node": "^22.10.0", "dotenv": "^16.4.7", "env-var": "^7.5.0", "runner": "^4.0.1" diff --git a/src/data.ts b/src/data.ts index 280f745..f881127 100644 --- a/src/data.ts +++ b/src/data.ts @@ -1,10 +1,13 @@ +import * as env from 'env-var'; + export type Medication = { brand_name: string; generic_name: string, product_ndc: string, rems_administrator: string, - rems_endpoint: string, + rems_endpoint: string | undefined, rems_approval_date: string, + rems_spl_date?: string rems_modification_date: string } @@ -13,32 +16,34 @@ export const medications : Medication[] = [ brand_name: "ADDYI", generic_name: "FLIBANSERINE", product_ndc: "58604-214", - rems_administrator: "REMS Prototype", - rems_endpoint: "http://localhost:3003/", + rems_administrator: "REMS Prototype Admin 1", + rems_endpoint: env.get('REMS_ADMIN_1_URL').asString(), rems_approval_date: "20240906", rems_modification_date: "20240906" }, { brand_name: "Isotretinoin", generic_name: "ISOTRETINOIN", - product_ndc: "0591-2433", - rems_administrator: "REMS Prototype", - rems_endpoint: "http://localhost:3003/", + product_ndc: "0245-0571-01", + rems_administrator: "REMS Prototype Admin 2", + rems_endpoint: env.get('REMS_ADMIN_2_URL').asString(), rems_approval_date: "20240906", - rems_modification_date: "20240906" + rems_modification_date: "20240906", + rems_spl_date: "20230912" }, { brand_name: "Fentanyl Citrate", generic_name: "FENTANYL CITRATE", - product_ndc: "0093-7865", - rems_administrator: "REMS Prototype", - rems_endpoint: "http://localhost:3003/", + product_ndc: "63459-502-30", + rems_administrator: "REMS Prototype Admin 1", + rems_endpoint: env.get('REMS_ADMIN_1_URL').asString(), rems_approval_date: "20240906", - rems_modification_date: "20240906" + rems_modification_date: "20240906", + rems_spl_date: "20230401" }, { brand_name: "Turalio", generic_name: "PEXIDARTINIB HYDROCHLORIDE", - product_ndc: "65597-407", - rems_administrator: "REMS Prototype", - rems_endpoint: "http://localhost:3003/", + product_ndc: "65597-402-20", + rems_administrator: "REMS Prototype Admin 2", + rems_endpoint: env.get('REMS_ADMIN_2_URL').asString(), rems_approval_date: "20240906", rems_modification_date: "20240906" } diff --git a/src/server.ts b/src/server.ts index c17b001..05e378c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,7 +1,8 @@ import { createServer } from 'node:http'; import { IncomingMessage, ServerResponse } from 'http'; import { URL } from 'node:url'; - +import { createReadStream } from 'node:fs'; +import { join } from 'node:path'; import 'dotenv/config'; import * as env from 'env-var'; @@ -11,6 +12,7 @@ import { Response, Meta, Results, ErrorResponse } from './response'; const hostname = env.get('SERVER_HOST').asString(); const port = env.get('SERVER_PORT').asInt(); +const spl_zip_path = env.get('SPL_ZIP_PATH').asString()!; const server = createServer((req: IncomingMessage, res: ServerResponse) => { const { method, url, headers } = req; @@ -21,7 +23,13 @@ const server = createServer((req: IncomingMessage, res: ServerResponse) => { // strip the end '/' from the pathname if it is there let pathname = parsedUrl.pathname.replace(/\/$/, '') - if (method === "GET" && pathname === "/drug/ndc.json") { + if (method === "GET" && pathname === "/health") { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() })); + } + + else if (method === "GET" && pathname === "/drug/ndc.json") { let response: Response | undefined; searchParams.forEach((value, key) => { @@ -81,6 +89,46 @@ const server = createServer((req: IncomingMessage, res: ServerResponse) => { res.end(JSON.stringify(errorResponse)); } } + + else if (method === "GET" && pathname === "/drugs/spl.zip") { + try { + + const filePath = join( process.cwd(), spl_zip_path); + + const fileStream = createReadStream(filePath); + + res.statusCode = 200; + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="${spl_zip_path.substring(spl_zip_path.lastIndexOf('/') + 1)}"`); + + fileStream.pipe(res); + + fileStream.on('error', (err) => { + console.error('Error streaming zip file:', err); + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + const errorResponse: ErrorResponse = { + error: { + code: 'INTERNAL_SERVER_ERROR', + message: 'Error streaming the requested file' + } + }; + res.end(JSON.stringify(errorResponse)); + }); + } catch (err) { + console.error('Error accessing zip file:', err); + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + const errorResponse: ErrorResponse = { + error: { + code: 'INTERNAL_SERVER_ERROR', + message: 'Error accessing the requested file' + } + }; + res.end(JSON.stringify(errorResponse)); + } + } + else { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); diff --git a/src/spl/TESTDATA_Docker_rems_document_and_rems_indexing_spl_files.zip b/src/spl/TESTDATA_Docker_rems_document_and_rems_indexing_spl_files.zip new file mode 100644 index 0000000..b53042c Binary files /dev/null and b/src/spl/TESTDATA_Docker_rems_document_and_rems_indexing_spl_files.zip differ diff --git a/src/spl/TESTDATA_rems_document_and_rems_indexing_spl_files.zip b/src/spl/TESTDATA_rems_document_and_rems_indexing_spl_files.zip new file mode 100644 index 0000000..3013038 Binary files /dev/null and b/src/spl/TESTDATA_rems_document_and_rems_indexing_spl_files.zip differ diff --git a/src/spl/disclaimer.md b/src/spl/disclaimer.md new file mode 100644 index 0000000..143c585 --- /dev/null +++ b/src/spl/disclaimer.md @@ -0,0 +1,3 @@ +# Disclaimer + +These SPL Zip file's information is test data not to be representative of real world SPL information. While they are based on the actual SPL Zip from FDA, the files have been modified for the purposes of the REMS prototype and may even be out of date. The data in these files are not intended for medical use. \ No newline at end of file