Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions migrations/202605081622.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MigrationParams } from "umzug";
import { Pool } from "mariadb";

/**
* Migration pour créer la table "testimony" dans la base de données.
* La table "testimony" est utilisée pour stocker les témoignages des utilisateurs.
* La migration crée la table avec quatre colonnes : "id" (identifiant unique), "name" (nom de l'utilisateur), "company" (nom de l'entreprise) et "content" (contenu du témoignage).
* La colonne "id" est définie comme clé primaire pour garantir l'unicité des identifiants.
* La fonction "up" est exécutée lors de l'application de la migration, tandis que la fonction "down" est exécutée lors du rollback de la migration.
*/

export async function up({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(`
CREATE TABLE IF NOT EXISTS testimony (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
company VARCHAR(255) NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
} finally {
conn.release();
}
}

export async function down({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query("DROP TABLE IF EXISTS testimony");
} finally {
conn.release();
}
}
3 changes: 3 additions & 0 deletions src/graphql/graphqlContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SettingsRepository } from "../repositories/SettingsRepository";
import { Pool } from "mariadb/*";
import jwt from "jsonwebtoken";
import { MediaRepository } from "../repositories/MediaRepository";
import TestimonyRepository from "../repositories/TestimonyRepository";

/**
* Fonction pour créer le contexte GraphQL, qui sera passé à tous les résolveurs.
Expand Down Expand Up @@ -45,6 +46,7 @@ export function getGraphqlContext({
coworkerRepo: CoworkerRepository;
projectRepo: ProjectRepository;
mediaRepo: MediaRepository;
testimonyRepo: TestimonyRepository;
} {
return {
user,
Expand All @@ -56,5 +58,6 @@ export function getGraphqlContext({
coworkerRepo: new CoworkerRepository(pool),
projectRepo: new ProjectRepository(pool),
mediaRepo: new MediaRepository(pool),
testimonyRepo: new TestimonyRepository(pool),
};
}
12 changes: 12 additions & 0 deletions src/graphql/graphqlSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ import {
import settingsResolver from "./resolvers/settingsResolver";
import { contactMutation, contactTypes } from "./schemas/contactSchema";
import contactResolver from "./resolvers/contactResolver";
import {
testimonyInputs,
testimonyMutations,
testimonyQueries,
testimonyTypes,
} from "./schemas/testimonySchema";
import testimonyResolver from "./resolvers/testimonyResolver";

/**
* Construit le schéma GraphQL en combinant les types, requêtes et mutations de tous les modules.
Expand All @@ -80,6 +87,8 @@ export function getSchema() {
${projectInputs}
${settingsTypes}
${contactTypes}
${testimonyTypes}
${testimonyInputs}
type Query {
${accountQueries}
${categoryQueries}
Expand All @@ -89,6 +98,7 @@ export function getSchema() {
${projectQueries}
${mediaQueries}
${settingsQueries}
${testimonyQueries}
}
type Mutation {
${authMutations}
Expand All @@ -101,6 +111,7 @@ export function getSchema() {
${mediaMutations}
${settingsMutations}
${contactMutation}
${testimonyMutations}
}
`);
}
Expand All @@ -121,5 +132,6 @@ export function getRoot() {
...mediaResolver,
...settingsResolver,
...contactResolver,
...testimonyResolver,
};
}
114 changes: 114 additions & 0 deletions src/graphql/resolvers/testimonyResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import jwt from "jsonwebtoken";
import {
isEmpty,
checkAuth,
validateId,
isValidDate,
} from "../../utils/validationUtils";
import { sanitizeString, sanitizeWysiwyg } from "../../utils/stringUtils";
import { Testimony } from "../../types/testimonyTypes";
import TestimonyRepository from "../../repositories/TestimonyRepository";

