// Single source of truth for the PocketBase schema + field builders. // Consumed by BOTH proxy/src/migrate.ts (idempotent bootstrap) and // proxy/scripts/seed.ts (fresh-store seed) so the schema isn't duplicated. // // Relations reference collections by name; the `ids` map maps collection // name -> runtime id (filled as each collection is created). export interface FieldDef { name: string; type: string; required?: boolean; unique?: boolean; max?: number; min?: number; values?: string[]; maxSelect?: number; collectionId?: string; cascadeDelete?: boolean; } export interface CollectionDef { name: string; type: string; fields: FieldDef[]; listRule?: string | null; viewRule?: string | null; createRule?: string | null; updateRule?: string | null; deleteRule?: string | null; } // ── Field builders ── export function text(name: string, required = false): FieldDef { return { name, type: "text", required }; } export function uniqueText(name: string): FieldDef { return { name, type: "text", required: true, unique: true }; } export function number(name: string, required = false): FieldDef { return { name, type: "number", required }; } export function bool(name: string): FieldDef { return { name, type: "bool" }; } export function date(name: string): FieldDef { return { name, type: "date" }; } export function jsonField(name: string): FieldDef { return { name, type: "json" }; } export function select(name: string, values: string[], required = false): FieldDef { return { name, type: "select", required, values, maxSelect: 1 }; } export function rel(name: string, collectionId: string, required = false): FieldDef { return { name, type: "relation", required, collectionId, maxSelect: 1, cascadeDelete: false, }; } // ── Collection builder ── export function col( name: string, fields: FieldDef[], rules: { listRule?: string | null; viewRule?: string | null } = {}, ): (ids: Record) => CollectionDef { return (ids) => ({ name, type: "base", listRule: rules.listRule ?? "", viewRule: rules.viewRule ?? "", createRule: null, updateRule: null, deleteRule: null, fields, }); } export type CollectionPlanEntry = { name: string; build: (ids: Record) => CollectionDef; }; // Ordered so every relation's target already exists (and its id is in `ids`) // when a collection is built. `fams` is superadmin-only (listRule/viewRule null). export const SCHEMA_PLAN: CollectionPlanEntry[] = [ { name: "fams", build: (ids) => col( "fams", [ text("name", true), uniqueText("slug"), text("inviteCode"), text("stripeCustomerId"), jsonField("featureFlags"), jsonField("seasons"), ], { listRule: null, viewRule: null }, )(ids), }, { name: "bonus_templates", build: (ids) => col("bonus_templates", [ rel("famId", ids.fams, true), text("name", true), text("description"), select("target", ["individual", "competitive", "collaborative"], true), select("type", ["threshold", "count", "manual"], true), select("occurrence", ["recurring", "once"], true), select("rewardType", ["points", "cash", "prize"], true), text("rewardValue", true), number("criteriaValue"), select("period", ["schedule", "daily", "weekly", "monthly"]), ])(ids), }, { name: "settings", build: (ids) => col("settings", [rel("famId", ids.fams, true), text("webhookUrl")])(ids), }, { name: "members", build: (ids) => col("members", [ rel("famId", ids.fams, true), text("name", true), text("color"), text("deviceToken"), text("deviceTokenHint"), ])(ids), }, { name: "chore_templates", build: (ids) => col("chore_templates", [ rel("famId", ids.fams, true), text("name", true), text("description"), select("defaultFrequency", ["daily", "weekly"], true), select("defaultType", ["points", "money"], true), number("defaultValue", true), ])(ids), }, { name: "fam_admins", build: (ids) => col("fam_admins", [ rel("famId", ids.fams, true), text("userId", true), text("email", true), text("name"), text("color"), ])(ids), }, { name: "bonus_configs", build: (ids) => col("bonus_configs", [ rel("famId", ids.fams, true), text("name", true), text("description"), select("target", ["individual", "competitive", "collaborative"], true), select("type", ["threshold", "count", "manual"], true), select("occurrence", ["recurring", "once"], true), select("rewardType", ["points", "cash", "prize"], true), text("rewardValue", true), number("criteriaValue"), rel("memberId", ids.members), select("period", ["schedule", "daily", "weekly", "monthly"]), select("status", ["active", "completed"], true), ])(ids), }, { name: "weekly_history", build: (ids) => col("weekly_history", [ rel("famId", ids.fams, true), rel("memberId", ids.members, true), date("weekStart"), number("pointsEarned"), number("moneyEarned"), number("choresCompleted"), number("bonusEarned"), ])(ids), }, { name: "seasons", build: (ids) => col("seasons", [ rel("famId", ids.fams, true), text("name", true), text("color"), bool("active"), date("autoDisable"), date("autoStart"), ])(ids), }, { name: "messages", build: (ids) => col("messages", [ rel("famId", ids.fams, true), select("authorType", ["admin", "member"], true), text("authorId", true), text("authorName", true), text("authorColor"), text("content", true), ])(ids), }, { name: "chat_typing", build: (ids) => col("chat_typing", [ rel("famId", ids.fams, true), text("actorId", true), select("actorType", ["admin", "member"], true), text("authorName", true), text("authorColor"), bool("typing"), ])(ids), }, { name: "rewards", build: (ids) => col("rewards", [ rel("famId", ids.fams, true), rel("memberId", ids.members, true), rel("bonusConfigId", ids.bonus_configs), text("label", true), number("value", true), select("rewardType", ["cash", "prize", "points"], true), select("status", ["unclaimed", "requested", "claimed"], true), select("claimable", ["immediate", "payday"], true), text("settleDate"), date("claimedAt"), date("requestedAt"), text("date"), ])(ids), }, { name: "assigned_chores", build: (ids) => col("assigned_chores", [ rel("famId", ids.fams, true), rel("memberId", ids.members, true), rel("templateId", ids.chore_templates, true), select("frequency", ["daily", "weekly"], true), select("type", ["points", "money"], true), number("value", true), text("customName"), jsonField("seasonIds"), ])(ids), }, { name: "completions", build: (ids) => col("completions", [ rel("famId", ids.fams, true), rel("memberId", ids.members, true), rel("assignedChoreId", ids.assigned_chores, true), date("date"), date("completedAt"), ])(ids), }, ];