refactor(db/schema): align TypeScript schema with production

Path A baseline reconciliation. drizzle-kit pull against tia_prod showed
prod had drifted well past schema.ts because legacy hand-rolled migrations
0003-0015 wrote to the DB but were never reflected back into TypeScript.

Shared-table drift fixed:
- users:          + password_hash, + password_updated_at
- families:       + tier, + max_children, + max_members
- children:       col is 'stage' (kept JS key currentStage -> stage);
                  'image_url' not 'profile_photo_url'; birth_date is DATE;
                  sex nullable; dropped phantom stage_overrides
- family_members: dropped phantom display_name
- family_invites: dropped phantom display_name, accepted_at
- audit_log:      + resource_id, + resource_type; metadata -> jsonb; +5 indexes
- memories:       + vision_tags (text[]), + vision_embedding (vector 1536)
- logs.ts:        'diapers' phantom table renamed to diapersLogs ('diapers_logs')

19 missing tables added across new files:
- admin.ts:     admins, admin_sessions, password_resets
- support.ts:   support_tickets, support_responses
- ai.ts:        chat_sessions, chat_messages, ai_usage
- medical.ts:   medicines, medication_doses, allergies, illness_logs, doctor_visits
- affiliate.ts: member_profiles, recommended_products, product_clicks
- logs.ts:      + milestone_achievements
- audit.ts:     + log_corrections

BUG FIX: schema/index.ts never re-exported ./logs — Drizzle was blind to
feeds/sleeps/vaccinations/growth/medications. Now exported.

Verified: tsc --noEmit has zero non-test errors. Dropped phantom columns
confirmed to have zero references in src/.
This commit is contained in:
Manohar Gupta 2026-05-23 12:17:20 +05:30
parent e7d68c2fc6
commit a3d3a140ed
11 changed files with 523 additions and 46 deletions

57
src/db/schema/admin.ts Normal file
View file

