From 7098339200e86b80d01a3d1334b03bf74bb54d7c Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 10 May 2026 04:08:39 +0530 Subject: [PATCH] feat: add Drizzle config and auth/family schema --- drizzle.config.ts | 12 +++++ src/db/index.ts | 17 +++++++ src/db/schema/auth.ts | 49 +++++++++++++++++++ src/db/schema/family.ts | 102 ++++++++++++++++++++++++++++++++++++++++ src/db/schema/index.ts | 2 + 5 files changed, 182 insertions(+) create mode 100644 drizzle.config.ts create mode 100644 src/db/index.ts create mode 100644 src/db/schema/auth.ts create mode 100644 src/db/schema/family.ts create mode 100644 src/db/schema/index.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..4f41ffd --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema/*", + dialect: "postgresql", + out: "./drizzle", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, + verbose: true, + strict: true, +}); \ No newline at end of file diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..52bdfd2 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,17 @@ +import { encodeBase64 } from "crypto"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema"; + +const connectionString = process.env.DATABASE_URL!; + +const queryClient = postgres(connectionString, { + max: 10, + idle_timeout: 20, + max_lifetime: 60 * 30, +}); + +export const db = drizzle(queryClient, { schema }); +export const sql = queryClient; + +export type Database = typeof db; \ No newline at end of file diff --git a/src/db/schema/auth.ts b/src/db/schema/auth.ts new file mode 100644 index 0000000..befa22e --- /dev/null +++ b/src/db/schema/auth.ts @@ -0,0 +1,49 @@ +import { pgTable, text, timestamp, uuid, uniqueIndex } from "drizzle-orm/pg-core"; + +// Users table +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name"), + email: text("email").notNull().unique(), + emailVerified: timestamp("email_verified"), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Accounts table (for OAuth providers) +export const accounts = pgTable("accounts", { + id: uuid("id").primaryKey().defaultRandom(), + userId: uuid("user_id").notNull(), + type: text("type").notNull(), + provider: text("provider").notNull(), + providerAccountId: text("provider_account_id").notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: timestamp("expires_at"), + token_type: text("token_type"), + scope: text("scope"), + id_token: text("id_token"), + session_state: text("session_state"), +}, (table) => [uniqueIndex("accounts_provider_idx").on(table.provider, table.providerAccountId)]); + +// Sessions table +export const sessions = pgTable("sessions", { + id: uuid("id").primaryKey().defaultRandom(), + sessionToken: text("session_token").notNull().unique(), + userId: uuid("user_id").notNull(), + expires: timestamp("expires").notNull(), +}); + +// Verification tokens (for magic links) +export const verificationTokens = pgTable("verification_tokens", { + identifier: text("identifier").notNull(), + token: text("token").notNull(), + expires: timestamp("expires").notNull(), +}, (table) => [uniqueIndex("verification_tokens_idx").on(table.identifier, table.token)]); + +// Type exports +export type User = typeof users.$inferSelect; +export type Account = typeof accounts.$inferSelect; +export type Session = typeof sessions.$inferSelect; +export type VerificationToken = typeof verificationTokens.$inferSelect; \ No newline at end of file diff --git a/src/db/schema/family.ts b/src/db/schema/family.ts new file mode 100644 index 0000000..468c56d --- /dev/null +++ b/src/db/schema/family.ts @@ -0,0 +1,102 @@ +import { + pgTable, + text, + timestamp, + uuid, + pgEnum, + uniqueIndex, + index, + jsonb, +} from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; + +// Enums +export const memberRoleEnum = pgEnum("member_role", ["admin", "caregiver", "viewer"]); +export const childSexEnum = pgEnum("child_sex", ["male", "female", "other"]); +export const childStageEnum = pgEnum("child_stage", [ + "newborn", + "infant", + "solids_start", + "toddler_early", + "toddler_late", + "preschool", +]); + +// Families table +export const families = pgTable("families", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name").notNull().default("The Gupta Family"), + pediatricianPhone: text("pediatrician_phone"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +// Family members table +export const familyMembers = pgTable( + "family_members", + { + id: uuid("id").primaryKey().defaultRandom(), + 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)] +); + +// Children table +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>().default({}), + profilePhotoUrl: text("profile_photo_url"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => [index("children_family_idx").on(table.familyId)] +); + +// Family invites table +export const familyInvites = pgTable( + "family_invites", + { + id: uuid("id").primaryKey().defaultRandom(), + familyId: uuid("family_id").notNull(), + email: text("email").notNull(), + role: memberRoleEnum("role").notNull(), + displayName: text("display_name").notNull(), + 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)] +); + +// Type exports +export type Family = typeof families.$inferSelect; +export type FamilyMember = typeof familyMembers.$inferSelect; +export type Child = typeof children.$inferSelect; +export type FamilyInvite = typeof familyInvites.$inferSelect; + +// Helper function for deriving stage +export function deriveStage(birthDate: Date): (typeof childStageEnum)["enumValues"][number] { + const now = new Date(); + const months = Math.floor( + (now.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30) + ); + + if (months < 3) return "newborn"; + if (months < 6) return "infant"; + if (months < 12) return "solids_start"; + if (months < 24) return "toddler_early"; + if (months < 36) return "toddler_late"; + return "preschool"; +} \ No newline at end of file diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts new file mode 100644 index 0000000..946a15b --- /dev/null +++ b/src/db/schema/index.ts @@ -0,0 +1,2 @@ +export * from "./auth"; +export * from "./family"; \ No newline at end of file