Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a55276394 | |||
| 146549ecbd | |||
| 03dd8db1a2 |
@@ -177,3 +177,17 @@
|
|||||||
- **Root cause**: the permanent dev PB container **`pb-dev`** (publishes `:8090`, data in host `./pb_data`) had a **broken bind mount** — it was serving an empty throwaway store, so the `debug@famchamp.dev` superuser didn't exist. The real `data.db` was on the host but the container wasn't seeing it.
|
- **Root cause**: the permanent dev PB container **`pb-dev`** (publishes `:8090`, data in host `./pb_data`) had a **broken bind mount** — it was serving an empty throwaway store, so the `debug@famchamp.dev` superuser didn't exist. The real `data.db` was on the host but the container wasn't seeing it.
|
||||||
- **Fix**: recreated `pb-dev` with the mount correctly attached (`-v "$PWD/pb_data:/pb_data"`, `pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data`). Superuser auth then returned 200 on both `127.0.0.1:8090` and the Tailscale `SERVER_IP:8090`.
|
- **Fix**: recreated `pb-dev` with the mount correctly attached (`-v "$PWD/pb_data:/pb_data"`, `pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data`). Superuser auth then returned 200 on both `127.0.0.1:8090` and the Tailscale `SERVER_IP:8090`.
|
||||||
- **Watch-out**: a careless `docker run` with a **fresh volume** (my first attempt, aborted in time) would have wiped the permanent PB data. Restore command is in RULES.md. Two containers (`pb-dev` :8090 and the docker app's internal PB :8091) currently **share the same host `./pb_data`** — be careful with both.
|
- **Watch-out**: a careless `docker run` with a **fresh volume** (my first attempt, aborted in time) would have wiped the permanent PB data. Restore command is in RULES.md. Two containers (`pb-dev` :8090 and the docker app's internal PB :8091) currently **share the same host `./pb_data`** — be careful with both.
|
||||||
|
|
||||||
|
### 2026-08-06 — Added root `shared/` for cross-package code
|
||||||
|
- Created `shared/timezone.ts` (moved from root `timezone.ts`). Imported by `frontend/src/routes/[fam]/[username]/+page.svelte`, `.../settings/+page.svelte`, and `proxy/src/index.ts`. Deleted the root `timezone.ts`.
|
||||||
|
- Created `shared/pb/schema.ts` — single source of truth for the PocketBase schema + field builders (`SCHEMA_PLAN` ordered collection plan + `text/select/rel/...` helpers). Both `proxy/src/migrate.ts` (`ensureSchema`) and `proxy/scripts/seed.ts` now iterate `SCHEMA_PLAN`; kills the previous duplicated schema/field-helper definitions in both files.
|
||||||
|
- Reason: `timezone.ts` and the PB schema are consumed by more than one package; `shared/` is the root location both can reach. Rule added to RULES.md: shared code lives in `shared/`, never inside `frontend/` or `proxy/`.
|
||||||
|
- Note: proxy `tsc --noEmit` already errors on `.ts`-extension imports (`allowImportingTsExtensions` unset) — pre-existing, not from this change. Runtime uses esbuild (build) + tsx (dev), both of which bundle the `shared/` imports correctly. Verified `pnpm build` clean for both packages.
|
||||||
|
|
||||||
|
### 2026-08-07 — `@shared/*` import alias (path alias, not a pnpm package)
|
||||||
|
- Moved `config.ts` → `shared/config.ts`. All `shared/` code is now imported as `@shared/*` instead of relative `../../shared/...`.
|
||||||
|
- This is a **path alias**, not a pnpm workspace package (`@shared` alone isn't a valid npm package name; a real package would need `@scope/name`).
|
||||||
|
- Proxy: `tsconfig.json` sets `paths: { "@shared/*": ["../shared/*"] }`; esbuild build adds `--alias:@shared=../shared`; tsx resolves via tsconfig paths. Proxy keeps `.ts` extensions (`@shared/config.ts`).
|
||||||
|
- Frontend: uses `kit.alias` in `vite.config.ts` (NOT `paths` in `frontend/tsconfig.json`, which SvelteKit warns against). Frontend imports shared files **without** the `.ts` extension (`@shared/timezone`) because `rewriteRelativeImportExtensions` only rewrites relative paths.
|
||||||
|
- Verified: proxy build + frontend build + `svelte-check` all clean for `@shared/*` (svelte-check still reports pre-existing `qrcode` types + CSS warnings).
|
||||||
|
- Docker note: runtime image only copies `frontend/build` + `proxy/dist` (both already bundle `shared/`), so `shared/` needn't be copied into the image.
|
||||||
|
|||||||
@@ -41,6 +41,12 @@
|
|||||||
## Monorepo
|
## Monorepo
|
||||||
- SvelteKit in `frontend/`, Hono in `proxy/`
|
- SvelteKit in `frontend/`, Hono in `proxy/`
|
||||||
- `pnpm dev` / `pnpm build` at root runs both in parallel
|
- `pnpm dev` / `pnpm build` at root runs both in parallel
|
||||||
|
- `shared/` at root for code shared across packages, imported as `@shared/*` (path alias):
|
||||||
|
- `shared/config.ts` — service ports (`FRONTEND_PORT`/`PROXY_PORT`/`PB_PORT`)
|
||||||
|
- `shared/timezone.ts` — timezone helpers (imported by frontend routes + proxy/src/index.ts)
|
||||||
|
- `shared/pb/schema.ts` — single source of truth for the PocketBase schema + field builders. Both `proxy/src/migrate.ts` (idempotent bootstrap) and `proxy/scripts/seed.ts` consume `SCHEMA_PLAN`; edit schema there, not in the consumers.
|
||||||
|
- **`@shared/*` alias:** wired per package — proxy via `tsconfig.json` `paths` + esbuild `--alias:@shared=../shared` (tsx reads tsconfig paths); frontend via `kit.alias` in `vite.config.ts` (don't set `paths` in `frontend/tsconfig.json` — SvelteKit warns). In the frontend, import shared files **without** the `.ts` extension (`@shared/timezone`), because `rewriteRelativeImportExtensions` only rewrites relative paths; the proxy keeps `.ts` extensions.
|
||||||
|
- **Shared-file convention:** keep shared code in `shared/` (root), never inside `frontend/` or `proxy/`. SvelteKit never imports `config.ts`.
|
||||||
- Decisions tracked in `MEMORY.md`
|
- Decisions tracked in `MEMORY.md`
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|||||||
+5
-5
@@ -4,11 +4,11 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "3001:80" # nginx / app (public)
|
- "${FRONTEND_PORT:-3001}:3001" # nginx / app (public)
|
||||||
- "8091:8090" # PocketBase admin UI (loopback only — SSH tunnel)
|
- "${PB_PORT:-8091}:8090" # PocketBase admin UI (private network)
|
||||||
volumes:
|
volumes:
|
||||||
- "./pb_data:/app/pb_data" # PB DB persistence
|
- "${PB_DATA:-./pb_data}:/app/pb_data" # PB DB persistence
|
||||||
environment:
|
environment:
|
||||||
PB_EMAIL: debug@famchamp.dev
|
PB_EMAIL: ${PB_EMAIL}
|
||||||
PB_PASSWORD: debug123
|
PB_PASSWORD: ${PB_PASSWORD}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
periodStart,
|
periodStart,
|
||||||
periodEnd,
|
periodEnd,
|
||||||
isCompleteForPeriod
|
isCompleteForPeriod
|
||||||
} from '../../../../../timezone.ts';
|
} from '@shared/timezone';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { famStore } from '$lib/stores/fam.svelte';
|
import { famStore } from '$lib/stores/fam.svelte';
|
||||||
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
|
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
|
||||||
import { COMMON_TIMEZONES } from '../../../../../../timezone.ts';
|
import { COMMON_TIMEZONES } from '@shared/timezone';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
import adapter from '@sveltejs/adapter-node';
|
import adapter from '@sveltejs/adapter-node';
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
@@ -13,6 +14,9 @@ export default defineConfig(() => {
|
|||||||
filename.split(/[/\\]/).includes('node_modules') ? undefined : true,
|
filename.split(/[/\\]/).includes('node_modules') ? undefined : true,
|
||||||
experimental: { async: true }
|
experimental: { async: true }
|
||||||
},
|
},
|
||||||
|
alias: {
|
||||||
|
'@shared': fileURLToPath(new URL('../shared', import.meta.url))
|
||||||
|
},
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
experimental: { remoteFunctions: true, handleRenderingErrors: true },
|
experimental: { remoteFunctions: true, handleRenderingErrors: true },
|
||||||
csrf: {
|
csrf: {
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@
|
|||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch --env-file-if-exists=../.env src/index.ts",
|
"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",
|
"start": "node dist/index.js",
|
||||||
"seed": "tsx --env-file-if-exists=../.env scripts/seed.ts"
|
"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";
|
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> {
|
async function getSuperadminToken(): Promise<string> {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`,
|
`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`,
|
||||||
@@ -67,322 +47,16 @@ async function createCollection(
|
|||||||
return data.id;
|
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() {
|
async function main() {
|
||||||
console.log("Connecting to PB at", PB_ENDPOINT);
|
console.log("Connecting to PB at", PB_ENDPOINT);
|
||||||
const token = await getSuperadminToken();
|
const token = await getSuperadminToken();
|
||||||
console.log("Authenticated as superadmin\n");
|
console.log("Authenticated as superadmin\n");
|
||||||
|
|
||||||
const ids: Record<string, string> = {};
|
const ids: Record<string, string> = {};
|
||||||
|
for (const entry of SCHEMA_PLAN) {
|
||||||
// ── Pass 1: Independent collections ──
|
const id = await createCollection(token, entry.build(ids));
|
||||||
console.log("--- Pass 1: Independent collections ---");
|
if (id) ids[entry.name] = id;
|
||||||
|
}
|
||||||
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"),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\n✅ All collections created successfully");
|
console.log("\n✅ All collections created successfully");
|
||||||
console.log("Collection IDs:", ids);
|
console.log("Collection IDs:", ids);
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@ import { serve } from "@hono/node-server";
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { pb } from "./pb.ts";
|
import { pb } from "./pb.ts";
|
||||||
import { migrate } from "./migrate.ts";
|
import { migrate } from "./migrate.ts";
|
||||||
import { PROXY_PORT } from "../../config.ts";
|
import { PROXY_PORT } from "@shared/config.ts";
|
||||||
import {
|
import {
|
||||||
weekStart as tzWeekStart,
|
weekStart as tzWeekStart,
|
||||||
addDaysStr,
|
addDaysStr,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
resolveTz,
|
resolveTz,
|
||||||
nextPaydayAfter as nextPaydayAfterTz,
|
nextPaydayAfter as nextPaydayAfterTz,
|
||||||
periodWindow,
|
periodWindow,
|
||||||
} from "../../timezone.ts";
|
} from "@shared/timezone.ts";
|
||||||
|
|
||||||
const app = new Hono();
|
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";
|
import { PB_ENDPOINT, PB_EMAIL, PB_PASSWORD } from "./env.ts";
|
||||||
|
|
||||||
let token: string | null = null;
|
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}`);
|
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).
|
// 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> {
|
async function ensureSchema(): Promise<void> {
|
||||||
if (await getCollection("fams")) {
|
if (await getCollection("fams")) {
|
||||||
console.log("[migrate] Base schema already present — skipping bootstrap.");
|
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...");
|
console.log("[migrate] Bootstrapping base schema on fresh PocketBase...");
|
||||||
|
|
||||||
// Pass 1: independent
|
const ids: Record<string, string> = {};
|
||||||
const famsId = (await createCollection(
|
for (const entry of SCHEMA_PLAN) {
|
||||||
colDef("fams", [
|
const createdId = await createCollection(entry.build(ids));
|
||||||
text("name", true),
|
if (createdId) ids[entry.name] = createdId;
|
||||||
uniqueText("slug"),
|
}
|
||||||
text("inviteCode"),
|
|
||||||
text("stripeCustomerId"),
|
|
||||||
jsonField("featureFlags"),
|
|
||||||
jsonField("seasons"),
|
|
||||||
]),
|
|
||||||
))!;
|
|
||||||
// fams is sensitive → superadmin-only (proxy/server reads). Not public.
|
// fams is sensitive → superadmin-only (proxy/server reads). Not public.
|
||||||
await updateCollection(famsId, { viewRule: null, listRule: null });
|
const famsId = ids.fams;
|
||||||
await createCollection(
|
if (famsId) await updateCollection(famsId, { viewRule: null, listRule: null });
|
||||||
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.");
|
console.log("[migrate] Base schema bootstrapped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -3,16 +3,16 @@
|
|||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"declaration": true,
|
"allowImportingTsExtensions": true,
|
||||||
"declarationMap": true,
|
"noEmit": true,
|
||||||
"sourceMap": true
|
"paths": {
|
||||||
|
"@shared/*": ["../shared/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*", "../shared/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
// 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),
|
||||||
|
},
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user