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
97 changes: 97 additions & 0 deletions migrations/202605111155.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Pool } from "mariadb/*";
import { MigrationParams } from "umzug";

/**
* Migration pour permettre plusieurs catégories par stack via une table de liaison.
* - Crée la table stack_category.
* - Migre les valeurs existantes de stack.category_id vers stack_category.
* - Ajoute un index pour les recherches par catégorie.
*/
export async function up({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(`
CREATE TABLE IF NOT EXISTS stack_category (
stack_id VARCHAR(255) NOT NULL,
category_id VARCHAR(255) NOT NULL,
PRIMARY KEY (stack_id, category_id),
FOREIGN KEY (stack_id) REFERENCES stack(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE
);
`);

await conn.query(`
INSERT IGNORE INTO stack_category (stack_id, category_id)
SELECT id, category_id
FROM stack
WHERE category_id IS NOT NULL;
`);

await conn.query(
`CREATE INDEX IF NOT EXISTS idx_stack_category_category_id ON stack_category(category_id)`,
);

const fkRows = await conn.query(
`SELECT CONSTRAINT_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'stack'
AND COLUMN_NAME = 'category_id'
AND REFERENCED_TABLE_NAME = 'category'`,
);
for (const row of fkRows as { CONSTRAINT_NAME: string }[]) {
await conn.query(
`ALTER TABLE stack DROP FOREIGN KEY \`${row.CONSTRAINT_NAME}\``,
);
}

await conn.query(
`ALTER TABLE IF EXISTS stack DROP COLUMN IF EXISTS category_id;`,
);

// Nettoyage défensif : si un index explicite subsiste, on le supprime.
await conn.query(`DROP INDEX IF EXISTS idx_stack_category_id ON stack`);
} finally {
conn.release();
}
}

export async function down({ context: pool }: MigrationParams<Pool>) {
const conn = await pool.getConnection();
try {
await conn.query(
`ALTER TABLE IF EXISTS stack ADD COLUMN IF NOT EXISTS category_id VARCHAR(255) NULL;`,
);

await conn.query(`
UPDATE stack s
LEFT JOIN (
SELECT stack_id, MIN(category_id) AS category_id
FROM stack_category
GROUP BY stack_id
) sc ON sc.stack_id = s.id
SET s.category_id = sc.category_id;
`);

await conn.query(
`CREATE INDEX IF NOT EXISTS idx_stack_category_id ON stack(category_id)`,
);

await conn
.query(
`
ALTER TABLE IF EXISTS stack
ADD CONSTRAINT fk_stack_category_id
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE SET NULL;
`,
)
.catch(() => undefined);

await conn.query(
`DROP INDEX IF EXISTS idx_stack_category_category_id ON stack_category`,
);
await conn.query("DROP TABLE IF EXISTS stack_category");
} finally {
conn.release();
}
}
7 changes: 5 additions & 2 deletions src/graphql/resolvers/stackResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ function sanitizeStackInput(input: Partial<Omit<Stack, "id">>) {
if (input.description) input.description = sanitizeString(input.description);
if (input.versions) input.versions = input.versions.map((v) => sanitizeString(v));
if (input.skills) input.skills = input.skills.map((s) => sanitizeString(s));
if (input.category && !validator.isUUID(input.category as string))
delete input.category;
if (input.categories) {
input.categories = input.categories.filter((category) =>
validator.isUUID(category as string),
);
}
}

