add first draft working docker setup

This commit is contained in:
JCEEE
2026-08-05 20:03:01 +01:00
parent fed2f32258
commit 0fb90de5a7
20 changed files with 318 additions and 38 deletions
+4 -3
View File
@@ -3,9 +3,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js",
"start": "node dist/index.js",
"seed": "tsx scripts/seed.ts"
},
"dependencies": {
@@ -14,6 +14,7 @@
},
"devDependencies": {
"@types/node": "^26.0.0",
"esbuild": "^0.28.1",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
+217 -4
View File
@@ -1,6 +1,4 @@
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts";
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "./env.ts";
let token: string | null = null;
@@ -32,7 +30,7 @@ async function getCollection(name: string): Promise<any | null> {
return data?.items?.[0] || null;
}
async function createCollection(col: any): Promise<void> {
async function createCollection(col: any): Promise<string | null> {
const t = await auth();
const res = await fetch(`${PB_ENDPOINT}/api/collections`, {
method: "POST",
@@ -46,6 +44,7 @@ async function createCollection(col: any): Promise<void> {
if (!res.ok)
throw new Error(`Create ${col.name} failed: ${JSON.stringify(data)}`);
console.log(` ✓ Created collection: ${col.name}`);
return data?.id || null;
}
async function updateCollection(id: string, col: any): Promise<void> {
@@ -64,9 +63,223 @@ 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.
async function ensureSchema(): Promise<void> {
if (await getCollection("fams")) {
console.log("[migrate] Base schema already present — skipping bootstrap.");
return;
}
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"),
]),
))!;
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"),
]),
);
console.log("[migrate] Base schema bootstrapped.");
}
export async function migrate(): Promise<void> {
console.log("[migrate] Checking PB collection schemas...");
await ensureSchema();
// ── 1. Create bonus_configs if missing ──
const existing = await getCollection("bonus_configs");
if (!existing) {
+1 -3
View File
@@ -1,6 +1,4 @@
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts";
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "./env.ts";
let adminToken: string | null = null;
let tokenExpiry = 0;