// Résolveur GraphQL pour les opérations liées aux témoignages
const testimonyResolver = {
/**
* Récupère tous les témoignages
* Appelle la méthode getAll du repository des témoignages pour récupérer tous les témoignages de la base de données.
* @param {Object} _args Les arguments de la requête, qui ne sont pas utilisés dans cette opération.
* @param {Object} context Le contexte de la requête, contenant le repository des témoignages.
* @returns {Promise<testimony[]>} Un tableau de témoignages récupérés de la base de données.
*/
testimonies: async (
_args: Record<string, never>,
context: { testimonyRepo: TestimonyRepository },
): Promise<Testimony[]> => {
return await context.testimonyRepo.getAll();
},

/**
* Crée un nouveau témoignage
* Vérifie que l'utilisateur est authentifié, puis appelle la méthode create du repository des témoignages pour créer un nouveau témoignage dans la base de données.
* Après la création, récupère et retourne le témoignage créé.
* @param {Object} _args Les arguments de la mutation, contenant les propriétés du témoignage à créer (sauf l'ID).
* @param {Object} context Le contexte de la requête, contenant les informations de l'utilisateur et le repository des témoignages.
* @returns {Promise<boolean>} Indique si la création du témoignage a réussi.
* @throws {Error} Une erreur si l'utilisateur n'est pas authentifié ou si le témoignage ne peut pas être trouvé après la création.
*/
createTestimony: async (
_args: { input: Omit<Testimony, "id"> },
context: {
user: jwt.JwtPayload | null;
testimonyRepo: TestimonyRepository;
},
): Promise<boolean> => {
checkAuth(context);
const input = { ..._args.input };
if (isEmpty(input.name)) throw new Error("Name is required");
if (isEmpty(input.content)) throw new Error("Content is required");
input.name = sanitizeString(input.name);
if (input.company) input.company = sanitizeString(input.company);
input.content = sanitizeWysiwyg(input.content);
input.createdAt = new Date(input.createdAt || Date.now());
if (!isValidDate(input.createdAt?.toISOString() ?? ""))
throw new Error("Invalid start date");
const result = await context.testimonyRepo.create(input);
if (!result) throw new Error("Failed to create testimony");
return result;
},

/**
* Met à jour un témoignage existant
* Vérifie que l'utilisateur est authentifié, puis appelle la méthode update du repository des témoignages pour mettre à jour les propriétés d'un témoignage existant dans la base de données.
* Après la mise à jour, récupère et retourne le témoignage mis à jour.
* @param {Object} _args Les arguments de la mutation, contenant les propriétés du témoignage à mettre à jour (doit inclure l'ID).
* @param {Object} context Le contexte de la requête, contenant les informations de l'utilisateur et le repository des témoignages.
* @returns {Promise<boolean>} Indique si la mise à jour du témoignage a réussi.
* @throws {Error} Une erreur si l'utilisateur n'est pas authentifié ou si le témoignage ne peut pas être trouvé après la mise à jour.
*/
updateTestimony: async (
_args: { id: string; input: Partial<Omit<Testimony, "id">> },
context: {
user: jwt.JwtPayload | null;
testimonyRepo: TestimonyRepository;
},
): Promise<boolean> => {
checkAuth(context);
validateId(_args.id);
const input = { ..._args.input, id: _args.id };
if (input.name) input.name = sanitizeString(input.name);
if (input.company) input.company = sanitizeString(input.company);
if (input.content) input.content = sanitizeWysiwyg(input.content);
if (input.createdAt) {
input.createdAt = new Date(input.createdAt);
if (!isValidDate(input.createdAt.toISOString()))
throw new Error("Invalid start date");
}
const result = await context.testimonyRepo.update(input);
if (!result) throw new Error("Failed to update testimony");
return result;
},

/**
* Supprime un témoignage existant
* Vérifie que l'utilisateur est authentifié, puis appelle la méthode delete du repository des témoignages pour supprimer un témoignage existant de la base de données.
* @param {Object} _args Les arguments de la mutation, contenant l'ID du témoignage à supprimer.
* @param {Object} context Le contexte de la requête, contenant les informations de l'utilisateur et le repository des témoignages.
* @returns {Promise<boolean>} Indique si la suppression du témoignage a réussi.
* @throws {Error} Une erreur si l'utilisateur n'est pas authentifié ou si le témoignage ne peut pas être trouvé pour la suppression.
*/
deleteTestimony: async (
_args: { id: string },
context: {
user: jwt.JwtPayload | null;
testimonyRepo: TestimonyRepository;
},
): Promise<boolean> => {
checkAuth(context);
validateId(_args.id);
const result = await context.testimonyRepo.delete(_args.id);
if (!result) throw new Error("Failed to delete testimony");
return result;
},
};

