update ui and other frontend updates
This commit is contained in:
+1071
-41
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,557 @@
|
||||
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts";
|
||||
|
||||
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
|
||||
|
||||
let token: string | null = null;
|
||||
|
||||
async function auth(): Promise<string> {
|
||||
if (token) return token;
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: PB_EMAIL, password: PB_PASSWORD }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`PB auth failed: ${JSON.stringify(data)}`);
|
||||
token = data.token;
|
||||
return token!;
|
||||
}
|
||||
|
||||
async function getCollection(name: string): Promise<any | null> {
|
||||
const t = await auth();
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections?filter=name='${name}'`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data?.items?.[0] || null;
|
||||
}
|
||||
|
||||
async function createCollection(col: any): Promise<void> {
|
||||
const t = await auth();
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify(col),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`Create ${col.name} failed: ${JSON.stringify(data)}`);
|
||||
console.log(` ✓ Created collection: ${col.name}`);
|
||||
}
|
||||
|
||||
async function updateCollection(id: string, col: any): Promise<void> {
|
||||
const t = await auth();
|
||||
const res = await fetch(`${PB_ENDPOINT}/api/collections/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify(col),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`Update collection ${id} failed: ${JSON.stringify(data)}`);
|
||||
console.log(` ✓ Updated collection: ${col.name || id}`);
|
||||
}
|
||||
|
||||
export async function migrate(): Promise<void> {
|
||||
console.log("[migrate] Checking PB collection schemas...");
|
||||
|
||||
// ── 1. Create bonus_configs if missing ──
|
||||
const existing = await getCollection("bonus_configs");
|
||||
if (!existing) {
|
||||
// Need to get fams collection ID first
|
||||
const famsCol = await getCollection("fams");
|
||||
if (!famsCol) throw new Error("fams collection not found");
|
||||
const famsId = famsCol.id;
|
||||
|
||||
console.log("[migrate] Creating bonus_configs collection...");
|
||||
await createCollection({
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{ name: "famId", type: "relation", required: true, collectionId: famsId, maxSelect: 1, cascadeDelete: false },
|
||||
{ name: "name", type: "text", required: true },
|
||||
{ name: "description", type: "text", required: false },
|
||||
{ name: "target", type: "select", required: true, values: ["individual", "competitive", "collaborative"], maxSelect: 1 },
|
||||
{ name: "type", type: "select", required: true, values: ["threshold", "count", "manual"], maxSelect: 1 },
|
||||
{ name: "occurrence", type: "select", required: true, values: ["recurring", "once"], maxSelect: 1 },
|
||||
{ name: "rewardType", type: "select", required: true, values: ["points", "cash", "prize"], maxSelect: 1 },
|
||||
{ name: "rewardValue", type: "text", required: true },
|
||||
{ name: "criteriaValue", type: "number", required: false },
|
||||
{ name: "period", type: "select", required: false, values: ["schedule", "weekly", "monthly"], maxSelect: 1 },
|
||||
{ name: "status", type: "select", required: true, values: ["active", "archived"], maxSelect: 1 },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ bonus_configs already exists`);
|
||||
|
||||
const targetField = existing.fields.find((f: any) => f.name === "target");
|
||||
const periodField = existing.fields.find((f: any) => f.name === "period");
|
||||
const needTargetUpdate = targetField && !targetField.values.includes("collaborative");
|
||||
const needPeriodUpdate = periodField && !periodField.values.includes("schedule");
|
||||
|
||||
if (needTargetUpdate || needPeriodUpdate) {
|
||||
console.log("[migrate] Updating bonus_configs fields...");
|
||||
if (needTargetUpdate) targetField.values = ["individual", "competitive", "collaborative"];
|
||||
if (needPeriodUpdate) periodField.values = ["schedule", "weekly", "monthly"];
|
||||
await updateCollection(existing.id, {
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: existing.fields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Update rewards collection ──
|
||||
const rewardsCol = await getCollection("rewards");
|
||||
if (rewardsCol) {
|
||||
const fieldNames = rewardsCol.fields.map((f: any) => f.name);
|
||||
const needsUpdate =
|
||||
!fieldNames.includes("date") ||
|
||||
fieldNames.includes("autoClaimed") ||
|
||||
fieldNames.includes("weekStart") ||
|
||||
fieldNames.includes("month");
|
||||
|
||||
if (needsUpdate) {
|
||||
console.log("[migrate] Updating rewards collection schema...");
|
||||
|
||||
const bonusConfigsCol = await getCollection("bonus_configs");
|
||||
|
||||
const keepFields = rewardsCol.fields.filter((f: any) =>
|
||||
["famId", "memberId", "label", "value", "claimed", "claimedAt", "rewardType", "bonusConfigId", "created", "updated", "id"].includes(f.name)
|
||||
);
|
||||
|
||||
const newFields = [
|
||||
...keepFields,
|
||||
...(bonusConfigsCol && !fieldNames.includes("bonusConfigId")
|
||||
? [{ name: "bonusConfigId", type: "relation", required: false, collectionId: bonusConfigsCol.id, maxSelect: 1, cascadeDelete: false }]
|
||||
: []),
|
||||
...(fieldNames.includes("rewardType") ? [] : [{ name: "rewardType", type: "select", required: true, values: ["cash", "prize", "points"], maxSelect: 1 }]),
|
||||
...(fieldNames.includes("claimedAt") ? [] : [{ name: "claimedAt", type: "date", required: false }]),
|
||||
{ name: "date", type: "text", required: false },
|
||||
];
|
||||
|
||||
await updateCollection(rewardsCol.id, {
|
||||
name: "rewards",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: newFields,
|
||||
});
|
||||
|
||||
// Backfill existing rewards
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const backfillRes = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections/rewards/records?perPage=200`,
|
||||
{ headers: { Authorization: `Bearer ${await auth()}` } },
|
||||
);
|
||||
const backfillData = await backfillRes.json();
|
||||
if (backfillData?.items) {
|
||||
for (const r of backfillData.items) {
|
||||
const t2 = await auth();
|
||||
const patches: Record<string, any> = {};
|
||||
if (!r.date) {
|
||||
patches.date = r.claimedAt?.slice(0, 10) || today;
|
||||
}
|
||||
if (r.rewardType === "points" && !r.claimed) {
|
||||
patches.claimed = true;
|
||||
patches.claimedAt = new Date().toISOString();
|
||||
}
|
||||
if (Object.keys(patches).length > 0) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/rewards/records/${r.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t2}` },
|
||||
body: JSON.stringify(patches),
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(` ✓ Backfilled ${backfillData.items.length} rewards`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ↳ rewards schema is current`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ↳ rewards collection not found (will be created by seed)`);
|
||||
}
|
||||
|
||||
// ── 3. Update settings collection (drop old fields) ──
|
||||
const settingsCol = await getCollection("settings");
|
||||
if (settingsCol) {
|
||||
const fieldNames = settingsCol.fields.map((f: any) => f.name);
|
||||
if (fieldNames.includes("pointsThreshold") || fieldNames.includes("weeklyBonus")) {
|
||||
console.log("[migrate] Updating settings collection (dropping old fields)...");
|
||||
const keepFields = settingsCol.fields.filter((f: any) =>
|
||||
["famId", "webhookUrl", "created", "updated", "id"].includes(f.name)
|
||||
);
|
||||
await updateCollection(settingsCol.id, {
|
||||
name: "settings",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: keepFields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ settings schema is current`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Create notifications collection if missing ──
|
||||
const notifCol = await getCollection("notifications");
|
||||
if (!notifCol) {
|
||||
const famsCol = await getCollection("fams");
|
||||
if (!famsCol) throw new Error("fams collection not found");
|
||||
const membersCol = await getCollection("members");
|
||||
if (!membersCol) throw new Error("members collection not found");
|
||||
console.log("[migrate] Creating notifications collection...");
|
||||
await createCollection({
|
||||
name: "notifications",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{ name: "famId", type: "relation", required: true, collectionId: famsCol.id, maxSelect: 1, cascadeDelete: false },
|
||||
{ name: "memberId", type: "relation", required: true, collectionId: membersCol.id, maxSelect: 1, cascadeDelete: false },
|
||||
{ name: "message", type: "text", required: true },
|
||||
{ name: "read", type: "bool", required: false },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ notifications already exists`);
|
||||
}
|
||||
|
||||
// ── 5. Add payday field to fams if missing ──
|
||||
const famsCol = await getCollection("fams");
|
||||
if (famsCol) {
|
||||
const hasPayday = famsCol.fields.some((f: any) => f.name === "payday");
|
||||
if (!hasPayday) {
|
||||
console.log("[migrate] Adding payday field to fams...");
|
||||
const paydayField = { name: "payday", type: "number", required: false, min: 0, max: 6 };
|
||||
famsCol.fields.push(paydayField);
|
||||
await updateCollection(famsCol.id, {
|
||||
name: "fams",
|
||||
type: "base",
|
||||
listRule: famsCol.listRule,
|
||||
viewRule: famsCol.viewRule,
|
||||
createRule: famsCol.createRule,
|
||||
updateRule: famsCol.updateRule,
|
||||
deleteRule: famsCol.deleteRule,
|
||||
fields: famsCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ fams.payday already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6. Backfill completions.date — strip timestamps to YYYY-MM-DD ──
|
||||
// PB v0.25 doesn't allow changing field type (date→text), so we keep it as `date`
|
||||
// and just normalise existing records. The frontend reads with .slice(0, 10) either way.
|
||||
try {
|
||||
const completionsCol = await getCollection("completions");
|
||||
if (completionsCol) {
|
||||
const t = await auth();
|
||||
const all = await fetch(`${PB_ENDPOINT}/api/collections/completions/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const data = await all.json();
|
||||
if (data?.items?.length) {
|
||||
let backfilled = 0;
|
||||
for (const rec of data.items) {
|
||||
const plain = rec.date?.slice(0, 10);
|
||||
if (plain && plain !== rec.date) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/completions/records/${rec.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({ date: plain }),
|
||||
});
|
||||
backfilled++;
|
||||
}
|
||||
}
|
||||
if (backfilled > 0) console.log(` ✓ Backfilled ${backfilled} completion dates`);
|
||||
else console.log(` ↳ completions.date already normalised`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ↳ Step 6 backfill skipped: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
|
||||
// ── 7. Add memberId field to bonus_configs ──
|
||||
const bonusConfigsCol = await getCollection("bonus_configs");
|
||||
if (bonusConfigsCol) {
|
||||
const hasMemberId = bonusConfigsCol.fields.some((f: any) => f.name === "memberId");
|
||||
if (!hasMemberId) {
|
||||
const membersCol = await getCollection("members");
|
||||
if (membersCol) {
|
||||
console.log("[migrate] Adding memberId field to bonus_configs...");
|
||||
bonusConfigsCol.fields.push({
|
||||
name: "memberId", type: "relation", required: false,
|
||||
collectionId: membersCol.id, maxSelect: 1, cascadeDelete: false,
|
||||
});
|
||||
await updateCollection(bonusConfigsCol.id, {
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: bonusConfigsCol.listRule,
|
||||
viewRule: bonusConfigsCol.viewRule,
|
||||
createRule: bonusConfigsCol.createRule,
|
||||
updateRule: bonusConfigsCol.updateRule,
|
||||
deleteRule: bonusConfigsCol.deleteRule,
|
||||
fields: bonusConfigsCol.fields,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(` ↳ bonus_configs.memberId already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 8. Add phase field to bonus_configs ──
|
||||
if (bonusConfigsCol) {
|
||||
const hasPhase = bonusConfigsCol.fields.some((f: any) => f.name === "phase");
|
||||
if (!hasPhase) {
|
||||
console.log("[migrate] Adding phase field to bonus_configs...");
|
||||
bonusConfigsCol.fields.push({
|
||||
name: "phase", type: "select", required: true,
|
||||
values: ["template", "ready", "active", "completed"], maxSelect: 1,
|
||||
});
|
||||
await updateCollection(bonusConfigsCol.id, {
|
||||
name: "bonus_configs",
|
||||
type: "base",
|
||||
listRule: bonusConfigsCol.listRule,
|
||||
viewRule: bonusConfigsCol.viewRule,
|
||||
createRule: bonusConfigsCol.createRule,
|
||||
updateRule: bonusConfigsCol.updateRule,
|
||||
deleteRule: bonusConfigsCol.deleteRule,
|
||||
fields: bonusConfigsCol.fields,
|
||||
});
|
||||
|
||||
// Backfill existing records → phase = "active"
|
||||
const t = await auth();
|
||||
const all = await fetch(`${PB_ENDPOINT}/api/collections/bonus_configs/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const data = await all.json();
|
||||
if (data?.items?.length) {
|
||||
for (const rec of data.items) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/bonus_configs/records/${rec.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({ phase: "active" }),
|
||||
});
|
||||
}
|
||||
console.log(` ✓ Backfilled ${data.items.length} bonus_configs → phase=active`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ↳ bonus_configs.phase already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 9. Add role + userId fields to members ──
|
||||
const membersCol = await getCollection("members");
|
||||
if (membersCol) {
|
||||
const hasRole = membersCol.fields.some((f: any) => f.name === "role");
|
||||
const hasUserId = membersCol.fields.some((f: any) => f.name === "userId");
|
||||
if (!hasRole || !hasUserId) {
|
||||
console.log("[migrate] Adding role/userId fields to members...");
|
||||
if (!hasRole) membersCol.fields.push({ name: "role", type: "select", required: false, values: ["parent", "child"], maxSelect: 1 });
|
||||
if (!hasUserId) membersCol.fields.push({ name: "userId", type: "text", required: false });
|
||||
await updateCollection(membersCol.id, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: membersCol.listRule,
|
||||
viewRule: membersCol.viewRule,
|
||||
createRule: membersCol.createRule,
|
||||
updateRule: membersCol.updateRule,
|
||||
deleteRule: membersCol.deleteRule,
|
||||
fields: membersCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ members.role/userId already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 10. Add email field to members ──
|
||||
if (membersCol) {
|
||||
const hasEmail = membersCol.fields.some((f: any) => f.name === "email");
|
||||
if (!hasEmail) {
|
||||
console.log("[migrate] Adding email field to members...");
|
||||
membersCol.fields.push({ name: "email", type: "text", required: false });
|
||||
await updateCollection(membersCol.id, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: membersCol.listRule,
|
||||
viewRule: membersCol.viewRule,
|
||||
createRule: membersCol.createRule,
|
||||
updateRule: membersCol.updateRule,
|
||||
deleteRule: membersCol.deleteRule,
|
||||
fields: membersCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ members.email already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 11. Backfill userId on existing member records ──
|
||||
if (membersCol) {
|
||||
try {
|
||||
const t = await auth();
|
||||
const allMembers = await fetch(`${PB_ENDPOINT}/api/collections/members/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const membersData = await allMembers.json();
|
||||
const needBackfill = (membersData?.items || []).filter((m: any) => !m.userId);
|
||||
if (needBackfill.length > 0) {
|
||||
console.log(`[migrate] Backfilling userId for ${needBackfill.length} members...`);
|
||||
const adminsRes = await fetch(`${PB_ENDPOINT}/api/collections/fam_admins/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const adminsData = await adminsRes.json();
|
||||
const adminsByFam = new Map<string, any[]>();
|
||||
for (const a of (adminsData?.items || [])) {
|
||||
const list = adminsByFam.get(a.famId) || [];
|
||||
list.push(a);
|
||||
adminsByFam.set(a.famId, list);
|
||||
}
|
||||
let count = 0;
|
||||
for (const m of needBackfill) {
|
||||
const admins = adminsByFam.get(m.famId) || [];
|
||||
if (admins.length === 1) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/members/records/${m.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify({ userId: admins[0].userId }),
|
||||
});
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) console.log(` ✓ Backfilled ${count} member userIds`);
|
||||
if (count < needBackfill.length) console.log(` ↳ Skipped ${needBackfill.length - count} members (no unique fam_admin match)`);
|
||||
} else {
|
||||
console.log(` ↳ members.userId already backfilled`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ↳ Backfill step skipped: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 12. Drop userId field from members ──
|
||||
if (membersCol) {
|
||||
const hasUserId = membersCol.fields.some((f: any) => f.name === "userId");
|
||||
if (hasUserId) {
|
||||
console.log("[migrate] Dropping userId field from members...");
|
||||
membersCol.fields = membersCol.fields.filter((f: any) => f.name !== "userId");
|
||||
await updateCollection(membersCol.id, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: membersCol.listRule,
|
||||
viewRule: membersCol.viewRule,
|
||||
createRule: membersCol.createRule,
|
||||
updateRule: membersCol.updateRule,
|
||||
deleteRule: membersCol.deleteRule,
|
||||
fields: membersCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ members.userId already removed`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 13. Add name + color fields to fam_admins ──
|
||||
const adminsCol = await getCollection("fam_admins");
|
||||
if (adminsCol) {
|
||||
const hasName = adminsCol.fields.some((f: any) => f.name === "name");
|
||||
const hasColor = adminsCol.fields.some((f: any) => f.name === "color");
|
||||
if (!hasName || !hasColor) {
|
||||
console.log("[migrate] Adding name/color to fam_admins...");
|
||||
if (!hasName) adminsCol.fields.push({ name: "name", type: "text", required: false });
|
||||
if (!hasColor) adminsCol.fields.push({ name: "color", type: "text", required: false });
|
||||
await updateCollection(adminsCol.id, {
|
||||
name: "fam_admins",
|
||||
type: "base",
|
||||
listRule: adminsCol.listRule || "",
|
||||
viewRule: adminsCol.viewRule || "",
|
||||
createRule: adminsCol.createRule,
|
||||
updateRule: adminsCol.updateRule,
|
||||
deleteRule: adminsCol.deleteRule,
|
||||
fields: adminsCol.fields,
|
||||
});
|
||||
// Backfill name from email prefix
|
||||
const t = await auth();
|
||||
const all = await fetch(`${PB_ENDPOINT}/api/collections/fam_admins/records?perPage=1000`, {
|
||||
headers: { Authorization: `Bearer ${t}` },
|
||||
});
|
||||
const data = await all.json();
|
||||
for (const a of (data?.items || [])) {
|
||||
const patch: Record<string, string> = {};
|
||||
if (!a.name) patch.name = (a.email || "admin").split("@")[0];
|
||||
if (!a.color) patch.color = "#6366f1";
|
||||
if (Object.keys(patch).length) {
|
||||
await fetch(`${PB_ENDPOINT}/api/collections/fam_admins/records/${a.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(` ✓ Backfilled name/color for ${(data?.items || []).length} admins`);
|
||||
} else {
|
||||
console.log(` ↳ fam_admins.name/color already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 14. Drop role field from members ──
|
||||
if (membersCol) {
|
||||
const hasRole = membersCol.fields.some((f: any) => f.name === "role");
|
||||
if (hasRole) {
|
||||
console.log("[migrate] Dropping role field from members...");
|
||||
membersCol.fields = membersCol.fields.filter((f: any) => f.name !== "role");
|
||||
await updateCollection(membersCol.id, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: membersCol.listRule,
|
||||
viewRule: membersCol.viewRule,
|
||||
createRule: membersCol.createRule,
|
||||
updateRule: membersCol.updateRule,
|
||||
deleteRule: membersCol.deleteRule,
|
||||
fields: membersCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ members.role already removed`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 15. Drop email field from members ──
|
||||
if (membersCol) {
|
||||
const hasEmail = membersCol.fields.some((f: any) => f.name === "email");
|
||||
if (hasEmail) {
|
||||
console.log("[migrate] Dropping email field from members...");
|
||||
membersCol.fields = membersCol.fields.filter((f: any) => f.name !== "email");
|
||||
await updateCollection(membersCol.id, {
|
||||
name: "members",
|
||||
type: "base",
|
||||
listRule: membersCol.listRule,
|
||||
viewRule: membersCol.viewRule,
|
||||
createRule: membersCol.createRule,
|
||||
updateRule: membersCol.updateRule,
|
||||
deleteRule: membersCol.deleteRule,
|
||||
fields: membersCol.fields,
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ members.email already removed`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[migrate] Done");
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { SERVER_IP, PB_PORT, PB_EMAIL, PB_PASSWORD } from "../../config.ts";
|
||||
|
||||
const PB_ENDPOINT = `http://${SERVER_IP}:${PB_PORT}`;
|
||||
|
||||
let adminToken: string | null = null;
|
||||
let tokenExpiry = 0;
|
||||
|
||||
async function ensureToken(): Promise<string> {
|
||||
if (adminToken && Date.now() < tokenExpiry) return adminToken;
|
||||
const res = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections/_superusers/auth-with-password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity: PB_EMAIL, password: PB_PASSWORD }),
|
||||
},
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(`PB auth failed: ${JSON.stringify(data)}`);
|
||||
adminToken = data.token;
|
||||
tokenExpiry = Date.now() + 23 * 60 * 60 * 1000;
|
||||
return adminToken!;
|
||||
}
|
||||
|
||||
async function request(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<Response> {
|
||||
const token = await ensureToken();
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
if (body) headers["Content-Type"] = "application/json";
|
||||
return fetch(`${PB_ENDPOINT}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export const pb = {
|
||||
async create(collection: string, data: Record<string, unknown>) {
|
||||
const res = await request("POST", `/api/collections/${collection}/records`, data);
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(`PB create ${collection}: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
},
|
||||
|
||||
async update(collection: string, id: string, data: Record<string, unknown>) {
|
||||
const res = await request("PATCH", `/api/collections/${collection}/records/${id}`, data);
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(`PB update ${collection}: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
},
|
||||
|
||||
async delete(collection: string, id: string) {
|
||||
const res = await request("DELETE", `/api/collections/${collection}/records/${id}`);
|
||||
if (!res.ok) throw new Error(`PB delete ${collection}: ${res.status}`);
|
||||
},
|
||||
|
||||
async getList(collection: string, filter = "") {
|
||||
const params = new URLSearchParams();
|
||||
if (filter) params.set("filter", filter);
|
||||
params.set("perPage", "200");
|
||||
const res = await request("GET", `/api/collections/${collection}/records?${params}`);
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(`PB list ${collection}: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
},
|
||||
|
||||
async authWithPassword(identity: string, password: string) {
|
||||
const res = await fetch(
|
||||
`${PB_ENDPOINT}/api/collections/users/auth-with-password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity, password }),
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(`PB auth: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
},
|
||||
|
||||
async createUser(email: string, password: string) {
|
||||
return pb.create("users", {
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user