@ -0,0 +1,57 @@
import {
pgTable,
text,
timestamp,
uuid,
varchar,
check,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// ---------------------------------------------------------------------------
// Admin / auth-extra schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Tables: admins, admin_sessions, password_resets
// (legacy migrations 0006_admin_auth, 0009_admin_sessions, 0004's password_resets).
//
// SECURITY NOTE: `admins` is the privileged back-office account table, fully
// separate from the `users` table. Password hashes here gate admin access —
// treat any change to this table as security-critical.
// ---------------------------------------------------------------------------
export const admins = pgTable(
"admins",
{
id: uuid("id").primaryKey().defaultRandom(),
username: varchar("username", { length: 50 }).notNull().unique(),
passwordHash: varchar("password_hash", { length: 255 }).notNull(),
role: varchar("role", { length: 20 }).default("admin"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
lastLogin: timestamp("last_login", { withTimezone: true }),
},
(table) => [
check(
"admins_role_check",
sql`(role)::text = ANY (ARRAY['super_admin','admin','support'])`
),
]
);
export const adminSessions = pgTable("admin_sessions", {
id: uuid("id").primaryKey().defaultRandom(),
adminId: uuid("admin_id").notNull(),
sessionToken: text("session_token").notNull().unique(),
expires: timestamp("expires", { withTimezone: true }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
export const passwordResets = pgTable("password_resets", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull(),
token: text("token").notNull().unique(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
usedAt: timestamp("used_at", { withTimezone: true }),
});
export type Admin = typeof admins.$inferSelect;
export type AdminSession = typeof adminSessions.$inferSelect;
export type PasswordReset = typeof passwordResets.$inferSelect;

View file

@ -0,0 +1,70 @@
import {
pgTable,
text,
timestamp,
uuid,
boolean,
integer,
index,
} from "drizzle-orm/pg-core";
// ---------------------------------------------------------------------------
// Affiliate schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Tables: member_profiles, recommended_products, product_clicks
// (legacy migration 0015_affiliate).
//
// Backs the public affiliate product page at tia.<domain>/m/[slug].
// ---------------------------------------------------------------------------
export const memberProfiles = pgTable(
"member_profiles",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull().unique(),
familyId: uuid("family_id").notNull(),
slug: text("slug").notNull(),
displayName: text("display_name").notNull(),
bio: text("bio"),
avatarUrl: text("avatar_url"),
isPublic: boolean("is_public").default(false).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [index("member_profiles_slug_idx").on(table.slug)]
);
export const recommendedProducts = pgTable(
"recommended_products",
{
id: uuid("id").primaryKey().defaultRandom(),
profileId: uuid("profile_id").notNull(),
title: text("title").notNull(),
description: text("description"),
url: text("url").notNull(),
imageUrl: text("image_url"),
category: text("category").default("general").notNull(),
displayOrder: integer("display_order").default(0).notNull(),
isActive: boolean("is_active").default(true).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("recommended_products_profile_idx").on(table.profileId, table.displayOrder),
]
);
// Click tracking — ipHash is a hash, never a raw IP (privacy).
export const productClicks = pgTable(
"product_clicks",
{
id: uuid("id").primaryKey().defaultRandom(),
productId: uuid("product_id").notNull(),
clickedAt: timestamp("clicked_at", { withTimezone: true }).defaultNow().notNull(),
referrer: text("referrer"),
ipHash: text("ip_hash"),
},
(table) => [index("product_clicks_product_idx").on(table.productId, table.clickedAt)]
);
export type MemberProfile = typeof memberProfiles.$inferSelect;
export type RecommendedProduct = typeof recommendedProducts.$inferSelect;
export type ProductClick = typeof productClicks.$inferSelect;

60
src/db/schema/ai.ts Normal file
View file

@ -0,0 +1,60 @@
import {
pgTable,
text,
timestamp,
uuid,
varchar,
integer,
numeric,
index,
} from "drizzle-orm/pg-core";
// ---------------------------------------------------------------------------
// AI schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Tables: chat_sessions, chat_messages, ai_usage
// (legacy migrations 0004_chat_sessions, 0013_ai_usage).
// ---------------------------------------------------------------------------
export const chatSessions = pgTable("chat_sessions", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
title: varchar("title", { length: 255 }).default("New conversation").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const chatMessages = pgTable("chat_messages", {
id: uuid("id").primaryKey().defaultRandom(),
sessionId: uuid("session_id").notNull(),
role: varchar("role", { length: 20 }).notNull(),
content: text("content").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
// Token + cost accounting for the LiteLLM-gateway AI calls.
// cost_estimate_paise stores cost in Indian paise (1 INR = 100 paise) so
// fractional model costs stay exact without float drift.
export const aiUsage = pgTable(
"ai_usage",
{
id: uuid("id").primaryKey().defaultRandom(),
familyId: uuid("family_id"),
userId: uuid("user_id"),
intent: text("intent"),
modelUsed: text("model_used"),
promptTokens: integer("prompt_tokens"),
completionTokens: integer("completion_tokens"),
totalTokens: integer("total_tokens"),
costEstimatePaise: numeric("cost_estimate_paise", { precision: 10, scale: 4 }),
durationMs: integer("duration_ms"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("ai_usage_created_idx").on(table.createdAt),
index("ai_usage_family_idx").on(table.familyId),
]
);
export type ChatSession = typeof chatSessions.$inferSelect;
export type ChatMessage = typeof chatMessages.$inferSelect;
export type AiUsage = typeof aiUsage.$inferSelect;

View file

@ -1,13 +1,60 @@
import { pgTable } from "drizzle-orm/pg-core";
import { text, timestamp, uuid } from "drizzle-orm/pg-core";
import {
pgTable,
text,
timestamp,
uuid,
varchar,
jsonb,
index,
} from "drizzle-orm/pg-core";
export const auditLog = pgTable("audit_log", {
// ---------------------------------------------------------------------------
// Audit schema — aligned to PRODUCTION as of 2026-05-19 baseline.
//
// Drift corrected at baseline:
// - audit_log: added resource_id / resource_type (legacy 0010_audit_log);
// metadata is JSONB (was modelled as text); action is varchar(50)
// - log_corrections: append-only-with-corrections record, was missing here
// ---------------------------------------------------------------------------
export const auditLog = pgTable(
"audit_log",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id"),
familyId: uuid("family_id"),
action: text("action").notNull(),
metadata: text("metadata"), // JSON string
ipAddress: text("ip_address"),
userId: uuid("user_id"),
action: varchar("action", { length: 50 }).notNull(),
resourceType: varchar("resource_type", { length: 50 }),
resourceId: uuid("resource_id"),
ipAddress: varchar("ip_address", { length: 45 }),
userAgent: text("user_agent"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
metadata: jsonb("metadata").default({}),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("audit_family_idx").on(table.familyId, table.createdAt),
index("idx_audit_log_action").on(table.action),
index("idx_audit_log_created").on(table.createdAt),
index("idx_audit_log_family").on(table.familyId),
index("idx_audit_log_user").on(table.userId),
]
);
// Append-only correction trail for medical dose logs.
export const logCorrections = pgTable(
"log_corrections",
{
id: uuid("id").primaryKey().defaultRandom(),
familyId: uuid("family_id").notNull(),
doseId: uuid("dose_id").notNull(),
originalValue: jsonb("original_value").notNull(),
correctedValue: jsonb("corrected_value").notNull(),
reason: text("reason"),
correctedBy: uuid("corrected_by"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [index("log_corrections_dose_idx").on(table.doseId)]
);
export type AuditLog = typeof auditLog.$inferSelect;
export type LogCorrection = typeof logCorrections.$inferSelect;

View file

@ -1,6 +1,13 @@
import { pgTable, text, timestamp, uuid, uniqueIndex } from "drizzle-orm/pg-core";
import { pgTable, text, timestamp, uuid, varchar, uniqueIndex } from "drizzle-orm/pg-core";
// ---------------------------------------------------------------------------
// Auth schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Source of truth: drizzle-kit pull against tia_prod.
// ---------------------------------------------------------------------------
// Users table
// NOTE: password_hash / password_updated_at were added by the legacy
// 0008_user_passwords migration and were missing from this file until baseline.
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name"),
@ -9,6 +16,8 @@ export const users = pgTable("users", {
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
passwordHash: varchar("password_hash", { length: 255 }),
passwordUpdatedAt: timestamp("password_updated_at", { withTimezone: true }),
});
// Accounts table (for OAuth providers)

View file

@ -4,11 +4,25 @@ import {
timestamp,
uuid,
pgEnum,
varchar,
integer,
date,
uniqueIndex,
index,
jsonb,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// ---------------------------------------------------------------------------
// Family schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Source of truth: drizzle-kit pull against tia_prod.
//
// Drift corrected at baseline:
// - families: added tier / maxChildren / maxMembers (legacy 0005)
// - children: col is `stage` not `current_stage`; `image_url` not
// `profile_photo_url`; dropped phantom `stage_overrides`;
// birth_date is DATE; sex is NULLABLE
// - family_members: dropped phantom `display_name`
// - family_invites: dropped phantom `display_name` / `accepted_at`
// ---------------------------------------------------------------------------
// Enums
export const memberRoleEnum = pgEnum("member_role", ["admin", "caregiver", "viewer"]);
@ -25,7 +39,11 @@ export const childStageEnum = pgEnum("child_stage", [
// Families table
export const families = pgTable("families", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull().default("The Gupta Family"),
name: text("name").notNull(),
// Tier system (legacy migration 0005_tier_system).
tier: varchar("tier", { length: 20 }).default("free"),
maxChildren: integer("max_children").default(1),
maxMembers: integer("max_members").default(2),
pediatricianPhone: text("pediatrician_phone"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
@ -39,24 +57,24 @@ export const familyMembers = pgTable(
familyId: uuid("family_id").notNull(),
userId: uuid("user_id").notNull(),
role: memberRoleEnum("role").notNull().default("caregiver"),
displayName: text("display_name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [uniqueIndex("family_user_unique").on(table.familyId, table.userId)]
(table) => [uniqueIndex("family_members_family_id_user_id_key").on(table.familyId, table.userId)]
);
// Children table
// NOTE (2b): the DB column is `stage`. We keep the JS-friendly key
// `currentStage` mapped onto it so application code reads naturally.
export const children = pgTable(
"children",
{
id: uuid("id").primaryKey().defaultRandom(),
familyId: uuid("family_id").notNull(),
name: text("name").notNull(),
birthDate: timestamp("birth_date").notNull(),
sex: childSexEnum("sex").notNull(),
currentStage: childStageEnum("current_stage"),
stageOverrides: jsonb("stage_overrides").$type<Record<string, unknown>>().default({}),
profilePhotoUrl: text("profile_photo_url"),
birthDate: date("birth_date").notNull(),
sex: childSexEnum("sex"), // nullable in prod
currentStage: childStageEnum("stage").notNull().default("newborn"),
imageUrl: text("image_url"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
@ -70,11 +88,9 @@ export const familyInvites = pgTable(
id: uuid("id").primaryKey().defaultRandom(),
familyId: uuid("family_id").notNull(),
email: text("email").notNull(),
role: memberRoleEnum("role").notNull(),
displayName: text("display_name").notNull(),
role: memberRoleEnum("role").notNull().default("viewer"),
token: text("token").notNull().unique(),
expiresAt: timestamp("expires_at").notNull(),
acceptedAt: timestamp("accepted_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [uniqueIndex("invite_token_idx").on(table.token)]

View file

@ -1,4 +1,17 @@
// Barrel export for the full Drizzle schema.
// Every schema file MUST be re-exported here so `import * as schema`
// in src/db/index.ts sees the complete table set.
//
// BUG FIXED AT BASELINE: ./logs was previously NOT exported here, so
// Drizzle never knew about feeds / sleeps / vaccinations / growth /
// medications / diapers. That is why those tables drifted unnoticed.
export * from "./auth";
export * from "./family";
export * from "./audit";
export * from "./media";
export * from "./logs";
export * from "./medical";
export * from "./admin";
export * from "./support";
export * from "./ai";
export * from "./affiliate";

View file

@ -1,8 +1,36 @@
import { pgTable, pgEnum, uuid, timestamp, text, integer, boolean, date, real } from "drizzle-orm/pg-core";
import {
pgTable,
pgEnum,
uuid,
timestamp,
text,
integer,
boolean,
date,
real,
index,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { children as childrenTable } from "./family";
const children = childrenTable;
// ---------------------------------------------------------------------------
// Logs schema — aligned to PRODUCTION as of 2026-05-19 baseline.
//
// Drift corrected at baseline:
// - `diapers` table renamed to `diapersLogs` (DB table is `diapers_logs`).
// The app already writes to diapers_logs via raw SQL; the old `diapers`
// pgTable was dead/phantom.
// - sleeps.startedAt is NULLABLE in prod.
// - milestone_achievements added (legacy 0014_milestones).
//
// RLS NOTE: feeds / diapers_logs / sleeps / vaccinations / growth carry a
// `family_isolation` row-level-security policy in prod. RLS policies are not
// modelled in these pgTable definitions — they live in the DB and are managed
// separately. Do not assume Drizzle will recreate them.
// ---------------------------------------------------------------------------
// Feed types
export const feedType = pgEnum("feed_type", ["breast_milk", "formula", "solid", "water", "other"]);
export const feedMethod = pgEnum("feed_method", ["bottle", "breast_left", "breast_right", "breast_both", "cup", "spoon", "finger", "self"]);
@ -25,8 +53,8 @@ export const feeds = pgTable("feeds", {
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Logs: diapers
export const diapers = pgTable("diapers", {
// Logs: diapers (DB table: diapers_logs)
export const diapersLogs = pgTable("diapers_logs", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").references(() => children.id).notNull(),
type: diaperType("type").notNull(),
@ -40,7 +68,7 @@ export const sleeps = pgTable("sleeps", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").references(() => children.id).notNull(),
type: sleepType("type").notNull(),
startedAt: timestamp("started_at").notNull(),
startedAt: timestamp("started_at"), // nullable in prod
endedAt: timestamp("ended_at"),
durationMinutes: integer("duration_minutes"),
notes: text("notes"),
@ -74,7 +102,7 @@ export const growth = pgTable("growth", {
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Logs: medications
// Logs: medications (the prescription record; doses are in medical.ts)
export const medications = pgTable("medications", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").references(() => children.id).notNull(),
@ -88,10 +116,32 @@ export const medications = pgTable("medications", {
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// Milestone achievements (legacy 0014_milestones)
export const milestoneAchievements = pgTable(
"milestone_achievements",
{
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
familyId: uuid("family_id").notNull(),
milestoneKey: text("milestone_key").notNull(),
achievedAt: date("achieved_at").notNull(),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("milestone_child_idx").on(table.childId),
uniqueIndex("milestone_achievements_child_milestone_unique").on(
table.childId,
table.milestoneKey
),
]
);
// Type exports
export type Feed = typeof feeds.$inferSelect;
export type Diaper = typeof diapers.$inferSelect;
export type DiaperLog = typeof diapersLogs.$inferSelect;
export type Sleep = typeof sleeps.$inferSelect;
export type Vaccination = typeof vaccinations.$inferSelect;
export type Growth = typeof growth.$inferSelect;
export type Medication = typeof medications.$inferSelect;
export type MilestoneAchievement = typeof milestoneAchievements.$inferSelect;

View file

@ -5,9 +5,19 @@ import {
uuid,
boolean,
integer,
vector,
index,
} from "drizzle-orm/pg-core";
// ---------------------------------------------------------------------------
// Media schema — aligned to PRODUCTION as of 2026-05-19 baseline.
//
// Drift corrected at baseline:
// - memories: vision_tags (text[]) and vision_embedding (vector(1536)) are
// now modelled with Drizzle's native column types instead of being
// "raw SQL only". The ivfflat index on the embedding is declared too.
// ---------------------------------------------------------------------------
// Processing states for background jobs
export type ProcessingStatus = "uploading" | "processing" | "ready" | "failed";
@ -19,7 +29,7 @@ export const memories = pgTable(
childId: uuid("child_id"),
title: text("title"),
description: text("description"),
takenAt: timestamp("taken_at"),
takenAt: timestamp("taken_at", { withTimezone: true }),
r2Key: text("r2_key").notNull(),
r2ThumbnailKey: text("r2_thumbnail_key"),
mimeType: text("mime_type"),
@ -27,17 +37,24 @@ export const memories = pgTable(
width: integer("width"),
height: integer("height"),
visionCaption: text("vision_caption"),
// vision_tags is text[] in DB — handled with raw SQL
// vision_embedding is vector(1536) in DB — handled with raw SQL
visionTags: text("vision_tags").array(),
visionEmbedding: vector("vision_embedding", { dimensions: 1536 }),
isPrivate: boolean("is_private").default(false).notNull(),
processingStatus: text("processing_status").$type<ProcessingStatus>().default("uploading").notNull(),
processingStatus: text("processing_status")
.$type<ProcessingStatus>()
.default("uploading")
.notNull(),
uploadedBy: uuid("uploaded_by"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("memories_family_idx").on(table.familyId),
index("memories_child_idx").on(table.childId),
// ivfflat index for cosine similarity search over CLIP/vision embeddings.
index("memories_embedding_idx")
.using("ivfflat", table.visionEmbedding.op("vector_cosine_ops"))
.with({ lists: 100 }),
]
);
@ -52,11 +69,9 @@ export const attachments = pgTable(
mimeType: text("mime_type"),
sizeBytes: integer("size_bytes"),
uploadedBy: uuid("uploaded_by"),
createdAt: timestamp("created_at").defaultNow().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("attachments_family_idx").on(table.familyId),
]
(table) => [index("attachments_family_idx").on(table.familyId)]
);
export type Memory = typeof memories.$inferSelect;

89
src/db/schema/medical.ts Normal file
View file

@ -0,0 +1,89 @@
import {
pgTable,
text,
timestamp,
uuid,
varchar,
date,
index,
} from "drizzle-orm/pg-core";
// ---------------------------------------------------------------------------
// Medical schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Tables: medicines, medication_doses, allergies, illness_logs, doctor_visits
// (legacy migrations 0003_medical_data, 0012_medical_doses).
//
// Note: `medicines` here is the per-child medicine list with reminder times.
// `medications` (in logs.ts) is a separate prescription-record table that
// predates it. Both exist in prod; keep them distinct.
// ---------------------------------------------------------------------------
export const medicines = pgTable("medicines", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
name: varchar("name", { length: 255 }).notNull(),
dose: varchar("dose", { length: 255 }),
notes: text("notes"),
reminderTime: varchar("reminder_time", { length: 10 }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
// Append-only dose administration log. Corrections go via log_corrections.
export const medicationDoses = pgTable(
"medication_doses",
{
id: uuid("id").primaryKey().defaultRandom(),
medicineId: uuid("medicine_id").notNull(),
familyId: uuid("family_id").notNull(),
administeredAt: timestamp("administered_at", { withTimezone: true })
.defaultNow()
.notNull(),
administeredBy: uuid("administered_by"),
amountGiven: text("amount_given"),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index("medication_doses_family_idx").on(table.familyId),
index("medication_doses_medicine_idx").on(table.medicineId),
]
);
export const allergies = pgTable("allergies", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
name: varchar("name", { length: 255 }).notNull(),
severity: varchar("severity", { length: 50 }).default("mild"),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const illnessLogs = pgTable("illness_logs", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
name: varchar("name", { length: 255 }).notNull(),
startDate: date("start_date").notNull(),
endDate: date("end_date"),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export const doctorVisits = pgTable("doctor_visits", {
id: uuid("id").primaryKey().defaultRandom(),
childId: uuid("child_id").notNull(),
doctorName: varchar("doctor_name", { length: 255 }).notNull(),
reason: varchar("reason", { length: 255 }),
visitDate: date("visit_date").notNull(),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
});
export type Medicine = typeof medicines.$inferSelect;
export type MedicationDose = typeof medicationDoses.$inferSelect;
export type Allergy = typeof allergies.$inferSelect;
export type IllnessLog = typeof illnessLogs.$inferSelect;
export type DoctorVisit = typeof doctorVisits.$inferSelect;

51
src/db/schema/support.ts Normal file
View file

@ -0,0 +1,51 @@
import {
pgTable,
text,
timestamp,
uuid,
varchar,
check,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
// ---------------------------------------------------------------------------
// Support schema — aligned to PRODUCTION as of 2026-05-19 baseline.
// Tables: support_tickets, support_responses (legacy 0007_support_tickets).
// ---------------------------------------------------------------------------
export const supportTickets = pgTable(
"support_tickets",
{
id: uuid("id").primaryKey().defaultRandom(),
familyId: uuid("family_id"),
userId: uuid("user_id"),
email: varchar("email", { length: 255 }).notNull(),
subject: varchar("subject", { length: 255 }).notNull(),
description: text("description"),
status: varchar("status", { length: 20 }).default("open"),
priority: varchar("priority", { length: 20 }).default("normal"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
},
(table) => [
check(
"support_tickets_status_check",
sql`(status)::text = ANY (ARRAY['open','in_progress','resolved','closed'])`
),
check(
"support_tickets_priority_check",
sql`(priority)::text = ANY (ARRAY['low','normal','high','urgent'])`
),
]
);
export const supportResponses = pgTable("support_responses", {
id: uuid("id").primaryKey().defaultRandom(),
ticketId: uuid("ticket_id"),
adminId: uuid("admin_id"),
message: text("message").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
});
export type SupportTicket = typeof supportTickets.$inferSelect;
export type SupportResponse = typeof supportResponses.$inferSelect;