diff --git a/tools/database/mysql2-schema-exporter.mjs b/tools/database/mysql2-schema-exporter.mjs index c22a0a0..6643bd2 100644 --- a/tools/database/mysql2-schema-exporter.mjs +++ b/tools/database/mysql2-schema-exporter.mjs @@ -6,6 +6,8 @@ import mysql from 'mysql2/promise'; const require = createRequire(import.meta.url); const mysql2Version = require('mysql2/package.json').version; const DEVELOPMENT_DATABASE = 'a_share_analysis_dev'; +const VALID_TRIGGER_TIMINGS = new Set(['BEFORE', 'AFTER']); +const VALID_TRIGGER_EVENTS = new Set(['INSERT', 'UPDATE', 'DELETE']); const CATALOG_QUERIES = Object.freeze({ tables: `SELECT TABLE_NAME, TABLE_TYPE @@ -127,11 +129,17 @@ function requireCatalogText(value) { } async function queryCatalogRows(connection, sql, database, limits) { - const result = await connection.query({ - sql, - values: [database], - timeout: limits.queryTimeoutMs, - }); + let result; + try { + result = await connection.query({ + sql, + values: [database], + timeout: limits.queryTimeoutMs, + }); + } catch { + // mysql2 错误可能包含连接 URL、用户名或密码,目录读取的公共接口一律不透传原始文本。 + throw new SchemaExportError('DATABASE_QUERY_FAILED', '开发库 Schema 查询失败'); + } if (!Array.isArray(result) || !Array.isArray(result[0])) { throw new SchemaExportError('CATALOG_SHAPE_INVALID', 'Schema 对象目录查询结果无效'); } @@ -189,15 +197,25 @@ export async function readSchemaCatalog(connection, database, limits = DEFAULT_E const triggerRows = await queryCatalogRows(connection, CATALOG_QUERIES.triggers, database, limits); const triggers = triggerRows.map((row) => { - const order = Number(row.ACTION_ORDER); - if (!Number.isInteger(order) || order < 0) { + const timing = requireCatalogText(row.ACTION_TIMING); + const event = requireCatalogText(row.EVENT_MANIPULATION); + const rawOrder = row.ACTION_ORDER; + const order = Number(rawOrder); + if ( + !VALID_TRIGGER_TIMINGS.has(timing) || + !VALID_TRIGGER_EVENTS.has(event) || + (typeof rawOrder !== 'number' && typeof rawOrder !== 'string') || + (typeof rawOrder === 'string' && rawOrder.trim().length === 0) || + !Number.isInteger(order) || + order < 1 + ) { throw new SchemaExportError('CATALOG_SHAPE_INVALID', 'Schema 触发器执行顺序无效'); } return { name: requireCatalogText(row.TRIGGER_NAME), table: requireCatalogText(row.EVENT_OBJECT_TABLE), - timing: requireCatalogText(row.ACTION_TIMING), - event: requireCatalogText(row.EVENT_MANIPULATION), + timing, + event, order, }; }); diff --git a/tools/database/mysql2-schema-exporter.test.mjs b/tools/database/mysql2-schema-exporter.test.mjs index 52c3716..472525e 100644 --- a/tools/database/mysql2-schema-exporter.test.mjs +++ b/tools/database/mysql2-schema-exporter.test.mjs @@ -232,6 +232,63 @@ test('目录拒绝未知表类型和超过对象上限', async () => { ); }); +test('目录拒绝非法触发器时机、事件和空执行顺序', async () => { + for (const trigger of [ + { + TRIGGER_NAME: 'invalid_timing', + EVENT_OBJECT_TABLE: 'market_snapshot', + ACTION_TIMING: 'DURING', + EVENT_MANIPULATION: 'INSERT', + ACTION_ORDER: 1, + }, + { + TRIGGER_NAME: 'invalid_event', + EVENT_OBJECT_TABLE: 'market_snapshot', + ACTION_TIMING: 'BEFORE', + EVENT_MANIPULATION: 'MERGE', + ACTION_ORDER: 1, + }, + { + TRIGGER_NAME: 'empty_order', + EVENT_OBJECT_TABLE: 'market_snapshot', + ACTION_TIMING: 'BEFORE', + EVENT_MANIPULATION: 'INSERT', + ACTION_ORDER: '', + }, + { + TRIGGER_NAME: 'missing_order', + EVENT_OBJECT_TABLE: 'market_snapshot', + ACTION_TIMING: 'BEFORE', + EVENT_MANIPULATION: 'INSERT', + ACTION_ORDER: null, + }, + ]) { + const connection = createCatalogConnection({ + 'information_schema.TRIGGERS': [trigger], + }); + await assert.rejects( + () => readSchemaCatalog(connection, 'a_share_analysis_dev', DEFAULT_EXPORT_LIMITS), + (error) => error instanceof SchemaExportError && error.code === 'CATALOG_SHAPE_INVALID', + ); + } +}); + +test('直接目录读取将数据库异常转为固定脱敏错误', async () => { + const connection = { + async query() { + throw new Error('connect mysql://schema_reader:not-for-logs@127.0.0.1:3306/a_share_analysis_dev'); + }, + }; + + await assert.rejects( + () => readSchemaCatalog(connection, 'a_share_analysis_dev', DEFAULT_EXPORT_LIMITS), + (error) => + error instanceof SchemaExportError && + error.code === 'DATABASE_QUERY_FAILED' && + error.message === '开发库 Schema 查询失败', + ); +}); + const completeCatalog = Object.freeze({ tables: ['market_snapshot'], views: ['market_dashboard', 'market_summary'],