diff --git a/scripts/setup-remote.ts b/scripts/setup-remote.ts index 026b288..0812352 100644 --- a/scripts/setup-remote.ts +++ b/scripts/setup-remote.ts @@ -580,6 +580,95 @@ async function ensureCardsCreatedByRoleValues(pb: PocketBase): Promise { console.log(` + cards.created_by_role values: ${missing.join(", ")}`); } +/** + * The intake_links schema first shipped with `submission_count` and + * `submission_count_today` as `required: true` and `min: 0`. PocketBase + * treats `0` as blank for required number fields, so any insert is rejected + * with `validation_required` until the field is marked optional. The + * counters default to 0 on creation; existing rows are unaffected. + */ +async function ensureIntakeLinkCountersOptional(pb: PocketBase): Promise { + const links = await pb.collections.getOne("intake_links"); + const fields = links.fields.map((f: { name: string; required?: boolean }) => { + if ( + (f.name === "submission_count" || f.name === "submission_count_today") && + f.required !== false + ) { + return { ...f, required: false }; + } + return f; + }); + + const changed = fields.some( + (f: { name: string; required?: boolean }, i: number) => + f.required !== links.fields[i].required + ); + if (!changed) { + console.log(" = intake_links submission_counters already optional"); + return; + } + + await pb.collections.update(links.id, { fields }); + console.log(" + intake_links submission_counters now optional"); +} + +/** + * The activity row for an anonymous public submission has no actor by + * definition. The route omits the field rather than sending an empty + * string (which the relation would reject). Making `actor` optional in + * the schema lets the route write that row cleanly. + */ +async function ensureActivityActorOptional(pb: PocketBase): Promise { + const activity = await pb.collections.getOne("activity"); + const fields = activity.fields.map((f: { name: string; required?: boolean }) => { + if (f.name === "actor" && f.required !== false) { + return { ...f, required: false }; + } + return f; + }); + + const changed = fields.some( + (f: { name: string; required?: boolean }, i: number) => + f.required !== activity.fields[i].required + ); + if (!changed) { + console.log(" = activity.actor already optional"); + return; + } + + await pb.collections.update(activity.id, { fields }); + console.log(" + activity.actor now optional"); +} + +/** + * `active: false` fails the same PocketBase quirk as `0` does for number + * fields and `""` does for relations — `required: true` rejects the falsy + * value. The owner can create an already-revoked link, so the field has to + * accept both values at insert time. Every code path that creates a link + * sets `active: true` explicitly. + */ +async function ensureIntakeLinkActiveOptional(pb: PocketBase): Promise { + const links = await pb.collections.getOne("intake_links"); + const fields = links.fields.map((f: { name: string; required?: boolean }) => { + if (f.name === "active" && f.required !== false) { + return { ...f, required: false }; + } + return f; + }); + + const changed = fields.some( + (f: { name: string; required?: boolean }, i: number) => + f.required !== links.fields[i].required + ); + if (!changed) { + console.log(" = intake_links.active already optional"); + return; + } + + await pb.collections.update(links.id, { fields }); + console.log(" + intake_links.active now optional"); +} + async function ensureProjectKindField(pb: PocketBase): Promise { const projects = await pb.collections.getOne("projects"); const have = new Set(projects.fields.map((f: { name: string }) => f.name)); @@ -984,14 +1073,14 @@ async function main(): Promise { { name: "submission_count", type: "number", - required: true, + required: false, onlyInt: true, min: 0, }, { name: "submission_count_today", type: "number", - required: true, + required: false, onlyInt: true, min: 0, }, @@ -1255,6 +1344,19 @@ async function main(): Promise { // Extend cards.created_by_role to include "anonymous" for the public route. await ensureCardsCreatedByRoleValues(pb); + // PocketBase rejects inserts to intake_links unless the counter fields are + // optional — `required: true` + `min: 0` is contradictory there. + await ensureIntakeLinkCountersOptional(pb); + + // The public route writes activity rows with no actor (the submitter is + // anonymous); the actor field must therefore be optional. + await ensureActivityActorOptional(pb); + + // Same PocketBase quirk as submission_count: `required: true` on a bool + // field rejects `false` as blank. The owner can mint an already-revoked + // link, so the field has to be optional. + await ensureIntakeLinkActiveOptional(pb); + console.log("Done."); } diff --git a/src/app/api/public/intake/route.ts b/src/app/api/public/intake/route.ts index f066ce1..f649d9c 100644 --- a/src/app/api/public/intake/route.ts +++ b/src/app/api/public/intake/route.ts @@ -125,10 +125,9 @@ export async function POST(request: Request): Promise { await pb.collection("activity").create({ project: link.project, card: card.id, - // Empty actor on purpose — the schema requires the field, but the submitter - // is anonymous by construction. The card detail page distinguishes - // anonymous submissions in the UI rather than via the actor field. - actor: "", + // No actor — the submitter is anonymous by construction. The card detail + // page distinguishes anonymous submissions via the + // card_submitted_anonymously action rather than the actor field. action: "card_submitted_anonymously", field: "", old_value: "",