Files
kanban/db/schema.ts
T
pastilhasandClaude Opus 5 6e54f223e6 Kanban — the todo board, extracted, over as many projects as you like
was `/todos` in the platform: a board, an API and a tree reader pointed at
`resolve(process.cwd(), 'todo')`. that is not a setting — it is the belief that
there is one project and it is the one the server happens to have been started in.

THE READER CAME ACROSS UNCHANGED except for that one line. the frontmatter parser
that never rewrites a body, the ordering, the index writer — all of it as it was,
and its 8 tests came too and still pass. everything that changed is that the
directory is now a parameter.

`writeIndex` no longer writes to `process.cwd()` either. that was the same
assumption wearing a different hat, and with two projects it would have had every
board overwriting one file. the map goes beside the folder it describes.

A PROJECT IS A NAME AND A PATH, and paths are confined to the owner's home by the
same three checks av-club uses — absolute, realpath BEFORE the containment test,
and `root` or `root + sep` rather than a bare startsWith. spelled out again rather
than cross-referenced, because the next person to add a path-taking route reads the
file they are in.

it stores NO ITEMS. one table, a list of folders. uninstalling with "delete its
data" ticked loses the list and never a line of anybody's work — which is the
property the markdown format was chosen for.

`/projects` is owner-only: pointing this at a directory is a statement about the
filesystem. moving a card is not, so a member granted `kanban` can work any board
they can see.

removing a project removes the ROW. the route says so in its response and the
button says so in its tooltip, because "delete" beside a folder full of somebody's
work should not be ambiguous.

README records the seam honestly: the platform keeps its own todo/ and its CLI, so
there are two copies of the reader and they will drift. the alternative was making
the platform's own backlog depend on a plugin being installed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 03:27:56 +01:00

67 lines
3.0 KiB
TypeScript

import { pgTable, serial, text, timestamp, integer, uniqueIndex } from 'drizzle-orm/pg-core';
/**
* The projects this board can show — one row per folder of todo files.
*
* ── Why this table exists at all ──
*
* The version of this in the platform read exactly one directory: `resolve(process.cwd(), 'todo')`,
* the platform's own. That is not a setting, it is the assumption that there is one project and it is
* the one the server happens to be running from. The moment somebody has a second repository with a
* `todo/` in it, there is nowhere to put it.
*
* So a project is a NAME and a PATH, and the board asks which one you are looking at.
*
* ── What is deliberately NOT here ──
*
* The items. They live on disk as markdown, one file each, and this table holds no copy of them —
* not a cache, not a mirror, not an index. That is the whole property the format was chosen for: the
* files are the truth, they are readable and diffable without Officer, and uninstalling this plugin
* with "delete its data" ticked loses a list of folders, never a line of anybody's work.
*
* A cache would be the thin end of it. If a board over a thousand items ever needs one, it belongs in
* memory or in the browser — not in a table that outlives the request and can disagree with the disk.
*/
export const kanbanProjects = pgTable(
'kanban_projects',
{
id: serial('id').primaryKey(),
/**
* Whose project it is.
*
* Not a foreign key onto `users`, deliberately: this plugin's schema is generated into the
* platform's barrel and a cross-schema reference would couple its migrations to the platform's.
* The routes scope every read and write by this column, which is where the enforcement belongs —
* see the note on `authorize` in api/router.ts.
*/
userId: integer('user_id').notNull(),
/** What the selector shows. The owner's words, not derived from the path. */
name: text('name').notNull(),
/**
* Absolute, and containing the theme folders directly — the directory that holds `<theme>/<item>.md`.
*
* Absolute for the same reason av-club's root is: a relative path resolves against a working
* directory the caller cannot see, and the old implementation's `process.cwd()` is exactly that
* mistake with the answer happening to be right on one machine.
*/
path: text('path').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
/*
One row per path per person.
`uniqueIndex`, never `unique().on(...)`: drizzle-kit mis-diffs named composite unique CONSTRAINTS
and re-creates them on every push, which stops `db:push` on a prompt nobody can answer
unattended. The platform's own databases/CLAUDE.md records this under "Composite keys".
*/
uniqueIndex('kanban_projects_user_path').on(table.userId, table.path),
],
);
export type KanbanProject = typeof kanbanProjects.$inferSelect;