@things-factory/shell 10.0.3 → 10.0.4
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.4",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"pg": "^8.7.3",
|
|
127
127
|
"sqlite3": "^5.0.8"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "c8f6dcfea3ffe7a9dd2a7195e83e2e4e201fd5ea"
|
|
130
130
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres 파라미터 타입 추론 함정 회귀 검사.
|
|
3
|
+
*
|
|
4
|
+
* 배경: `domains.owner` 는 사용자 id(uuid 문자열)를 담지만 컬럼 타입이 varchar 다. 반면
|
|
5
|
+
* `domain_owners.user_id` 등 다른 사용자 참조는 진짜 uuid 컬럼이다.
|
|
6
|
+
*
|
|
7
|
+
* 한 쿼리에서 **같은 이름의 파라미터**를 varchar 컬럼과 uuid 컬럼 양쪽에 쓰면, Postgres 드라이버가
|
|
8
|
+
* 그 파라미터를 하나의 플레이스홀더(`$1`)로 재사용하고 Postgres 는 첫 사용처로 타입을 추론한다.
|
|
9
|
+
* 그 결과 `uuid = $1` 비교가 `operator does not exist: uuid = text` 로 실패한다.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ sqlite 드라이버는 같은 이름을 별도 플레이스홀더(`?`, `?`)로 복제하기 때문에 이 결함이
|
|
12
|
+
* 통과한다. 즉 이 저장소의 sqlite 기반 테스트로는 원리적으로 잡히지 않는다. 그래서
|
|
13
|
+
* 서버 없이 Postgres 용 SQL 만 생성해(`buildMetadatas`) 플레이스홀더 번호를 직접 확인한다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { DataSource } from 'typeorm'
|
|
17
|
+
|
|
18
|
+
import { Domain, NamingStrategy, entities as shellEntities } from '@things-factory/shell'
|
|
19
|
+
|
|
20
|
+
let dataSource: DataSource
|
|
21
|
+
|
|
22
|
+
/** 서버에 연결하지 않고 Postgres 방언으로 SQL 만 생성할 수 있게 메타데이터만 만든다. */
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
dataSource = new DataSource({
|
|
25
|
+
type: 'postgres',
|
|
26
|
+
host: 'localhost',
|
|
27
|
+
database: 'unused',
|
|
28
|
+
namingStrategy: new NamingStrategy(),
|
|
29
|
+
entities: [...shellEntities],
|
|
30
|
+
logging: false
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await (dataSource as any).buildMetadatas()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
function placeholderOf(sql: string, column: string): string | undefined {
|
|
37
|
+
return new RegExp(`"${column}" = (\\$\\d+)`).exec(sql)?.[1]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('domains.owner 는 varchar, 사용자 참조는 uuid', () => {
|
|
41
|
+
it('컬럼 타입이 실제로 다르다 (이 검사의 전제)', () => {
|
|
42
|
+
const ownerColumn = dataSource.getMetadata(Domain).columns.find(column => column.databaseName === 'owner')
|
|
43
|
+
const idColumn = dataSource.getMetadata(Domain).columns.find(column => column.databaseName === 'id')
|
|
44
|
+
|
|
45
|
+
expect(idColumn.type).toBe('uuid')
|
|
46
|
+
/* varchar 는 TypeORM 기본값이라 type 이 비어 있다 — uuid 가 아니라는 점이 핵심이다. */
|
|
47
|
+
expect(ownerColumn.type).not.toBe('uuid')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('같은 파라미터를 두 컬럼에 쓰면 Postgres 는 플레이스홀더를 재사용한다 (금지 형태)', () => {
|
|
51
|
+
const [sql] = dataSource
|
|
52
|
+
.getRepository(Domain)
|
|
53
|
+
.createQueryBuilder('DOMAIN')
|
|
54
|
+
.where('DOMAIN.owner = :sameParam')
|
|
55
|
+
.orWhere('DOMAIN.id = :sameParam')
|
|
56
|
+
.setParameter('sameParam', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
|
57
|
+
.getQueryAndParameters()
|
|
58
|
+
|
|
59
|
+
/* 이 형태가 운영에서 `operator does not exist: uuid = text` 를 만든다. */
|
|
60
|
+
expect(placeholderOf(sql, 'owner')).toBe(placeholderOf(sql, 'id'))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('파라미터를 분리하면 각각 독립적으로 추론된다 (요구 형태)', () => {
|
|
64
|
+
const [sql] = dataSource
|
|
65
|
+
.getRepository(Domain)
|
|
66
|
+
.createQueryBuilder('DOMAIN')
|
|
67
|
+
.where('DOMAIN.owner = :ownerId')
|
|
68
|
+
.orWhere('DOMAIN.id = :domainId')
|
|
69
|
+
.setParameter('ownerId', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
|
70
|
+
.setParameter('domainId', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
|
|
71
|
+
.getQueryAndParameters()
|
|
72
|
+
|
|
73
|
+
expect(placeholderOf(sql, 'owner')).not.toBe(placeholderOf(sql, 'id'))
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('소스 검사 — owner 를 비교하는 쿼리에서 파라미터를 공유하지 않는다', () => {
|
|
78
|
+
/**
|
|
79
|
+
* 쿼리빌더 체인 단위로 잘라, `owner` 비교가 있는 체인에서 같은 파라미터가 두 번 이상
|
|
80
|
+
* 쓰였는지 본다. 실행 없이 소스만 보는 검사라 새로 추가되는 코드도 함께 걸린다.
|
|
81
|
+
*/
|
|
82
|
+
const CHAIN = /createQueryBuilder\([\s\S]{0,4000}?\.(getMany|getOne|getRawMany|getRawOne|getExists|getCount|getManyAndCount)\(\)/g
|
|
83
|
+
|
|
84
|
+
function offendingChains(source: string): string[] {
|
|
85
|
+
const offenders: string[] = []
|
|
86
|
+
|
|
87
|
+
for (const [chain] of source.matchAll(CHAIN)) {
|
|
88
|
+
if (!/\.owner\s*(=|IN)/.test(chain)) {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const names = [...chain.matchAll(/:(?:\.\.\.)?([a-zA-Z_]\w*)/g)].map(match => match[1])
|
|
93
|
+
const repeated = names.filter((name, index) => names.indexOf(name) !== index)
|
|
94
|
+
if (repeated.length) {
|
|
95
|
+
offenders.push(`${chain.slice(0, 60)}… (중복 파라미터: ${[...new Set(repeated)].join(', ')})`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return offenders
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it('금지 형태를 실제로 검출한다 (검사 자체의 변별력)', () => {
|
|
103
|
+
const bad = `
|
|
104
|
+
createQueryBuilder('DOMAIN')
|
|
105
|
+
.where('DOMAIN.owner = :userId')
|
|
106
|
+
.orWhere('DOMAIN_OWNER.user = :userId')
|
|
107
|
+
.getMany()
|
|
108
|
+
`
|
|
109
|
+
expect(offendingChains(bad)).toHaveLength(1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('체크인 판정 코드에 금지 형태가 없다', () => {
|
|
113
|
+
/* auth-base 의 소유권·체크인 조회가 이 함정의 유일한 실제 발생 지점이었다. */
|
|
114
|
+
const target = require('path').resolve(
|
|
115
|
+
__dirname,
|
|
116
|
+
'../../auth-base/server/utils/checkin-domain-facts.ts'
|
|
117
|
+
)
|
|
118
|
+
const source = require('fs').readFileSync(target, 'utf8')
|
|
119
|
+
|
|
120
|
+
expect(offendingChains(source)).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
})
|