feat(db): hierarchy record class schema + witnesses (contract 1, M4-1a) (#1459)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
This commit was merged in pull request #1459.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* drizzle-kit reads this file directly (avoids CJS/ESM extension issues).
|
||||
*/
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
pgTable,
|
||||
pgEnum,
|
||||
@@ -13,6 +14,8 @@ import {
|
||||
jsonb,
|
||||
index,
|
||||
uniqueIndex,
|
||||
unique,
|
||||
check,
|
||||
real,
|
||||
integer,
|
||||
bigint,
|
||||
@@ -1048,3 +1051,104 @@ export const federationEnrollmentTokens = pgTable('federation_enrollment_tokens'
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
// ─── Hierarchy (tenancy/authorization structure record class) ────────────────
|
||||
// Contract: docs/requirements/hierarchy-schema.md (D2, ratified 2026-08-27).
|
||||
// Five tables: companies → estates → platform_projects → workspaces, plus
|
||||
// hierarchy_grants. Class rows carry parentage, naming, grant, and
|
||||
// audit-linkage data only — the column sets below are exhaustive (§2.7) and
|
||||
// witnessed against information_schema (§6.2). No owner_id: ownership is the
|
||||
// grant structure (§4.4). All writes flow through the Gateway hierarchy
|
||||
// command family only (§5.1), enforced by the writer-coverage assertion
|
||||
// (§6.3b) — do not add writers outside that allowlist.
|
||||
|
||||
export const companies = pgTable('companies', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull().unique(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const estates = pgTable(
|
||||
'estates',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id, { onDelete: 'restrict' }),
|
||||
},
|
||||
(t) => [unique('estates_company_slug_uniq').on(t.companyId, t.slug)],
|
||||
);
|
||||
|
||||
export const platformProjects = pgTable(
|
||||
'platform_projects',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull(),
|
||||
estateId: uuid('estate_id')
|
||||
.notNull()
|
||||
.references(() => estates.id, { onDelete: 'restrict' }),
|
||||
},
|
||||
(t) => [unique('platform_projects_estate_slug_uniq').on(t.estateId, t.slug)],
|
||||
);
|
||||
|
||||
export const workspaces = pgTable(
|
||||
'workspaces',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull(),
|
||||
platformProjectId: uuid('platform_project_id')
|
||||
.notNull()
|
||||
.references(() => platformProjects.id, { onDelete: 'restrict' }),
|
||||
},
|
||||
(t) => [unique('workspaces_platform_project_slug_uniq').on(t.platformProjectId, t.slug)],
|
||||
);
|
||||
|
||||
export const hierarchyGrants = pgTable(
|
||||
'hierarchy_grants',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
// Subject: exactly one of user/team (CHECK below). Principal FKs are
|
||||
// RESTRICT until a deletion-and-retention contract rules otherwise (§3.3).
|
||||
userId: text('user_id').references(() => users.id, { onDelete: 'restrict' }),
|
||||
teamId: uuid('team_id').references(() => teams.id, { onDelete: 'restrict' }),
|
||||
// Target: exactly one of the three grantable levels (CHECK below).
|
||||
// Target FKs CASCADE — the one permitted cascade in the class (§3.3);
|
||||
// cascaded grant deletions are audited by the command family (§5.2).
|
||||
companyId: uuid('company_id').references(() => companies.id, { onDelete: 'cascade' }),
|
||||
estateId: uuid('estate_id').references(() => estates.id, { onDelete: 'cascade' }),
|
||||
platformProjectId: uuid('platform_project_id').references(() => platformProjects.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
// Role vocabulary and its CHECK constraint are contract 2 §2 (M4-2).
|
||||
role: text('role').notNull(),
|
||||
grantedBy: text('granted_by')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'restrict' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check('hierarchy_grants_subject_check', sql`num_nonnulls(user_id, team_id) = 1`),
|
||||
check(
|
||||
'hierarchy_grants_target_check',
|
||||
sql`num_nonnulls(company_id, estate_id, platform_project_id) = 1`,
|
||||
),
|
||||
// At most one grant per (subject, target, role) across all six
|
||||
// subject×target forms — NULLS NOT DISTINCT so nullable columns
|
||||
// participate (§3.2).
|
||||
unique('hierarchy_grants_subject_target_role_uniq')
|
||||
.on(t.userId, t.teamId, t.companyId, t.estateId, t.platformProjectId, t.role)
|
||||
.nullsNotDistinct(),
|
||||
index('hierarchy_grants_company_id_idx').on(t.companyId),
|
||||
index('hierarchy_grants_estate_id_idx').on(t.estateId),
|
||||
index('hierarchy_grants_platform_project_id_idx').on(t.platformProjectId),
|
||||
index('hierarchy_grants_user_id_idx').on(t.userId),
|
||||
index('hierarchy_grants_team_id_idx').on(t.teamId),
|
||||
index('hierarchy_grants_granted_by_idx').on(t.grantedBy),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user