AI Implementation feature(893): Admin Area General Settings and Categories 1.11 (#33)
This commit was merged in pull request #33.
This commit is contained in:
@@ -85,6 +85,22 @@ export class DatabaseInitService implements OnApplicationBootstrap {
|
||||
if (categoryCount < 6) {
|
||||
this.logger.warn(`Expected at least 6 seeded categories, found ${categoryCount}`);
|
||||
}
|
||||
try {
|
||||
const systemRows: any[] = await this.dataSource.query(
|
||||
`SELECT system_key, abbreviation FROM "category" WHERE "system_key" IS NOT NULL ORDER BY abbreviation`,
|
||||
);
|
||||
const expected = ['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB'];
|
||||
const present = new Set(systemRows.map((r) => String(r.system_key)));
|
||||
const missing = expected.filter((k) => !present.has(k));
|
||||
const duplicateSystemKeys = systemRows.length - new Set(systemRows.map((r) => String(r.system_key))).size;
|
||||
if (missing.length > 0 || duplicateSystemKeys > 0) {
|
||||
this.logger.warn(
|
||||
`Canonical system categories mismatch: missing=[${missing.join(',')}] duplicateSystemRows=${duplicateSystemKeys}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`verifySeed system-category check skipped: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async hasCategoryTable(): Promise<boolean> {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { InitSchema1700000000000 } from './migrations/1700000000000-InitSchema';
|
||||
import { SeedSystemData1700000000100 } from './migrations/1700000000100-SeedSystemData';
|
||||
import { AddCategoryTimestampsAndUniqueAbbrev1700000000200 } from './migrations/1700000000200-AddCategoryTimestampsAndUniqueAbbrev';
|
||||
import { UpdateSystemCategoryKeys1700000000300 } from './migrations/1700000000300-UpdateSystemCategoryKeys';
|
||||
import { RepairCategorySchemaAndSystemCategories1700000000400 } from './migrations/1700000000400-RepairCategorySchemaAndSystemCategories';
|
||||
import { DatabaseInitService } from './database-init.service';
|
||||
|
||||
const ENTITIES = [
|
||||
@@ -33,6 +34,7 @@ const MIGRATIONS = [
|
||||
SeedSystemData1700000000100,
|
||||
AddCategoryTimestampsAndUniqueAbbrev1700000000200,
|
||||
UpdateSystemCategoryKeys1700000000300,
|
||||
RepairCategorySchemaAndSystemCategories1700000000400,
|
||||
];
|
||||
|
||||
@Global()
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export const CANONICAL_SYSTEM_CATEGORY_KEYS = ['CRY', 'HW', 'MSC', 'PWN', 'REV', 'WEB'] as const;
|
||||
|
||||
export interface CanonicalSystemCategory {
|
||||
key: string;
|
||||
name: string;
|
||||
abbreviation: string;
|
||||
description: string;
|
||||
iconPath: string;
|
||||
}
|
||||
|
||||
export const CANONICAL_SYSTEM_CATEGORIES: ReadonlyArray<CanonicalSystemCategory> = [
|
||||
{
|
||||
key: 'CRY',
|
||||
name: 'Cryptography',
|
||||
abbreviation: 'CRY',
|
||||
description: 'Cryptographic challenges',
|
||||
iconPath: '/uploads/icons/CRY.png',
|
||||
},
|
||||
{
|
||||
key: 'HW',
|
||||
name: 'Hardware',
|
||||
abbreviation: 'HW',
|
||||
description: 'Hardware challenges',
|
||||
iconPath: '/uploads/icons/HW.png',
|
||||
},
|
||||
{
|
||||
key: 'MSC',
|
||||
name: 'Misc',
|
||||
abbreviation: 'MSC',
|
||||
description: 'Miscellaneous challenges',
|
||||
iconPath: '/uploads/icons/MSC.png',
|
||||
},
|
||||
{
|
||||
key: 'PWN',
|
||||
name: 'Pwn',
|
||||
abbreviation: 'PWN',
|
||||
description: 'Binary exploitation',
|
||||
iconPath: '/uploads/icons/PWN.png',
|
||||
},
|
||||
{
|
||||
key: 'REV',
|
||||
name: 'Reverse Engineering',
|
||||
abbreviation: 'REV',
|
||||
description: 'Reverse engineering challenges',
|
||||
iconPath: '/uploads/icons/REV.png',
|
||||
},
|
||||
{
|
||||
key: 'WEB',
|
||||
name: 'Web',
|
||||
abbreviation: 'WEB',
|
||||
description: 'Web exploitation challenges',
|
||||
iconPath: '/uploads/icons/WEB.png',
|
||||
},
|
||||
];
|
||||
|
||||
export class RepairCategorySchemaAndSystemCategories1700000000400
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'RepairCategorySchemaAndSystemCategories1700000000400';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await this.ensureCategoryTimestamps(queryRunner);
|
||||
await this.reconcileSystemCategories(queryRunner);
|
||||
await this.ensureCategoryIndexes(queryRunner);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_system_key" ON "category"("system_key") WHERE "system_key" IS NOT NULL;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_abbreviation" ON "category"("abbreviation");`,
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureCategoryTimestamps(queryRunner: QueryRunner): Promise<void> {
|
||||
const cols: any[] = await queryRunner.query(`PRAGMA table_info("category")`);
|
||||
const names = new Set(cols.map((c: any) => String(c?.name ?? '')));
|
||||
|
||||
if (!names.has('created_at')) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "category" ADD COLUMN "created_at" TEXT NOT NULL DEFAULT '';`,
|
||||
);
|
||||
}
|
||||
if (!names.has('updated_at')) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "category" ADD COLUMN "updated_at" TEXT NOT NULL DEFAULT '';`,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(
|
||||
`UPDATE "category" SET "created_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "created_at" IS NULL OR "created_at" = '';`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "category" SET "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "updated_at" IS NULL OR "updated_at" = '';`,
|
||||
);
|
||||
}
|
||||
|
||||
private async reconcileSystemCategories(queryRunner: QueryRunner): Promise<void> {
|
||||
const desired = CANONICAL_SYSTEM_CATEGORIES.map((c) => ({ ...c }));
|
||||
const desiredKeys = new Set(desired.map((d) => d.key));
|
||||
const desiredAbbrs = new Set(desired.map((d) => d.abbreviation));
|
||||
|
||||
const existingSystemRows: any[] = await queryRunner.query(
|
||||
`SELECT id, system_key, abbreviation FROM "category" WHERE "system_key" IS NOT NULL`,
|
||||
);
|
||||
|
||||
const bySystemKey = new Map<string, any>();
|
||||
const byAbbreviation = new Map<string, any>();
|
||||
for (const row of existingSystemRows) {
|
||||
if (row.system_key) bySystemKey.set(String(row.system_key), row);
|
||||
if (row.abbreviation) byAbbreviation.set(String(row.abbreviation), row);
|
||||
}
|
||||
|
||||
for (const row of existingSystemRows) {
|
||||
const systemKey = String(row.system_key ?? '');
|
||||
const abbreviation = String(row.abbreviation ?? '');
|
||||
const isCanonicalKey = desiredKeys.has(systemKey);
|
||||
const isCanonicalAbbr = desiredAbbrs.has(abbreviation);
|
||||
|
||||
if (!isCanonicalKey) {
|
||||
await queryRunner.query(`DELETE FROM "category" WHERE "id" = ?`, [row.id]);
|
||||
byAbbreviation.delete(abbreviation);
|
||||
}
|
||||
}
|
||||
|
||||
for (const d of desired) {
|
||||
const keyRow = bySystemKey.get(d.key);
|
||||
if (keyRow) {
|
||||
await queryRunner.query(
|
||||
`UPDATE "category" SET "name" = ?, "abbreviation" = ?, "description" = ?, "icon_path" = ?, "created_at" = COALESCE(NULLIF("created_at",''), strftime('%Y-%m-%dT%H:%M:%fZ','now')), "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "id" = ?`,
|
||||
[d.name, d.abbreviation, d.description, d.iconPath, keyRow.id],
|
||||
);
|
||||
byAbbreviation.delete(d.abbreviation);
|
||||
continue;
|
||||
}
|
||||
|
||||
const abbrRow = byAbbreviation.get(d.abbreviation);
|
||||
if (abbrRow) {
|
||||
await queryRunner.query(
|
||||
`UPDATE "category" SET "system_key" = ?, "name" = ?, "description" = ?, "icon_path" = ?, "created_at" = COALESCE(NULLIF("created_at",''), strftime('%Y-%m-%dT%H:%M:%fZ','now')), "updated_at" = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE "id" = ?`,
|
||||
[d.key, d.name, d.description, d.iconPath, abbrRow.id],
|
||||
);
|
||||
byAbbreviation.delete(d.abbreviation);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { v4: uuid } = await import('uuid');
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "category" ("id","system_key","name","abbreviation","description","icon_path","created_at","updated_at") VALUES (?,?,?,?,?,?, strftime('%Y-%m-%dT%H:%M:%fZ','now'), strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
|
||||
[uuid(), d.key, d.name, d.abbreviation, d.description, d.iconPath],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCategoryIndexes(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_system_key" ON "category"("system_key") WHERE "system_key" IS NOT NULL;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS "uq_category_abbreviation" ON "category"("abbreviation");`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user