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 `/.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;