brief-20/DATABASE_SCHEMA_PLAN.md
Avnyr ef934a8599 docs: add implementation plans for authentication and database schema
Documented comprehensive implementation plans for the authentication system and database schema, including architecture, module structure, API integration, security measures, and GDPR compliance details.
2025-05-15 13:10:00 +02:00

15 KiB

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

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

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

export const gender = pgEnum('gender', ['MALE', 'FEMALE', 'NON_BINARY']);

1.4 Enum oralEaseLevel

export const oralEaseLevel = pgEnum('oralEaseLevel', ['SHY', 'RESERVED', 'COMFORTABLE']);

1.5 Table persons

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

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

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

1.8 Table tags

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)

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)

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)

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

// 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

// 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 :

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 :

{
  "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 :

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 :

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<string>('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 :

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.