update compose to be coolifyable
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch --env-file-if-exists=../.env src/index.ts",
|
||||
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
|
||||
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --alias:@shared=../shared",
|
||||
"start": "node dist/index.js",
|
||||
"seed": "tsx --env-file-if-exists=../.env scripts/seed.ts"
|
||||
},
|
||||
|
||||
+8
-334
@@ -1,29 +1,9 @@
|
||||
import {
|
||||
SCHEMA_PLAN,
|
||||
type CollectionDef,
|
||||
} from "@shared/pb/schema.ts";
|
||||
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "../src/env.ts";
|
||||
|
||||
interface FieldDef {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
unique?: boolean;
|
||||
max?: number;
|
||||
min?: number;
|
||||
values?: string[];
|
||||
maxSelect?: number;
|
||||
collectionId?: string;
|
||||
cascadeDelete?: boolean;
|
||||
}
|
||||
|
||||
interface CollectionDef {
|
||||
name: string;
|
||||
type: string;
|
||||
fields: FieldDef[];
|
||||
listRule?: string | null;
|
||||
viewRule?: string | null;
|
||||
createRule?: string | null;
|
||||
updateRule?: string | null;
|
||||
deleteRule?: string | null;
|
||||
}
|
||||
|
||||
async function getSuperadminToken(): Promise<string> {
|
||||
const res = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`,
|
||||
@@ -67,322 +47,16 @@ async function createCollection(
|
||||
return data.id;
|
||||
}
|
||||
|
||||
function text(name: string, required = false): FieldDef {
|
||||
return { name, type: "text", required };
|
||||
}
|
||||
|
||||
function uniqueText(name: string): FieldDef {
|
||||
return { name, type: "text", required: true, unique: true };
|
||||
}
|
||||
|
||||
function number(name: string, required = false): FieldDef {
|
||||
return { name, type: "number", required };
|
||||
}
|
||||
|
||||
function bool(name: string): FieldDef {
|
||||
return { name, type: "bool" };
|
||||
}
|
||||
|
||||
function date(name: string): FieldDef {
|
||||
return { name, type: "date" };
|
||||
}
|
||||
|
||||
function jsonField(name: string): FieldDef {
|
||||
return { name, type: "json" };
|
||||
}
|
||||
|
||||
function select(name: string, values: string[], required = false): FieldDef {
|
||||
return { name, type: "select", required, values, maxSelect: 1 };
|
||||
}
|
||||
|
||||
function rel(name: string, collectionId: string, required = false): FieldDef {
|
||||
return {
|
||||
name,
|
||||
type: "relation",
|
||||
required,
|
||||
collectionId,
|
||||
maxSelect: 1,
|
||||
cascadeDelete: false,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Connecting to PB at", PB_ENDPOINT);
|
||||
const token = await getSuperadminToken();
|
||||
console.log("Authenticated as superadmin\n");
|
||||
|
||||
const ids: Record<string, string> = {};
|
||||
|
||||
// ── Pass 1: Independent collections ──
|
||||
console.log("--- Pass 1: Independent collections ---");
|
||||
|
||||
ids.fams = await createCollection(token, {
|
||||
name: "fams",
|
||||
type: "base",
|
||||
listRule: null,
|
||||
viewRule: null,
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
text("name", true),
|
||||
uniqueText("slug"),
|
||||
text("inviteCode"),
|
||||
text("stripeCustomerId"),
|
||||
jsonField("featureFlags"),
|
||||
jsonField("seasons"),
|
||||
],
|
||||
});
|
||||
|
||||
// ── Pass 2: Collections that reference fams ──
|
||||
console.log("\n--- Pass 2: fam-scoped collections ---");
|
||||
|
||||
ids.settings = await createCollection(token, {
|
||||
name: "settings",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [rel("famId", ids.fams!, true), text("webhookUrl")],
|
||||
});
|
||||
|
||||
ids.members = await createCollection(token, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("color"),
|
||||
text("deviceToken"),
|
||||
text("deviceTokenHint"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.chore_templates = await createCollection(token, {
|
||||
name: "chore_templates",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("description"),
|
||||
select("defaultFrequency", ["daily", "weekly"], true),
|
||||
select("defaultType", ["points", "money"], true),
|
||||
number("defaultValue", true),
|
||||
],
|
||||
});
|
||||
|
||||
ids.fam_admins = await createCollection(token, {
|
||||
name: "fam_admins",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("userId", true),
|
||||
text("email", true),
|
||||
text("name"),
|
||||
text("color"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.bonus_configs = await createCollection(token, {
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
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.bonus_templates = await createCollection(token, {
|
||||
name: "bonus_templates",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
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"]),
|
||||
],
|
||||
});
|
||||
|
||||
// ── Pass 3: Collections with member/template deps ──
|
||||
console.log("\n--- Pass 3: Nested collections ---");
|
||||
|
||||
ids.weekly_history = await createCollection(token, {
|
||||
name: "weekly_history",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
date("weekStart"),
|
||||
number("pointsEarned"),
|
||||
number("moneyEarned"),
|
||||
number("choresCompleted"),
|
||||
number("bonusEarned"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.rewards = await createCollection(token, {
|
||||
name: "rewards",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
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.assigned_chores = await createCollection(token, {
|
||||
name: "assigned_chores",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
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.seasons = await createCollection(token, {
|
||||
name: "seasons",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("name", true),
|
||||
text("color"),
|
||||
bool("active"),
|
||||
date("autoDisable"),
|
||||
date("autoStart"),
|
||||
],
|
||||
});
|
||||
|
||||
ids.completions = await createCollection(token, {
|
||||
name: "completions",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
rel("memberId", ids.members!, true),
|
||||
rel("assignedChoreId", ids.assigned_chores!, true),
|
||||
date("date"),
|
||||
date("completedAt"),
|
||||
],
|
||||
});
|
||||
|
||||
// Chat collections (global family messages + transient typing presence)
|
||||
ids.messages = await createCollection(token, {
|
||||
name: "messages",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
select("authorType", ["admin", "member"], true),
|
||||
text("authorId", true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
text("content", true),
|
||||
],
|
||||
});
|
||||
|
||||
ids.chat_typing = await createCollection(token, {
|
||||
name: "chat_typing",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("actorId", true),
|
||||
select("actorType", ["admin", "member"], true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
bool("typing"),
|
||||
],
|
||||
});
|
||||
for (const entry of SCHEMA_PLAN) {
|
||||
const id = await createCollection(token, entry.build(ids));
|
||||
if (id) ids[entry.name] = id;
|
||||
}
|
||||
|
||||
console.log("\n✅ All collections created successfully");
|
||||
console.log("Collection IDs:", ids);
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { pb } from "./pb.ts";
|
||||
import { migrate } from "./migrate.ts";
|
||||
import { PROXY_PORT } from "../../config.ts";
|
||||
import { PROXY_PORT } from "@shared/config.ts";
|
||||
import {
|
||||
weekStart as tzWeekStart,
|
||||
addDaysStr,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
resolveTz,
|
||||
nextPaydayAfter as nextPaydayAfterTz,
|
||||
periodWindow,
|
||||
} from "../../timezone.ts";
|
||||
} from "@shared/timezone.ts";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
|
||||
+11
-202
@@ -1,3 +1,4 @@
|
||||
import { SCHEMA_PLAN } from "@shared/pb/schema.ts";
|
||||
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "./env.ts";
|
||||
|
||||
let token: string | null = null;
|
||||
@@ -63,53 +64,9 @@ async function updateCollection(id: string, col: any): Promise<void> {
|
||||
console.log(` ✓ Updated collection: ${col.name || id}`);
|
||||
}
|
||||
|
||||
// ── Field helpers (mirror proxy/scripts/seed.ts) ──
|
||||
function text(name: string, required = false) {
|
||||
return { name, type: "text", required };
|
||||
}
|
||||
function uniqueText(name: string) {
|
||||
return { name, type: "text", required: true, unique: true };
|
||||
}
|
||||
function number(name: string, required = false) {
|
||||
return { name, type: "number", required };
|
||||
}
|
||||
function bool(name: string) {
|
||||
return { name, type: "bool" };
|
||||
}
|
||||
function date(name: string) {
|
||||
return { name, type: "date" };
|
||||
}
|
||||
function jsonField(name: string) {
|
||||
return { name, type: "json" };
|
||||
}
|
||||
function select(name: string, values: string[], required = false) {
|
||||
return { name, type: "select", required, values, maxSelect: 1 };
|
||||
}
|
||||
function rel(name: string, collectionId: string, required = false) {
|
||||
return {
|
||||
name,
|
||||
type: "relation",
|
||||
required,
|
||||
collectionId,
|
||||
maxSelect: 1,
|
||||
cascadeDelete: false,
|
||||
};
|
||||
}
|
||||
function colDef(name: string, fields: any[]): any {
|
||||
return {
|
||||
name,
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
// Bootstrap the full base schema on a fresh PocketBase (docker first boot).
|
||||
// Idempotent — skips collections that already exist.
|
||||
// Idempotent — skips collections that already exist. Schema is shared with
|
||||
// seed.ts via shared/pb/schema.ts.
|
||||
async function ensureSchema(): Promise<void> {
|
||||
if (await getCollection("fams")) {
|
||||
console.log("[migrate] Base schema already present — skipping bootstrap.");
|
||||
@@ -117,163 +74,15 @@ async function ensureSchema(): Promise<void> {
|
||||
}
|
||||
console.log("[migrate] Bootstrapping base schema on fresh PocketBase...");
|
||||
|
||||
// Pass 1: independent
|
||||
const famsId = (await createCollection(
|
||||
colDef("fams", [
|
||||
text("name", true),
|
||||
uniqueText("slug"),
|
||||
text("inviteCode"),
|
||||
text("stripeCustomerId"),
|
||||
jsonField("featureFlags"),
|
||||
jsonField("seasons"),
|
||||
]),
|
||||
))!;
|
||||
const ids: Record<string, string> = {};
|
||||
for (const entry of SCHEMA_PLAN) {
|
||||
const createdId = await createCollection(entry.build(ids));
|
||||
if (createdId) ids[entry.name] = createdId;
|
||||
}
|
||||
|
||||
// fams is sensitive → superadmin-only (proxy/server reads). Not public.
|
||||
await updateCollection(famsId, { viewRule: null, listRule: null });
|
||||
await createCollection(
|
||||
colDef("bonus_templates", [
|
||||
rel("famId", famsId, 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"]),
|
||||
]),
|
||||
);
|
||||
|
||||
// Pass 2: reference fams
|
||||
await createCollection(
|
||||
colDef("settings", [rel("famId", famsId, true), text("webhookUrl")]),
|
||||
);
|
||||
const membersId = (await createCollection(
|
||||
colDef("members", [
|
||||
rel("famId", famsId, true),
|
||||
text("name", true),
|
||||
text("color"),
|
||||
text("deviceToken"),
|
||||
text("deviceTokenHint"),
|
||||
]),
|
||||
))!;
|
||||
const choreTemplatesId = (await createCollection(
|
||||
colDef("chore_templates", [
|
||||
rel("famId", famsId, true),
|
||||
text("name", true),
|
||||
text("description"),
|
||||
select("defaultFrequency", ["daily", "weekly"], true),
|
||||
select("defaultType", ["points", "money"], true),
|
||||
number("defaultValue", true),
|
||||
]),
|
||||
))!;
|
||||
await createCollection(
|
||||
colDef("fam_admins", [
|
||||
rel("famId", famsId, true),
|
||||
text("userId", true),
|
||||
text("email", true),
|
||||
text("name"),
|
||||
text("color"),
|
||||
]),
|
||||
);
|
||||
const bonusConfigsId = (await createCollection(
|
||||
colDef("bonus_configs", [
|
||||
rel("famId", famsId, 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", membersId),
|
||||
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
||||
select("status", ["active", "completed"], true),
|
||||
]),
|
||||
))!;
|
||||
|
||||
// Pass 3: nested deps
|
||||
await createCollection(
|
||||
colDef("weekly_history", [
|
||||
rel("famId", famsId, true),
|
||||
rel("memberId", membersId, true),
|
||||
date("weekStart"),
|
||||
number("pointsEarned"),
|
||||
number("moneyEarned"),
|
||||
number("choresCompleted"),
|
||||
number("bonusEarned"),
|
||||
]),
|
||||
);
|
||||
await createCollection(
|
||||
colDef("rewards", [
|
||||
rel("famId", famsId, true),
|
||||
rel("memberId", membersId, true),
|
||||
rel("bonusConfigId", bonusConfigsId),
|
||||
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"),
|
||||
]),
|
||||
);
|
||||
const assignedChoresId = (await createCollection(
|
||||
colDef("assigned_chores", [
|
||||
rel("famId", famsId, true),
|
||||
rel("memberId", membersId, true),
|
||||
rel("templateId", choreTemplatesId, true),
|
||||
select("frequency", ["daily", "weekly"], true),
|
||||
select("type", ["points", "money"], true),
|
||||
number("value", true),
|
||||
text("customName"),
|
||||
jsonField("seasonIds"),
|
||||
]),
|
||||
))!;
|
||||
await createCollection(
|
||||
colDef("seasons", [
|
||||
rel("famId", famsId, true),
|
||||
text("name", true),
|
||||
text("color"),
|
||||
bool("active"),
|
||||
date("autoDisable"),
|
||||
date("autoStart"),
|
||||
]),
|
||||
);
|
||||
await createCollection(
|
||||
colDef("completions", [
|
||||
rel("famId", famsId, true),
|
||||
rel("memberId", membersId, true),
|
||||
rel("assignedChoreId", assignedChoresId, true),
|
||||
date("date"),
|
||||
date("completedAt"),
|
||||
]),
|
||||
);
|
||||
await createCollection(
|
||||
colDef("messages", [
|
||||
rel("famId", famsId, true),
|
||||
select("authorType", ["admin", "member"], true),
|
||||
text("authorId", true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
text("content", true),
|
||||
date("createdAt"),
|
||||
]),
|
||||
);
|
||||
await createCollection(
|
||||
colDef("chat_typing", [
|
||||
rel("famId", famsId, true),
|
||||
text("actorId", true),
|
||||
select("actorType", ["admin", "member"], true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
bool("typing"),
|
||||
]),
|
||||
);
|
||||
const famsId = ids.fams;
|
||||
if (famsId) await updateCollection(famsId, { viewRule: null, listRule: null });
|
||||
console.log("[migrate] Base schema bootstrapped.");
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -3,16 +3,16 @@
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@shared/*": ["../shared/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*", "../shared/**/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user