// Résolveur GraphQL pour les opérations liées aux stacks
Expand Down
4 changes: 2 additions & 2 deletions src/graphql/schemas/stackSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const stackTypes = `
description: String
versions: [String!]
skills: [String!]
category: ID
categories: [ID!]
}
`;
export const stackInputs = `
Expand All @@ -17,7 +17,7 @@ export const stackInputs = `
description: String
versions: [String!]
skills: [String!]
category: ID
categories: [ID!]
}
`;

Expand Down
51 changes: 38 additions & 13 deletions src/repositories/StackRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export default class StackRepository extends BaseRepository {
async getAll(): Promise<Stack[]> {
return withConnection(this.pool, async (conn) => {
const rows = await conn.query(`
SELECT s.*, v.version, ss.skill
SELECT s.*, v.version, ss.skill, sc.category_id
FROM stack s
LEFT JOIN stack_version v ON v.stack_id = s.id
LEFT JOIN stack_skill ss ON ss.stack_id = s.id
LEFT JOIN stack_category sc ON sc.stack_id = s.id
ORDER BY s.label
`);
const stackMap = new Map();
Expand All @@ -36,16 +37,18 @@ export default class StackRepository extends BaseRepository {
description: row.description,
versions: [],
skills: [],
category: row.category_id,
categories: [],
});
}
if (row.version) stackMap.get(row.id).versions.push(row.version);
if (row.skill) stackMap.get(row.id).skills.push(row.skill);
if (row.category_id) stackMap.get(row.id).categories.push(row.category_id);
}
// Dédupliquer les versions et skills
for (const stack of stackMap.values()) {
stack.versions = Array.from(new Set(stack.versions));
stack.skills = Array.from(new Set(stack.skills));
stack.categories = Array.from(new Set(stack.categories));
}
return Array.from(stackMap.values());
});
Expand All @@ -62,14 +65,8 @@ export default class StackRepository extends BaseRepository {
const id = this.generateId();
await withTransaction(this.pool, async (conn) => {
await conn.query(
`INSERT INTO stack (id, label, icon_id, description, category_id) VALUES (?, ?, ?, ?, ?);`,
[
id,
stack.label,
stack.icon,
stack.description || null,
stack.category || null,
],
`INSERT INTO stack (id, label, icon_id, description) VALUES (?, ?, ?, ?);`,
[id, stack.label, stack.icon, stack.description || null],
);
if (stack.versions && stack.versions.length > 0) {
await conn.query(
Expand All @@ -83,6 +80,12 @@ export default class StackRepository extends BaseRepository {
stack.skills.flatMap((skill) => [id, skill]),
);
}
if (stack.categories && stack.categories.length > 0) {
await conn.query(
`INSERT INTO stack_category (stack_id, category_id) VALUES ${stack.categories.map(() => "(?, ?)").join(", ")};`,
stack.categories.flatMap((category) => [id, category]),
);
}
});
return true;
}
Expand All @@ -103,8 +106,6 @@ export default class StackRepository extends BaseRepository {
label: stack.label || undefined,
icon_id: stack.icon ? (stack.icon as string) : undefined,
description: stack.description,
category_id:
stack.category !== undefined ? stack.category || null : undefined,
});
if (set) {
await conn.query(`UPDATE stack SET ${set.sql} WHERE id = ?`, [
Expand Down Expand Up @@ -161,7 +162,31 @@ export default class StackRepository extends BaseRepository {
}
}

if (!set && !stack.versions && !stack.skills) {
if (stack.categories) {
const existingRows = await conn.query(
"SELECT category_id FROM stack_category WHERE stack_id = ?",
[stack.id],
);
const existing: string[] = existingRows.map(
(r: { category_id: string }) => r.category_id,
);
const toAdd = stack.categories.filter((c) => !existing.includes(c));
const toRemove = existing.filter((c) => !stack.categories!.includes(c));
if (toRemove.length) {
await conn.batch(
"DELETE FROM stack_category WHERE stack_id = ? AND category_id = ?",
toRemove.map((c) => [stack.id, c]),
);
}
if (toAdd.length) {
await conn.batch(
"INSERT INTO stack_category (stack_id, category_id) VALUES (?, ?)",
toAdd.map((c) => [stack.id, c]),
);
}
}

if (!set && !stack.versions && !stack.skills && !stack.categories) {
return false;
}
return true;
Expand Down
2 changes: 1 addition & 1 deletion src/types/stackTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export interface Stack {
description?: string;
versions: string[];
skills: string[];
category?: string | null;
categories?: string[];
}
Loading