# Plan d'Implémentation du Schéma de Base de Données Ce document détaille le plan d'implémentation du schéma de base de données pour l'application de création de groupes, basé sur le modèle de données spécifié dans le cahier des charges. ## 1. Schéma DrizzleORM Le schéma sera implémenté en utilisant DrizzleORM avec PostgreSQL. Voici la définition des tables et leurs relations. ### 1.1 Table `users` ```typescript import { pgTable, uuid, varchar, text, timestamp, jsonb } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: uuid('id').primaryKey().defaultRandom(), // UUIDv7 pour l'ordre chronologique name: varchar('name', { length: 100 }).notNull(), avatar: text('avatar'), // URL depuis l'API Github githubId: varchar('githubId', { length: 50 }).notNull().unique(), gdprTimestamp: timestamp('gdprTimestamp', { withTimezone: true }), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull(), metadata: jsonb('metadata').default({}) }, (table) => { return { githubIdIdx: index('githubId_idx').on(table.githubId), createdAtIdx: index('createdAt_idx').on(table.createdAt) }; }); ``` ### 1.2 Table `projects` ```typescript import { pgTable, uuid, varchar, text, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core'; import { users } from './users'; export const projects = pgTable('projects', { id: uuid('id').primaryKey().defaultRandom(), name: varchar('name', { length: 100 }).notNull(), description: text('description'), ownerId: uuid('ownerId').notNull().references(() => users.id, { onDelete: 'cascade' }), settings: jsonb('settings').default({}), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { nameIdx: index('name_idx').on(table.name), ownerIdIdx: index('ownerId_idx').on(table.ownerId), createdAtIdx: index('createdAt_idx').on(table.createdAt) }; }); ``` ### 1.3 Enum `gender` ```typescript export const gender = pgEnum('gender', ['MALE', 'FEMALE', 'NON_BINARY']); ``` ### 1.4 Enum `oralEaseLevel` ```typescript export const oralEaseLevel = pgEnum('oralEaseLevel', ['SHY', 'RESERVED', 'COMFORTABLE']); ``` ### 1.5 Table `persons` ```typescript import { pgTable, uuid, varchar, smallint, boolean, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core'; import { projects } from './projects'; import { gender, oralEaseLevel } from './enums'; export const persons = pgTable('persons', { id: uuid('id').primaryKey().defaultRandom(), firstName: varchar('firstName', { length: 50 }).notNull(), lastName: varchar('lastName', { length: 50 }).notNull(), gender: gender('gender').notNull(), technicalLevel: smallint('technicalLevel').notNull(), hasTechnicalTraining: boolean('hasTechnicalTraining').notNull().default(false), frenchSpeakingLevel: smallint('frenchSpeakingLevel').notNull(), oralEaseLevel: oralEaseLevel('oralEaseLevel').notNull(), age: smallint('age'), projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }), attributes: jsonb('attributes').default({}), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { firstNameIdx: index('firstName_idx').on(table.firstName), lastNameIdx: index('lastName_idx').on(table.lastName), projectIdIdx: index('projectId_idx').on(table.projectId), nameCompositeIdx: index('name_composite_idx').on(table.firstName, table.lastName) }; }); ``` ### 1.6 Table `groups` ```typescript import { pgTable, uuid, varchar, timestamp, jsonb, foreignKey } from 'drizzle-orm/pg-core'; import { projects } from './projects'; export const groups = pgTable('groups', { id: uuid('id').primaryKey().defaultRandom(), name: varchar('name', { length: 100 }).notNull(), projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }), metadata: jsonb('metadata').default({}), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { nameIdx: index('name_idx').on(table.name), projectIdIdx: index('projectId_idx').on(table.projectId) }; }); ``` ### 1.7 Enum `tagType` ```typescript export const tagType = pgEnum('tagType', ['PROJECT', 'PERSON']); ``` ### 1.8 Table `tags` ```typescript import { pgTable, uuid, varchar, timestamp, foreignKey } from 'drizzle-orm/pg-core'; import { tagType } from './enums'; export const tags = pgTable('tags', { id: uuid('id').primaryKey().defaultRandom(), name: varchar('name', { length: 50 }).notNull(), color: varchar('color', { length: 7 }).notNull(), type: tagType('type').notNull(), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updatedAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { nameIdx: index('name_idx').on(table.name), typeIdx: index('type_idx').on(table.type) }; }); ``` ### 1.9 Table `personToGroup` (Relation) ```typescript import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core'; import { persons } from './persons'; import { groups } from './groups'; export const personToGroup = pgTable('person_to_group', { id: uuid('id').primaryKey().defaultRandom(), personId: uuid('personId').notNull().references(() => persons.id, { onDelete: 'cascade' }), groupId: uuid('groupId').notNull().references(() => groups.id, { onDelete: 'cascade' }), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { personIdIdx: index('personId_idx').on(table.personId), groupIdIdx: index('groupId_idx').on(table.groupId), personGroupUniqueIdx: uniqueIndex('person_group_unique_idx').on(table.personId, table.groupId) }; }); ``` ### 1.10 Table `personToTag` (Relation) ```typescript import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core'; import { persons } from './persons'; import { tags } from './tags'; export const personToTag = pgTable('person_to_tag', { id: uuid('id').primaryKey().defaultRandom(), personId: uuid('personId').notNull().references(() => persons.id, { onDelete: 'cascade' }), tagId: uuid('tagId').notNull().references(() => tags.id, { onDelete: 'cascade' }), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { personIdIdx: index('personId_idx').on(table.personId), tagIdIdx: index('tagId_idx').on(table.tagId), personTagUniqueIdx: uniqueIndex('person_tag_unique_idx').on(table.personId, table.tagId) }; }); ``` ### 1.11 Table `projectToTag` (Relation) ```typescript import { pgTable, uuid, timestamp, foreignKey } from 'drizzle-orm/pg-core'; import { projects } from './projects'; import { tags } from './tags'; export const projectToTag = pgTable('project_to_tag', { id: uuid('id').primaryKey().defaultRandom(), projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }), tagId: uuid('tagId').notNull().references(() => tags.id, { onDelete: 'cascade' }), createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull() }, (table) => { return { projectIdIdx: index('projectId_idx').on(table.projectId), tagIdIdx: index('tagId_idx').on(table.tagId), projectTagUniqueIdx: uniqueIndex('project_tag_unique_idx').on(table.projectId, table.tagId) }; }); ``` ## 2. Relations et Types ### 2.1 Relations ```typescript // Définition des relations pour les requêtes export const relations = { users: { projects: one(users, { fields: [users.id], references: [projects.ownerId], }), }, projects: { owner: many(projects, { fields: [projects.ownerId], references: [users.id], }), persons: one(projects, { fields: [projects.id], references: [persons.projectId], }), groups: one(projects, { fields: [projects.id], references: [groups.projectId], }), tags: many(projects, { through: { table: projectToTag, fields: [projectToTag.projectId, projectToTag.tagId], references: [projects.id, tags.id], }, }), }, persons: { project: many(persons, { fields: [persons.projectId], references: [projects.id], }), group: many(persons, { through: { table: personToGroup, fields: [personToGroup.personId, personToGroup.groupId], references: [persons.id, groups.id], }, }), tags: many(persons, { through: { table: personToTag, fields: [personToTag.personId, personToTag.tagId], references: [persons.id, tags.id], }, }), }, groups: { project: many(groups, { fields: [groups.projectId], references: [projects.id], }), persons: many(groups, { through: { table: personToGroup, fields: [personToGroup.groupId, personToGroup.personId], references: [groups.id, persons.id], }, }), }, tags: { persons: many(tags, { through: { table: personToTag, fields: [personToTag.tagId, personToTag.personId], references: [tags.id, persons.id], }, }), projects: many(tags, { through: { table: projectToTag, fields: [projectToTag.tagId, projectToTag.projectId], references: [tags.id, projects.id], }, }), }, }; ``` ### 2.2 Types Inférés ```typescript // Types inférés à partir du schéma export type User = typeof users.$inferSelect; export type NewUser = typeof users.$inferInsert; export type Project = typeof projects.$inferSelect; export type NewProject = typeof projects.$inferInsert; export type Person = typeof persons.$inferSelect; export type NewPerson = typeof persons.$inferInsert; export type Group = typeof groups.$inferSelect; export type NewGroup = typeof groups.$inferInsert; export type Tag = typeof tags.$inferSelect; export type NewTag = typeof tags.$inferInsert; export type PersonToGroup = typeof personToGroup.$inferSelect; export type NewPersonToGroup = typeof personToGroup.$inferInsert; export type PersonToTag = typeof personToTag.$inferSelect; export type NewPersonToTag = typeof personToTag.$inferInsert; export type ProjectToTag = typeof projectToTag.$inferSelect; export type NewProjectToTag = typeof projectToTag.$inferInsert; ``` ## 3. Migrations ### 3.1 Configuration de Drizzle Kit Créer un fichier `drizzle.config.ts` à la racine du projet backend : ```typescript import type { Config } from 'drizzle-kit'; import * as dotenv from 'dotenv'; dotenv.config(); export default { schema: './src/database/schema/*.ts', out: './src/database/migrations', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/groupmaker', }, verbose: true, strict: true, } satisfies Config; ``` ### 3.2 Scripts pour les Migrations Ajouter les scripts suivants au `package.json` du backend : ```json { "scripts": { "db:generate": "drizzle-kit generate:pg", "db:migrate": "ts-node src/database/migrate.ts", "db:studio": "drizzle-kit studio" } } ``` ### 3.3 Script de Migration Créer un fichier `src/database/migrate.ts` : ```typescript import { drizzle } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; import { Pool } from 'pg'; import * as dotenv from 'dotenv'; dotenv.config(); const main = async () => { const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); const db = drizzle(pool); console.log('Running migrations...'); await migrate(db, { migrationsFolder: './src/database/migrations' }); console.log('Migrations completed successfully'); await pool.end(); }; main().catch((err) => { console.error('Migration failed'); console.error(err); process.exit(1); }); ``` ## 4. Module de Base de Données ### 4.1 Module Database Créer un fichier `src/database/database.module.ts` : ```typescript import { Module, Global } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { Pool } from 'pg'; import { drizzle } from 'drizzle-orm/node-postgres'; import * as schema from './schema'; export const DATABASE_POOL = 'DATABASE_POOL'; export const DRIZZLE = 'DRIZZLE'; @Global() @Module({ imports: [ConfigModule], providers: [ { provide: DATABASE_POOL, inject: [ConfigService], useFactory: async (configService: ConfigService) => { const pool = new Pool({ connectionString: configService.get('DATABASE_URL'), }); // Test the connection const client = await pool.connect(); try { await client.query('SELECT NOW()'); console.log('Database connection established successfully'); } finally { client.release(); } return pool; }, }, { provide: DRIZZLE, inject: [DATABASE_POOL], useFactory: (pool: Pool) => { return drizzle(pool, { schema }); }, }, ], exports: [DATABASE_POOL, DRIZZLE], }) export class DatabaseModule {} ``` ### 4.2 Index des Schémas Créer un fichier `src/database/schema/index.ts` pour exporter tous les schémas : ```typescript export * from './users'; export * from './projects'; export * from './persons'; export * from './groups'; export * from './tags'; export * from './personToGroup'; export * from './personToTag'; export * from './projectToTag'; export * from './enums'; export * from './relations'; ``` ## 5. Stratégie d'Indexation Les index suivants seront créés pour optimiser les performances des requêtes : 1. **Index Primaires** : Sur toutes les clés primaires (UUIDv7) 2. **Index Secondaires** : Sur les clés étrangères pour accélérer les jointures 3. **Index Composites** : Sur les champs fréquemment utilisés ensemble dans les requêtes 4. **Index Partiels** : Pour les requêtes filtrées fréquentes 5. **Index de Texte** : Pour les recherches sur les champs textuels (noms, descriptions) ## 6. Optimisation des Formats de Données Les types de données PostgreSQL seront optimisés pour chaque cas d'usage : 1. **UUID** : Pour les identifiants (UUIDv7 pour l'ordre chronologique) 2. **JSONB** : Pour les données flexibles et semi-structurées (metadata, settings, attributes) 3. **ENUM** : Types PostgreSQL natifs pour les valeurs fixes (gender, oralEaseLevel, tagType) 4. **VARCHAR** : Avec contraintes pour les chaînes de caractères variables 5. **TIMESTAMP WITH TIME ZONE** : Pour les dates avec gestion des fuseaux horaires 6. **SMALLINT** : Pour les valeurs numériques entières de petite taille (technicalLevel, age) 7. **BOOLEAN** : Pour les valeurs booléennes (hasTechnicalTraining) Ces optimisations permettront d'améliorer les performances des requêtes, de réduire l'empreinte mémoire et d'assurer l'intégrité des données.