the officer shell: designs per user, and the handoff desk
manifest, schema, router and two panels. the editor is hosted and saving; the last step is not finished and the readme says so. designs are rows scoped per user, with userId in every WHERE rather than checked after the read — another person's design is 'not found', so the id space cannot be probed. the Doc is stored whole as jsonb because upstream adds fields to it constantly and none of them should be a migration here. the handoff is the reason the plugin exists and it is the part that does not work: /api/chat has NO session-creation route, a session is constructed by the browser opening a websocket, so a plugin has nothing to POST to. onExport returns false and the clipboard still runs, which is upstream's own behaviour — degraded, not broken. docs/handoff.md names the one platform change that would finish it, and argues it should be generic rather than ours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { pgTable, serial, text, integer, timestamp, jsonb, uniqueIndex } 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) => ({
|
||||
// Composite via uniqueIndex, never `unique().on(...)` — drizzle-kit mis-diffs named composite
|
||||
// unique CONSTRAINTS and re-creates them on every push. `databases/CLAUDE.md`, "Composite keys".
|
||||
byUser: uniqueIndex('m3e_canvas_documents_user_title').on(table.userId, table.title),
|
||||
}),
|
||||
);
|
||||
|
||||
export type M3eCanvasDocument = typeof m3eCanvasDocuments.$inferSelect;
|
||||
Reference in New Issue
Block a user