From 9b09e57af68dfbbebc7f1960c0e74e30ade04101 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Thu, 26 Feb 2026 14:55:11 -0500 Subject: [PATCH 01/18] added test data --- {importer => migrations}/test-data/Subset_Phase-0-Metadata.txt | 0 {importer => migrations}/test-data/Subset_Phase-0_RNA-TPM.tsv | 0 .../test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv | 0 .../test-data/Subset_Phase_0_ATAC_zscores.tsv | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {importer => migrations}/test-data/Subset_Phase-0-Metadata.txt (100%) rename {importer => migrations}/test-data/Subset_Phase-0_RNA-TPM.tsv (100%) rename {importer => migrations}/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv (100%) rename {importer => migrations}/test-data/Subset_Phase_0_ATAC_zscores.tsv (100%) diff --git a/importer/test-data/Subset_Phase-0-Metadata.txt b/migrations/test-data/Subset_Phase-0-Metadata.txt similarity index 100% rename from importer/test-data/Subset_Phase-0-Metadata.txt rename to migrations/test-data/Subset_Phase-0-Metadata.txt diff --git a/importer/test-data/Subset_Phase-0_RNA-TPM.tsv b/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv similarity index 100% rename from importer/test-data/Subset_Phase-0_RNA-TPM.tsv rename to migrations/test-data/Subset_Phase-0_RNA-TPM.tsv diff --git a/importer/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv b/migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv similarity index 100% rename from importer/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv rename to migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv diff --git a/importer/test-data/Subset_Phase_0_ATAC_zscores.tsv b/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv similarity index 100% rename from importer/test-data/Subset_Phase_0_ATAC_zscores.tsv rename to migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv From 14f14de73dc9808a86c46015840c5aa0d808292a Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Thu, 26 Feb 2026 16:01:56 -0500 Subject: [PATCH 02/18] added docker compose for tests --- importer/src/atac.ts | 2 +- importer/src/meta.ts | 6 ++--- importer/src/rna.ts | 2 +- importer/src/utils.ts | 35 ++++++++++++++++++++++++-- migrations/docker-compose.test.yml | 40 ++++++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) mode change 100644 => 100755 importer/src/meta.ts mode change 100644 => 100755 importer/src/utils.ts create mode 100644 migrations/docker-compose.test.yml diff --git a/importer/src/atac.ts b/importer/src/atac.ts index 22d6b8d..a0591de 100644 --- a/importer/src/atac.ts +++ b/importer/src/atac.ts @@ -2,7 +2,7 @@ import { streamImport } from "./utils"; import { sql } from "./db"; const filePath = - "https://users.wenglab.org/niship/Phase_0_ATAC_zscores.tsv"; + process.env.ATAC_ZSCORE_FILE || "https://users.wenglab.org/niship/Phase_0_ATAC_zscores.tsv"; export async function createATACTables() { await sql` diff --git a/importer/src/meta.ts b/importer/src/meta.ts old mode 100644 new mode 100755 index d990309..2725bf2 --- a/importer/src/meta.ts +++ b/importer/src/meta.ts @@ -1,9 +1,9 @@ import { streamImport } from "./utils"; import { sql } from "./db"; -const rnaFilePath = "https://users.wenglab.org/niship/Phase-0-Metadata.txt"; -const atacFilePath = - "https://users.wenglab.org/niship/Phase_0_ATAC_Metadata_with_entity.tsv"; +const rnaFilePath = process.env.RNA_META_FILE || "https://users.wenglab.org/niship/Phase-0-Metadata.txt"; +const atacFilePath = process.env.ATAC_META_FILE || "https://users.wenglab.org/niship/Phase_0_ATAC_Metadata_with_entity.tsv"; + const sexMap: Record = { "1": "male", "2": "female" }; diff --git a/importer/src/rna.ts b/importer/src/rna.ts index bba557c..2ea7c20 100644 --- a/importer/src/rna.ts +++ b/importer/src/rna.ts @@ -1,6 +1,6 @@ import { streamImport } from "./utils"; import { sql } from "./db"; -const filePath = "https://users.wenglab.org/niship/Phase-0_RNA-TPM.tsv"; +const filePath = process.env.RNA_TPM_FILE || "https://users.wenglab.org/niship/Phase-0_RNA-TPM.tsv"; export async function createRNATables() { await sql` diff --git a/importer/src/utils.ts b/importer/src/utils.ts old mode 100644 new mode 100755 index b04ee59..066d7ae --- a/importer/src/utils.ts +++ b/importer/src/utils.ts @@ -6,8 +6,11 @@ export async function streamImport( parseLine: (line: string) => Record, ) { console.log(`streaming ${table} from ${url}...`); - const response = await fetch(url); - const reader = response.body!.getReader(); + //const response = await fetch(url); + //const reader = response.body!.getReader(); + + const reader = await getReader(url); + const decoder = new TextDecoder(); let buffer = ""; @@ -50,6 +53,34 @@ export async function streamImport( console.log(`inserted ${total} ${table} rows`); } +async function getReader(source: string) { + + // HTTP or HTTPS + if (source.startsWith("http://") || source.startsWith("https://")) { + const response = await fetch(source); + if (!response.ok) { + throw new Error(`Failed to fetch ${source}: ${response.statusText}`); + } + return response.body!.getReader(); + } + + // file:// path + if (source.startsWith("file://")) { + const path = source.replace("file://", ""); + const file = Bun.file(path); + return file.stream().getReader(); + } + + // assume local file path + const file = Bun.file(source); + if (!(await file.exists())) { + throw new Error(`Local file not found: ${source}`); + } + + return file.stream().getReader(); +} + + async function insertRows(table: string, rows: Record[]) { await sql`INSERT INTO ${sql(table)} ${sql(rows)}`; } diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml new file mode 100644 index 0000000..4275425 --- /dev/null +++ b/migrations/docker-compose.test.yml @@ -0,0 +1,40 @@ +version: "3.9" + +services: + postgres: + image: postgres:15 + container_name: test-postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + importer: + build: + context: ../importer + container_name: test-importer + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb + RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt + ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv + RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv + ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv + volumes: + - .:/migrations + command: > + bun run src/index.ts + --datatype meta + --datatype rna + --datatype atac + --schema test_schema_v1 \ No newline at end of file From 44db4d96ca482c26ad55d08b607cffb4a9107940 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Thu, 26 Feb 2026 16:16:47 -0500 Subject: [PATCH 03/18] added importer tests for metadata --- importer/Dockerfile | 2 +- importer/tests/meta.test.ts | 66 ++++++++++++++++++++++++++++++ migrations/docker-compose.test.yml | 11 ++--- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 importer/tests/meta.test.ts diff --git a/importer/Dockerfile b/importer/Dockerfile index 8017f84..8051fc8 100644 --- a/importer/Dockerfile +++ b/importer/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /usr/src/app COPY . . -ENTRYPOINT [ "bun", "run", "src/index.ts" ] +CMD [ "bun", "run", "src/index.ts" ] diff --git a/importer/tests/meta.test.ts b/importer/tests/meta.test.ts new file mode 100644 index 0000000..1c95430 --- /dev/null +++ b/importer/tests/meta.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, test, beforeAll, afterAll } from "bun:test"; +import { SQL } from "bun"; + +const schema = "test_schema_v1"; + +const sql = new SQL({ + url: "postgresql://postgres:postgres@postgres:5432/testdb", + max: 1, + connectionTimeout: 3, +}); + +beforeAll(async () => { + await sql`SET search_path TO ${sql(schema)}`; +}); +afterAll(async () => { + await sql.end(); +}); + +describe("Metadata Tables Integration Tests", () => { + test("Schema exists", async () => { + const result = await sql` + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = ${schema} + `; + expect(result.length).toBe(1); + }); + + test("atac_metadata table exists", async () => { + const result = await sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = ${schema} + AND table_name = 'atac_metadata' + `; + expect(result.length).toBe(1); + }); + + test("rna_metadata table exists", async () => { + const result = await sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = ${schema} + AND table_name = 'rna_metadata' + `; + expect(result.length).toBe(1); + }); + + + + test("RNA Metadata was inserted", async () => { + const count = await sql` + SELECT COUNT(*)::int AS total + FROM ${sql(schema)}.rna_metadata + `; + expect(count[0].total).toBeGreaterThan(0); + }); + + test("atac Metadata was inserted", async () => { + const count = await sql` + SELECT COUNT(*)::int AS total + FROM ${sql(schema)}.atac_metadata + `; + expect(count[0].total).toBeGreaterThan(0); + }); +}); \ No newline at end of file diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml index 4275425..d359e79 100644 --- a/migrations/docker-compose.test.yml +++ b/migrations/docker-compose.test.yml @@ -33,8 +33,9 @@ services: volumes: - .:/migrations command: > - bun run src/index.ts - --datatype meta - --datatype rna - --datatype atac - --schema test_schema_v1 \ No newline at end of file + sh -c " + echo 'RUNNING IMPORTER' && + bun run src/index.ts --datatype meta --schema test_schema_v1 && + echo 'RUNNING TESTS' && + bun test + " \ No newline at end of file From fd666023bd1b1fbc049f3bbab6e52928e8933a03 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Mon, 2 Mar 2026 07:00:32 -0500 Subject: [PATCH 04/18] added test importer scripts --- importer/Dockerfile | 2 +- migrations/docker-compose.test.yml | 14 ++++---------- migrations/scripts/test-importer.sh | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100755 migrations/scripts/test-importer.sh diff --git a/importer/Dockerfile b/importer/Dockerfile index 8051fc8..8017f84 100644 --- a/importer/Dockerfile +++ b/importer/Dockerfile @@ -3,4 +3,4 @@ WORKDIR /usr/src/app COPY . . -CMD [ "bun", "run", "src/index.ts" ] +ENTRYPOINT [ "bun", "run", "src/index.ts" ] diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml index d359e79..e8f72ff 100644 --- a/migrations/docker-compose.test.yml +++ b/migrations/docker-compose.test.yml @@ -1,10 +1,8 @@ -version: "3.9" - services: postgres: image: postgres:15 container_name: test-postgres - restart: unless-stopped + restart: "no" environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -32,10 +30,6 @@ services: ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv volumes: - .:/migrations - command: > - sh -c " - echo 'RUNNING IMPORTER' && - bun run src/index.ts --datatype meta --schema test_schema_v1 && - echo 'RUNNING TESTS' && - bun test - " \ No newline at end of file + + + \ No newline at end of file diff --git a/migrations/scripts/test-importer.sh b/migrations/scripts/test-importer.sh new file mode 100755 index 0000000..dbff5d6 --- /dev/null +++ b/migrations/scripts/test-importer.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +cd "$(dirname "$(dirname "$0")")" + +trap 'docker compose -f docker-compose.test.yml down -v' EXIT + +docker compose -f docker-compose.test.yml up -d postgres + +# Wait for postgres +until docker exec test-postgres psql -U postgres -d testdb -c "select 1" > /dev/null 2>&1; do + sleep 2 +done + +# Seed database +docker compose -f docker-compose.test.yml run --rm importer \ + bun run src/index.ts --datatype meta --datatype atac --datatype rna --schema test_schema_v1 + +# Run tests +docker compose -f docker-compose.test.yml run --rm importer bun test \ No newline at end of file From 4304cb9c16fee622c972b7d9f6a97d94bf62a922 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Mon, 2 Mar 2026 14:11:18 -0500 Subject: [PATCH 05/18] rework api tests - not complete --- migrations/docker-compose.test.yml | 11 ++ migrations/scripts/test-importer.sh | 6 +- .../test-data/Subset_Phase-0_RNA-TPM.tsv | 20 ++-- .../test-data/Subset_Phase_0_ATAC_zscores.tsv | 20 ++-- service/Dockerfile | 7 ++ service/package.json | 1 + service/{test => tests}/down.ts | 0 service/{test => tests}/integration.test.ts | 100 +++++++++--------- service/{test => tests}/seed.ts | 0 service/{test => tests}/up.ts | 0 10 files changed, 94 insertions(+), 71 deletions(-) rename service/{test => tests}/down.ts (100%) rename service/{test => tests}/integration.test.ts (59%) rename service/{test => tests}/seed.ts (100%) rename service/{test => tests}/up.ts (100%) diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml index e8f72ff..4a340c1 100644 --- a/migrations/docker-compose.test.yml +++ b/migrations/docker-compose.test.yml @@ -30,6 +30,17 @@ services: ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv volumes: - .:/migrations + + service: + build: + context: ../service + working_dir: /usr/src/app + container_name: test-service + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb + depends_on: + postgres: + condition: service_healthy \ No newline at end of file diff --git a/migrations/scripts/test-importer.sh b/migrations/scripts/test-importer.sh index dbff5d6..491c137 100755 --- a/migrations/scripts/test-importer.sh +++ b/migrations/scripts/test-importer.sh @@ -17,4 +17,8 @@ docker compose -f docker-compose.test.yml run --rm importer \ bun run src/index.ts --datatype meta --datatype atac --datatype rna --schema test_schema_v1 # Run tests -docker compose -f docker-compose.test.yml run --rm importer bun test \ No newline at end of file +docker compose -f docker-compose.test.yml run --rm --entrypoint "" importer bun test + + +docker compose -f docker-compose.test.yml run --rm --entrypoint "" service ls +docker compose -f docker-compose.test.yml run --rm --entrypoint "" service bun test \ No newline at end of file diff --git a/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv b/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv index 6922a5f..f33ba3e 100644 --- a/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv +++ b/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv @@ -1,10 +1,10 @@ -Gene MOHD_ER100001 MOHD_ER100002 MOHD_ER100003 MOHD_ER100004 MOHD_ER100005 MOHD_ER100006 MOHD_ER100007 MOHD_ER100008 MOHD_ER100009 MOHD_ER100010 MOHD_ER100011 MOHD_ER100012 MOHD_ER100013 MOHD_ER100014 MOHD_ER100015 -ENSG00000000003.17 0.00 0.15 0.03 0.45 0.02 0.13 0.09 0.39 0.73 0.21 0.04 0.07 0.03 0.42 0.06 -ENSG00000000005.6 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -ENSG00000000419.15 4.27 3.80 5.11 7.99 3.61 5.46 5.92 9.07 7.69 6.42 5.26 7.37 3.54 13.43 7.31 -ENSG00000000457.15 3.33 6.45 4.14 6.37 2.96 4.65 4.88 7.69 6.33 3.43 3.62 5.39 3.32 6.23 5.09 -ENSG00000000460.18 1.67 2.60 1.96 2.88 1.24 2.15 2.49 3.91 2.60 1.71 1.09 3.06 1.13 3.06 2.63 -ENSG00000000938.14 82.43 132.31 104.58 113.67 167.47 158.13 101.36 101.87 86.66 97.19 156.05 134.56 85.97 157.44 136.27 -ENSG00000000971.18 0.81 1.50 1.13 1.78 0.25 0.19 0.22 0.53 0.45 0.33 0.70 0.72 0.33 0.42 1.41 -ENSG00000001036.15 3.66 5.90 4.87 5.06 3.14 3.34 5.16 6.58 4.89 5.10 5.40 5.56 5.05 12.33 5.22 -ENSG00000001084.14 8.15 7.24 6.45 7.00 4.45 4.78 8.14 9.52 6.52 5.82 7.49 8.83 6.32 10.74 6.35 +Gene MOHD_ER100001 MOHD_ER100002 MOHD_ER100003 MOHD_ER100004 MOHD_ER100005 MOHD_ER100006 MOHD_ER100007 MOHD_ER100008 MOHD_ER100009 +ENSG00000000003.17 0.00 0.15 0.03 0.45 0.02 0.13 0.09 0.39 0.73 +ENSG00000000005.6 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +ENSG00000000419.15 4.27 3.80 5.11 7.99 3.61 5.46 5.92 9.07 7.69 +ENSG00000000457.15 3.33 6.45 4.14 6.37 2.96 4.65 4.88 7.69 6.33 +ENSG00000000460.18 1.67 2.60 1.96 2.88 1.24 2.15 2.49 3.91 2.60 +ENSG00000000938.14 82.43 132.31 104.58 113.67 167.47 158.13 101.36 101.87 86.66 +ENSG00000000971.18 0.81 1.50 1.13 1.78 0.25 0.19 0.22 0.53 0.45 +ENSG00000001036.15 3.66 5.90 4.87 5.06 3.14 3.34 5.16 6.58 4.89 +ENSG00000001084.14 8.15 7.24 6.45 7.00 4.45 4.78 8.14 9.52 6.52 diff --git a/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv b/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv index 84802a1..770210c 100644 --- a/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv +++ b/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv @@ -1,10 +1,10 @@ -accession MOHD_EA100001 MOHD_EA100002 MOHD_EA100003 MOHD_EA100004 MOHD_EA100005 MOHD_EA100006 MOHD_EA100007 MOHD_EA100008 MOHD_EA100009 MOHD_EA100010 MOHD_EA100011 MOHD_EA100012 MOHD_EA100013 MOHD_EA100014 MOHD_EA100016 MOHD_EA100017 MOHD_EA100018 MOHD_EA100019 MOHD_EA100020 MOHD_EA100021 MOHD_EA100022 MOHD_EA100023 MOHD_EA100024 MOHD_EA100025 MOHD_EA100026 MOHD_EA100027 MOHD_EA100028 MOHD_EA100029 MOHD_EA100030 MOHD_EA100031 MOHD_EA100032 MOHD_EA100033 MOHD_EA100034 -EH38E0064571 1.3421206470903115 -1.6171268124361105 0.7120356675079318 -0.2169437516611677 0.716299144697867 0.0197587218207491 -0.0648128430274946 0.4853006494151133 -0.4555953284288295 0.0009097010682366 -0.4690392605692212 -0.4734515669176924 0.0367696148026724 0.7085090608613637 0.1721936114189396 0.0079615610910205 -0.4340745498430713 0.0825529108095185 0.3119494078495279 1.0021678815331534 -0.8776742374534849 1.1070229696677254 0.2004045578346122 0.6936314891685978 0.2928514373688832 -10.0 -0.5115410174999657 0.2787081291118575 0.9366159216970418 -0.2103058822530265 0.2963774350997418 1.154240918882228 0.4739575485195661 -EH38E1055789 0.4284099465225757 1.273932444136992 -0.0169923560545649 0.4812930735071549 1.026905539102142 -0.2076881768262488 0.9223861115526087 0.9079480080096052 0.3392838504337237 0.4085226817568155 0.7466749760877669 -0.6093253834178148 -0.4822212867614853 -0.5417245877888092 -0.7231926346287499 -0.6107164327808381 0.2340299819309995 0.8371969945792291 -0.1967169697493316 0.333860681592139 0.631273960736324 0.1853946106608008 0.6247316044583012 0.4759753811322245 0.6250312089062642 0.7411699971233073 -0.2730988248910775 0.6612708809738558 -0.2868749054076486 0.4289314231016457 -0.2190977308695406 0.035221043709454 0.4210398728974287 -EH38E0065181 1.0946240100666855 0.4868701973395669 1.0629996465922815 0.8640328296909696 1.0110857021003308 0.9886279879711136 0.824645181275024 0.2436264401456017 0.5781007415649599 0.3848360206540924 1.338069947635351 1.0142325725622043 1.2732353184101666 0.7488972180008012 0.8117053129726035 0.8572388019210867 1.1869761968272872 1.0704691580395989 0.833464223190211 1.510994726295849 0.2944131564854509 0.4668417406439437 0.6113613093456043 0.6677565704671354 0.8574191418398389 -1.0301266405924516 1.0974911912844607 0.845085019299412 0.955606284747348 0.815725940479918 0.9565016070106456 0.8428460018566734 0.3125620620694577 -EH38E0066213 0.4814397484684051 0.5679480603280043 0.1253742920583956 -0.0922480321020566 0.4671366523718899 -0.5234206752413854 -0.1230649987585363 0.5243880975731914 0.2519154040244888 0.7413579889427866 0.1690743742619633 -0.2793895539017587 0.6664530119740445 -0.0469034547733503 -0.6474789069443777 -3.0062225274462144 0.2748218356244776 -0.1269308490540661 0.0985024392299895 0.5014112416553567 0.032425939487355 0.0039742461650955 0.357179003826741 0.6207034169174329 0.0457652338301071 -0.4936102142241074 -0.4692081790684012 -1.5556272188964075 -0.4868975990607362 -0.3602226837764667 0.264808059573663 0.7124472222264988 0.8827589118157361 -EH38E1315534 1.5939057328168995 1.6671530833160049 1.5292501763617428 1.0225499125607591 0.9624958073049644 1.3749815937558236 0.7158071122213411 0.8357498137019542 0.889192222632198 1.0209738322831097 0.9051835931287096 0.2650740143629349 -0.2635359902215493 0.9710023883143802 -0.0648104463401439 0.7693926411536653 1.4996636410719193 1.4463445540991804 0.8294829388169643 1.3330747562219547 0.5723807290957688 1.226986818202973 -1.24092782016381 0.9253178915215626 -0.1399832863271921 -0.5314664745277747 1.4956192814402816 -0.8854515170615129 1.1622012547157718 1.411542404964449 0.6843712743655505 1.2264281164293884 -1.4197950905359726 -EH38E0068841 1.2578984023069406 0.4100570429119903 0.2889092935386638 1.557459605931634 1.7161601365809531 1.6067491480628109 0.4555582193245628 1.0510897947748974 1.1570687131476525 1.944980186214765 0.3485609616525506 0.3777296241259997 0.5706433677270668 1.735958089468518 1.352284342268179 1.1015083858327737 0.9194071523814208 1.2906964198559787 1.0371653601146027 0.7417185429089325 0.2776152647277053 1.6523540238899264 0.3331629900504306 0.5587113800213992 1.0707999871014764 0.1224385766706843 1.430597267075837 0.2582562116272757 1.4070858226269252 1.2809381059273717 0.7473905894513166 0.1785946618483618 0.622559926827013 -EH38E0070245 1.0089267967812914 0.4619674720764506 0.5757483865745583 0.980101139858179 0.492931242476721 0.2278273664371812 0.0490382755014444 1.402866558701378 0.8937650579501188 1.1092762935327378 1.2796905217424095 -0.1882547152304856 0.1358579742169229 0.7544916892671985 0.4759219783394565 0.576877086925099 0.816486606215686 0.4443639166813039 0.1978756914025247 0.7482631428872549 0.1562025523023937 1.0204944003225118 1.239655088288675 0.3940075179701999 0.6969188099729964 0.8659065584935368 0.6959689167367388 0.6095242398362599 0.3439774728099877 0.3496885430495927 0.8251821274678731 0.4872672563047439 1.0250930101503704 -EH38E1057390 -0.4980563178586394 -1.4165205000984973 -1.1863362678877047 -0.1738428627238113 -0.545331363592249 -10.0 -2.1687104873341108 -0.7937898168904047 0.0194712667989814 -10.0 0.0973761590466777 -0.3104997995727087 -1.2494015162033876 -10.0 0.3940346756781438 -0.3295964650630695 0.0330471888560071 -0.5826334707003606 -4.109046602748172 -10.0 0.2601004224563404 -1.0564261711168732 -1.0707122251446224 -0.2801239518586575 -2.803241218325707 -10.0 0.1632319509612132 0.0369874131976827 -10.0 -0.0629704973218181 -0.9846385318936745 -2.0397140726138128 0.5081511184572305 -EH38E2790704 1.305770987803671 0.4938405151868142 1.079001151069677 0.7766293367876772 0.8180083420783284 1.236240729674574 0.7515879801206895 0.639705356870892 0.9470653981822968 1.078424019439883 1.0027568470794688 1.000986620467109 1.017046224290334 0.8829647851973255 0.9384754183250448 0.7391963608117991 1.050002227851405 0.7516533921967222 1.0274133087652957 -0.2467755121884355 -0.6442457186906779 0.9718140676139798 0.5624330179669071 0.782685028732327 0.8765108710752221 0.9923540437441376 0.9113953589408555 0.8672166493832891 -0.1390242290941879 1.6137010315915457 0.4952635348880063 1.2800978661693418 1.5642308974566932 +accession MOHD_EA100001 MOHD_EA100002 MOHD_EA100003 MOHD_EA100004 MOHD_EA100005 MOHD_EA100006 MOHD_EA100007 MOHD_EA100008 MOHD_EA100009 +EH38E0064571 1.3421206470903115 -1.6171268124361105 0.7120356675079318 -0.2169437516611677 0.716299144697867 0.0197587218207491 -0.0648128430274946 0.4853006494151133 -0.4555953284288295 +EH38E1055789 0.4284099465225757 1.273932444136992 -0.0169923560545649 0.4812930735071549 1.026905539102142 -0.2076881768262488 0.9223861115526087 0.9079480080096052 0.3392838504337237 +EH38E0065181 1.0946240100666855 0.4868701973395669 1.0629996465922815 0.8640328296909696 1.0110857021003308 0.9886279879711136 0.824645181275024 0.2436264401456017 0.5781007415649599 +EH38E0066213 0.4814397484684051 0.5679480603280043 0.1253742920583956 -0.0922480321020566 0.4671366523718899 -0.5234206752413854 -0.1230649987585363 0.5243880975731914 0.2519154040244888 +EH38E1315534 1.5939057328168995 1.6671530833160049 1.5292501763617428 1.0225499125607591 0.9624958073049644 1.3749815937558236 0.7158071122213411 0.8357498137019542 0.889192222632198 +EH38E0068841 1.2578984023069406 0.4100570429119903 0.2889092935386638 1.557459605931634 1.7161601365809531 1.6067491480628109 0.4555582193245628 1.0510897947748974 1.1570687131476525 +EH38E0070245 1.0089267967812914 0.4619674720764506 0.5757483865745583 0.980101139858179 0.492931242476721 0.2278273664371812 0.0490382755014444 1.402866558701378 0.8937650579501188 +EH38E1057390 -0.4980563178586394 -1.4165205000984973 -1.1863362678877047 -0.1738428627238113 -0.545331363592249 -10.0 -2.1687104873341108 -0.7937898168904047 0.0194712667989814 +EH38E2790704 1.305770987803671 0.4938405151868142 1.079001151069677 0.7766293367876772 0.8180083420783284 1.236240729674574 0.7515879801206895 0.639705356870892 0.9470653981822968 diff --git a/service/Dockerfile b/service/Dockerfile index c49a208..41a3116 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -16,7 +16,14 @@ RUN bun run build # release stage FROM oven/bun:1 AS release +WORKDIR /usr/src/app +COPY --from=build /usr/src/app/dist ./dist +COPY --from=build /usr/src/app/node_modules ./node_modules +COPY --from=build /usr/src/app/package.json ./package.json +COPY --from=build /usr/src/app/. . + COPY --from=build /usr/src/app/dist/index.js . +#COPY --from=build /usr/src/app/* . # run the api USER bun diff --git a/service/package.json b/service/package.json index 316ba93..8d2ccd9 100644 --- a/service/package.json +++ b/service/package.json @@ -4,6 +4,7 @@ "dev": "bun run --hot src/index.ts", "build": "bun build ./src/index.ts --target=bun --outdir=./dist", "start": "bun dist/index.js", + "test": "bun test", "db:start": "docker compose up -d db", "db:stop": "docker compose down", "db:up": "bun test/up.ts", diff --git a/service/test/down.ts b/service/tests/down.ts similarity index 100% rename from service/test/down.ts rename to service/tests/down.ts diff --git a/service/test/integration.test.ts b/service/tests/integration.test.ts similarity index 59% rename from service/test/integration.test.ts rename to service/tests/integration.test.ts index 7a42bfa..45b2c5b 100644 --- a/service/test/integration.test.ts +++ b/service/tests/integration.test.ts @@ -33,58 +33,58 @@ describe("graphql atac_zscore and metadata", () => { ); const body = (await res.json()) as any; const atac_metadata = body.data.atac_metadata[0]; - expect(atac_metadata.sample_id).toBe("SAMPLE_001"); - expect(atac_metadata.site).toBe("st1"); - expect(atac_metadata.opc_id).toBe("opcA"); - expect(atac_metadata.protocol).toBe("prtA"); - expect(atac_metadata.status).toBe("case"); - expect(atac_metadata.sex).toBe("male"); - expect(atac_metadata.entity_id).toBe("entA"); - expect(atac_metadata.umap_x).toBe(1.0); - expect(atac_metadata.umap_y).toBe(2.0); - expect(atac_metadata.biospecimen).toBe("bsA"); + expect(atac_metadata.sample_id).toBe("MOHD_EA100001"); + expect(atac_metadata.site).toBe("CCH"); + expect(atac_metadata.opc_id).toBe("CCH_0001"); + expect(atac_metadata.protocol).toBe("Buffy Coat method"); + expect(atac_metadata.status).toBe("Case"); + expect(atac_metadata.sex).toBe("female"); + expect(atac_metadata.entity_id).toBe("CCH_0001_BC_01"); + expect(atac_metadata.umap_x).toBe(4.974631); + expect(atac_metadata.umap_y).toBe(1.9259008); + expect(atac_metadata.biospecimen).toBe("buffy coat"); }) test("single accession returns samples with metadata", async () => { const res = await app.request( gql( - '{ atac_zscore(accessions: ["0"]) { accession, samples { value, metadata { sample_id, site, opc_id, protocol, status, sex, entity_id, umap_x, umap_y, biospecimen } } } }', + '{ atac_zscore(accessions: ["EH38E0064571"]) { accession, samples { value, metadata { sample_id, site, opc_id, protocol, status, sex, entity_id, umap_x, umap_y, biospecimen } } } }', ), ); const body = (await res.json()) as any; const acc = body.data.atac_zscore[0]; - expect(acc.accession).toBe("0"); - expect(acc.samples).toHaveLength(5); - expect(acc.samples[0].value).toBe(0); - expect(acc.samples[0].metadata.sample_id).toBe("SAMPLE_001"); - expect(acc.samples[0].metadata.site).toBe("st1"); - expect(acc.samples[0].metadata.opc_id).toBe("opcA"); - expect(acc.samples[0].metadata.protocol).toBe("prtA"); - expect(acc.samples[0].metadata.status).toBe("case"); - expect(acc.samples[0].metadata.sex).toBe("male"); - expect(acc.samples[0].metadata.entity_id).toBe("entA"); - expect(acc.samples[0].metadata.umap_x).toBe(1.0); - expect(acc.samples[0].metadata.umap_y).toBe(2.0); - expect(acc.samples[0].metadata.biospecimen).toBe("bsA"); - // + expect(acc.accession).toBe("EH38E0064571"); + expect(acc.samples).toHaveLength(9); + expect(acc.samples[0].value).toBe(1.3421206470903115); + expect(acc.samples[0].sample_id).toBe("MOHD_EA100001"); + expect(acc.samples[0].site).toBe("CCH"); + expect(acc.samples[0].opc_id).toBe("CCH_0001"); + expect(acc.samples[0].protocol).toBe("Buffy Coat method"); + expect(acc.samples[0].status).toBe("Case"); + expect(acc.samples[0].sex).toBe("female"); + expect(acc.samples[0].entity_id).toBe("CCH_0001_BC_01"); + expect(acc.samples[0].umap_x).toBe(4.974631); + expect(acc.samples[0].umap_y).toBe(1.9259008); + expect(acc.samples[0].biospecimen).toBe("buffy coat"); + }); test("multiple accessions", async () => { const res = await app.request( gql( - '{ atac_zscore(accessions: ["0", "1"]) { accession, samples { value } } }', + '{ atac_zscore(accessions: ["EH38E0064571", "EH38E1055789"]) { accession, samples { value } } }', ), ); const body = (await res.json()) as any; const accs = body.data.atac_zscore; expect(accs).toHaveLength(2); - expect(accs[0].accession).toBe("0"); - expect(accs[1].accession).toBe("1"); + expect(accs[0].accession).toBe("EH38E0064571"); + expect(accs[1].accession).toBe("EH38E1055789"); // accession "1" first sample should be 1.00 - expect(accs[1].samples[0].value).toBe(1); + //expect(accs[1].samples[0].value).toBe(1); }); test("non-existent accession returns empty samples", async () => { @@ -129,47 +129,47 @@ describe("graphql rna_tpm", () => { const gene_metadata = body.data.rna_metadata[0]; - expect(gene_metadata.sample_id).toBe("SAMPLE_001"); - expect(gene_metadata.kit).toBe("kitA"); - expect(gene_metadata.site).toBe("st1"); + expect(gene_metadata.sample_id).toBe("MOHD_ER100001"); + expect(gene_metadata.kit).toBe("CCH_0001"); + expect(gene_metadata.site).toBe("CCH"); expect(gene_metadata.status).toBe("case"); - expect(gene_metadata.sex).toBe("male"); - expect(gene_metadata.umap_x).toBe(1.0); - expect(gene_metadata.umap_y).toBe(2.0); + expect(gene_metadata.sex).toBe("female"); + expect(gene_metadata.umap_x).toBe(7.639448); + expect(gene_metadata.umap_y).toBe(22.995956); }); test("single gene returns tpm values with metadata", async () => { const res = await app.request( gql( - '{ rna_tpm(gene_ids: ["0"]) { gene_id, samples { value, metadata {sample_id, kit, site, status, sex, umap_x, umap_y } } } }', + '{ rna_tpm(gene_ids: ["ENSG00000000003"]) { gene_id, samples { value, metadata {sample_id, kit, site, status, sex, umap_x, umap_y } } } }', ), ); const body = (await res.json()) as any; const gene = body.data.rna_tpm[0]; - expect(gene.gene_id).toBe("0"); - expect(gene.samples).toHaveLength(5); - expect(gene.samples[0].value).toBe(0); - expect(gene.samples[0].metadata.sample_id).toBe("SAMPLE_001"); - expect(gene.samples[0].metadata.kit).toBe("kitA"); - expect(gene.samples[0].metadata.site).toBe("st1"); - expect(gene.samples[0].metadata.status).toBe("case"); - expect(gene.samples[0].metadata.sex).toBe("male"); - expect(gene.samples[0].metadata.umap_x).toBe(1.0); - expect(gene.samples[0].metadata.umap_y).toBe(2.0); + expect(gene.gene_id).toBe("ENSG00000000003"); + expect(gene.samples).toHaveLength(9); + expect(gene.samples[0].value).toBe(0.00); + expect(gene.samples[0].sample_id).toBe("MOHD_ER100001"); + expect(gene.samples[0].kit).toBe("CCH_0001"); + expect(gene.samples[0].site).toBe("CCH"); + expect(gene.samples[0].status).toBe("case"); + expect(gene.samples[0].sex).toBe("female"); + expect(gene.samples[0].umap_x).toBe(7.639448); + expect(gene.samples[0].umap_y).toBe(22.995956); }); test("multiple genes", async () => { const res = await app.request( - gql('{ rna_tpm(gene_ids: ["0", "1"]) { gene_id, samples { value } } }'), + gql('{ rna_tpm(gene_ids: ["ENSG00000000003", "ENSG00000000005"]) { gene_id, samples { value } } }'), ); const body = (await res.json()) as any; const genes = body.data.rna_tpm; expect(genes).toHaveLength(2); - expect(genes[0].gene_id).toBe("0"); - expect(genes[1].gene_id).toBe("1"); + expect(genes[0].gene_id).toBe("ENSG00000000003"); + expect(genes[1].gene_id).toBe("ENSG00000000005"); // gene "1" first sample should be 1.00 - expect(genes[1].samples[0].value).toBe(1); + // expect(genes[1].samples[0].value).toBe(1); }); test("non-existent gene returns empty samples", async () => { diff --git a/service/test/seed.ts b/service/tests/seed.ts similarity index 100% rename from service/test/seed.ts rename to service/tests/seed.ts diff --git a/service/test/up.ts b/service/tests/up.ts similarity index 100% rename from service/test/up.ts rename to service/tests/up.ts From 0c02395e88ab61c98ea1a0cfd67d24cd5072a56f Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Mon, 2 Mar 2026 14:16:43 -0500 Subject: [PATCH 06/18] added test script in package json --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 95af77c..8c686d7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "mohd-api", "private": true, - "workspaces": ["service", "importer"], + "workspaces": [ + "service", + "importer" + ], "scripts": { "dev": "bun run --filter mohd-service dev", "build": "bun run --filter mohd-service build", @@ -12,7 +15,7 @@ "db:down": "bun run --filter mohd-service db:down", "db:seed": "bun run --filter mohd-service db:seed", "db:reset": "bun run --filter mohd-service db:reset", - "test": "bun run --filter mohd-service test", + "test": "./migrations/scripts/test-importer.sh", "deploy:api": "bun run --filter mohd-service deploy:api", "deploy:importer": "bun run --filter mohd-importer deploy" } From b6cb694f539af7444e73b1d6c2fb9e1deacee8d9 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Mon, 2 Mar 2026 15:09:04 -0500 Subject: [PATCH 07/18] added api tests --- migrations/docker-compose.test.yml | 90 ++++++++++++++--------------- migrations/scripts/test-importer.sh | 5 +- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml index 4a340c1..4c8e4e1 100644 --- a/migrations/docker-compose.test.yml +++ b/migrations/docker-compose.test.yml @@ -1,46 +1,46 @@ -services: - postgres: - image: postgres:15 - container_name: test-postgres - restart: "no" - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb - ports: - - "5432:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 - - importer: - build: - context: ../importer - container_name: test-importer - depends_on: - postgres: - condition: service_healthy - environment: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb - RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt - ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv - RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv - ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv - volumes: - - .:/migrations - - service: - build: - context: ../service - working_dir: /usr/src/app - container_name: test-service - environment: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb - depends_on: - postgres: - condition: service_healthy - - +services: + postgres: + image: postgres:15 + container_name: test-postgres + restart: "no" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + + importer: + build: + context: ../importer + container_name: test-importer + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb + RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt + ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv + RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv + ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv + volumes: + - .:/migrations + + service: + build: + context: ../service + working_dir: /usr/src/app + container_name: test-service + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb?search_path=test_schema_v1 + depends_on: + postgres: + condition: service_healthy + + \ No newline at end of file diff --git a/migrations/scripts/test-importer.sh b/migrations/scripts/test-importer.sh index 491c137..2da97f7 100755 --- a/migrations/scripts/test-importer.sh +++ b/migrations/scripts/test-importer.sh @@ -12,6 +12,9 @@ until docker exec test-postgres psql -U postgres -d testdb -c "select 1" > /dev/ sleep 2 done +docker compose -f docker-compose.test.yml build --no-cache importer + +docker compose -f docker-compose.test.yml build --no-cache importer # Seed database docker compose -f docker-compose.test.yml run --rm importer \ bun run src/index.ts --datatype meta --datatype atac --datatype rna --schema test_schema_v1 @@ -19,6 +22,6 @@ docker compose -f docker-compose.test.yml run --rm importer \ # Run tests docker compose -f docker-compose.test.yml run --rm --entrypoint "" importer bun test +docker compose -f docker-compose.test.yml build --no-cache service -docker compose -f docker-compose.test.yml run --rm --entrypoint "" service ls docker compose -f docker-compose.test.yml run --rm --entrypoint "" service bun test \ No newline at end of file From 199d452fad7d7ecbe64d8a0dd5672703418dc0ed Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Tue, 3 Mar 2026 10:20:20 -0500 Subject: [PATCH 08/18] added more importer tests --- importer/src/meta.ts | 2 +- importer/tests/atac.test.ts | 59 +++++++++++++++++++++++++++++++ importer/tests/meta.test.ts | 49 ++++++++++++++++++++++++- importer/tests/rna.test.ts | 59 +++++++++++++++++++++++++++++++ service/tests/integration.test.ts | 42 +++++++++++----------- 5 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 importer/tests/atac.test.ts create mode 100644 importer/tests/rna.test.ts diff --git a/importer/src/meta.ts b/importer/src/meta.ts index 2725bf2..28a3ecb 100755 --- a/importer/src/meta.ts +++ b/importer/src/meta.ts @@ -5,7 +5,7 @@ const rnaFilePath = process.env.RNA_META_FILE || "https://users.wenglab.org/ni const atacFilePath = process.env.ATAC_META_FILE || "https://users.wenglab.org/niship/Phase_0_ATAC_Metadata_with_entity.tsv"; -const sexMap: Record = { "1": "male", "2": "female" }; +const sexMap: Record = { "1": "female", "2": "male" }; export async function createMetaTables() { diff --git a/importer/tests/atac.test.ts b/importer/tests/atac.test.ts new file mode 100644 index 0000000..dd5c470 --- /dev/null +++ b/importer/tests/atac.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test, beforeAll, afterAll } from "bun:test"; +import { SQL } from "bun"; + +const schema = "test_schema_v1"; + +const sql = new SQL({ + url: "postgresql://postgres:postgres@postgres:5432/testdb", + max: 1, + connectionTimeout: 3, +}); + +beforeAll(async () => { + await sql`SET search_path TO ${sql(schema)}`; +}); +afterAll(async () => { + await sql.end(); +}); + +describe("ATAC ZScore Tables Integration Tests", () => { + test("Schema exists", async () => { + const result = await sql` + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = ${schema} + `; + expect(result.length).toBe(1); + }); + + test("atac_zscore table exists", async () => { + const result = await sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = ${schema} + AND table_name = 'atac_zscore' + `; + expect(result.length).toBe(1); + }); + + + test("ATAC ZScore was inserted", async () => { + const count = await sql` + SELECT COUNT(*)::int AS total + FROM ${sql(schema)}.atac_zscore + `; + expect(count[0].total).toBeGreaterThan(0); + }); + + test("First ATAC zscore accession is correct when ordered", async () => { + const result = await sql` + SELECT * + FROM ${sql(schema)}.atac_zscore ORDER BY accession LIMIT 1 + `; + expect(result[0].accession).toBe("EH38E0064571"); + expect(result[0].zscore_values.length).toBe(9); + + }); + + +}); \ No newline at end of file diff --git a/importer/tests/meta.test.ts b/importer/tests/meta.test.ts index 1c95430..b8094c4 100644 --- a/importer/tests/meta.test.ts +++ b/importer/tests/meta.test.ts @@ -56,11 +56,58 @@ describe("Metadata Tables Integration Tests", () => { expect(count[0].total).toBeGreaterThan(0); }); - test("atac Metadata was inserted", async () => { + test("ATAC Metadata was inserted", async () => { const count = await sql` SELECT COUNT(*)::int AS total FROM ${sql(schema)}.atac_metadata `; expect(count[0].total).toBeGreaterThan(0); }); + + test("ATAC sex column only contains valid enum values", async () => { + const result = await sql` + SELECT DISTINCT sex::text + FROM ${sql(schema)}.atac_metadata + `; + + for (const row of result) { + expect(["male", "female"]).toContain(row.sex); + } + }); + + + test("First ATAC sample_id is correct when ordered", async () => { + const result = await sql` + SELECT * + FROM ${sql(schema)}.atac_metadata ORDER BY sample_id LIMIT 1 + `; + expect(result[0].sample_id).toBe("MOHD_EA100001"); + expect(result[0].site).toBe("CCH"); + expect(result[0].opc_id).toBe("CCH_0001"); + expect(result[0].protocol).toBe("Buffy Coat method"); + expect(result[0].status).toBe("case"); + expect(result[0].sex).toBe("female"); + expect(result[0].entity_id).toBe("CCH_0001_BC_01"); + expect(result[0].umap_x).toBe("4.974631"); + expect(result[0].umap_y).toBe("1.925901"); + expect(result[0].biospecimen).toBe("buffy coat"); + + }); + + test("First RNA sample_id is correct when ordered", async () => { + const result = await sql` + SELECT * + FROM ${sql(schema)}.rna_metadata ORDER BY sample_id LIMIT 1 + `; + expect(result[0].sample_id).toBe("MOHD_ER100001"); + expect(result[0].kit).toBe("CCH_0001"); + expect(result[0].site).toBe("CCH"); + expect(result[0].status).toBe("case"); + expect(result[0].sex).toBe("female"); + expect(result[0].umap_x).toBe("7.639448"); + expect(result[0].umap_y).toBe("22.995956"); + + }); + + }); \ No newline at end of file diff --git a/importer/tests/rna.test.ts b/importer/tests/rna.test.ts new file mode 100644 index 0000000..ebe5e5c --- /dev/null +++ b/importer/tests/rna.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test, beforeAll, afterAll } from "bun:test"; +import { SQL } from "bun"; + +const schema = "test_schema_v1"; + +const sql = new SQL({ + url: "postgresql://postgres:postgres@postgres:5432/testdb", + max: 1, + connectionTimeout: 3, +}); + +beforeAll(async () => { + await sql`SET search_path TO ${sql(schema)}`; +}); +afterAll(async () => { + await sql.end(); +}); + +describe("RNA ZScore Tables Integration Tests", () => { + test("Schema exists", async () => { + const result = await sql` + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = ${schema} + `; + expect(result.length).toBe(1); + }); + + test("rna_tpm table exists", async () => { + const result = await sql` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = ${schema} + AND table_name = 'rna_tpm' + `; + expect(result.length).toBe(1); + }); + + + test("RNA TPM was inserted", async () => { + const count = await sql` + SELECT COUNT(*)::int AS total + FROM ${sql(schema)}.rna_tpm + `; + expect(count[0].total).toBeGreaterThan(0); + }); + + test("First RNA tpm gene_id is correct when ordered", async () => { + const result = await sql` + SELECT * + FROM ${sql(schema)}.rna_tpm ORDER BY gene_id LIMIT 1 + `; + expect(result[0].gene_id).toBe("ENSG00000000003"); + expect(result[0].tpm_values.length).toBe(9); + + }); + + +}); \ No newline at end of file diff --git a/service/tests/integration.test.ts b/service/tests/integration.test.ts index 45b2c5b..0090e52 100644 --- a/service/tests/integration.test.ts +++ b/service/tests/integration.test.ts @@ -37,11 +37,11 @@ describe("graphql atac_zscore and metadata", () => { expect(atac_metadata.site).toBe("CCH"); expect(atac_metadata.opc_id).toBe("CCH_0001"); expect(atac_metadata.protocol).toBe("Buffy Coat method"); - expect(atac_metadata.status).toBe("Case"); + expect(atac_metadata.status).toBe("case"); expect(atac_metadata.sex).toBe("female"); expect(atac_metadata.entity_id).toBe("CCH_0001_BC_01"); expect(atac_metadata.umap_x).toBe(4.974631); - expect(atac_metadata.umap_y).toBe(1.9259008); + expect(atac_metadata.umap_y).toBe(1.925901); expect(atac_metadata.biospecimen).toBe("buffy coat"); @@ -57,17 +57,19 @@ describe("graphql atac_zscore and metadata", () => { expect(acc.accession).toBe("EH38E0064571"); expect(acc.samples).toHaveLength(9); - expect(acc.samples[0].value).toBe(1.3421206470903115); - expect(acc.samples[0].sample_id).toBe("MOHD_EA100001"); - expect(acc.samples[0].site).toBe("CCH"); - expect(acc.samples[0].opc_id).toBe("CCH_0001"); - expect(acc.samples[0].protocol).toBe("Buffy Coat method"); - expect(acc.samples[0].status).toBe("Case"); - expect(acc.samples[0].sex).toBe("female"); - expect(acc.samples[0].entity_id).toBe("CCH_0001_BC_01"); - expect(acc.samples[0].umap_x).toBe(4.974631); - expect(acc.samples[0].umap_y).toBe(1.9259008); - expect(acc.samples[0].biospecimen).toBe("buffy coat"); + + expect(acc.samples[0].value).toBe(1.34); + + expect(acc.samples[0].metadata.site).toBe("CCH"); + expect(acc.samples[0].metadata.opc_id).toBe("CCH_0001"); + expect(acc.samples[0].metadata.protocol).toBe("Buffy Coat method"); + expect(acc.samples[0].metadata.status).toBe("case"); + expect(acc.samples[0].metadata.sex).toBe("female"); + expect(acc.samples[0].metadata.entity_id).toBe("CCH_0001_BC_01"); + expect(acc.samples[0].metadata.umap_x).toBe(4.974631); + expect(acc.samples[0].metadata.umap_y).toBe(1.925901); + expect(acc.samples[0].metadata.biospecimen).toBe("buffy coat"); + expect(acc.samples[0].metadata.sample_id).toBe("MOHD_EA100001"); }); @@ -149,13 +151,13 @@ describe("graphql rna_tpm", () => { expect(gene.gene_id).toBe("ENSG00000000003"); expect(gene.samples).toHaveLength(9); expect(gene.samples[0].value).toBe(0.00); - expect(gene.samples[0].sample_id).toBe("MOHD_ER100001"); - expect(gene.samples[0].kit).toBe("CCH_0001"); - expect(gene.samples[0].site).toBe("CCH"); - expect(gene.samples[0].status).toBe("case"); - expect(gene.samples[0].sex).toBe("female"); - expect(gene.samples[0].umap_x).toBe(7.639448); - expect(gene.samples[0].umap_y).toBe(22.995956); + expect(gene.samples[0].metadata.sample_id).toBe("MOHD_ER100001"); + expect(gene.samples[0].metadata.kit).toBe("CCH_0001"); + expect(gene.samples[0].metadata.site).toBe("CCH"); + expect(gene.samples[0].metadata.status).toBe("case"); + expect(gene.samples[0].metadata.sex).toBe("female"); + expect(gene.samples[0].metadata.umap_x).toBe(7.639448); + expect(gene.samples[0].metadata.umap_y).toBe(22.995956); }); test("multiple genes", async () => { From 09adac228ea8d1c4f1bb45821854a12a58313b68 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Tue, 3 Mar 2026 10:27:07 -0500 Subject: [PATCH 09/18] refactored dockerfile --- service/Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index 41a3116..aa07a51 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -17,13 +17,8 @@ RUN bun run build # release stage FROM oven/bun:1 AS release WORKDIR /usr/src/app -COPY --from=build /usr/src/app/dist ./dist -COPY --from=build /usr/src/app/node_modules ./node_modules -COPY --from=build /usr/src/app/package.json ./package.json COPY --from=build /usr/src/app/. . - COPY --from=build /usr/src/app/dist/index.js . -#COPY --from=build /usr/src/app/* . # run the api USER bun From 8944d7f82b01920a8048e026a378c4a918a67560 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Tue, 3 Mar 2026 10:44:33 -0500 Subject: [PATCH 10/18] updated github workflow --- .github/workflows/test.yaml | 42 +++++++++++++------ .../scripts/{test-importer.sh => test.sh} | 0 package.json | 2 +- 3 files changed, 30 insertions(+), 14 deletions(-) rename migrations/scripts/{test-importer.sh => test.sh} (100%) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 275b89a..89c3574 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,28 +1,44 @@ name: Test on: + push: + branches: + - rework_api_db_tests # <- replace with your branch pull_request: - branches: [main] + branches: + - [main] # optional: also run on PRs to this branch + jobs: test: runs-on: ubuntu-latest services: postgres: image: postgres - env: + env: + POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb ports: - 5432:5432 options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-cmd="pg_isready -U postgres" + --health-interval=5s + --health-timeout=5s + --health-retries=10 env: - POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/postgres + POSTGRES_URL: postgres://postgres:postgres@localhost:5432/testdb?search_path=test_schema_v1 + DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb?search_path=test_schema_v1 + RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt + ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv + RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv + ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - run: bun run db:up - - run: bun run db:seed - - run: bun test + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Compose + uses: docker/setup-buildx-action@v3 + + - name: Build and run test.sh + run: | + chmod +x ./migrations/scripts/test.sh + ./migrations/scripts/test.sh diff --git a/migrations/scripts/test-importer.sh b/migrations/scripts/test.sh similarity index 100% rename from migrations/scripts/test-importer.sh rename to migrations/scripts/test.sh diff --git a/package.json b/package.json index 8c686d7..8a7c942 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "db:down": "bun run --filter mohd-service db:down", "db:seed": "bun run --filter mohd-service db:seed", "db:reset": "bun run --filter mohd-service db:reset", - "test": "./migrations/scripts/test-importer.sh", + "test": "./migrations/scripts/test.sh", "deploy:api": "bun run --filter mohd-service deploy:api", "deploy:importer": "bun run --filter mohd-importer deploy" } From e120f5a2d389b1a34ffed0f5ec962b1fdfd61852 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Tue, 3 Mar 2026 10:46:22 -0500 Subject: [PATCH 11/18] updated github workflow --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 89c3574..699a7b5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,10 +2,10 @@ name: Test on: push: branches: - - rework_api_db_tests # <- replace with your branch + - '*' # <- replace with your branch pull_request: branches: - - [main] # optional: also run on PRs to this branch + - ['main'] # optional: also run on PRs to this branch jobs: test: From 549680c10b0364f1cfb91ca40f4b8722ed877e36 Mon Sep 17 00:00:00 2001 From: NishiPhalke Date: Tue, 3 Mar 2026 10:48:48 -0500 Subject: [PATCH 12/18] updated github workflow - fix --- .github/workflows/test.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 699a7b5..b2036a2 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,11 +1,10 @@ name: Test on: push: - branches: - - '*' # <- replace with your branch + pull_request: branches: - - ['main'] # optional: also run on PRs to this branch + - main # optional: also run on PRs to this branch jobs: test: From 04fef145b98069b13bd9669f277091933a027620 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 11:03:21 -0500 Subject: [PATCH 13/18] fixed github workflow --- .github/workflows/test.yaml | 23 +---------------------- migrations/docker-compose.test.yml | 4 ++-- migrations/scripts/test.sh | 16 +++++++++++----- package.json | 2 +- 4 files changed, 15 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b2036a2..1391dcc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,32 +4,11 @@ on: pull_request: branches: - - main # optional: also run on PRs to this branch + - main jobs: test: runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: testdb - ports: - - 5432:5432 - options: >- - --health-cmd="pg_isready -U postgres" - --health-interval=5s - --health-timeout=5s - --health-retries=10 - env: - POSTGRES_URL: postgres://postgres:postgres@localhost:5432/testdb?search_path=test_schema_v1 - DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb?search_path=test_schema_v1 - RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt - ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv - RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv - ATAC_ZSCORE_FILE: /migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml index 4c8e4e1..3c45ca3 100644 --- a/migrations/docker-compose.test.yml +++ b/migrations/docker-compose.test.yml @@ -23,7 +23,7 @@ services: postgres: condition: service_healthy environment: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb + POSTGRES_URL: postgres://postgres:postgres@postgres:5432/testdb RNA_META_FILE: /migrations/test-data/Subset_Phase-0-Metadata.txt ATAC_META_FILE: /migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv RNA_TPM_FILE: /migrations/test-data/Subset_Phase-0_RNA-TPM.tsv @@ -37,7 +37,7 @@ services: working_dir: /usr/src/app container_name: test-service environment: - DATABASE_URL: postgres://postgres:postgres@postgres:5432/testdb?search_path=test_schema_v1 + POSTGRES_URL: postgres://postgres:postgres@postgres:5432/testdb?search_path=test_schema_v1 depends_on: postgres: condition: service_healthy diff --git a/migrations/scripts/test.sh b/migrations/scripts/test.sh index 2da97f7..56dcdf8 100755 --- a/migrations/scripts/test.sh +++ b/migrations/scripts/test.sh @@ -5,23 +5,29 @@ cd "$(dirname "$(dirname "$0")")" trap 'docker compose -f docker-compose.test.yml down -v' EXIT +# Setup Postgres docker compose -f docker-compose.test.yml up -d postgres -# Wait for postgres until docker exec test-postgres psql -U postgres -d testdb -c "select 1" > /dev/null 2>&1; do sleep 2 done +# Setup importer docker compose -f docker-compose.test.yml build --no-cache importer -docker compose -f docker-compose.test.yml build --no-cache importer # Seed database docker compose -f docker-compose.test.yml run --rm importer \ - bun run src/index.ts --datatype meta --datatype atac --datatype rna --schema test_schema_v1 + bun run src/index.ts \ + --datatype meta \ + --datatype atac \ + --datatype rna \ + --schema test_schema_v1 -# Run tests +# Run importer tests docker compose -f docker-compose.test.yml run --rm --entrypoint "" importer bun test +# Setup service docker compose -f docker-compose.test.yml build --no-cache service -docker compose -f docker-compose.test.yml run --rm --entrypoint "" service bun test \ No newline at end of file +# Run service tests +docker compose -f docker-compose.test.yml run --rm --entrypoint "" service bun test diff --git a/package.json b/package.json index 8a7c942..d8feec5 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ "dev": "bun run --filter mohd-service dev", "build": "bun run --filter mohd-service build", "import": "bun run --filter mohd-importer start", + "test": "./migrations/scripts/test.sh", "db:start": "bun run --filter mohd-service db:start", "db:stop": "bun run --filter mohd-service db:stop", "db:up": "bun run --filter mohd-service db:up", "db:down": "bun run --filter mohd-service db:down", "db:seed": "bun run --filter mohd-service db:seed", "db:reset": "bun run --filter mohd-service db:reset", - "test": "./migrations/scripts/test.sh", "deploy:api": "bun run --filter mohd-service deploy:api", "deploy:importer": "bun run --filter mohd-importer deploy" } From 32413e7164b4c3c1609569a9e535130730ed2597 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 11:06:30 -0500 Subject: [PATCH 14/18] ignore opencode --- .gitignore | 1 + .opencode/opencode.jsonc | 14 -------------- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 .opencode/opencode.jsonc diff --git a/.gitignore b/.gitignore index deed335..b45782b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ .env +.opencode diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc deleted file mode 100644 index 02e4eda..0000000 --- a/.opencode/opencode.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "mcp": { - "bun": { - "type": "remote", - "url": "https://bun.com/docs/mcp", - }, - "gcloud": { - "type": "local", - "command": ["npx", "-y", "@google-cloud/gcloud-mcp"], - "enabled": true, - }, - }, -} From c806599d89e595fb4cc0dd67321fb2f5fdf34974 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 13:24:53 -0500 Subject: [PATCH 15/18] rework api tests and cleanup --- .github/workflows/test.yaml | 6 +- README.md | 69 ++++++++++++++++++ importer/README.md | 15 ---- package.json | 8 +-- scripts/db-reset.sh | 25 +++++++ service/README.md | 65 ----------------- service/package.json | 4 -- service/tests/down.ts | 24 ------- service/tests/seed.ts | 139 ------------------------------------ service/tests/up.ts | 49 ------------- 10 files changed, 99 insertions(+), 305 deletions(-) create mode 100644 README.md delete mode 100644 importer/README.md create mode 100755 scripts/db-reset.sh delete mode 100644 service/README.md delete mode 100644 service/tests/down.ts delete mode 100644 service/tests/seed.ts delete mode 100644 service/tests/up.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1391dcc..ccc26b4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,7 +16,5 @@ jobs: - name: Set up Docker Compose uses: docker/setup-buildx-action@v3 - - name: Build and run test.sh - run: | - chmod +x ./migrations/scripts/test.sh - ./migrations/scripts/test.sh + - name: Build and run test:ci + run: bun run test:ci diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d0156d --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# mohd-api + +Bun monorepo: GraphQL API + data importer for MOHD. + +## Quick Start + +```bash +bun install +bun run db:start +bun run db:reset +bun run dev +``` + +## Commands + +| Command | What | +|---------|------| +| `dev` | Start API locally | +| `db:start` | Start Postgres | +| `db:reset` | Import test data | +| `db:stop` | Stop Postgres | +| `test:service` | Run local tests | +| `test:ci` | Run full test suite (CI) | +| `import` | Run data importer | +| `deploy:api` | Deploy API to Cloud Run | +| `deploy:importer` | Deploy importer job | + +## Structure + +- `service/` - GraphQL API +- `importer/` - Data import pipeline +- `migrations/` - Docker setup + test data + +## Env + +Copy `.env.example` to `.env` in each package. + +Cloud Run needs: +- `POSTGRES_URL` - DB connection string +- `POSTGRES_PATH` - Cloud SQL socket (optional) + +## Cloud SQL Proxy + +Local access to Cloud SQL: + +```bash +cloud-sql-proxy PROJECT:REGION:INSTANCE --port=5432 +``` + +Set env: + +``` +POSTGRES_URL="postgresql://USER:PASSWORD@localhost:PORT/DATABASE" +``` + +## Importer + +```bash +bun run import # import all test data +``` + +Or manually: + +```bash +cd importer +bun run src/index.ts --datatype meta --schema public +bun run src/index.ts --datatype rna --schema public +bun run src/index.ts --datatype atac --schema public +``` diff --git a/importer/README.md b/importer/README.md deleted file mode 100644 index 07c9d0e..0000000 --- a/importer/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Importer for MOHD Data - -This package downloads and inserts MOHD data into the database. - -## How to run - -Use bun to run the importer, and specify the data to be imported -```bash -bun run src/index.ts --datatype DATATYPE --schema SCHEMA -``` - -Datatypes allowed: -- rna (Gene RNA seq TPM) -- atac (ATAC z-scores) -- meta (metadata) diff --git a/package.json b/package.json index d8feec5..3c90507 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,11 @@ "dev": "bun run --filter mohd-service dev", "build": "bun run --filter mohd-service build", "import": "bun run --filter mohd-importer start", - "test": "./migrations/scripts/test.sh", + "test:ci": "./migrations/scripts/test.sh", + "test:service": "cd service && POSTGRES_URL=\"postgresql://postgres:postgres@localhost:5432/postgres\" bun test", "db:start": "bun run --filter mohd-service db:start", "db:stop": "bun run --filter mohd-service db:stop", - "db:up": "bun run --filter mohd-service db:up", - "db:down": "bun run --filter mohd-service db:down", - "db:seed": "bun run --filter mohd-service db:seed", - "db:reset": "bun run --filter mohd-service db:reset", + "db:reset": "./scripts/db-reset.sh", "deploy:api": "bun run --filter mohd-service deploy:api", "deploy:importer": "bun run --filter mohd-importer deploy" } diff --git a/scripts/db-reset.sh b/scripts/db-reset.sh new file mode 100755 index 0000000..89be47c --- /dev/null +++ b/scripts/db-reset.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")/.." + +echo "Waiting for PostgreSQL to be ready..." +for i in {1..30}; do + if docker exec service-db-1 pg_isready -U postgres > /dev/null 2>&1; then + echo "PostgreSQL is ready!" + # Additional wait for initialization + sleep 2 + break + fi + sleep 1 +done + +cd importer + +export POSTGRES_URL="postgresql://postgres:postgres@localhost:5432/postgres" +export RNA_META_FILE="../migrations/test-data/Subset_Phase-0-Metadata.txt" +export ATAC_META_FILE="../migrations/test-data/Subset_Phase_0_ATAC_Metadata_with_entity.tsv" +export RNA_TPM_FILE="../migrations/test-data/Subset_Phase-0_RNA-TPM.tsv" +export ATAC_ZSCORE_FILE="../migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv" + +bun run src/index.ts --datatype meta --datatype atac --datatype rna --schema public diff --git a/service/README.md b/service/README.md deleted file mode 100644 index 1abf580..0000000 --- a/service/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# mohd-api - -Bun + Hono GraphQL API for MOHD data. - -Deployment on GCP with Cloud Run and Cloud SQL. - -## Development - -```bash -bun install -bun run dev -``` - -## Local Database (Docker) - -```bash -cp .env.example .env -bun run db:start # start postgres container -bun run db:up # run migrations -bun run db:seed # seed test data -``` - -Other commands: `db:down`, `db:reset`, `db:stop` - -## Testing - -```bash -bun test -``` - -## Deployment - -```bash -bun run deploy:api # deploy API to Cloud Run -bun run deploy:importer # deploy importer as Cloud Run job -``` - -Set these environment variables in Cloud Run: - -``` -POSTGRES_URL=postgresql://USER:PASSWORD@localhost:PORT/DATABASE -POSTGRES_PATH=/cloudsql/PROJECT:REGION:INSTANCE/.s.PGSQL.PORT -``` - -Requirements: -- Add Cloud SQL instance under "Connections" tab -- Service account needs `Cloud SQL Client` role - -## Local Access to Cloud SQL - -```bash -cloud-sql-proxy PROJECT:REGION:INSTANCE --port=PORT -``` - -``` -POSTGRES_URL="postgresql://USER:PASSWORD@localhost:PORT/DATABASE" -``` - -## Importer - -Run locally: -```bash -bun run importer/index.ts meta # import metadata -bun run importer/index.ts rna # import RNA TPM data -``` diff --git a/service/package.json b/service/package.json index 8d2ccd9..08acc1c 100644 --- a/service/package.json +++ b/service/package.json @@ -7,10 +7,6 @@ "test": "bun test", "db:start": "docker compose up -d db", "db:stop": "docker compose down", - "db:up": "bun test/up.ts", - "db:down": "bun test/down.ts", - "db:seed": "bun test/seed.ts", - "db:reset": "bun test/down.ts && bun test/up.ts && bun test/seed.ts", "deploy:api": "gcloud run deploy mohd-api --source . --region us-east4" }, "dependencies": { diff --git a/service/tests/down.ts b/service/tests/down.ts deleted file mode 100644 index 6728fbf..0000000 --- a/service/tests/down.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { sql } from "../src/db"; - -try { - // Drop all tables in the public schema - const tables = await sql` - SELECT tablename FROM pg_tables WHERE schemaname = 'public' - `; - for (const { tablename } of tables) { - await sql`DROP TABLE IF EXISTS ${sql(tablename)} CASCADE`; - } - - // Drop all custom types (enums) in the public schema - const types = await sql` - SELECT typname FROM pg_type - WHERE typnamespace = 'public'::regnamespace AND typtype = 'e' - `; - for (const { typname } of types) { - await sql`DROP TYPE IF EXISTS ${sql(typname)} CASCADE`; - } - - console.log(`dropped ${tables.length} tables and ${types.length} types`); -} catch (e) { - console.log(e); -} diff --git a/service/tests/seed.ts b/service/tests/seed.ts deleted file mode 100644 index 3afb46d..0000000 --- a/service/tests/seed.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { sql } from "../src/db"; - -const metadata = [ - { - sample_id: "SAMPLE_001", - kit: "kitA", - site: "st1", - status: "case", - sex: "male", - umap_x: "1.0", - umap_y: "2.0", - }, - { - sample_id: "SAMPLE_002", - kit: "kitB", - site: "st2", - status: "control", - sex: "female", - umap_x: "3.0", - umap_y: "4.0", - }, - { - sample_id: "SAMPLE_003", - kit: "kitA", - site: "st1", - status: "case", - sex: "female", - umap_x: "5.0", - umap_y: "6.0", - }, - { - sample_id: "SAMPLE_004", - kit: "kitB", - site: "st2", - status: "control", - sex: "male", - umap_x: "7.0", - umap_y: "8.0", - }, - { - sample_id: "SAMPLE_005", - kit: "kitA", - site: "st1", - status: "unknown", - sex: "male", - umap_x: "9.0", - umap_y: "10.0", - }, -]; - -const rna_tpm = []; -for (let i = 0; i < 10; i++) { - const values = metadata.map((_, j) => (i + j).toFixed(2)); - rna_tpm.push({ - gene_id: String(i), - tpm_values: `{${values.join(",")}}`, - }); -} - -const atac_metadata = [ - { - sample_id: "SAMPLE_001", - site: "st1", - opc_id: "opcA", - protocol: "prtA", - status: "case", - sex: "male", - entity_id: "entA", - umap_x: "1.0", - umap_y: "2.0", - biospecimen: "bsA" - }, - { - sample_id: "SAMPLE_002", - site: "st2", - opc_id: "opcB", - protocol: "prtB", - status: "control", - sex: "female", - entity_id: "entB", - umap_x: "3.0", - umap_y: "4.0", - biospecimen: "bsB" - }, - { - sample_id: "SAMPLE_003", - site: "st1", - opc_id: "opcA", - protocol: "prtA", - status: "case", - sex: "female", - entity_id: "entC", - umap_x: "5.0", - umap_y: "6.0", - biospecimen: "bsC" - }, - { - sample_id: "SAMPLE_004", - site: "st2", - opc_id: "opcB", - protocol: "prtB", - status: "control", - sex: "male", - entity_id: "entD", - umap_x: "7.0", - umap_y: "8.0", - biospecimen: "bsD" - }, - { - sample_id: "SAMPLE_005", - site: "st1", - opc_id: "opcA", - protocol: "prtA", - status: "unknown", - sex: "male", - entity_id: "entE", - umap_x: "9.0", - umap_y: "10.0", - biospecimen: "bsE" - }, -]; - -const atac_zscore = []; -for (let i = 0; i < 10; i++) { - const values = atac_metadata.map((_, j) => (i + j).toFixed(2)); - atac_zscore.push({ - accession: String(i), - zscore_values: `{${values.join(",")}}`, - }); -} - -try { - await sql`INSERT INTO rna_metadata ${sql(metadata)}`; - await sql`INSERT INTO rna_tpm ${sql(rna_tpm)}`; - await sql`INSERT INTO atac_metadata ${sql(atac_metadata)}`; - await sql`INSERT INTO atac_zscore ${sql(atac_zscore)}`; -} catch (e) { - console.log(e); -} diff --git a/service/tests/up.ts b/service/tests/up.ts deleted file mode 100644 index 2670922..0000000 --- a/service/tests/up.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { sql } from "../src/db"; - -try { - await sql` - CREATE TABLE IF NOT EXISTS rna_tpm ( - gene_id VARCHAR(15) PRIMARY KEY, - tpm_values NUMERIC(10, 2)[] - ) - `; - - await sql`CREATE TYPE status AS ENUM ('case', 'control', 'unknown');`; - await sql`CREATE TYPE sex AS ENUM ('male', 'female');`; - - await sql` - CREATE TABLE IF NOT EXISTS atac_metadata ( - sample_id VARCHAR(15) PRIMARY KEY, - site VARCHAR(3) NOT NULL, - opc_id VARCHAR(10) NOT NULL, - protocol VARCHAR(20) NOT NULL, - status status NOT NULL, - sex sex NOT NULL, - entity_id VARCHAR(20) NOT NULL, - umap_x NUMERIC(10, 6), - umap_y NUMERIC(10, 6), - biospecimen VARCHAR(20) NOT NULL - ) - `; - - await sql` - CREATE TABLE IF NOT EXISTS rna_metadata ( - sample_id VARCHAR(15) PRIMARY KEY, - kit VARCHAR(10) NOT NULL, - site VARCHAR(3) NOT NULL, - status status NOT NULL, - sex sex NOT NULL, - umap_x NUMERIC(10, 6), - umap_y NUMERIC(10, 6) - ) - `; - - await sql` - CREATE TABLE IF NOT EXISTS atac_zscore ( - accession VARCHAR(20) PRIMARY KEY, - zscore_values NUMERIC(10, 2)[] - ) - `; -} catch (e) { - console.log(e); -} From 32f7cbbad3853e444e4a28ea77be5e050429bf73 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 13:45:27 -0500 Subject: [PATCH 16/18] add bun setup to github workflow --- .github/workflows/test.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ccc26b4..496135f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,8 +13,14 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + - name: Set up Docker Compose uses: docker/setup-buildx-action@v3 - - name: Build and run test:ci + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run tests run: bun run test:ci From 3d3286afa2f94b3c85a44b8fac3b62ad10f5d3c0 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 13:49:50 -0500 Subject: [PATCH 17/18] add push trigger to build workflow --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b2e8da5..518b47e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,5 +1,7 @@ name: Build on: + push: + pull_request: branches: [main] jobs: From b44e3deeb7e455bdfe2eec8edf6cee9aa3613a13 Mon Sep 17 00:00:00 2001 From: Jair Meza Date: Tue, 3 Mar 2026 14:43:15 -0500 Subject: [PATCH 18/18] refine push trigger to only run on main branch --- .github/workflows/build.yaml | 1 + .github/workflows/test.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 518b47e..dee99b4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,7 @@ name: Build on: push: + branches: [main] pull_request: branches: [main] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 496135f..b3708ec 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,6 +1,7 @@ name: Test on: push: + branches: [main] pull_request: branches: