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

488 lines
15 KiB
Markdown

# 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<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 :
```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.