import { sql } from "drizzle-orm"; import { check, index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; /** * Core-owned storage for how the Ekklesia Dashboard *presents* installed modules. * * This is presentation configuration, and it is deliberately the only thing here. A group owns no * tables, no migrations, no routes, and no lifecycle; it never participates in dependency * resolution, discovery order, or module startup. **Modules stay flat** (ADR 0021): there is no * parent module, no child module, no owning module, and nothing in this schema is ever read by * anything that decides what a module may do or when it starts. Moving a module between groups * changes a row here and nothing else. * * Core stores this; it renders nothing. `surfaces/shell` decides what the arrangement looks like. * * Platform-global, because Ekklesia has no organization or tenant model to scope it to. When one * exists, these two tables gain a scope column; inventing a second notion of organization here to * anticipate it would be worse than adding a column later. * * Table and index names are explicit and namespaced exactly like `core/src/authorization/schema.ts`, * so ownership is unambiguous inside the shared public PostgreSQL schema. */ /** * A Dashboard navigation group — "Media", "Planning", "Sunday Production". * * `id` is an **opaque, stable key**, never displayed and never re-derived from `label`. Renaming a * group changes only `label`, so every placement pointing at it survives untouched; that invariant * is what keeps a rename from silently re-organizing the Dashboard. A slug is used rather than a * generated UUID purely so the configuration API and its tests stay legible — it carries no meaning * the platform reads. * * There is no parent group column. Nesting is deliberately out of scope, and leaving the shape flat * is what keeps it out. */ export const ekklesiaCoreDashboardGroup = pgTable( "ekklesia_core_dashboard_group", { id: text("id").primaryKey(), label: text("label").notNull(), sortOrder: integer("sort_order").notNull().default(0), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ekklesia_core_dashboard_group_order_idx").on(table.sortOrder, table.id), // Defense in depth beneath the application-layer identifier and label contracts, which never // produce either of these: an empty id would be an unaddressable group, and an empty or // whitespace-only label would render as a blank heading no administrator could find again. check("ekklesia_core_dashboard_group_id_check", sql`length(${table.id}) > 0`), check("ekklesia_core_dashboard_group_label_check", sql`length(btrim(${table.label})) > 0`), ], ); /** * Where one module sits in the Dashboard. One row per module at most — the primary key enforces it * — so a module can never be placed in two groups, which is the database-level expression of "one * level of grouping, and no module ownership graph." * * `group_id` is nullable and that is a real state, not a missing value: **ungrouped is a placement**, * chosen by an administrator who ordered a module outside every group. A module with no row at all * is a different state — never configured — and both render in the same ungrouped area (see * `../dashboard/navigation.js` for the exact ordering rule). * * `on delete set null` is what makes group deletion safe: deleting a group cannot leave a dangling * reference, and its modules become ungrouped rather than disappearing. That is enforced by the * database, not by application cleanup that could be skipped. * * `module_id` deliberately carries **no foreign key to any module**, because modules are not rows — * they are packages discovered on disk at startup. A placement for a module that is not installed * is therefore normal and is retained: uninstalling and reinstalling a module keeps its position * rather than silently resetting it. Nothing renders such a row (see `resolveDashboardNavigation`). */ export const ekklesiaCoreDashboardPlacement = pgTable( "ekklesia_core_dashboard_placement", { moduleId: text("module_id").primaryKey(), groupId: text("group_id").references(() => ekklesiaCoreDashboardGroup.id, { onDelete: "set null", }), sortOrder: integer("sort_order").notNull().default(0), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ekklesia_core_dashboard_placement_group_idx").on(table.groupId, table.sortOrder), check("ekklesia_core_dashboard_placement_module_check", sql`length(${table.moduleId}) > 0`), ], ); /** Explicit map passed to Drizzle, mirroring `authorizationSchema`'s explicit-mapping style. */ export const dashboardSchema = { dashboardGroup: ekklesiaCoreDashboardGroup, dashboardPlacement: ekklesiaCoreDashboardPlacement, } as const;