chore(plan): add targeted hosted schema command

This commit is contained in:
Dave 2026-09-22 08:30:20 +02:00
parent 48458f495f
commit a6785f74ee
6 changed files with 92 additions and 28 deletions

View file

@ -108,9 +108,11 @@ in `projects.plan_data`, separate from `cards.start_date` (which also controls
commit linking). Client plan writes verify the signed-in account and project
ownership before updating the project through the admin connection.
After deploying the new code, run the schema command above once to add the
optional `projects.plan_data` JSON field. Existing projects start with an empty
draft; no existing card dates are overwritten.
Before using the new planner on the hosted instance, load `.env.local` as shown
above and run `npm run setup-plan-schema`. This targeted command only adds the
optional `projects.plan_data` JSON field; it does not run the full setup script
or rewrite existing records. Existing projects start with an empty draft, and
no existing card dates are overwritten.
## Specs and plans

View file

@ -12,6 +12,7 @@
"test:all": "vitest run",
"seed": "tsx scripts/seed-demo.ts",
"setup-remote": "tsx scripts/setup-remote.ts",
"setup-plan-schema": "tsx scripts/apply-plan-schema.ts",
"lint": "next lint"
},
"dependencies": {

View file

@ -0,0 +1,23 @@
/** Apply only the Plan field to the shared PocketBase instance. */
import PocketBase from "pocketbase";
import { ensureProjectPlanDataField } from "./plan-schema";
async function main() {
const url = process.env.NEXT_PUBLIC_PB_URL;
const email = process.env.PB_SUPERUSER_EMAIL;
const password = process.env.PB_SUPERUSER_PASSWORD;
if (!url || !email || !password) {
throw new Error("NEXT_PUBLIC_PB_URL and PocketBase superuser credentials are required");
}
const pb = new PocketBase(url);
pb.autoCancellation(false);
await pb.collection("_superusers").authWithPassword(email, password);
const added = await ensureProjectPlanDataField(pb);
console.log(added ? "Added projects.plan_data" : "projects.plan_data already exists");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

24
scripts/plan-schema.ts Normal file
View file

@ -0,0 +1,24 @@
import type PocketBase from "pocketbase";
/** Add the optional JSON field used by the collaborative project planner. */
export async function ensureProjectPlanDataField(pb: PocketBase): Promise<boolean> {
const projects = await pb.collections.getOne("projects");
if (projects.fields.some((field: { name: string }) => field.name === "plan_data")) {
return false;
}
await pb.collections.update(projects.id, {
fields: [
...(projects.fields as unknown[]),
{
name: "plan_data",
type: "json",
required: false,
maxSize: 400_000,
presentable: false,
hidden: false,
system: false,
},
],
});
return true;
}

View file

@ -16,6 +16,7 @@
*/
import PocketBase from "pocketbase";
import { ensureProjectPlanDataField } from "./plan-schema";
const PB_URL =
process.env.NEXT_PUBLIC_PB_URL ??
@ -182,30 +183,6 @@ async function ensureProjectIngestFields(pb: PocketBase): Promise<void> {
}
}
/** One project JSON record keeps plan submission and approval together. */
async function ensureProjectPlanDataField(pb: PocketBase): Promise<void> {
const projects = await pb.collections.getOne("projects");
if (projects.fields.some((field: { name: string }) => field.name === "plan_data")) {
console.log(" = projects.plan_data already present");
return;
}
await pb.collections.update(projects.id, {
fields: [
...(projects.fields as unknown[]),
{
name: "plan_data",
type: "json",
required: false,
maxSize: 400_000,
presentable: false,
hidden: false,
system: false,
},
],
});
console.log(" + projects field: plan_data");
}
/**
* Add the `summary`, `summary_version`, `summary_attempts` fields to commits.
*
@ -1578,7 +1555,9 @@ async function main(): Promise<void> {
// Add the project/prototype kind field for Phase 6.
await ensureProjectKindField(pb);
await ensureProjectBackgroundMdField(pb);
await ensureProjectPlanDataField(pb);
console.log((await ensureProjectPlanDataField(pb))
? " + projects field: plan_data"
: " = projects.plan_data already present");
// Add the per-commit summary fields used by Phase 3b's AI one-liner job.
await ensureCommitSummaryFields(pb);

View file

@ -0,0 +1,35 @@
import { describe, expect, it, vi } from "vitest";
import { ensureProjectPlanDataField } from "../../../scripts/plan-schema";
describe("targeted plan schema update", () => {
it("adds only the optional project plan field", async () => {
const update = vi.fn(async () => ({}));
const pb = {
collections: {
getOne: vi.fn(async () => ({ id: "projects-id", fields: [{ name: "name", type: "text" }] })),
update,
},
};
expect(await ensureProjectPlanDataField(pb as never)).toBe(true);
expect(update).toHaveBeenCalledOnce();
expect(update).toHaveBeenCalledWith("projects-id", {
fields: [
{ name: "name", type: "text" },
expect.objectContaining({ name: "plan_data", type: "json", required: false }),
],
});
});
it("does nothing when the field already exists", async () => {
const update = vi.fn();
const pb = {
collections: {
getOne: vi.fn(async () => ({ id: "projects-id", fields: [{ name: "plan_data" }] })),
update,
},
};
expect(await ensureProjectPlanDataField(pb as never)).toBe(false);
expect(update).not.toHaveBeenCalled();
});
});