brief-20/docs/implementation/DATABASE_SCHEMA_PLAN.md
Avnyr cf292de428 docs: update documentation to reflect module completion and testing progress
Updated `PROJECT_STATUS.md` with completed modules (`auth`, `groups`, `tags`) and unit testing progress, including marked tests for controllers and services as done. Added logical and conceptual database models (`DATABASE_SCHEMA_PLAN.md`) and revised implementation statuses in `IMPLEMENTATION_GUIDE.md`.
2025-05-15 20:57:59 +02:00

16 KiB

Plan du Schéma de Base de Données

Ce document détaille le plan du schéma de base de données pour l'application de création de groupes, utilisant DrizzleORM avec PostgreSQL.

1. Vue d'Ensemble

Le schéma de base de données est conçu pour supporter les fonctionnalités suivantes :

  • Gestion des utilisateurs et authentification
  • Création et gestion de projets
  • Gestion des personnes et de leurs attributs
  • Création et gestion de groupes
  • Système de tags pour catégoriser les personnes et les projets

1.1 Modèle Conceptuel de Données (MCD)

Le MCD représente les entités principales et leurs relations à un niveau conceptuel.

erDiagram
    USER ||--o{ PROJECT : "possède"
    USER ||--o{ PROJECT_COLLABORATOR : "collabore sur"
    PROJECT ||--o{ PERSON : "contient"
    PROJECT ||--o{ GROUP : "organise"
    PROJECT ||--o{ PROJECT_COLLABORATOR : "a des collaborateurs"
    PROJECT }o--o{ TAG : "est catégorisé par"
    PERSON }o--o{ GROUP : "appartient à"
    PERSON }o--o{ TAG : "est catégorisé par"

    USER {
        uuid id PK
        string githubId
        string name
        string avatar
        string role
        datetime gdprTimestamp
    }

    PROJECT {
        uuid id PK
        string name
        string description
        json settings
        uuid ownerId FK
        boolean isPublic
    }

    PERSON {
        uuid id PK
        string name
        string email
        int technicalLevel
        string gender
        json attributes
        uuid projectId FK
    }

    GROUP {
        uuid id PK
        string name
        string description
        json settings
        uuid projectId FK
    }

    TAG {
        uuid id PK
        string name
        string description
        string color
        enum type
    }

    PROJECT_COLLABORATOR {
        uuid projectId FK
        uuid userId FK
    }

1.2 Modèle Logique de Données (MLD)

Le MLD représente la structure de la base de données avec toutes les tables, y compris les tables de jonction pour les relations many-to-many.

erDiagram
    users ||--o{ projects : "owns"
    users ||--o{ project_collaborators : "collaborates on"
    projects ||--o{ persons : "contains"
    projects ||--o{ groups : "organizes"
    projects ||--o{ project_collaborators : "has collaborators"
    projects ||--o{ project_to_tag : "is categorized by"
    persons ||--o{ person_to_group : "belongs to"
    persons ||--o{ person_to_tag : "is categorized by"
    groups ||--o{ person_to_group : "contains"
    tags ||--o{ person_to_tag : "categorizes"
    tags ||--o{ project_to_tag : "categorizes"

    users {
        uuid id PK
        string github_id
        string name
        string avatar
        string role
        datetime gdpr_timestamp
        datetime created_at
        datetime updated_at
    }

    projects {
        uuid id PK
        string name
        string description
        json settings
        uuid owner_id FK
        boolean is_public
        datetime created_at
        datetime updated_at
    }

    persons {
        uuid id PK
        string name
        string email
        int technical_level
        string gender
        json attributes
        uuid project_id FK
        datetime created_at
        datetime updated_at
    }

    groups {
        uuid id PK
        string name
        string description
        json settings
        uuid project_id FK
        datetime created_at
        datetime updated_at
    }

    tags {
        uuid id PK
        string name
        string description
        string color
        enum type
        datetime created_at
        datetime updated_at
    }

    person_to_group {
        uuid id PK
        uuid person_id FK
        uuid group_id FK
        datetime created_at
    }

    person_to_tag {
        uuid id PK
        uuid person_id FK
        uuid tag_id FK
        datetime created_at
    }

    project_to_tag {
        uuid id PK
        uuid project_id FK
        uuid tag_id FK
        datetime created_at
    }

    project_collaborators {
        uuid id PK
        uuid project_id FK
        uuid user_id FK
        datetime created_at
    }

2. Tables Principales

2.1 Table users

Stocke les informations des utilisateurs de l'application.

// src/database/schema/users.ts
import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  githubId: varchar('github_id', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
  avatar: varchar('avatar', { length: 1024 }),
  role: varchar('role', { length: 50 }).notNull().default('USER'),
  gdprTimestamp: timestamp('gdpr_timestamp').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;

2.2 Table projects

Stocke les informations des projets créés par les utilisateurs.

// src/database/schema/projects.ts
import { pgTable, uuid, varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core';
import { users } from './users';

export const projects = pgTable('projects', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  description: text('description'),
  settings: jsonb('settings').notNull().default({}),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  isPublic: boolean('is_public').notNull().default(false),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export type Project = typeof projects.$inferSelect;
export type NewProject = typeof projects.$inferInsert;

2.3 Table persons

Stocke les informations des personnes qui seront placées dans les groupes.

// src/database/schema/persons.ts
import { pgTable, uuid, varchar, text, integer, timestamp, jsonb } from 'drizzle-orm/pg-core';
import { projects } from './projects';

export const persons = pgTable('persons', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }),
  technicalLevel: integer('technical_level'),
  gender: varchar('gender', { length: 50 }),
  attributes: jsonb('attributes').notNull().default({}),
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export type Person = typeof persons.$inferSelect;
export type NewPerson = typeof persons.$inferInsert;

2.4 Table groups

Stocke les informations des groupes créés dans le cadre d'un projet.

// src/database/schema/groups.ts
import { pgTable, uuid, varchar, text, timestamp, jsonb } from 'drizzle-orm/pg-core';
import { projects } from './projects';

export const groups = pgTable('groups', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  description: text('description'),
  settings: jsonb('settings').notNull().default({}),
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export type Group = typeof groups.$inferSelect;
export type NewGroup = typeof groups.$inferInsert;

2.5 Table tags

Stocke les tags qui peuvent être associés aux personnes et aux projets.

// src/database/schema/tags.ts
import { pgTable, uuid, varchar, text, timestamp, pgEnum } from 'drizzle-orm/pg-core';

export const tagTypeEnum = pgEnum('tag_type', ['PROJECT', 'PERSON']);

export const tags = pgTable('tags', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  description: text('description'),
  color: varchar('color', { length: 50 }),
  type: tagTypeEnum('type').notNull(),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export type Tag = typeof tags.$inferSelect;
export type NewTag = typeof tags.$inferInsert;

3. Tables de Relations

3.1 Table personToGroup

Relation many-to-many entre les personnes et les groupes.

// src/database/schema/personToGroup.ts
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { groups } from './groups';

export const personToGroup = pgTable('person_to_group', {
  personId: uuid('person_id').notNull().references(() => persons.id, { onDelete: 'cascade' }),
  groupId: uuid('group_id').notNull().references(() => groups.id, { onDelete: 'cascade' }),
}, (t) => ({
  pk: primaryKey({ columns: [t.personId, t.groupId] }),
}));

3.2 Table personToTag

Relation many-to-many entre les personnes et les tags.

// src/database/schema/personToTag.ts
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
import { persons } from './persons';
import { tags } from './tags';

export const personToTag = pgTable('person_to_tag', {
  personId: uuid('person_id').notNull().references(() => persons.id, { onDelete: 'cascade' }),
  tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
}, (t) => ({
  pk: primaryKey({ columns: [t.personId, t.tagId] }),
}));

3.3 Table projectToTag

Relation many-to-many entre les projets et les tags.

// src/database/schema/projectToTag.ts
import { pgTable, uuid, primaryKey } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { tags } from './tags';

export const projectToTag = pgTable('project_to_tag', {
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  tagId: uuid('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
}, (t) => ({
  pk: primaryKey({ columns: [t.projectId, t.tagId] }),
}));

3.4 Table projectCollaborators

Relation many-to-many entre les projets et les utilisateurs pour la collaboration.

// src/database/schema/projectCollaborators.ts
import { pgTable, uuid, primaryKey, varchar } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { users } from './users';

export const projectCollaborators = pgTable('project_collaborators', {
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  role: varchar('role', { length: 50 }).notNull().default('VIEWER'),
}, (t) => ({
  pk: primaryKey({ columns: [t.projectId, t.userId] }),
}));

4. Fichier d'Index

Fichier qui exporte toutes les tables pour faciliter leur utilisation.

// src/database/schema/index.ts
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 './projectCollaborators';

5. Configuration de DrizzleORM

5.1 Module de Base de Données

// src/database/database.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';

@Global()
@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        const connectionString = configService.get<string>('DATABASE_URL');
        const pool = new Pool({ connectionString });
        return drizzle(pool, { schema });
      },
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

6. Migrations

6.1 Configuration des Migrations

// drizzle.config.ts
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,
  },
} satisfies Config;

6.2 Script de Migration

// src/database/migrations/migrate.ts
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();

async function main() {
  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!', err);
  process.exit(1);
});

7. Optimisations

7.1 Indexation

Pour optimiser les performances des requêtes fréquentes, nous ajouterons des index sur les colonnes suivantes :

// Exemple d'ajout d'index sur la table persons
import { pgTable, uuid, varchar, text, integer, timestamp, jsonb, index } from 'drizzle-orm/pg-core';
import { projects } from './projects';

export const persons = pgTable('persons', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }),
  technicalLevel: integer('technical_level'),
  gender: varchar('gender', { length: 50 }),
  attributes: jsonb('attributes').notNull().default({}),
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
}, (table) => ({
  nameIdx: index('person_name_idx').on(table.name),
  projectIdIdx: index('person_project_id_idx').on(table.projectId),
  technicalLevelIdx: index('person_technical_level_idx').on(table.technicalLevel),
}));

7.2 Types de Données Optimisés

Utilisation de types de données optimisés pour réduire l'espace de stockage et améliorer les performances :

  • uuid pour les identifiants uniques
  • varchar avec longueur limitée pour les chaînes de caractères
  • jsonb pour les données structurées flexibles
  • timestamp pour les dates et heures

7.3 Contraintes d'Intégrité

Utilisation de contraintes d'intégrité référentielle pour garantir la cohérence des données :

  • Clés primaires sur toutes les tables
  • Clés étrangères avec actions en cascade pour la suppression
  • Contraintes d'unicité sur les colonnes appropriées

8. Requêtes Communes

8.1 Récupération d'un Projet avec ses Personnes et Groupes

// Exemple de requête pour récupérer un projet avec ses personnes et groupes
const getProjectWithPersonsAndGroups = async (db, projectId) => {
  const project = await db.query.projects.findFirst({
    where: (projects, { eq }) => eq(projects.id, projectId),
    with: {
      persons: true,
      groups: {
        with: {
          persons: true,
        },
      },
    },
  });

  return project;
};

8.2 Récupération des Personnes avec leurs Tags

// Exemple de requête pour récupérer les personnes avec leurs tags
const getPersonsWithTags = async (db, projectId) => {
  const persons = await db.query.persons.findMany({
    where: (persons, { eq }) => eq(persons.projectId, projectId),
    with: {
      tags: {
        columns: {
          name: true,
          color: true,
        },
      },
    },
  });

  return persons;
};

9. Conclusion

Ce schéma de base de données fournit une structure solide pour l'application de création de groupes, avec une conception qui prend en compte les performances, la flexibilité et l'intégrité des données. Les relations entre les entités sont clairement définies, et les types de données sont optimisés pour les besoins de l'application.

L'utilisation de DrizzleORM permet une intégration transparente avec NestJS et offre une expérience de développement type-safe, facilitant la maintenance et l'évolution du schéma au fil du temps.