mirror of
https://github.com/temetro/temetro.git
synced 2026-09-02 13:57:56 +00:00
Add TypeScript + Express + Postgres backend with Better Auth
Implements the first temetro backend: an Express 5 API on Postgres via Drizzle ORM, with authentication and multi-tenant clinics powered by Better Auth. - Auth: email/password with required email verification, password reset, rate limiting, CSRF/trusted-origins, secure cookies, session audit hook. - Organizations (clinics) with RBAC (owner/admin/member/viewer) and an extended `patient` permission set; member invitations by email. - Org-scoped patient records mirroring the frontend Patient shape, with CRUD endpoints gated by permission (read/write/delete). - Email helper logs links to the console when SMTP is unset (zero-setup local dev); Dockerfile + docker-compose (db + backend + frontend) with migrations applied on startup and a configurable Postgres host port. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import type {
|
||||
AllergySeverity,
|
||||
LabFlag,
|
||||
PatientStatus,
|
||||
Sex,
|
||||
Trend,
|
||||
} from "../../types/patient.js";
|
||||
import { organization, user } from "./auth.js";
|
||||
|
||||
// One row per patient, scoped to a clinic (organization). `fileNumber` (MRN)
|
||||
// is the chat lookup key and is unique within an organization. Current vitals
|
||||
// live as columns; the two headline sparkline series and the freeform alert
|
||||
// list are stored as JSONB. Child collections are separate tables below.
|
||||
export const patients = pgTable(
|
||||
"patients",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
organizationId: text("organization_id")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
fileNumber: text("file_number").notNull(),
|
||||
name: text("name").notNull(),
|
||||
age: integer("age").notNull(),
|
||||
sex: text("sex").$type<Sex>().notNull(),
|
||||
pcp: text("pcp").notNull(),
|
||||
status: text("status").$type<PatientStatus>().notNull(),
|
||||
initials: text("initials").notNull(),
|
||||
alerts: jsonb("alerts").$type<string[]>().notNull(),
|
||||
vitalsBp: text("vitals_bp").notNull(),
|
||||
vitalsHr: text("vitals_hr").notNull(),
|
||||
vitalsTemp: text("vitals_temp").notNull(),
|
||||
vitalsSpo2: text("vitals_spo2").notNull(),
|
||||
vitalsTakenAt: text("vitals_taken_at").notNull(),
|
||||
vitalsTrend: jsonb("vitals_trend").$type<Trend>().notNull(),
|
||||
labTrend: jsonb("lab_trend").$type<Trend>().notNull(),
|
||||
createdBy: text("created_by").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("patients_org_file_uidx").on(t.organizationId, t.fileNumber),
|
||||
index("patients_org_idx").on(t.organizationId),
|
||||
],
|
||||
);
|
||||
|
||||
export const allergies = pgTable(
|
||||
"patient_allergies",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
patientId: uuid("patient_id")
|
||||
.notNull()
|
||||
.references(() => patients.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull().default(0),
|
||||
substance: text("substance").notNull(),
|
||||
reaction: text("reaction").notNull(),
|
||||
severity: text("severity").$type<AllergySeverity>().notNull(),
|
||||
},
|
||||
(t) => [index("allergies_patient_idx").on(t.patientId)],
|
||||
);
|
||||
|
||||
export const medications = pgTable(
|
||||
"patient_medications",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
patientId: uuid("patient_id")
|
||||
.notNull()
|
||||
.references(() => patients.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull().default(0),
|
||||
name: text("name").notNull(),
|
||||
dose: text("dose").notNull(),
|
||||
frequency: text("frequency").notNull(),
|
||||
},
|
||||
(t) => [index("medications_patient_idx").on(t.patientId)],
|
||||
);
|
||||
|
||||
export const problems = pgTable(
|
||||
"patient_problems",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
patientId: uuid("patient_id")
|
||||
.notNull()
|
||||
.references(() => patients.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull().default(0),
|
||||
label: text("label").notNull(),
|
||||
since: text("since").notNull(),
|
||||
},
|
||||
(t) => [index("problems_patient_idx").on(t.patientId)],
|
||||
);
|
||||
|
||||
export const labs = pgTable(
|
||||
"patient_labs",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
patientId: uuid("patient_id")
|
||||
.notNull()
|
||||
.references(() => patients.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull().default(0),
|
||||
name: text("name").notNull(),
|
||||
value: text("value").notNull(),
|
||||
flag: text("flag").$type<LabFlag>().notNull(),
|
||||
takenAt: text("taken_at").notNull(),
|
||||
},
|
||||
(t) => [index("labs_patient_idx").on(t.patientId)],
|
||||
);
|
||||
|
||||
export const encounters = pgTable(
|
||||
"patient_encounters",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
patientId: uuid("patient_id")
|
||||
.notNull()
|
||||
.references(() => patients.id, { onDelete: "cascade" }),
|
||||
position: integer("position").notNull().default(0),
|
||||
date: text("date").notNull(),
|
||||
type: text("type").notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
summary: text("summary").notNull(),
|
||||
},
|
||||
(t) => [index("encounters_patient_idx").on(t.patientId)],
|
||||
);
|
||||
|
||||
export const patientChildTables = {
|
||||
allergies,
|
||||
medications,
|
||||
problems,
|
||||
labs,
|
||||
encounters,
|
||||
} as const;
|
||||
Reference in New Issue
Block a user