diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b2e8da5..dee99b4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,5 +1,8 @@ name: Build on: + push: + branches: [main] + pull_request: branches: [main] jobs: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 275b89a..b3708ec 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,28 +1,27 @@ name: Test on: - pull_request: + push: branches: [main] + + pull_request: + branches: + - main + jobs: test: runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/postgres 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 Bun + uses: oven-sh/setup-bun@v2 + + - name: Set up Docker Compose + uses: docker/setup-buildx-action@v3 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run tests + 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/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..28a3ecb --- a/importer/src/meta.ts +++ b/importer/src/meta.ts @@ -1,11 +1,11 @@ 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" }; + +const sexMap: Record = { "1": "female", "2": "male" }; export async function createMetaTables() { 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/importer/test-data/Subset_Phase-0_RNA-TPM.tsv b/importer/test-data/Subset_Phase-0_RNA-TPM.tsv deleted file mode 100644 index 6922a5f..0000000 --- a/importer/test-data/Subset_Phase-0_RNA-TPM.tsv +++ /dev/null @@ -1,10 +0,0 @@ -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 diff --git a/importer/test-data/Subset_Phase_0_ATAC_zscores.tsv b/importer/test-data/Subset_Phase_0_ATAC_zscores.tsv deleted file mode 100644 index 84802a1..0000000 --- a/importer/test-data/Subset_Phase_0_ATAC_zscores.tsv +++ /dev/null @@ -1,10 +0,0 @@ -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 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 new file mode 100644 index 0000000..b8094c4 --- /dev/null +++ b/importer/tests/meta.test.ts @@ -0,0 +1,113 @@ +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); + }); + + 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/migrations/docker-compose.test.yml b/migrations/docker-compose.test.yml new file mode 100644 index 0000000..3c45ca3 --- /dev/null +++ b/migrations/docker-compose.test.yml @@ -0,0 +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: 10 + + importer: + build: + context: ../importer + container_name: test-importer + depends_on: + postgres: + condition: service_healthy + environment: + 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 + 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: + POSTGRES_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.sh b/migrations/scripts/test.sh new file mode 100755 index 0000000..56dcdf8 --- /dev/null +++ b/migrations/scripts/test.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +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 + +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 + +# 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 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 + +# Run service tests +docker compose -f docker-compose.test.yml run --rm --entrypoint "" service bun test 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/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv b/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv new file mode 100644 index 0000000..f33ba3e --- /dev/null +++ b/migrations/test-data/Subset_Phase-0_RNA-TPM.tsv @@ -0,0 +1,10 @@ +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/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/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv b/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv new file mode 100644 index 0000000..770210c --- /dev/null +++ b/migrations/test-data/Subset_Phase_0_ATAC_zscores.tsv @@ -0,0 +1,10 @@ +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/package.json b/package.json index 95af77c..3c90507 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "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", "import": "bun run --filter mohd-importer start", + "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", - "test": "bun run --filter mohd-service test", + "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/Dockerfile b/service/Dockerfile index c49a208..aa07a51 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -16,6 +16,8 @@ RUN bun run build # release stage FROM oven/bun:1 AS release +WORKDIR /usr/src/app +COPY --from=build /usr/src/app/. . COPY --from=build /usr/src/app/dist/index.js . # run the api 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 316ba93..08acc1c 100644 --- a/service/package.json +++ b/service/package.json @@ -4,12 +4,9 @@ "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", - "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/test/down.ts b/service/test/down.ts deleted file mode 100644 index 6728fbf..0000000 --- a/service/test/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/test/seed.ts b/service/test/seed.ts deleted file mode 100644 index 3afb46d..0000000 --- a/service/test/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/test/up.ts b/service/test/up.ts deleted file mode 100644 index 2670922..0000000 --- a/service/test/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); -} diff --git a/service/test/integration.test.ts b/service/tests/integration.test.ts similarity index 60% rename from service/test/integration.test.ts rename to service/tests/integration.test.ts index 7a42bfa..0090e52 100644 --- a/service/test/integration.test.ts +++ b/service/tests/integration.test.ts @@ -33,58 +33,60 @@ 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.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("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.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.925901); + 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.accession).toBe("EH38E0064571"); + expect(acc.samples).toHaveLength(9); + + 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("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.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"); + }); 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 +131,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.gene_id).toBe("ENSG00000000003"); + expect(gene.samples).toHaveLength(9); + expect(gene.samples[0].value).toBe(0.00); + 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("male"); - expect(gene.samples[0].metadata.umap_x).toBe(1.0); - expect(gene.samples[0].metadata.umap_y).toBe(2.0); + 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 () => { 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 () => {