chore(api): UUID partout pour les PK et FK
Convention dure : tous les identifiants applicatifs sont des UUID v4 générés par PG (default gen_random_uuid()), aucun increments/serial même pour les tables techniques. - CLAUDE.md → "Conventions techniques" : règle énoncée explicitement (anti-énumération, multi-tenant, génération côté client, dumps propres). - docs/tech/backend.md §4.0 : exemple de migration + raisons. - 4 migrations existantes réécrites en uuid (users, auth_access_tokens, organizations, alter users.organization_id). Les access tokens d'Adonis acceptent un tokenable_id uuid sans changement côté provider. - Transformers nettoyés : plus de String(id), les UUID sont déjà des string. - DB régénérée from scratch (migrations sont éditées avant tout déploiement, pas un cas où un autre dev a une DB en prod).
This commit is contained in:
parent
eeb4ce25b8
commit
1d3b6a3f8f
@ -124,6 +124,10 @@ Voir `/docs/decisions.md` pour le log complet avec rationale.
|
||||
|
||||
**Décisions cadres** : ADR-014 (stack), ADR-015 (monorepo), ADR-016 (PG Proxmox existant), ADR-017 (Bearer tokens), ADR-018 (MinIO existant).
|
||||
|
||||
### Conventions techniques (cross-cutting)
|
||||
|
||||
- **Identifiants : UUID partout.** Toutes les PK et FK applicatives sont des UUID v4 (PG `uuid` avec default `gen_random_uuid()`). **Jamais d'`increments`/`serial`**, même pour les tables internes (auth tokens, sessions, refresh tokens, etc.). Les UUID protègent de l'énumération, simplifient la fédération multi-tenant et évitent les fuites de volumes par incrément. Les transformers exposent les UUID directement en string — pas de cast nécessaire.
|
||||
|
||||
## Documents associés
|
||||
|
||||
| Fichier | Rôle |
|
||||
|
||||
@ -5,9 +5,8 @@ export default class OrganizationTransformer extends BaseTransformer<Organizatio
|
||||
toObject() {
|
||||
const o = this.resource
|
||||
return {
|
||||
// IDs sérialisés en string pour rester aligné sur le contrat SPA
|
||||
// (cf. packages/shared/src/types/user.ts).
|
||||
id: String(o.id),
|
||||
// UUID natif (cf. CLAUDE.md → Conventions techniques).
|
||||
id: o.id,
|
||||
name: o.name,
|
||||
siret: o.siret,
|
||||
monthlyVolumeBucket: o.monthlyVolumeBucket,
|
||||
|
||||
@ -5,11 +5,11 @@ export default class UserTransformer extends BaseTransformer<User> {
|
||||
toObject() {
|
||||
const u = this.resource
|
||||
return {
|
||||
// IDs sérialisés en string — cf. packages/shared/src/types/user.ts
|
||||
id: String(u.id),
|
||||
// id et organizationId sont des UUID (cf. CLAUDE.md → Conventions techniques).
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
fullName: u.fullName,
|
||||
organizationId: u.organizationId !== null ? String(u.organizationId) : null,
|
||||
organizationId: u.organizationId,
|
||||
signature: u.signature,
|
||||
initials: u.initials,
|
||||
createdAt: u.createdAt.toISO()!,
|
||||
|
||||
@ -5,7 +5,9 @@ export default class extends BaseSchema {
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id').notNullable()
|
||||
// UUID v4 généré par PG (gen_random_uuid() est built-in depuis PG 13).
|
||||
// Convention projet : tous les ID sont des UUID, jamais des increments.
|
||||
table.uuid('id').primary().notNullable().defaultTo(this.raw('gen_random_uuid()'))
|
||||
table.string('full_name').nullable()
|
||||
table.string('email', 254).notNullable().unique()
|
||||
table.string('password').notNullable()
|
||||
|
||||
@ -5,11 +5,12 @@ export default class extends BaseSchema {
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id')
|
||||
// PK uuid (cf. convention projet : pas d'increments).
|
||||
table.uuid('id').primary().notNullable().defaultTo(this.raw('gen_random_uuid()'))
|
||||
// FK vers users — type aligné sur users.id (uuid).
|
||||
table
|
||||
.integer('tokenable_id')
|
||||
.uuid('tokenable_id')
|
||||
.notNullable()
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('CASCADE')
|
||||
|
||||
@ -5,7 +5,8 @@ export default class extends BaseSchema {
|
||||
|
||||
async up() {
|
||||
this.schema.createTable(this.tableName, (table) => {
|
||||
table.increments('id').notNullable()
|
||||
// UUID v4 (cf. convention projet).
|
||||
table.uuid('id').primary().notNullable().defaultTo(this.raw('gen_random_uuid()'))
|
||||
table.string('name', 120).notNullable().defaultTo('')
|
||||
table.string('siret', 14).nullable()
|
||||
table.string('monthly_volume_bucket', 20).nullable()
|
||||
|
||||
@ -5,12 +5,11 @@ export default class extends BaseSchema {
|
||||
|
||||
async up() {
|
||||
this.schema.alterTable(this.tableName, (table) => {
|
||||
// FK vers organizations. Nullable pour permettre la création d'un
|
||||
// FK uuid vers organizations. Nullable pour permettre la création d'un
|
||||
// utilisateur sans org (cas très limite, surtout V1) — en pratique
|
||||
// le signup crée toujours une org puis l'user dans la même tx.
|
||||
table
|
||||
.integer('organization_id')
|
||||
.unsigned()
|
||||
.uuid('organization_id')
|
||||
.nullable()
|
||||
.references('id')
|
||||
.inTable('organizations')
|
||||
|
||||
@ -19,13 +19,13 @@ export class AuthAccessTokenSchema extends BaseModel {
|
||||
@column()
|
||||
declare hash: string
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
declare id: string
|
||||
@column.dateTime()
|
||||
declare lastUsedAt: DateTime | null
|
||||
@column()
|
||||
declare name: string | null
|
||||
@column()
|
||||
declare tokenableId: number
|
||||
declare tokenableId: string
|
||||
@column()
|
||||
declare type: string
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
@ -38,7 +38,7 @@ export class OrganizationSchema extends BaseModel {
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
declare id: string
|
||||
@column()
|
||||
declare monthlyVolumeBucket: string | null
|
||||
@column()
|
||||
@ -63,9 +63,9 @@ export class UserSchema extends BaseModel {
|
||||
@column()
|
||||
declare fullName: string | null
|
||||
@column({ isPrimary: true })
|
||||
declare id: number
|
||||
declare id: string
|
||||
@column()
|
||||
declare organizationId: number | null
|
||||
declare organizationId: string | null
|
||||
@column({ serializeAs: null })
|
||||
declare password: string
|
||||
@column()
|
||||
|
||||
@ -203,6 +203,30 @@ apps/api/
|
||||
|
||||
Le modèle est **scopé par `organization_id` partout** — V1 mono-user mais multi-tenant ready dès le schéma. Tous les Lucid queries doivent filtrer sur `organization_id` (en pratique : un middleware ou un scope global qui injecte ce filtre).
|
||||
|
||||
### 4.0 Identifiants — UUID partout
|
||||
|
||||
**Convention dure** : toutes les PK et FK applicatives sont des UUID (PG `uuid` avec default `gen_random_uuid()`). **Pas d'`increments`/`serial`**, y compris pour les tables techniques (`auth_access_tokens`, `refresh_tokens`, `import_batches`, etc.).
|
||||
|
||||
```ts
|
||||
// migration
|
||||
table.uuid('id').primary().notNullable().defaultTo(this.raw('gen_random_uuid()'))
|
||||
table.uuid('organization_id').notNullable().references('id').inTable('organizations').onDelete('CASCADE')
|
||||
```
|
||||
|
||||
```ts
|
||||
// model — id est typé string, le schema generator le déduit du type uuid
|
||||
@column({ isPrimary: true })
|
||||
declare id: string
|
||||
```
|
||||
|
||||
Côté transformers, on expose l'UUID tel quel (pas de cast `String(id)`).
|
||||
|
||||
**Pourquoi** :
|
||||
1. **Anti-énumération** : `/invoices/42` révèle qu'on en a au moins 42, ce qui peut leak des volumes business.
|
||||
2. **Fédération multi-tenant** : pas besoin d'un namespace par tenant pour éviter les collisions, l'UUID est globalement unique.
|
||||
3. **Génération côté client** : permet d'optimiste-créer un objet (UUID généré côté SPA) avant le round-trip API.
|
||||
4. **Pas de surprise sur les exports** : un dump CSV qui sort des `id=1,2,3` est un bug latent.
|
||||
|
||||
### 4.1 User
|
||||
|
||||
```ts
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user