Documented comprehensive implementation plans for the authentication system and database schema, including architecture, module structure, API integration, security measures, and GDPR compliance details.
488 lines
15 KiB
Markdown
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. |