From bd4817cb777635e1cf25790e33e9347c675f90b6 Mon Sep 17 00:00:00 2001 From: marcuspaico Date: Mon, 17 Aug 2026 15:17:37 -0700 Subject: [PATCH] feat(db): lab_drafts, lab_draws, biomarkers tables Refs PAI-91. --- server/drizzle/0001_labs.sql | 33 +++ server/drizzle/meta/0001_snapshot.json | 279 +++++++++++++++++++++++++ server/drizzle/meta/_journal.json | 7 + server/src/db/schema.ts | 36 +++- server/test/db-labs.test.ts | 21 ++ 5 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 server/drizzle/0001_labs.sql create mode 100644 server/drizzle/meta/0001_snapshot.json create mode 100644 server/test/db-labs.test.ts diff --git a/server/drizzle/0001_labs.sql b/server/drizzle/0001_labs.sql new file mode 100644 index 0000000..ae1ed3e --- /dev/null +++ b/server/drizzle/0001_labs.sql @@ -0,0 +1,33 @@ +CREATE TABLE `biomarkers` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `draw_id` text NOT NULL, + `panel` text NOT NULL, + `name` text NOT NULL, + `marker` text NOT NULL, + `analyte_key` text, + `value` text NOT NULL, + `value_num` real, + `unit` text, + `reference_range` text, + `flagged` integer DEFAULT 0 NOT NULL, + `value_canonical` real, + `canonical_unit` text +); +--> statement-breakpoint +CREATE TABLE `lab_drafts` ( + `id` text PRIMARY KEY NOT NULL, + `filename` text NOT NULL, + `file_path` text NOT NULL, + `status` text NOT NULL, + `extracted` text, + `error` text, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `lab_draws` ( + `id` text PRIMARY KEY NOT NULL, + `collected_at` text NOT NULL, + `lab_name` text, + `draft_id` text, + `created_at` integer NOT NULL +); diff --git a/server/drizzle/meta/0001_snapshot.json b/server/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..3678871 --- /dev/null +++ b/server/drizzle/meta/0001_snapshot.json @@ -0,0 +1,279 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8d0cf0e5-668d-4526-a0cb-5a264d813be3", + "prevId": "383d7bd7-3ebf-456e-8c5c-f39cebab14c4", + "tables": { + "biomarkers": { + "name": "biomarkers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "draw_id": { + "name": "draw_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "panel": { + "name": "panel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "marker": { + "name": "marker", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "analyte_key": { + "name": "analyte_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_num": { + "name": "value_num", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_range": { + "name": "reference_range", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "flagged": { + "name": "flagged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "value_canonical": { + "name": "value_canonical", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "canonical_unit": { + "name": "canonical_unit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lab_drafts": { + "name": "lab_drafts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "extracted": { + "name": "extracted", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lab_draws": { + "name": "lab_draws", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "collected_at": { + "name": "collected_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lab_name": { + "name": "lab_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "draft_id": { + "name": "draft_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/server/drizzle/meta/_journal.json b/server/drizzle/meta/_journal.json index 99f3b33..5424167 100644 --- a/server/drizzle/meta/_journal.json +++ b/server/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1787000672259, "tag": "0000_init", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1787005028172, + "tag": "0001_labs", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts index e27527b..178221e 100644 --- a/server/src/db/schema.ts +++ b/server/src/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const settings = sqliteTable("settings", { key: text("key").primaryKey(), @@ -10,3 +10,37 @@ export const sessions = sqliteTable("sessions", { createdAt: integer("created_at").notNull(), expiresAt: integer("expires_at").notNull(), }); + +export const labDrafts = sqliteTable("lab_drafts", { + id: text("id").primaryKey(), + filename: text("filename").notNull(), + filePath: text("file_path").notNull(), + status: text("status").notNull(), // pending | confirmed | discarded + extracted: text("extracted"), + error: text("error"), + createdAt: integer("created_at").notNull(), +}); + +export const labDraws = sqliteTable("lab_draws", { + id: text("id").primaryKey(), + collectedAt: text("collected_at").notNull(), // ISO date YYYY-MM-DD + labName: text("lab_name"), + draftId: text("draft_id"), + createdAt: integer("created_at").notNull(), +}); + +export const biomarkers = sqliteTable("biomarkers", { + id: integer("id").primaryKey({ autoIncrement: true }), + drawId: text("draw_id").notNull(), + panel: text("panel").notNull(), + name: text("name").notNull(), + marker: text("marker").notNull(), + analyteKey: text("analyte_key"), + value: text("value").notNull(), + valueNum: real("value_num"), + unit: text("unit"), + referenceRange: text("reference_range"), + flagged: integer("flagged").notNull().default(0), + valueCanonical: real("value_canonical"), + canonicalUnit: text("canonical_unit"), +}); diff --git a/server/test/db-labs.test.ts b/server/test/db-labs.test.ts new file mode 100644 index 0000000..5aa613f --- /dev/null +++ b/server/test/db-labs.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { openDb } from "../src/db"; +import { biomarkers, labDraws } from "../src/db/schema"; + +describe("labs schema", () => { + test("draw + biomarker round-trip", async () => { + const db = openDb(mkdtempSync(join(tmpdir(), "helios-"))); + await db.insert(labDraws).values({ id: "d1", collectedAt: "2026-01-15", labName: "Acme Lab", draftId: null, createdAt: 1 }); + await db.insert(biomarkers).values({ + drawId: "d1", panel: "lipids", name: "LDL Cholesterol", marker: "ldl", analyteKey: "ldl", + value: "3.1", valueNum: 3.1, unit: "mmol/L", referenceRange: "< 3.4", flagged: 0, + valueCanonical: 3.1, canonicalUnit: "mmol/L", + }); + const rows = await db.select().from(biomarkers); + expect(rows).toHaveLength(1); + expect(rows[0].marker).toBe("ldl"); + }); +});