@gaonjs/config 0.11.0 → 0.13.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 CHANGED
@@ -1,9 +1,10 @@
1
1
  export interface TestDbHandle {
2
- /** 등록한 모든 커넥션을 종료한다(afterAll 에서 호출). */
2
+ /** 등록한 모든 커넥션을 종료하고 배터리를 리셋한다(afterAll 에서 호출). */
3
3
  close(): Promise<void>;
4
4
  }
5
5
  /**
6
- * 테스트용 DB 커넥션을 등록한다(결정 111). 각 커넥션을 `<db>_test` 로 파생해
7
- * poolMax:1 붙인다. 반환값의 close() afterAll 에서 호출해 정리한다.
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,24 @@
1
- // @gaonjs/config · 테스트 DB 커넥션 배선 (결정 111)
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
- // wireGaon 과 달리 Redis·NATS·mail 배선하지 않는다 DB 격리만 담당한다.
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';
16
+ import { connectNats, configureJobs, configureEvents, configureBroadcast, resetJobs, resetEvents, resetBroadcast, } from '@gaonjs/async';
9
17
  import { createDb, registerConnection, deriveTestDatabaseConfig, destroyAllConnections, } from '@gaonjs/data';
10
18
  /**
11
- * 테스트용 DB 커넥션을 등록한다(결정 111). 각 커넥션을 `<db>_test` 로 파생해
12
- * poolMax:1 붙인다. 반환값의 close() afterAll 에서 호출해 정리한다.
19
+ * 테스트용 DB 커넥션 + DB 외 배터리(mail·storage·i18n)를 배선한다(결정 111·136).
20
+ * 커넥션을 `<db>_test` 파생해 poolMax:1 붙이고, 스토리지·메일을 테스트
21
+ * 격리 값으로 배선한다. 반환값의 close() 를 afterAll 에서 호출해 정리한다.
13
22
  */
14
23
  export async function connectTestDatabase(cwd = process.cwd()) {
15
24
  const config = await loadGaonConfig(cwd);
@@ -20,9 +29,33 @@ export async function connectTestDatabase(cwd = process.cwd()) {
20
29
  registerConnection(key, createDb(testCfg), base.adapter);
21
30
  }
22
31
  }
32
+ // 결정 136: 스토리지·메일·i18n 을 serve·work 와 같은 경로로 배선(테스트 격리 값).
33
+ await configureNonDbBatteries(config, cwd, {
34
+ bucketFor: (b) => `${b}-test`,
35
+ localRootFor: (r) => `${r}-test`,
36
+ });
37
+ // 결정 143(12차 W5): 잡·이벤트 전송을 serve·work 와 같은 경로로 배선한다. 이게 없으면
38
+ // 테스트에서 도는 도메인 서비스가 `.later()`·emit() 를 할 때 "전송 미설정" 으로 깨졌다 —
39
+ // 운영과 같은 코드가 테스트에서만 실패하는 사각(11차 W4 연장). 스트림/subject 격리는
40
+ // GAON_STREAM_PREFIX(결정 130 · gaon test 가 세팅)로 자동 적용된다.
41
+ let nats;
42
+ if (config.nats) {
43
+ nats = await connectNats({ servers: config.nats.url, name: config.nats.name ?? 'gaon-test' });
44
+ configureJobs(nats);
45
+ configureEvents(nats);
46
+ configureBroadcast(nats); // 결정 126: 잡·리스너에서 broadcast() 사용.
47
+ }
23
48
  return {
24
49
  close: async () => {
25
50
  await destroyAllConnections();
51
+ resetStorage();
52
+ resetI18n();
53
+ // 잡·이벤트·broadcast 배선 해제 후 NATS 종료(다음 테스트 파일로 상태 누출 방지).
54
+ resetJobs();
55
+ resetEvents();
56
+ resetBroadcast();
57
+ if (nats)
58
+ await nats.close();
26
59
  },
27
60
  };
28
61
  }
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
@@ -7,8 +7,8 @@
7
7
  // 세션은 세션 옵션이 세팅된 앱에만 등록된다. AppSessionOptions.secret 은
8
8
  // 필수이므로 auth 앱은 명시적으로 secret 을 제공해야 한다(env 로 주입 권장).
9
9
  import { Redis } from 'ioredis';
10
- import { createDb, registerConnection, destroyAllConnections, } from '@gaonjs/data';
11
- import { connectNats, configureJobs } from '@gaonjs/async';
10
+ import { createDb, registerConnection, destroyAllConnections, getConnection, hasConnection, setServiceTxWrapper, } from '@gaonjs/data';
11
+ import { connectNats, configureJobs, configureEvents, outboxServiceTxWrapper, ensureOutboxTable, } from '@gaonjs/async';
12
12
  import { configureMailer, smtpTransport } from '@gaonjs/mail';
13
13
  import { configureStorage, localDisk, s3Disk, resetStorage } from '@gaonjs/storage';
14
14
  import { configureI18n, loadLocales, resetI18n } from '@gaonjs/i18n';
@@ -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 — SMTP 전송(개발은 MailPit, 운영은 SMTP).
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
- // 3) storage — 로컬(기본) 또는 S3.
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 root2 = d.root.startsWith('/') ? d.root : join(root, d.root);
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
- // 4) i18n — locales/ 로드 후 초기화.
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 앱을 만든다.
@@ -110,15 +125,22 @@ export async function wireGaon(config, cwd = process.cwd()) {
110
125
  if (config.redis) {
111
126
  redis = new Redis(config.redis.url, { lazyConnect: false, maxRetriesPerRequest: 3 });
112
127
  }
113
- // 3) NATS — 웹 프로세스도 채널·잡 publish 를 위해 연결한다.
114
- // 연결만으로는 부족하다 — 전송 런타임(configureJobs)까지 배선해야
115
- // 컨트롤러의 `.later()` 실제로 publish 된다. 이걸 빠뜨리면 웹에서
116
- // 발행 "잡 전송이 설정되지 않았습니다" 로 깨진다(work 는 runWork
117
- // 가 이미 설정하지만 serve 는 여기서 해야 한다).
128
+ // 3) NATS — 웹 프로세스도 채널·잡·이벤트 publish 를 위해 연결한다.
129
+ // 연결만으로는 부족하다 — 전송 런타임까지 배선해야 컨트롤러·서비스의
130
+ // `.later()`·`emit()` 실제로 publish 된다(work runWork 가 이미 설정하지만
131
+ // serve 여기서 해야 한다).
118
132
  let nats;
119
133
  if (config.nats) {
120
134
  nats = await connectNats({ servers: config.nats.url, name: config.nats.name ?? 'gaon-serve' });
121
135
  configureJobs(nats);
136
+ // 결정 144(F2a): 이벤트 전송 배선 — 이게 없으면 웹 요청 emit() 이 "전송 미설정" 500.
137
+ configureEvents(nats);
138
+ // 결정 144(A): 서비스 트랜잭션 emit → 같은 트랜잭션으로 _gaon_outbox 적재(이중 쓰기
139
+ // 방지). @gaonjs/async 의 아웃박스 래퍼를 data 의 service() 트랜잭션에 주입하고,
140
+ // 적재 대상 테이블을 main 커넥션에 보장한다(worker 의 릴레이가 그 행을 발행한다).
141
+ setServiceTxWrapper(outboxServiceTxWrapper());
142
+ if (hasConnection('main'))
143
+ await ensureOutboxTable(getConnection('main'));
122
144
  }
123
145
  // 5) 앱 스캔 · createApp.
124
146
  const apps = await resolveApps(root);
@@ -193,6 +215,8 @@ export async function wireGaon(config, cwd = process.cwd()) {
193
215
  await nats.close();
194
216
  if (redis)
195
217
  redis.disconnect();
218
+ // 결정 144: 서비스 트랜잭션 래퍼 배선 해제(다음 wireGaon 인스턴스로 누출 방지).
219
+ setServiceTxWrapper(undefined);
196
220
  await domain.closeDomain();
197
221
  },
198
222
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/config",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Gaon 루트 설정 로더·자동 배선 (gaon.config.ts · §3.3 · M9-A)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,13 +25,13 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "ioredis": "^5.4.1",
28
- "@gaonjs/async": "0.8.0",
29
- "@gaonjs/data": "0.13.1",
28
+ "@gaonjs/async": "0.9.0",
30
29
  "@gaonjs/core": "0.2.1",
31
- "@gaonjs/storage": "0.1.2",
30
+ "@gaonjs/i18n": "0.1.2",
31
+ "@gaonjs/data": "0.15.0",
32
32
  "@gaonjs/mail": "0.1.3",
33
- "@gaonjs/web": "0.14.0",
34
- "@gaonjs/i18n": "0.1.2"
33
+ "@gaonjs/storage": "0.1.2",
34
+ "@gaonjs/web": "0.15.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "fastify": "^5.0.0"