Files
famapp/src/modules/_core/schema.ts
T

266 lines
9.6 KiB
TypeScript

import type { AdapterAccountType } from "@auth/core/adapters";
import { sql } from "drizzle-orm";
import {
boolean,
index,
integer,
jsonb,
pgEnum,
pgTable,
primaryKey,
text,
timestamp,
uniqueIndex,
uuid,
varchar,
} from "drizzle-orm/pg-core";
export const roleEnum = pgEnum("household_member_role", ["owner", "member"]);
// Auth.js-compatible users table. "name" and "image" are populated from OIDC claims.
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
name: text("name"),
email: varchar("email", { length: 255 }).notNull().unique(),
emailVerified: timestamp("email_verified", { withTimezone: true }),
image: text("image"),
themePalette: text("theme_palette").notNull().default("clay"),
themeMode: text("theme_mode").notNull().default("system"),
themeFontPair: text("theme_font_pair").notNull().default("serif-sans"),
themeDensity: text("theme_density").notNull().default("regular"),
themeDashLayout: text("theme_dash_layout").notNull().default("classic"),
themeCalView: text("theme_cal_view").notNull().default("month"),
themeNavStyle: text("theme_nav_style").notNull().default("rail-desktop"),
completionVisibilityHours: integer("completion_visibility_hours").notNull().default(24),
notifPush: boolean("notif_push").notNull().default(true),
notifInApp: boolean("notif_inapp").notNull().default(true),
notifNtfy: boolean("notif_ntfy").notNull().default(false),
assistantEnabled: boolean("assistant_enabled").notNull().default(false),
assistantName: text("assistant_name").notNull().default("Assistant"),
assistantSystemPrompt: text("assistant_system_prompt"),
assistantModelRoute: text("assistant_model_route"),
assistantModel: text("assistant_model"),
defaultEventReminderOffsets: jsonb("default_event_reminder_offsets")
.notNull()
.$type<number[]>()
.default([30]),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
// Auth.js adapter tables
export const accounts = pgTable(
"accounts",
{
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccountType>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(t) => [primaryKey({ columns: [t.provider, t.providerAccountId] })],
);
export const sessions = pgTable("sessions", {
sessionToken: text("session_token").primaryKey(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { withTimezone: true }).notNull(),
});
export const verificationTokens = pgTable(
"verification_tokens",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { withTimezone: true }).notNull(),
},
(t) => [primaryKey({ columns: [t.identifier, t.token] })],
);
export const households = pgTable("households", {
id: uuid("id").primaryKey().defaultRandom(),
name: varchar("name", { length: 255 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const householdMembers = pgTable(
"household_members",
{
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
role: roleEnum("role").notNull().default("member"),
},
(t) => [primaryKey({ columns: [t.householdId, t.userId] })],
);
export const activityLog = pgTable(
"activity_log",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
actorId: uuid("actor_id").references(() => users.id, { onDelete: "set null" }),
action: text("action").notNull(),
payload: jsonb("payload").$type<Record<string, unknown> | null>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index("activity_log_household_created_idx").on(t.householdId, t.createdAt)],
);
export const shareLinks = pgTable(
"share_links",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
token: text("token").notNull().unique(),
capabilities: jsonb("capabilities").$type<{ read: boolean; write: boolean }>().notNull(),
createdBy: uuid("created_by")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expiresAt: timestamp("expires_at", { withTimezone: true }),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("share_links_household_idx").on(t.householdId),
index("share_links_entity_idx").on(t.entityType, t.entityId),
],
);
export const dashboards = pgTable(
"dashboards",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
slug: text("slug").notNull(),
isDefault: boolean("is_default").notNull().default(false),
position: integer("position").notNull().default(0),
layout: jsonb("layout").notNull().default({ version: 1, widgets: [] }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [uniqueIndex("dashboards_user_slug_uq").on(t.userId, t.slug)],
);
export const reminders = pgTable(
"reminders",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
fireAt: timestamp("fire_at", { withTimezone: true }).notNull(),
channel: text("channel").notNull().default("auto"),
title: text("title"),
body: text("body"),
offsetMinutes: integer("offset_minutes"),
firedAt: timestamp("fired_at", { withTimezone: true }),
createdBy: uuid("created_by").references(() => users.id, { onDelete: "set null" }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index("reminders_entity_idx").on(t.entityType, t.entityId),
index("reminders_household_fire_at_idx").on(t.householdId, t.fireAt),
],
);
export const pushSubscriptions = pgTable(
"push_subscriptions",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
endpoint: text("endpoint").notNull().unique(),
p256dh: text("p256dh").notNull(),
auth: text("auth").notNull(),
userAgent: text("user_agent"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index("push_subscriptions_user_idx").on(t.userId)],
);
export const notifications = pgTable(
"notifications",
{
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
title: text("title").notNull(),
body: text("body").notNull(),
url: text("url"),
readAt: timestamp("read_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index("notifications_user_read_idx").on(t.userId, t.readAt)],
);
export const comments = pgTable(
"comments",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
entityType: text("entity_type").notNull(),
entityId: uuid("entity_id").notNull(),
authorId: uuid("author_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
body: text("body").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index("comments_entity_idx").on(t.entityType, t.entityId, t.createdAt)],
);
export const householdApiTokens = pgTable(
"household_api_tokens",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
tokenHash: text("token_hash").notNull(),
name: text("name").notNull().default("default"),
createdBy: uuid("created_by")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
revokedAt: timestamp("revoked_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex("household_api_tokens_active_household_uq")
.on(t.householdId)
.where(sql`${t.revokedAt} IS NULL`),
index("household_api_tokens_hash_idx").on(t.tokenHash),
],
);