Files
famdone/shared/pb/schema.ts
T
2026-08-07 11:38:39 +01:00

280 lines
7.5 KiB
TypeScript

// 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<string, string>) => 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<string, string>) => 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),
},
];