- care reminders now fire with plant-specific body ("Time to water Pothos")
via title/body columns on the reminders table (migration 0016)
- bi-directional task sync: checking off a garden-linked task list item
creates a care log; logging care from garden marks the linked task done
(list_items.metadata stores gardenPlantId + gardenCareType linkage)
- upload error response no longer leaks debug detail; error message
corrected from "5 MB" to "100 MB"
212 lines
7.6 KiB
TypeScript
212 lines
7.6 KiB
TypeScript
import type { AdapterAccountType } from "@auth/core/adapters";
|
|
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),
|
|
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"),
|
|
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) => [
|
|
uniqueIndex("reminders_entity_unique").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)],
|
|
);
|