fix(intake-links): three PocketBase quirks the schema shipped with
The schema the plan specified had three places where a PocketBase quirk blocked normal use of intake_links / activity rows. None of them surfaced in Task 1 because the unit-level test there only checked rule application, not inserts. 1. intake_links.submission_count and submission_count_today were required: true with min: 0. PocketBase treats 0 as blank for required number fields, so every insert 400'd. Made optional. 2. intake_links.active was required: true, which rejects false as blank (the same quirk). The owner can mint an already-revoked link (the test exercises this); made optional. 3. activity.actor was a required relation. The public route writes a card_submitted_anonymously row for a submitter who has no user account; empty string is not a valid relation. Made optional and dropped actor: '' from the route.
This commit is contained in:
parent
3d8bc12ed8
commit
c11d378483
2 changed files with 107 additions and 6 deletions
|
|
@ -580,6 +580,95 @@ async function ensureCardsCreatedByRoleValues(pb: PocketBase): Promise<void> {
|
|||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
|||
{
|
||||
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<void> {
|
|||
// 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.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,10 +125,9 @@ export async function POST(request: Request): Promise<Response> {
|
|||
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: "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue