feat(schema): intake_links and anonymous submitter role
Phase 8a Task 1. Adds the new intake_links collection (per-token public entry point, owner-only rules), extends cards.created_by_role with 'anonymous', and extends activity.action with 'card_submitted_anonymously'. Note: getOrCreateCollection is a no-op on existing collections, so the two existing-value extensions are applied by new/extended ensure functions (ensureCardsCreatedByRoleValues and ensureActivityActionValues respectively). Re-runs are idempotent.
This commit is contained in:
parent
3e868f910a
commit
c99be4d49e
2 changed files with 150 additions and 5 deletions
|
|
@ -525,11 +525,16 @@ async function ensureActivityActionValues(pb: PocketBase): Promise<void> {
|
|||
if (!field) {
|
||||
throw new Error("activity.action field missing — cannot extend values");
|
||||
}
|
||||
const additions = ["column_changed", "date_proposed", "date_proposal_resolved"];
|
||||
const additions = [
|
||||
"column_changed",
|
||||
"date_proposed",
|
||||
"date_proposal_resolved",
|
||||
"card_submitted_anonymously",
|
||||
];
|
||||
const existing = new Set(field.values ?? []);
|
||||
const missing = additions.filter((v) => !existing.has(v));
|
||||
if (missing.length === 0) {
|
||||
console.log(" = activity.action values already include puzzle actions");
|
||||
console.log(" = activity.action values already up to date");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -542,6 +547,39 @@ async function ensureActivityActionValues(pb: PocketBase): Promise<void> {
|
|||
console.log(` + activity.action values: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend `cards.created_by_role` to include `"anonymous"` so the public
|
||||
* intake route can produce Inbox cards without a session. The base set
|
||||
* `["owner", "client"]` was set at first collection creation; re-running
|
||||
* `getOrCreateCollection` on an existing `cards` row is a no-op, so the
|
||||
* value set has to be patched explicitly here. Safe to re-run: values
|
||||
* are not duplicated.
|
||||
*/
|
||||
async function ensureCardsCreatedByRoleValues(pb: PocketBase): Promise<void> {
|
||||
const cards = await pb.collections.getOne("cards");
|
||||
const field = cards.fields.find(
|
||||
(f: { name: string }) => f.name === "created_by_role"
|
||||
) as { values?: string[] } | undefined;
|
||||
if (!field) {
|
||||
throw new Error("cards.created_by_role field missing — cannot extend values");
|
||||
}
|
||||
const additions = ["anonymous"];
|
||||
const existing = new Set(field.values ?? []);
|
||||
const missing = additions.filter((v) => !existing.has(v));
|
||||
if (missing.length === 0) {
|
||||
console.log(" = cards.created_by_role values already include anonymous");
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = cards.fields.map((f: { name: string; values?: string[] }) => {
|
||||
if (f.name !== "created_by_role") return f;
|
||||
return { ...f, values: [...(f.values ?? []), ...missing] };
|
||||
});
|
||||
|
||||
await pb.collections.update(cards.id, { fields });
|
||||
console.log(` + cards.created_by_role values: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
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));
|
||||
|
|
@ -730,7 +768,7 @@ async function main(): Promise<void> {
|
|||
type: "select",
|
||||
required: true,
|
||||
maxSelect: 1,
|
||||
values: ["owner", "client"],
|
||||
values: ["owner", "client", "anonymous"],
|
||||
},
|
||||
{ name: "accepted", type: "bool", required: false },
|
||||
{ name: "start_date", type: "date", required: false },
|
||||
|
|
@ -864,6 +902,7 @@ async function main(): Promise<void> {
|
|||
"comment_added",
|
||||
"subtask_toggled",
|
||||
"card_archived",
|
||||
"card_submitted_anonymously",
|
||||
],
|
||||
},
|
||||
{ name: "field", type: "text", required: false, max: 60 },
|
||||
|
|
@ -902,6 +941,76 @@ async function main(): Promise<void> {
|
|||
],
|
||||
});
|
||||
|
||||
// 11. intake_links — public, per-token entry points to a project's intake.
|
||||
// Owner-only at the rule level: the public route reads and writes through
|
||||
// the superuser connection, so PocketBase rules are not what protects
|
||||
// them. They exist so a signed-in client manager cannot enumerate them.
|
||||
await getOrCreateCollection(pb, {
|
||||
name: "intake_links",
|
||||
type: "base",
|
||||
listRule: OWNER,
|
||||
viewRule: OWNER,
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: OWNER,
|
||||
fields: [
|
||||
{
|
||||
name: "project",
|
||||
type: "relation",
|
||||
required: true,
|
||||
collectionId: (await pb.collections.getOne("projects")).id,
|
||||
cascadeDelete: true,
|
||||
minSelect: 0,
|
||||
maxSelect: 1,
|
||||
},
|
||||
{ name: "label", type: "text", required: true, max: 80 },
|
||||
{
|
||||
name: "token",
|
||||
type: "text",
|
||||
required: true,
|
||||
max: 64,
|
||||
pattern: "^[A-Za-z0-9_-]{32}$",
|
||||
},
|
||||
{ name: "active", type: "bool", required: true },
|
||||
{ name: "expires_at", type: "date", required: false },
|
||||
{
|
||||
name: "max_submissions_per_day",
|
||||
type: "number",
|
||||
required: false,
|
||||
onlyInt: true,
|
||||
min: 0,
|
||||
max: 10000,
|
||||
},
|
||||
{
|
||||
name: "submission_count",
|
||||
type: "number",
|
||||
required: true,
|
||||
onlyInt: true,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: "submission_count_today",
|
||||
type: "number",
|
||||
required: true,
|
||||
onlyInt: true,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: "submission_count_today_date",
|
||||
type: "date",
|
||||
required: false,
|
||||
},
|
||||
{ name: "last_submission_at", type: "date", required: false },
|
||||
{ name: "notes", type: "text", required: false, max: 1000 },
|
||||
{ name: "created", type: "autodate", onCreate: true, onUpdate: false },
|
||||
{ name: "updated", type: "autodate", onCreate: true, onUpdate: true },
|
||||
],
|
||||
indexes: [
|
||||
"CREATE UNIQUE INDEX idx_intake_links_token ON intake_links (token)",
|
||||
"CREATE INDEX idx_intake_links_project ON intake_links (project, active)",
|
||||
],
|
||||
});
|
||||
|
||||
console.log("Applying API rules...");
|
||||
|
||||
await applyRules(pb, "clients", {
|
||||
|
|
@ -960,6 +1069,14 @@ async function main(): Promise<void> {
|
|||
deleteRule: null,
|
||||
});
|
||||
|
||||
await applyRules(pb, "intake_links", {
|
||||
listRule: OWNER,
|
||||
viewRule: OWNER,
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: OWNER,
|
||||
});
|
||||
|
||||
// 9. commits — ingest target. Created via webhook/poll/backfill only; never
|
||||
// from a browser.
|
||||
await getOrCreateCollection(pb, {
|
||||
|
|
@ -1135,6 +1252,9 @@ async function main(): Promise<void> {
|
|||
// Extend activity.action with the three new puzzle actions.
|
||||
await ensureActivityActionValues(pb);
|
||||
|
||||
// Extend cards.created_by_role to include "anonymous" for the public route.
|
||||
await ensureCardsCreatedByRoleValues(pb);
|
||||
|
||||
console.log("Done.");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export type Role = "owner" | "client_manager";
|
|||
export type Priority = "critical" | "high" | "medium" | "low";
|
||||
export type ProjectStatus = "active" | "paused" | "completed" | "archived";
|
||||
export type ProjectKind = "project" | "prototype";
|
||||
export type CreatedByRole = "owner" | "client";
|
||||
export type CreatedByRole = "owner" | "client" | "anonymous";
|
||||
|
||||
export type ActivityAction =
|
||||
| "card_created"
|
||||
|
|
@ -18,7 +18,8 @@ export type ActivityAction =
|
|||
| "card_accepted"
|
||||
| "comment_added"
|
||||
| "subtask_toggled"
|
||||
| "card_archived";
|
||||
| "card_archived"
|
||||
| "card_submitted_anonymously";
|
||||
|
||||
interface BaseRecord {
|
||||
id: string;
|
||||
|
|
@ -171,4 +172,28 @@ export interface ActivityRecord extends Omit<BaseRecord, "updated"> {
|
|||
field: string;
|
||||
old_value: string;
|
||||
new_value: string;
|
||||
}
|
||||
|
||||
export interface IntakeLinkRecord extends BaseRecord {
|
||||
project: string;
|
||||
/** The owner's name for this link, e.g. "Acme — till phone". */
|
||||
label: string;
|
||||
/** 32-char URL-safe token; never sent to a client manager. */
|
||||
token: string;
|
||||
/** false stops every submission immediately, regardless of token shape. */
|
||||
active: boolean;
|
||||
/** Past this date the link behaves as if active = false. "" when unset. */
|
||||
expires_at: string;
|
||||
/** Per-local-day cap. 0 means "no submissions". Undefined → default in code. */
|
||||
max_submissions_per_day: number;
|
||||
/** Total successful submissions since creation. */
|
||||
submission_count: number;
|
||||
/** Counter reset lazily by `submission_count_today_date` not matching today. */
|
||||
submission_count_today: number;
|
||||
/** ISO date (YYYY-MM-DD) of the day this counter applies to. */
|
||||
submission_count_today_date: string;
|
||||
/** ISO timestamp of the most recent submission. "" when none yet. */
|
||||
last_submission_at: string;
|
||||
/** Owner's private note. Never leaves the admin page. */
|
||||
notes: string;
|
||||
}
|
||||
Loading…
Reference in a new issue