@things-factory/headless-twin 10.0.10 → 10.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ /*
2
+ * 컬럼 타입은 **드라이버 방언을 적지 않는다.**
3
+ *
4
+ * ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
5
+ * 릴리즈 부팅이 Postgres 에서 막혔다:
6
+ *
7
+ * DataTypeNotSupportedError: Data type "datetime" in "TwinEvent.eventTime" is not supported by "postgres"
8
+ *
9
+ * `datetime` 은 **MySQL·MSSQL 의 이름**이다(Postgres·Oracle 은 거절한다). 거꾸로 `timestamp` 는 MySQL
10
+ * 이 받지만 예전 주석은 「sqlite 가 지원하지 않는다」고 적어 두었다 — sqlite 드라이버는 타입 목록이
11
+ * 비어 있어 **아무 이름도 검증하지 않는다**(그래서 개발 중에는 무엇을 적어도 통과했다). 결국 다섯
12
+ * 드라이버에 다 통하는 날짜 이름은 **없다.**
13
+ *
14
+ * 그래서 이름을 고르는 일을 TypeORM 에 맡긴다: `emitDecoratorMetadata` 가 켜져 있으므로 `Date` 속성에
15
+ * `@Column()` 만 달면 드라이버별로 알맞게 바뀐다(Postgres `timestamp without time zone` · sqlite·MySQL
16
+ * `datetime` · Oracle `timestamp`) — sqlite 거동은 그대로다.
17
+ *
18
+ * 이 시험은 그 규율을 **소스에서** 지킨다: 이 패키지의 엔티티가 명시한 타입 이름은 네 드라이버(검증을
19
+ * 하는 쪽) 모두가 아는 것이어야 한다. 개발 DB 가 sqlite 라 아무것도 못 잡아 주기 때문에 필요하다.
20
+ */
21
+ import { test } from 'node:test'
22
+ import assert from 'node:assert/strict'
23
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
24
+ import { fileURLToPath } from 'node:url'
25
+ import { createRequire } from 'node:module'
26
+ import { join } from 'node:path'
27
+
28
+ const req = createRequire(import.meta.url)
29
+
30
+ /** 드라이버가 아는 타입 이름 — **TypeORM 소스에서 읽는다**(우리가 목록을 옮겨 적지 않는다). */
31
+ function supportedTypes(driverPath: string): Set<string> {
32
+ const src = readFileSync(req.resolve(driverPath), 'utf-8')
33
+ const m = src.match(/supportedDataTypes = \[([\s\S]*?)\];/)
34
+ assert.ok(m, `${driverPath}: supportedDataTypes 를 찾지 못했다(TypeORM 구조가 바뀌었다)`)
35
+ return new Set((m![1].match(/"[^"]+"/g) ?? []).map(s => s.slice(1, -1)))
36
+ }
37
+
38
+ /*
39
+ * sqlite 는 목록이 비어 있다 = **검증하지 않는다.** 그래서 여기 넣지 않는다 — 넣으면 모든 타입이
40
+ * 「sqlite 가 모른다」로 걸려 이 시험이 소음이 된다. 개발 DB 가 아무것도 못 잡는다는 사실이 이 시험의 이유다.
41
+ */
42
+ const DRIVERS: Record<string, string> = {
43
+ postgres: 'typeorm/driver/postgres/PostgresDriver.js',
44
+ mysql: 'typeorm/driver/mysql/MysqlDriver.js',
45
+ oracle: 'typeorm/driver/oracle/OracleDriver.js',
46
+ mssql: 'typeorm/driver/sqlserver/SqlServerDriver.js'
47
+ }
48
+
49
+ function entitySources(): { file: string; src: string }[] {
50
+ const root = fileURLToPath(new URL('../server', import.meta.url))
51
+ const out: { file: string; src: string }[] = []
52
+ const walk = (dir: string) => {
53
+ for (const name of readdirSync(dir)) {
54
+ const path = join(dir, name)
55
+ if (statSync(path).isDirectory()) { walk(path); continue }
56
+ if (!name.endsWith('.ts')) continue
57
+ const src = readFileSync(path, 'utf-8')
58
+ if (/@Entity\(/.test(src)) out.push({ file: path.slice(root.length + 1), src })
59
+ }
60
+ }
61
+ walk(root)
62
+ return out
63
+ }
64
+
65
+ test('엔티티가 있다 — 훑을 대상이 없으면 이 시험은 아무것도 지키지 않는다', () => {
66
+ assert.ok(entitySources().length >= 5, '이 패키지의 엔티티를 찾아야 한다')
67
+ })
68
+
69
+ /*
70
+ * **드라이버마다 이름이 다른 컬럼은 코드가 고른다.**
71
+ *
72
+ * 긴 글이 그렇다: `text` 를 Oracle 이 모르고(그쪽은 `clob`), MySQL 계열은 `longtext`, MSSQL 은
73
+ * `nvarchar(MAX)` 를 쓴다. 이 레포의 정본 관행은 `DATABASE_TYPE` 으로 분기하는 것이다
74
+ * (`board-service` 의 `Board.model`) — 그 분기는 이 시험의 리터럴 검사에 걸리지 않는다(정상이다).
75
+ *
76
+ * 그래서 면제 목록을 두지 않는다: 고정 리터럴은 전부 잡고, 갈라야 하는 컬럼은 관행대로 가른다.
77
+ */
78
+ const KNOWN_GAPS = new Set<string>()
79
+
80
+ test('명시한 컬럼 타입은 네 드라이버가 모두 아는 이름이다 — 개발 DB(sqlite)는 아무것도 잡아 주지 않는다', () => {
81
+ const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)]))
82
+ const violations: string[] = []
83
+ for (const { file, src } of entitySources()) {
84
+ for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) {
85
+ const type = m[1]
86
+ const missing = Object.entries(supported).filter(([, s]) => !s.has(type)).map(([n]) => n)
87
+ if (!missing.length) continue
88
+ const line = `${file}: type '${type}' — ${missing.join(', ')} 가 모른다`
89
+ if (!KNOWN_GAPS.has(line)) violations.push(line)
90
+ }
91
+ }
92
+ assert.deepEqual(violations, [], `드라이버 방언을 적은 컬럼:\n${violations.join('\n')}`)
93
+ })
94
+
95
+ test('면제를 남겨 두면 그것이 아직 사실인지 확인한다 — 유령 면제를 남기지 않는다', () => {
96
+ const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)]))
97
+ const present = new Set<string>()
98
+ for (const { file, src } of entitySources()) {
99
+ for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) {
100
+ const missing = Object.entries(supported).filter(([, s]) => !s.has(m[1])).map(([n]) => n)
101
+ if (missing.length) present.add(`${file}: type '${m[1]}' — ${missing.join(', ')} 가 모른다`)
102
+ }
103
+ }
104
+ for (const gap of KNOWN_GAPS) assert.ok(present.has(gap), `이 예외는 이제 사실이 아니다(목록에서 지워라): ${gap}`)
105
+ })
106
+
107
+ test('날짜 컬럼은 이름을 적지 않는다 — TypeORM 이 드라이버별로 고르게 둔다', () => {
108
+ for (const { file, src } of entitySources()) {
109
+ assert.equal(/type:\s*'(datetime|timestamp|timestamptz|datetime2|smalldatetime)'/.test(src), false, `${file}: 날짜 타입 이름을 적었다`)
110
+ }
111
+ })
112
+
113
+ /*
114
+ * 드라이버별로 갈라야 하는 컬럼은 **갈린 상태로** 있어야 한다 — 다시 고정 리터럴로 돌아가면 그 드라이버가
115
+ * 부팅에서 거절한다(감사 사유는 사람이 쓰는 글이라 길이를 자르지 않는다).
116
+ */
117
+ test('긴 글 컬럼은 DATABASE_TYPE 으로 갈린다 — 관행을 되돌리지 않는다', () => {
118
+ const src = readFileSync(fileURLToPath(new URL('../server/service/twin-audit/twin-audit-event.ts', import.meta.url)), 'utf-8')
119
+ assert.match(src, /const DATABASE_TYPE = config\.get\('ormconfig', \{\}\)\.type/, '관행과 같은 자리에서 읽는다')
120
+ for (const name of ['longtext', 'clob', 'nvarchar', 'text']) assert.ok(src.includes(`'${name}'`), `${name} 갈래가 있어야 한다`)
121
+ assert.match(src, /length: DATABASE_TYPE == 'mssql' \? 'MAX' : undefined/, 'MSSQL 은 길이까지 말해야 잘리지 않는다')
122
+ })