/* * 컬럼 타입은 **드라이버 방언을 적지 않는다.** * * ── 무엇이 났나 (2026-08-19) ──────────────────────────────────────────────── * 릴리즈 부팅이 Postgres 에서 막혔다: * * DataTypeNotSupportedError: Data type "datetime" in "TwinEvent.eventTime" is not supported by "postgres" * * `datetime` 은 **MySQL·MSSQL 의 이름**이다(Postgres·Oracle 은 거절한다). 거꾸로 `timestamp` 는 MySQL * 이 받지만 예전 주석은 「sqlite 가 지원하지 않는다」고 적어 두었다 — sqlite 드라이버는 타입 목록이 * 비어 있어 **아무 이름도 검증하지 않는다**(그래서 개발 중에는 무엇을 적어도 통과했다). 결국 다섯 * 드라이버에 다 통하는 날짜 이름은 **없다.** * * 그래서 이름을 고르는 일을 TypeORM 에 맡긴다: `emitDecoratorMetadata` 가 켜져 있으므로 `Date` 속성에 * `@Column()` 만 달면 드라이버별로 알맞게 바뀐다(Postgres `timestamp without time zone` · sqlite·MySQL * `datetime` · Oracle `timestamp`) — sqlite 거동은 그대로다. * * 이 시험은 그 규율을 **소스에서** 지킨다: 이 패키지의 엔티티가 명시한 타입 이름은 네 드라이버(검증을 * 하는 쪽) 모두가 아는 것이어야 한다. 개발 DB 가 sqlite 라 아무것도 못 잡아 주기 때문에 필요하다. */ import { test } from 'node:test' import assert from 'node:assert/strict' import { readdirSync, readFileSync, statSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { createRequire } from 'node:module' import { join } from 'node:path' const req = createRequire(import.meta.url) /** 드라이버가 아는 타입 이름 — **TypeORM 소스에서 읽는다**(우리가 목록을 옮겨 적지 않는다). */ function supportedTypes(driverPath: string): Set { const src = readFileSync(req.resolve(driverPath), 'utf-8') const m = src.match(/supportedDataTypes = \[([\s\S]*?)\];/) assert.ok(m, `${driverPath}: supportedDataTypes 를 찾지 못했다(TypeORM 구조가 바뀌었다)`) return new Set((m![1].match(/"[^"]+"/g) ?? []).map(s => s.slice(1, -1))) } /* * sqlite 는 목록이 비어 있다 = **검증하지 않는다.** 그래서 여기 넣지 않는다 — 넣으면 모든 타입이 * 「sqlite 가 모른다」로 걸려 이 시험이 소음이 된다. 개발 DB 가 아무것도 못 잡는다는 사실이 이 시험의 이유다. */ const DRIVERS: Record = { postgres: 'typeorm/driver/postgres/PostgresDriver.js', mysql: 'typeorm/driver/mysql/MysqlDriver.js', oracle: 'typeorm/driver/oracle/OracleDriver.js', mssql: 'typeorm/driver/sqlserver/SqlServerDriver.js' } function entitySources(): { file: string; src: string }[] { const root = fileURLToPath(new URL('../server', import.meta.url)) const out: { file: string; src: string }[] = [] const walk = (dir: string) => { for (const name of readdirSync(dir)) { const path = join(dir, name) if (statSync(path).isDirectory()) { walk(path); continue } if (!name.endsWith('.ts')) continue const src = readFileSync(path, 'utf-8') if (/@Entity\(/.test(src)) out.push({ file: path.slice(root.length + 1), src }) } } walk(root) return out } test('엔티티가 있다 — 훑을 대상이 없으면 이 시험은 아무것도 지키지 않는다', () => { assert.ok(entitySources().length >= 5, '이 패키지의 엔티티를 찾아야 한다') }) /* * **드라이버마다 이름이 다른 컬럼은 코드가 고른다.** * * 긴 글이 그렇다: `text` 를 Oracle 이 모르고(그쪽은 `clob`), MySQL 계열은 `longtext`, MSSQL 은 * `nvarchar(MAX)` 를 쓴다. 이 레포의 정본 관행은 `DATABASE_TYPE` 으로 분기하는 것이다 * (`board-service` 의 `Board.model`) — 그 분기는 이 시험의 리터럴 검사에 걸리지 않는다(정상이다). * * 그래서 면제 목록을 두지 않는다: 고정 리터럴은 전부 잡고, 갈라야 하는 컬럼은 관행대로 가른다. */ const KNOWN_GAPS = new Set() test('명시한 컬럼 타입은 네 드라이버가 모두 아는 이름이다 — 개발 DB(sqlite)는 아무것도 잡아 주지 않는다', () => { const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)])) const violations: string[] = [] for (const { file, src } of entitySources()) { for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) { const type = m[1] const missing = Object.entries(supported).filter(([, s]) => !s.has(type)).map(([n]) => n) if (!missing.length) continue const line = `${file}: type '${type}' — ${missing.join(', ')} 가 모른다` if (!KNOWN_GAPS.has(line)) violations.push(line) } } assert.deepEqual(violations, [], `드라이버 방언을 적은 컬럼:\n${violations.join('\n')}`) }) test('면제를 남겨 두면 그것이 아직 사실인지 확인한다 — 유령 면제를 남기지 않는다', () => { const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)])) const present = new Set() for (const { file, src } of entitySources()) { for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) { const missing = Object.entries(supported).filter(([, s]) => !s.has(m[1])).map(([n]) => n) if (missing.length) present.add(`${file}: type '${m[1]}' — ${missing.join(', ')} 가 모른다`) } } for (const gap of KNOWN_GAPS) assert.ok(present.has(gap), `이 예외는 이제 사실이 아니다(목록에서 지워라): ${gap}`) }) test('날짜 컬럼은 이름을 적지 않는다 — TypeORM 이 드라이버별로 고르게 둔다', () => { for (const { file, src } of entitySources()) { assert.equal(/type:\s*'(datetime|timestamp|timestamptz|datetime2|smalldatetime)'/.test(src), false, `${file}: 날짜 타입 이름을 적었다`) } }) /* * 드라이버별로 갈라야 하는 컬럼은 **갈린 상태로** 있어야 한다 — 다시 고정 리터럴로 돌아가면 그 드라이버가 * 부팅에서 거절한다(감사 사유는 사람이 쓰는 글이라 길이를 자르지 않는다). */ test('긴 글 컬럼은 DATABASE_TYPE 으로 갈린다 — 관행을 되돌리지 않는다', () => { const src = readFileSync(fileURLToPath(new URL('../server/service/twin-audit/twin-audit-event.ts', import.meta.url)), 'utf-8') assert.match(src, /const DATABASE_TYPE = config\.get\('ormconfig', \{\}\)\.type/, '관행과 같은 자리에서 읽는다') for (const name of ['longtext', 'clob', 'nvarchar', 'text']) assert.ok(src.includes(`'${name}'`), `${name} 갈래가 있어야 한다`) assert.match(src, /length: DATABASE_TYPE == 'mssql' \? 'MAX' : undefined/, 'MSSQL 은 길이까지 말해야 잘리지 않는다') })