import { pgTable, serial, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core'; /** * A design, and the editor state that goes with it. * * ── Why the document is `jsonb` and not columns ── * * A `Doc` belongs to the vendored editor, not to Officer. Upstream adds fields to it constantly — * `theme`, `platform` and `dynamicColor` all arrived after the first release — and every one of those * would be a migration here for a shape we do not own and cannot influence. * * So it is stored whole. Officer never reads inside it except to show a title, which means an upstream * field addition is a re-vendor rather than a schema change. The cost is honest: no querying by * anything inside a design, which nothing wants to do. * * ── Per user, and enforced here ── * * `userId` is on the row and every query filters by it. That is the multi-user answer being a property * of the schema rather than a filter somebody has to remember on each new endpoint — the same reasoning * `service_connections` uses, and the mistake `dashboards.id` made by being a global primary key fed by * `slugify(name)`, so that two members naming a thing the same way collide. */ export const m3eCanvasDocuments = pgTable( 'm3e_canvas_documents', { id: serial('id').primaryKey(), userId: integer('user_id').notNull(), /** Lifted out of the document so a list does not have to parse every design to draw itself. */ title: text('title').notNull().default('Untitled'), /** The whole upstream `Doc`. Officer does not look inside it. */ doc: jsonb('doc').notNull(), /** * Panel widths, favourites, which language — upstream's `UI_KEY` state. * * Separate from the document because it is not the work: losing it costs a person their panel * sizes, and losing a design costs them the design. Different things deserve different care. */ ui: jsonb('ui'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => ({ /* Ordinary, NOT unique — and it used to be `uniqueIndex(userId, title)`, which could not work. Every design starts life called "Untitled": `titleOf` returns it for a document with no title of its own, and the API defaults to it. So the first design saved, the second one collided, and the insert threw under a panel that could only say "Changes are not being saved." Renaming one design to match another did the same thing. Two designs may share a name. That is true of files, of notes and of dashboards, and the alternative — uniquifying the title on save — renames somebody's work behind their back to satisfy an index. Nothing here looks a design up BY title, so uniqueness bought nothing and cost the ordinary case. The lesson from `dashboards.id` that the note above cites is that a key must be scoped to its owner. `userId` on the row does that. Making the pair unique as well imported the collision it was citing, one account at a time. The columns are the list query: filter by user, newest first. */ byUser: index('m3e_canvas_documents_user_updated').on(table.userId, table.updatedAt), }), ); export type M3eCanvasDocument = typeof m3eCanvasDocuments.$inferSelect;