@gaonjs/config 0.10.0 → 0.12.0
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/dist/testkit.d.ts +4 -3
- package/dist/testkit.js +19 -4
- package/dist/wire.d.ts +18 -0
- package/dist/wire.js +68 -13
- package/package.json +5 -5
package/dist/testkit.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export interface TestDbHandle {
|
|
2
|
-
/** 등록한 모든 커넥션을
|
|
2
|
+
/** 등록한 모든 커넥션을 종료하고 배터리를 리셋한다(afterAll 에서 호출). */
|
|
3
3
|
close(): Promise<void>;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* 테스트용 DB
|
|
7
|
-
*
|
|
6
|
+
* 테스트용 DB 커넥션 + DB 외 배터리(mail·storage·i18n)를 배선한다(결정 111·136).
|
|
7
|
+
* 각 커넥션을 `<db>_test` 로 파생해 poolMax:1 로 붙이고, 스토리지·메일을 테스트
|
|
8
|
+
* 격리 값으로 배선한다. 반환값의 close() 를 afterAll 에서 호출해 정리한다.
|
|
8
9
|
*/
|
|
9
10
|
export declare function connectTestDatabase(cwd?: string): Promise<TestDbHandle>;
|
package/dist/testkit.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
// @gaonjs/config · 테스트 DB
|
|
1
|
+
// @gaonjs/config · 테스트 DB·배터리 배선 (결정 111 · 결정 136)
|
|
2
2
|
//
|
|
3
3
|
// gaon.config 의 각 커넥션을 `<db>_test` 로 파생해 등록한다 — `gaon test` 가 미리
|
|
4
4
|
// 만들어 마이그레이션한 그 DB 다. 스캐폴드 `test/setup.ts` 가 부트스트랩으로 쓰고,
|
|
5
5
|
// 매 테스트 뒤 `truncateAll()` 로 격리한다(§9 실 인프라 · 근거는 결정 111).
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// Redis·NATS 는 배선하지 않는다(웹 전용). mail·storage·i18n 은 serve·work 와 같은
|
|
8
|
+
// 경로(configureNonDbBatteries)로 배선한다 — 스토리지·메일을 쓰는 잡·서비스가
|
|
9
|
+
// 테스트에서도 운영과 같은 코드로 돌아야 한다(결정 136 · "wireDomain 안 태워 테스트만
|
|
10
|
+
// 깨지던" 사각). 스토리지 s3 버킷은 `<bucket>-test`, 로컬 root 는 `<root>-test` 로
|
|
11
|
+
// 격리한다(<db>_test 대칭). MailPit 은 dev 이자 테스트 sink(캡처 · 외부 발송 없음).
|
|
8
12
|
import { loadGaonConfig } from './load.js';
|
|
13
|
+
import { configureNonDbBatteries } from './wire.js';
|
|
14
|
+
import { resetStorage } from '@gaonjs/storage';
|
|
15
|
+
import { resetI18n } from '@gaonjs/i18n';
|
|
9
16
|
import { createDb, registerConnection, deriveTestDatabaseConfig, destroyAllConnections, } from '@gaonjs/data';
|
|
10
17
|
/**
|
|
11
|
-
* 테스트용 DB
|
|
12
|
-
*
|
|
18
|
+
* 테스트용 DB 커넥션 + DB 외 배터리(mail·storage·i18n)를 배선한다(결정 111·136).
|
|
19
|
+
* 각 커넥션을 `<db>_test` 로 파생해 poolMax:1 로 붙이고, 스토리지·메일을 테스트
|
|
20
|
+
* 격리 값으로 배선한다. 반환값의 close() 를 afterAll 에서 호출해 정리한다.
|
|
13
21
|
*/
|
|
14
22
|
export async function connectTestDatabase(cwd = process.cwd()) {
|
|
15
23
|
const config = await loadGaonConfig(cwd);
|
|
@@ -20,9 +28,16 @@ export async function connectTestDatabase(cwd = process.cwd()) {
|
|
|
20
28
|
registerConnection(key, createDb(testCfg), base.adapter);
|
|
21
29
|
}
|
|
22
30
|
}
|
|
31
|
+
// 결정 136: 스토리지·메일·i18n 을 serve·work 와 같은 경로로 배선(테스트 격리 값).
|
|
32
|
+
await configureNonDbBatteries(config, cwd, {
|
|
33
|
+
bucketFor: (b) => `${b}-test`,
|
|
34
|
+
localRootFor: (r) => `${r}-test`,
|
|
35
|
+
});
|
|
23
36
|
return {
|
|
24
37
|
close: async () => {
|
|
25
38
|
await destroyAllConnections();
|
|
39
|
+
resetStorage();
|
|
40
|
+
resetI18n();
|
|
26
41
|
},
|
|
27
42
|
};
|
|
28
43
|
}
|
package/dist/wire.d.ts
CHANGED
|
@@ -27,6 +27,24 @@ export interface WiredDomain {
|
|
|
27
27
|
* 세션·NATS 잡 발행·Fastify)은 여기 두지 않는다 — wireGaon 이 이 위에 얹는다.
|
|
28
28
|
*/
|
|
29
29
|
export declare function wireDomain(config: GaonConfig, cwd?: string): Promise<WiredDomain>;
|
|
30
|
+
/** DB 외 배터리 배선 옵션. 테스트 하네스가 격리 값을 주입할 때 쓴다(결정 136). */
|
|
31
|
+
export interface NonDbBatteryOptions {
|
|
32
|
+
/**
|
|
33
|
+
* s3 디스크 버킷명 변환기. 테스트 하네스가 `<bucket>-test` 로 격리할 때 준다
|
|
34
|
+
* (`<db>_test` 대칭). 생략 = 원본 버킷 그대로. 로컬 디스크는 root 를 변환한다.
|
|
35
|
+
*/
|
|
36
|
+
readonly bucketFor?: (bucket: string) => string;
|
|
37
|
+
/** 로컬 디스크 root 변환기(테스트 임시 디렉터리 격리). 생략 = 원본 그대로. */
|
|
38
|
+
readonly localRootFor?: (root: string) => string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* mail · storage · i18n 을 프로세스 전역에 배선한다(결정 136). wireDomain(serve·work)
|
|
42
|
+
* 과 테스트 하네스(connectTestDatabase)가 공유하는 단일 경로다 — 스토리지·메일을
|
|
43
|
+
* 쓰는 잡·서비스가 테스트에서도 운영과 같은 코드로 돌아야 한다("wireDomain 안 태워
|
|
44
|
+
* 테스트만 깨지던" 사각 · §13.9). DB 는 여기서 다루지 않는다(테스트는 <db>_test 로,
|
|
45
|
+
* 운영은 config.db 로 각자 등록).
|
|
46
|
+
*/
|
|
47
|
+
export declare function configureNonDbBatteries(config: GaonConfig, cwd?: string, opts?: NonDbBatteryOptions): Promise<void>;
|
|
30
48
|
/**
|
|
31
49
|
* gaon.config.ts + apps/ 를 바탕으로 배터리를 배선하고 Fastify 앱을 만든다.
|
|
32
50
|
*
|
package/dist/wire.js
CHANGED
|
@@ -32,7 +32,29 @@ export async function wireDomain(config, cwd = process.cwd()) {
|
|
|
32
32
|
registerConnection(key, db, cfg.adapter);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
// 2) mail —
|
|
35
|
+
// 2~4) mail · storage · i18n — DB 외 프로세스 전역 배터리. serve·work·테스트
|
|
36
|
+
// 하네스가 공유한다(결정 136 · gaon test 도 이 경로를 태워야 스토리지·메일
|
|
37
|
+
// 잡이 테스트에서 실제로 돈다).
|
|
38
|
+
await configureNonDbBatteries(config, root);
|
|
39
|
+
return {
|
|
40
|
+
config,
|
|
41
|
+
async closeDomain() {
|
|
42
|
+
await destroyAllConnections();
|
|
43
|
+
resetStorage();
|
|
44
|
+
resetI18n();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* mail · storage · i18n 을 프로세스 전역에 배선한다(결정 136). wireDomain(serve·work)
|
|
50
|
+
* 과 테스트 하네스(connectTestDatabase)가 공유하는 단일 경로다 — 스토리지·메일을
|
|
51
|
+
* 쓰는 잡·서비스가 테스트에서도 운영과 같은 코드로 돌아야 한다("wireDomain 안 태워
|
|
52
|
+
* 테스트만 깨지던" 사각 · §13.9). DB 는 여기서 다루지 않는다(테스트는 <db>_test 로,
|
|
53
|
+
* 운영은 config.db 로 각자 등록).
|
|
54
|
+
*/
|
|
55
|
+
export async function configureNonDbBatteries(config, cwd = process.cwd(), opts = {}) {
|
|
56
|
+
const root = resolvePath(cwd);
|
|
57
|
+
// mail — SMTP 전송(개발·테스트는 MailPit sink, 운영은 실 SMTP).
|
|
36
58
|
if (config.mail) {
|
|
37
59
|
const m = config.mail;
|
|
38
60
|
configureMailer({
|
|
@@ -45,18 +67,19 @@ export async function wireDomain(config, cwd = process.cwd()) {
|
|
|
45
67
|
defaultFrom: m.defaultFrom,
|
|
46
68
|
});
|
|
47
69
|
}
|
|
48
|
-
//
|
|
70
|
+
// storage — 로컬(기본) 또는 S3. 테스트는 버킷/roots 를 격리 값으로 변환(opts).
|
|
49
71
|
if (config.storage) {
|
|
50
72
|
const disks = {};
|
|
51
73
|
for (const [name, d] of Object.entries(config.storage.disks)) {
|
|
52
74
|
if (d.driver === 'local') {
|
|
53
75
|
// root 는 프로젝트 상대 경로면 cwd 로 절대화.
|
|
54
|
-
const
|
|
76
|
+
const base = d.root.startsWith('/') ? d.root : join(root, d.root);
|
|
77
|
+
const root2 = opts.localRootFor ? opts.localRootFor(base) : base;
|
|
55
78
|
disks[name] = localDisk({ root: root2, baseUrl: d.publicUrl });
|
|
56
79
|
}
|
|
57
80
|
else {
|
|
58
81
|
disks[name] = s3Disk({
|
|
59
|
-
bucket: d.bucket,
|
|
82
|
+
bucket: opts.bucketFor ? opts.bucketFor(d.bucket) : d.bucket,
|
|
60
83
|
region: d.region,
|
|
61
84
|
endpoint: d.endpoint,
|
|
62
85
|
credentials: d.accessKeyId && d.secretAccessKey
|
|
@@ -69,7 +92,7 @@ export async function wireDomain(config, cwd = process.cwd()) {
|
|
|
69
92
|
}
|
|
70
93
|
configureStorage({ default: config.storage.default, disks });
|
|
71
94
|
}
|
|
72
|
-
//
|
|
95
|
+
// i18n — locales/ 로드 후 초기화.
|
|
73
96
|
if (config.i18n) {
|
|
74
97
|
const dir = config.i18n.dir ?? 'locales';
|
|
75
98
|
const abs = dir.startsWith('/') ? dir : join(root, dir);
|
|
@@ -80,14 +103,6 @@ export async function wireDomain(config, cwd = process.cwd()) {
|
|
|
80
103
|
supportedLngs: config.i18n.supportedLngs,
|
|
81
104
|
});
|
|
82
105
|
}
|
|
83
|
-
return {
|
|
84
|
-
config,
|
|
85
|
-
async closeDomain() {
|
|
86
|
-
await destroyAllConnections();
|
|
87
|
-
resetStorage();
|
|
88
|
-
resetI18n();
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
106
|
}
|
|
92
107
|
/**
|
|
93
108
|
* gaon.config.ts + apps/ 를 바탕으로 배터리를 배선하고 Fastify 앱을 만든다.
|
|
@@ -169,6 +184,8 @@ export async function wireGaon(config, cwd = process.cwd()) {
|
|
|
169
184
|
cookieSecret: config.web?.cookieSecret,
|
|
170
185
|
// 보안 기본값 조정(생략 시 코어 기본값 전부 켬 · CLAUDE.md 규칙 8).
|
|
171
186
|
security: config.web?.security,
|
|
187
|
+
// 결정 131: 스토리지 오리진 CSP 자동 배선(설정 있으면 img-src·connect-src 에 부착).
|
|
188
|
+
storageOrigins: storageCspOrigins(config.storage),
|
|
172
189
|
// 클라이언트 IP 정책(결정 120 · 생략 시 'direct'). 프록시 뒤 배치면 선언.
|
|
173
190
|
clientIp: config.web?.clientIp,
|
|
174
191
|
realtime: nats ? { nats, hubAddr: config.hub?.addr, heartbeatMs: config.hub?.heartbeatMs } : undefined,
|
|
@@ -197,3 +214,41 @@ export async function wireGaon(config, cwd = process.cwd()) {
|
|
|
197
214
|
}
|
|
198
215
|
// redisSessionStore 는 참조만 유지(트리 셰이킹 방지 · 향후 캐시 재사용).
|
|
199
216
|
void redisSessionStore;
|
|
217
|
+
/**
|
|
218
|
+
* 결정 131: storage 설정에서 CSP 오리진을 계산한다(s3 드라이버만). endpoint = 브라우저 직접
|
|
219
|
+
* presigned PUT/GET 오리진(connect-src) 겸 presigned 표시 폴백(img-src). publicUrl = 공개/CDN
|
|
220
|
+
* 표시 오리진(img-src). 로컬 디스크는 same-origin 상대 URL 이라 CSP 와 무관(제외). 오리진은
|
|
221
|
+
* `new URL(v).origin`(scheme+host+port) 만 — 경로·쿼리는 배제한다.
|
|
222
|
+
*/
|
|
223
|
+
function storageCspOrigins(storage) {
|
|
224
|
+
if (!storage)
|
|
225
|
+
return undefined;
|
|
226
|
+
const img = new Set();
|
|
227
|
+
const connect = new Set();
|
|
228
|
+
for (const [name, d] of Object.entries(storage.disks)) {
|
|
229
|
+
if (d.driver !== 's3')
|
|
230
|
+
continue; // 로컬 디스크(상대 baseUrl)는 same-origin — CSP 불필요
|
|
231
|
+
if (d.endpoint) {
|
|
232
|
+
const origin = parseStorageOrigin(d.endpoint, `storage.disks.${name}.endpoint`);
|
|
233
|
+
connect.add(origin);
|
|
234
|
+
img.add(origin);
|
|
235
|
+
}
|
|
236
|
+
if (d.publicUrl) {
|
|
237
|
+
img.add(parseStorageOrigin(d.publicUrl, `storage.disks.${name}.publicUrl`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (img.size === 0 && connect.size === 0)
|
|
241
|
+
return undefined;
|
|
242
|
+
return { img: [...img], connect: [...connect] };
|
|
243
|
+
}
|
|
244
|
+
/** 절대 URL 의 오리진(scheme+host+port)만 뽑는다. 비절대 URL 은 부팅 에러(수리 안내 · §7.5.3). */
|
|
245
|
+
function parseStorageOrigin(value, where) {
|
|
246
|
+
try {
|
|
247
|
+
return new URL(value).origin;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
throw new Error(`[@gaonjs/config] ${where} = ${JSON.stringify(value)} 가 절대 URL 이 아닙니다(CSP 오리진 계산 불가).\n` +
|
|
251
|
+
` → gaon.config.ts 의 storage 설정에서 scheme+host 를 갖춘 절대 URL 로 지정하세요` +
|
|
252
|
+
` (예: 'https://cdn.example.com' · 'http://127.0.0.1:9000').`);
|
|
253
|
+
}
|
|
254
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts · §3.3 · M9-A)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"ioredis": "^5.4.1",
|
|
28
28
|
"@gaonjs/async": "0.8.0",
|
|
29
|
+
"@gaonjs/core": "0.2.1",
|
|
30
|
+
"@gaonjs/data": "0.14.0",
|
|
29
31
|
"@gaonjs/i18n": "0.1.2",
|
|
30
|
-
"@gaonjs/data": "0.13.1",
|
|
31
32
|
"@gaonjs/mail": "0.1.3",
|
|
32
|
-
"@gaonjs/
|
|
33
|
-
"@gaonjs/storage": "0.1.2"
|
|
34
|
-
"@gaonjs/web": "0.13.1"
|
|
33
|
+
"@gaonjs/web": "0.14.0",
|
|
34
|
+
"@gaonjs/storage": "0.1.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"fastify": "^5.0.0"
|