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`.
572 lines
16 KiB
Markdown
572 lines
16 KiB
Markdown
# 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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 :
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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.
|