From ddf5026e50daa0fff82de9efd951f1bca37face6 Mon Sep 17 00:00:00 2001 From: NKoelblen Date: Fri, 8 May 2026 16:38:52 +0200 Subject: [PATCH 1/5] feat: implement testimony feature with GraphQL schema, resolver, and database migration Co-authored-by: Copilot --- migrations/202605081622.ts | 36 +++++++ src/graphql/graphqlContext.ts | 3 + src/graphql/graphqlSchema.ts | 12 +++ src/graphql/resolvers/testimonyResolver.ts | 114 +++++++++++++++++++++ src/graphql/schemas/testimonySchema.ts | 28 +++++ src/repositories/TestimonyRepository.ts | 65 ++++++++++++ src/types/testimonyTypes.ts | 8 ++ 7 files changed, 266 insertions(+) create mode 100644 migrations/202605081622.ts create mode 100644 src/graphql/resolvers/testimonyResolver.ts create mode 100644 src/graphql/schemas/testimonySchema.ts create mode 100644 src/repositories/TestimonyRepository.ts create mode 100644 src/types/testimonyTypes.ts diff --git a/migrations/202605081622.ts b/migrations/202605081622.ts new file mode 100644 index 0000000..97a19ac --- /dev/null +++ b/migrations/202605081622.ts @@ -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) { + 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) { + const conn = await pool.getConnection(); + try { + await conn.query("DROP TABLE IF EXISTS testimony"); + } finally { + conn.release(); + } +} diff --git a/src/graphql/graphqlContext.ts b/src/graphql/graphqlContext.ts index 28cbd30..b60ad80 100644 --- a/src/graphql/graphqlContext.ts +++ b/src/graphql/graphqlContext.ts @@ -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. @@ -45,6 +46,7 @@ export function getGraphqlContext({ coworkerRepo: CoworkerRepository; projectRepo: ProjectRepository; mediaRepo: MediaRepository; + testimonyRepo: TestimonyRepository; } { return { user, @@ -56,5 +58,6 @@ export function getGraphqlContext({ coworkerRepo: new CoworkerRepository(pool), projectRepo: new ProjectRepository(pool), mediaRepo: new MediaRepository(pool), + testimonyRepo: new TestimonyRepository(pool), }; } diff --git a/src/graphql/graphqlSchema.ts b/src/graphql/graphqlSchema.ts index 8248a3d..73a3c4c 100644 --- a/src/graphql/graphqlSchema.ts +++ b/src/graphql/graphqlSchema.ts @@ -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. @@ -80,6 +87,8 @@ export function getSchema() { ${projectInputs} ${settingsTypes} ${contactTypes} + ${testimonyTypes} + ${testimonyInputs} type Query { ${accountQueries} ${categoryQueries} @@ -89,6 +98,7 @@ export function getSchema() { ${projectQueries} ${mediaQueries} ${settingsQueries} + ${testimonyQueries} } type Mutation { ${authMutations} @@ -101,6 +111,7 @@ export function getSchema() { ${mediaMutations} ${settingsMutations} ${contactMutation} + ${testimonyMutations} } `); } @@ -121,5 +132,6 @@ export function getRoot() { ...mediaResolver, ...settingsResolver, ...contactResolver, + ...testimonyResolver, }; } diff --git a/src/graphql/resolvers/testimonyResolver.ts b/src/graphql/resolvers/testimonyResolver.ts new file mode 100644 index 0000000..cf85e19 --- /dev/null +++ b/src/graphql/resolvers/testimonyResolver.ts @@ -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} Un tableau de témoignages récupérés de la base de données. + */ + testimonies: async ( + _args: Record, + context: { testimonyRepo: TestimonyRepository }, + ): Promise => { + 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} 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 }, + context: { + user: jwt.JwtPayload | null; + testimonyRepo: TestimonyRepository; + }, + ): Promise => { + 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} 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> }, + context: { + user: jwt.JwtPayload | null; + testimonyRepo: TestimonyRepository; + }, + ): Promise => { + 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} 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 => { + 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; diff --git a/src/graphql/schemas/testimonySchema.ts b/src/graphql/schemas/testimonySchema.ts new file mode 100644 index 0000000..4c252fc --- /dev/null +++ b/src/graphql/schemas/testimonySchema.ts @@ -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! +`; diff --git a/src/repositories/TestimonyRepository.ts b/src/repositories/TestimonyRepository.ts new file mode 100644 index 0000000..e51de6d --- /dev/null +++ b/src/repositories/TestimonyRepository.ts @@ -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} 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 { + return withConnection(this.pool, (conn) => + conn.query( + `SELECT id, name, company, content, created_at AS createdAt FROM testimony ORDER BY created_at ASC`, + ), + ); + } + + /** + * 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 Les propriétés du témoignage à créer, à l'exception de l'ID qui est généré automatiquement. + * @returns {Promise} 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): Promise { + 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 Les propriétés du témoignage à mettre à jour, qui doivent inclure l'ID du témoignage à mettre à jour. + * @returns {Promise} 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): Promise { + 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, + }); + } +} diff --git a/src/types/testimonyTypes.ts b/src/types/testimonyTypes.ts new file mode 100644 index 0000000..a3c0aee --- /dev/null +++ b/src/types/testimonyTypes.ts @@ -0,0 +1,8 @@ +// Interface représentant un témoignage +export interface Testimony { + id: string; + name: string; + company?: string; + content: string; + createdAt: Date; +} From 46916934e38279a56d62dc4561f118c086a1cad6 Mon Sep 17 00:00:00 2001 From: NKoelblen Date: Fri, 8 May 2026 16:46:29 +0200 Subject: [PATCH 2/5] fix: change order of testimonies retrieval to descending by creation date Co-authored-by: Copilot --- src/repositories/TestimonyRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/TestimonyRepository.ts b/src/repositories/TestimonyRepository.ts index e51de6d..1ebfeae 100644 --- a/src/repositories/TestimonyRepository.ts +++ b/src/repositories/TestimonyRepository.ts @@ -16,7 +16,7 @@ export default class TestimonyRepository extends BaseRepository { async getAll(): Promise { return withConnection(this.pool, (conn) => conn.query( - `SELECT id, name, company, content, created_at AS createdAt FROM testimony ORDER BY created_at ASC`, + `SELECT id, name, company, content, created_at AS createdAt FROM testimony ORDER BY created_at DESC`, ), ); } From 572b190d1f291fbb83857602852926a6f72f80ad Mon Sep 17 00:00:00 2001 From: Onoko Date: Sun, 10 May 2026 12:10:00 +0200 Subject: [PATCH 3/5] feat: update testimony schema and repository to allow optional name and createdAt fields, and add insert column Co-authored-by: Copilot --- migrations/202605101156.ts | 35 ++++++++++++++++++++++ migrations/202605101159.ts | 29 ++++++++++++++++++ src/graphql/resolvers/testimonyResolver.ts | 3 +- src/graphql/schemas/testimonySchema.ts | 8 +++-- src/repositories/TestimonyRepository.ts | 6 ++-- src/types/testimonyTypes.ts | 5 ++-- 6 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 migrations/202605101156.ts create mode 100644 migrations/202605101159.ts diff --git a/migrations/202605101156.ts b/migrations/202605101156.ts new file mode 100644 index 0000000..7e51a30 --- /dev/null +++ b/migrations/202605101156.ts @@ -0,0 +1,35 @@ +import { Pool } from "mariadb/*"; +import { MigrationParams } from "umzug"; + +/** + * Migration pour définir une valeur par défaut vide sur la colonne "name" + * de la table "testimony" tout en la conservant NOT NULL. + */ + +export async function up({ context: pool }: MigrationParams) { + const conn = await pool.getConnection(); + try { + await conn.query( + `ALTER TABLE IF EXISTS testimony MODIFY COLUMN IF EXISTS name VARCHAR(255) NULL;`, + ); + await conn.query( + `ALTER TABLE IF EXISTS testimony MODIFY COLUMN IF EXISTS created_at VARCHAR(255) NULL;`, + ); + } finally { + conn.release(); + } +} + +export async function down({ context: pool }: MigrationParams) { + const conn = await pool.getConnection(); + try { + await conn.query( + `ALTER TABLE IF EXISTS testimony MODIFY COLUMN IF EXISTS name VARCHAR(255) NOT NULL;`, + ); + await conn.query( + `ALTER TABLE IF EXISTS testimony MODIFY COLUMN IF EXISTS created_at VARCHAR(255) NOT NULL;`, + ); + } finally { + conn.release(); + } +} diff --git a/migrations/202605101159.ts b/migrations/202605101159.ts new file mode 100644 index 0000000..c97013f --- /dev/null +++ b/migrations/202605101159.ts @@ -0,0 +1,29 @@ +import { Pool } from "mariadb/*"; +import { MigrationParams } from "umzug"; + +/** + * Migration pour définir une valeur par défaut vide sur la colonne "name" + * de la table "testimony" tout en la conservant NOT NULL. + */ + +export async function up({ context: pool }: MigrationParams) { + const conn = await pool.getConnection(); + try { + await conn.query( + "ALTER TABLE IF EXISTS testimony ADD COLUMN IF NOT EXISTS `insert` BOOLEAN;", + ); + } finally { + conn.release(); + } +} + +export async function down({ context: pool }: MigrationParams) { + const conn = await pool.getConnection(); + try { + await conn.query( + "ALTER TABLE IF EXISTS testimony DROP COLUMN IF EXISTS `insert`;", + ); + } finally { + conn.release(); + } +} diff --git a/src/graphql/resolvers/testimonyResolver.ts b/src/graphql/resolvers/testimonyResolver.ts index cf85e19..1bce0aa 100644 --- a/src/graphql/resolvers/testimonyResolver.ts +++ b/src/graphql/resolvers/testimonyResolver.ts @@ -43,9 +43,8 @@ const testimonyResolver = { ): Promise => { 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.name) 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()); diff --git a/src/graphql/schemas/testimonySchema.ts b/src/graphql/schemas/testimonySchema.ts index 4c252fc..15428e1 100644 --- a/src/graphql/schemas/testimonySchema.ts +++ b/src/graphql/schemas/testimonySchema.ts @@ -2,18 +2,20 @@ export const testimonyTypes = ` type Testimony { id: ID! - name: String! + name: String company: String content: String! - createdAt: String! + createdAt: String + insert: Boolean } `; export const testimonyInputs = ` input TestimonyInput { - name: String! + name: String company: String content: String! createdAt: String + insert: Boolean } `; diff --git a/src/repositories/TestimonyRepository.ts b/src/repositories/TestimonyRepository.ts index 1ebfeae..024065f 100644 --- a/src/repositories/TestimonyRepository.ts +++ b/src/repositories/TestimonyRepository.ts @@ -16,7 +16,7 @@ export default class TestimonyRepository extends BaseRepository { async getAll(): Promise { return withConnection(this.pool, (conn) => conn.query( - `SELECT id, name, company, content, created_at AS createdAt FROM testimony ORDER BY created_at DESC`, + `SELECT id, name, company, content, created_at, \`insert\` FROM testimony ORDER BY created_at DESC`, ), ); } @@ -33,13 +33,14 @@ export default class TestimonyRepository extends BaseRepository { const id = this.generateId(); await withConnection(this.pool, (conn) => conn.query( - `INSERT INTO testimony (id, name, company, content, created_at) VALUES (?, ?, ?, ?, ?)`, + `INSERT INTO testimony (id, name, company, content, created_at, \`insert\`) VALUES (?, ?, ?, ?, ?, ?)`, [ id, testimony.name, testimony.company || null, testimony.content, testimony.createdAt || new Date().toISOString(), + testimony.insert || false, ], ), ); @@ -60,6 +61,7 @@ export default class TestimonyRepository extends BaseRepository { name: testimony.name || undefined, company: testimony.company || undefined, content: testimony.content || undefined, + insert: testimony.insert || undefined, }); } } diff --git a/src/types/testimonyTypes.ts b/src/types/testimonyTypes.ts index a3c0aee..f2806d1 100644 --- a/src/types/testimonyTypes.ts +++ b/src/types/testimonyTypes.ts @@ -1,8 +1,9 @@ // Interface représentant un témoignage export interface Testimony { id: string; - name: string; + name?: string; company?: string; content: string; - createdAt: Date; + createdAt?: Date; + insert?: boolean; } From 414b00ba07726e2f7b243f143d168c6299f68aa6 Mon Sep 17 00:00:00 2001 From: Onoko Date: Sun, 10 May 2026 12:34:21 +0200 Subject: [PATCH 4/5] feat: enhance update method to handle SQL conflict by using backticks for insert key Co-authored-by: Copilot --- src/repositories/TestimonyRepository.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/repositories/TestimonyRepository.ts b/src/repositories/TestimonyRepository.ts index 024065f..5977ecb 100644 --- a/src/repositories/TestimonyRepository.ts +++ b/src/repositories/TestimonyRepository.ts @@ -57,11 +57,15 @@ export default class TestimonyRepository extends BaseRepository { */ async update(testimony: Partial): Promise { if (!testimony.id) throw new Error("ID is required for update"); - return this.updateOne(testimony.id, { + // Utilise la clé '`insert`' pour éviter le conflit SQL + const updateData: Record = { name: testimony.name || undefined, company: testimony.company || undefined, content: testimony.content || undefined, - insert: testimony.insert || undefined, - }); + }; + if (typeof testimony.insert !== "undefined") { + updateData["`insert`"] = testimony.insert; + } + return this.updateOne(testimony.id, updateData); } } From 8bf59687ff11ee979e32245d479d5a37ecc25829 Mon Sep 17 00:00:00 2001 From: Onoko Date: Sun, 10 May 2026 17:33:34 +0200 Subject: [PATCH 5/5] feat: log retrieved testimonies and update SQL query to order by createdAt and name --- src/graphql/resolvers/testimonyResolver.ts | 4 +++- src/repositories/TestimonyRepository.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/graphql/resolvers/testimonyResolver.ts b/src/graphql/resolvers/testimonyResolver.ts index 1bce0aa..b1d4f7a 100644 --- a/src/graphql/resolvers/testimonyResolver.ts +++ b/src/graphql/resolvers/testimonyResolver.ts @@ -22,7 +22,9 @@ const testimonyResolver = { _args: Record, context: { testimonyRepo: TestimonyRepository }, ): Promise => { - return await context.testimonyRepo.getAll(); + const testimonies = await context.testimonyRepo.getAll(); + console.log("Retrieved testimonies:", testimonies); + return testimonies; }, /** diff --git a/src/repositories/TestimonyRepository.ts b/src/repositories/TestimonyRepository.ts index 5977ecb..666a600 100644 --- a/src/repositories/TestimonyRepository.ts +++ b/src/repositories/TestimonyRepository.ts @@ -16,7 +16,7 @@ export default class TestimonyRepository extends BaseRepository { async getAll(): Promise { return withConnection(this.pool, (conn) => conn.query( - `SELECT id, name, company, content, created_at, \`insert\` FROM testimony ORDER BY created_at DESC`, + `SELECT id, name, company, content, created_at AS createdAt, \`insert\` FROM testimony ORDER BY createdAt DESC, name DESC`, ), ); } @@ -62,6 +62,7 @@ export default class TestimonyRepository extends BaseRepository { name: testimony.name || undefined, company: testimony.company || undefined, content: testimony.content || undefined, + created_at: testimony.createdAt || undefined, }; if (typeof testimony.insert !== "undefined") { updateData["`insert`"] = testimony.insert;