export default testimonyResolver;
28 changes: 28 additions & 0 deletions src/graphql/schemas/testimonySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Types GraphQL pour les témoignages
export const testimonyTypes = `
type Testimony {
id: ID!
name: String!
company: String
content: String!
createdAt: String!
}
`;
export const testimonyInputs = `
input TestimonyInput {
name: String!
company: String
content: String!
createdAt: String
}
`;

// Requête GraphQL pour les témoignages
export const testimonyQueries = `testimonies: [Testimony!]!`;

// Mutations GraphQL pour les témoignages
export const testimonyMutations = `
createTestimony(input: TestimonyInput): Boolean!
updateTestimony(id: ID!, input: TestimonyInput): Boolean!
deleteTestimony(id: ID!): Boolean!
`;
65 changes: 65 additions & 0 deletions src/repositories/TestimonyRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Testimony } from "../types/testimonyTypes";
import { withConnection } from "../database/dbHelpers";
import { BaseRepository } from "./BaseRepository";

// Repository pour les opérations liées aux témoignages dans la base de données
export default class TestimonyRepository extends BaseRepository {
protected readonly tableName = "testimony";

/**
* Récupère tous les témoignages de la base de données.
* La méthode exécute une requête SQL pour sélectionner tous les témoignages de la table "testimony" de la base de données, en récupérant tous les champs disponibles.
* Les résultats sont retournés sous forme d'un tableau d'objets Testimony, où chaque objet représente un témoignage avec ses propriétés correspondantes.
* @returns {Promise<Testimony[]>} Un tableau de témoignages récupérés de la base de données, avec toutes leurs propriétés.
* @throws {Error} Une erreur si la récupération des témoignages échoue pour une raison quelconque.
*/
async getAll(): Promise<Testimony[]> {
return withConnection(this.pool, (conn) =>
conn.query(
`SELECT id, name, company, content, created_at AS createdAt FROM testimony ORDER BY created_at DESC`,
),
);
}

/**
* Crée un nouveau témoignage dans la base de données en utilisant les propriétés fournies.
* La méthode génère un ID unique pour le nouveau témoignage, puis insère les données dans la table "testimony" de la base de données.
* Après l'insertion, la méthode retourne un booléen indiquant si la création a réussi.
* @param {Omit<Testimony, "id">} testimony Les propriétés du témoignage à créer, à l'exception de l'ID qui est généré automatiquement.
* @returns {Promise<boolean>} Une promesse qui se résout avec true lorsque la création est terminée, ou rejette une erreur si la création échoue.
* @throws {Error} Une erreur si la création échoue pour une raison quelconque.
*/
async create(testimony: Omit<Testimony, "id">): Promise<boolean> {
const id = this.generateId();
await withConnection(this.pool, (conn) =>
conn.query(
`INSERT INTO testimony (id, name, company, content, created_at) VALUES (?, ?, ?, ?, ?)`,
[
id,
testimony.name,
testimony.company || null,
testimony.content,
testimony.createdAt || new Date().toISOString(),
],
),
);
return true;
}

/**
* Met à jour un témoignage existant dans la base de données en fonction des propriétés fournies.
* La méthode vérifie que l'ID du témoignage est fourni, puis construit dynamiquement la requête SQL pour mettre à jour les champs spécifiés.
* Après l'exécution de la requête de mise à jour, la méthode retourne un booléen indiquant si la mise à jour a réussi.
* @param {Partial<Testimony>} testimony Les propriétés du témoignage à mettre à jour, qui doivent inclure l'ID du témoignage à mettre à jour.
* @returns {Promise<boolean>} Une promesse qui se résout avec true lorsque la mise à jour est terminée, ou rejette une erreur si l'ID n'est pas fourni ou si la mise à jour échoue.
* @throws {Error} Une erreur si l'ID du témoignage n'est pas fourni, ou si la mise à jour échoue pour une raison quelconque.
*/
async update(testimony: Partial<Testimony>): Promise<boolean> {
if (!testimony.id) throw new Error("ID is required for update");
return this.updateOne(testimony.id, {
name: testimony.name || undefined,
company: testimony.company || undefined,
content: testimony.content || undefined,
});
}
}
8 changes: 8 additions & 0 deletions src/types/testimonyTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Interface représentant un témoignage
export interface Testimony {
id: string;
name: string;
company?: string;
content: string;
createdAt: Date;
}
Loading