feat(structures): add Region/Delegation/Structure schema + seed (ADR-0027 PR 1) #228
Reference in New Issue
Block a user
Delete Branch "feat/adr-0027-pr1-org-hierarchy-schema"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
First implementation PR for ADR-0027 (
Region/Delegation/Structureportal-side organisational hierarchy). Schema + seed + drift-gate extension only — no consumer code yet (thePrismaScopeResolverthat dereferencesStructure.codefromUserScope.valuelives in ADR-0026 PR 2, which depends on this PR landing first).Independent of ADR-0026 PR 1 at the schema level — both can ship in parallel; ADR-0026 PR 2 depends on both.
What lands
apps/portal-bff/prisma/schema.prismaRegion(INSEE code PK + name + delegations[]).Delegation(dept code PK + name + regionCode FK + structures[]).Structure(portal code PK + name + kind discriminator + nullable unique finess/siret/codePaie + nullable delegationCode FK). All in thepublicschema; matches the ADR-0027 schema sketch verbatim.apps/portal-bff/prisma/migrations/20260526143000_add_org_hierarchy/migration.sqlCHECK ("kind" IN (...))constraint mirroringSTRUCTURE_KINDS. Indexes (FK columns + kind + uniques). Inline INSERT seed: Région Nouvelle-Aquitaine (75), Délégation Gironde (33), structures0330800013+0330800021(médico-social, FINESS = code) +siege(APF national, no delegation/finess).apps/portal-bff/src/structures/structure-kind.tsSTRUCTURE_KINDS = [medico_social, antenne, dispositif, entreprise_adaptee, mouvement, administratif, siege] as const.type StructureKindderived from the union,isStructureKindtype guard.apps/portal-bff/src/structures/structure-kind.spec.tsscripts/check-catalogue-drift.mjsextractStructureKinds(path)+findStructureKindViolationsInFile(path, validKinds, sourceText?). Property-literal scanner: detectskind: 'X'in object literals, restricted to files that import fromstructure-kind.ts(cheap text pre-filter —kindis a common property name on unrelated objects and we'd false-positive everywhere otherwise). Integrated intoscanWorkspace. Error-message formatter switched on callee shape (@Foo('x')for decorators,kind: 'x'for property literals). Closing hint updated to reference both ADR-0025 and ADR-0027 catalogue locations.scripts/check-catalogue-drift.spec.mjsstructure-kind.tsalongsideauthorization.types.ts. New tests: extract STRUCTURE_KINDS, throw on missing constant, skip files without the import, no-violation on catalogue values, flag off-catalogue values, skip non-literal initialisers, line/column tracking, scanWorkspace aggregation (decorator + Structure.kind together), scanWorkspace exposes the structureKinds set. 22 tests total, all passing.Defense in depth
Three layers stack for
Structure.kind, deliberately:StructureKind— compile-time check at every typed assignment.CHECKconstraint in the migration — runtime enforcement at INSERT / UPDATE, defends raw SQL / casts / untrusted API input.scripts/check-catalogue-drift.mjs— CI gate asserting everykind: 'X'literal in structure-context files is in the catalogue.The
Privilege/FunctionalRolecatalogues from ADR-0025 only have layer 1 + layer 3 (no DB enforcement — those values aren't persisted as schema-checked columns). ADR-0027'sStructure.kindis persisted, so layer 2 was practical to add — bulletproof against any code path that bypasses the type system.Seed scope (and what's deliberately NOT in it)
Just what the 19 test-tenant personas reference per
notes/test-tenant-role-assignments.md:Region75 Nouvelle-Aquitaine (only region the personas exercise)Delegation33 Gironde (only delegation)Structure0330800013 (APF Bordeaux, medico-social, FINESS = code)Structure0330800021 (Complexe Mérignac, medico-social)Structuresiege(APF national, kind=siege, no FK to a delegation, no FINESS)No placeholder
entreprise_adaptee,antenne, ordispositifrow — those kinds are valid per the catalogue but the test tenant doesn't exercise them, and adding gold-plate seed data would be misleading ("what is this row used for?"). The full APF inventory lands with ADR-0029's cascade sync; this seed is superseded (not extended) by that sync.Notes for the reviewer
Usermodel. The currentschema.prismaalready has aUsermodel — but it's the ADR-0020 user-directory cache (Entraoidas PK, written byUserDirectoryService.recordSignIn). ADR-0026 PR 1 introduces a differentUser(UUID PK, FK toPerson,lastSignInAt). Out of scope here — ADR-0027 PR 1 doesn't touchUser. Flagging now so ADR-0026 PR 1 can plan the migration path (likely: rename the existingUsertoUserDirectoryEntryor fold it into the new Person + User pair).init_audit_schema,users_directory). Timestamp20260526143000chosen so it sorts after the existing20260514192014_users_directory.@@schema("public")required on every new model because the audit log usesmultiSchema(per ADR-0013) — the public/audit split is configured at the datasource.@Foo('x'), property-literal violations print askind: 'x'to match the offending code shape.pnpm ci:checkimpact expected at the bff level beyond the new spec;pnpm ci:catalogue-driftcontinues to report clean (catalogues: 4 privileges, 24 roles, 7 structure kinds).Test plan
node scripts/check-catalogue-drift.mjs— clean (4 / 24 / 7).node --test scripts/check-catalogue-drift.spec.mjs— 22 tests passing.pnpm exec prettier --checkclean on the touched files.pnpm prisma migrate devapplies the migration cleanly against a freshinfra/local/dev.compose.ymlpostgres.pnpm prisma studioshows the seeded Region / Delegation / Structure rows.pnpm exec nx test portal-bffruns the newstructure-kind.spec.tsgreen.Structuremodel shape (kind discriminator, nullable unique columns, FK to Delegation), the inline seed values vsnotes/test-tenant-role-assignments.md, the drift-gate property-literal scanner's restriction to files importing fromstructure-kind.ts.What's next
Per ADR-0027 §"Phasing":
Person/User/UserScopeschema +PersonAndUserProvisioner+ drift gate extension forPerson.source+ updatedPrincipalBuilder. Independent of (1), can ship in parallel. Needs to resolve the existing-User-name collision.PrismaScopeResolverreplacingStubScopeResolver+/admin/users/:id/scopesadmin screen +prisma/seed.tspopulating the 19 personas'user_scopesrows pointing at this PR'sStructure.codevalues. Depends on both (1) and (2).Pole/Service/ per-source enrichment extensions to this PR's hierarchy.