import type { MySqlPool } from './mysql.js'; type LegacyEntity = 'stores' | 'rooms' | 'orders' | 'devices'; export interface LegacyTableMapping { table: string; idColumn: string; tenantColumn: string; parentColumn?: string; nameColumn?: string; statusColumn?: string; codeColumn?: string; startColumn?: string; endColumn?: string; } export interface LegacyReadOptions { tenantId: number; parentId?: number; limit?: number; } export interface LegacyRecord { legacyId: string; tenantId: string; parentId: string | null; name: string | null; code: string | null; status: string | null; startAt: Date | string | null; endAt: Date | string | null; } export const defaultLegacyMappings: Record = { stores: { table: 'member_store_info', idColumn: 'id', tenantColumn: 'tenant_id', nameColumn: 'store_name', statusColumn: 'status' }, rooms: { table: 'member_room_info', idColumn: 'id', tenantColumn: 'tenant_id', parentColumn: 'store_id', nameColumn: 'room_name', codeColumn: 'room_no', statusColumn: 'status' }, orders: { table: 'member_order_info', idColumn: 'id', tenantColumn: 'tenant_id', parentColumn: 'room_id', codeColumn: 'order_no', statusColumn: 'status', startColumn: 'start_time', endColumn: 'end_time' }, devices: { table: 'member_device_info', idColumn: 'id', tenantColumn: 'tenant_id', parentColumn: 'room_id', nameColumn: 'device_name', codeColumn: 'device_id', statusColumn: 'status' } }; const identifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/; function quoteIdentifier(identifier: string): string { if (!identifierPattern.test(identifier)) { throw new Error(`Unsafe legacy SQL identifier: ${identifier}`); } return `\`${identifier}\``; } function selectedColumn(column: string | undefined, alias: string): string { return column ? `${quoteIdentifier(column)} AS ${quoteIdentifier(alias)}` : `NULL AS ${quoteIdentifier(alias)}`; } function normalizeLimit(limit = 100): number { if (!Number.isInteger(limit) || limit < 1 || limit > 500) { throw new Error('Legacy read limit must be an integer between 1 and 500.'); } return limit; } export class LegacyReadRepository { constructor( private readonly pool: Pick, private readonly mappings: Record = defaultLegacyMappings ) {} listStores(options: LegacyReadOptions): Promise { return this.list('stores', options); } listRooms(options: LegacyReadOptions): Promise { return this.list('rooms', options); } listOrders(options: LegacyReadOptions): Promise { return this.list('orders', options); } listDevices(options: LegacyReadOptions): Promise { return this.list('devices', options); } private async list(entity: LegacyEntity, options: LegacyReadOptions): Promise { const mapping = this.mappings[entity]; const limit = normalizeLimit(options.limit); const clauses = [`${quoteIdentifier(mapping.tenantColumn)} = ?`]; const parameters: Array = [options.tenantId]; if (mapping.parentColumn && options.parentId !== undefined) { clauses.push(`${quoteIdentifier(mapping.parentColumn)} = ?`); parameters.push(options.parentId); } parameters.push(limit); const sql = [ 'SELECT', `${quoteIdentifier(mapping.idColumn)} AS ${quoteIdentifier('legacyId')},`, `${quoteIdentifier(mapping.tenantColumn)} AS ${quoteIdentifier('tenantId')},`, `${selectedColumn(mapping.parentColumn, 'parentId')},`, `${selectedColumn(mapping.nameColumn, 'name')},`, `${selectedColumn(mapping.codeColumn, 'code')},`, `${selectedColumn(mapping.statusColumn, 'status')},`, `${selectedColumn(mapping.startColumn, 'startAt')},`, selectedColumn(mapping.endColumn, 'endAt'), `FROM ${quoteIdentifier(mapping.table)}`, `WHERE ${clauses.join(' AND ')}`, `ORDER BY ${quoteIdentifier(mapping.idColumn)} ASC`, 'LIMIT ?' ].join(' '); const [rows] = await this.pool.query(sql, parameters); return rows as LegacyRecord[]; } }