forked from wangziqi/gongxue-base
feat: add CASL authorization and AI configuration
This commit is contained in:
@@ -9,10 +9,97 @@ export class DatabaseMigrationsService implements OnApplicationBootstrap {
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async onApplicationBootstrap(): Promise<void> {
|
||||
await this.ensureAiConfigTable();
|
||||
await this.backfillOrganizations();
|
||||
await this.normalizeClassDates();
|
||||
}
|
||||
|
||||
private async ensureAiConfigTable(): Promise<void> {
|
||||
const runner = this.dataSource.createQueryRunner();
|
||||
await runner.connect();
|
||||
try {
|
||||
const tables = await runner.getTables(['ai_config']);
|
||||
const isMySQL = this.dataSource.options.type === 'mysql';
|
||||
|
||||
if (tables.length === 0) {
|
||||
const pkDef = isMySQL
|
||||
? 'id INTEGER PRIMARY KEY AUTO_INCREMENT'
|
||||
: 'id INTEGER PRIMARY KEY AUTOINCREMENT';
|
||||
const boolType = isMySQL ? 'TINYINT(1)' : 'BOOLEAN';
|
||||
const datetimeFn = isMySQL ? 'CURRENT_TIMESTAMP' : 'CURRENT_TIMESTAMP';
|
||||
|
||||
await runner.query(`
|
||||
CREATE TABLE ai_config (
|
||||
${pkDef},
|
||||
singleton_key VARCHAR(20) NOT NULL DEFAULT 'GLOBAL',
|
||||
provider VARCHAR(50) NOT NULL DEFAULT 'OPENAI',
|
||||
base_url VARCHAR(500),
|
||||
encrypted_api_key TEXT,
|
||||
api_key_iv VARCHAR(50),
|
||||
api_key_auth_tag VARCHAR(50),
|
||||
key_last4 VARCHAR(4),
|
||||
default_model VARCHAR(100),
|
||||
enabled ${boolType} DEFAULT 0,
|
||||
timeout_ms INT DEFAULT 30000,
|
||||
verified ${boolType} DEFAULT 0,
|
||||
last_tested_at DATETIME,
|
||||
last_test_latency_ms INT,
|
||||
created_at DATETIME NOT NULL DEFAULT ${datetimeFn},
|
||||
updated_at DATETIME NOT NULL DEFAULT ${datetimeFn}
|
||||
)
|
||||
`);
|
||||
|
||||
if (isMySQL) {
|
||||
try {
|
||||
await runner.query(
|
||||
'CREATE UNIQUE INDEX uq_ai_config_singleton ON ai_config(singleton_key)',
|
||||
);
|
||||
} catch {
|
||||
// Index may already exist; MySQL has no IF NOT EXISTS for indexes
|
||||
}
|
||||
} else {
|
||||
await runner.query(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_config_singleton ON ai_config(singleton_key)',
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log('已创建 ai_config 表');
|
||||
} else {
|
||||
// Check for missing columns
|
||||
const table = await runner.getTable('ai_config');
|
||||
const columnNames = new Set(table?.columns.map((c) => c.name) ?? []);
|
||||
|
||||
const desiredColumns: Array<{ name: string; def: string }> = [
|
||||
{ name: 'id', def: '' }, // skip — primary key
|
||||
{ name: 'singleton_key', def: "VARCHAR(20) NOT NULL DEFAULT 'GLOBAL'" },
|
||||
{ name: 'provider', def: "VARCHAR(50) NOT NULL DEFAULT 'OPENAI'" },
|
||||
{ name: 'base_url', def: 'VARCHAR(500)' },
|
||||
{ name: 'encrypted_api_key', def: 'TEXT' },
|
||||
{ name: 'api_key_iv', def: 'VARCHAR(50)' },
|
||||
{ name: 'api_key_auth_tag', def: 'VARCHAR(50)' },
|
||||
{ name: 'key_last4', def: 'VARCHAR(4)' },
|
||||
{ name: 'default_model', def: 'VARCHAR(100)' },
|
||||
{ name: 'enabled', def: isMySQL ? 'TINYINT(1) DEFAULT 0' : 'BOOLEAN DEFAULT 0' },
|
||||
{ name: 'timeout_ms', def: 'INT DEFAULT 30000' },
|
||||
{ name: 'verified', def: isMySQL ? 'TINYINT(1) DEFAULT 0' : 'BOOLEAN DEFAULT 0' },
|
||||
{ name: 'last_tested_at', def: 'DATETIME' },
|
||||
{ name: 'last_test_latency_ms', def: 'INT' },
|
||||
{ name: 'created_at', def: 'DATETIME' },
|
||||
{ name: 'updated_at', def: 'DATETIME' },
|
||||
];
|
||||
|
||||
for (const col of desiredColumns) {
|
||||
if (col.def && !columnNames.has(col.name)) {
|
||||
await runner.query(`ALTER TABLE ai_config ADD COLUMN ${col.name} ${col.def}`);
|
||||
this.logger.log(`已为 ai_config 表添加列: ${col.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await runner.release();
|
||||
}
|
||||
}
|
||||
|
||||
private async backfillOrganizations(): Promise<void> {
|
||||
const runner = this.dataSource.createQueryRunner();
|
||||
await runner.connect();
|
||||
|
||||
Reference in New Issue
Block a user