@gaonjs/cli 0.47.0 → 0.55.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/commands/check.d.ts +3 -1
- package/dist/commands/check.js +44 -2
- package/dist/commands/db.js +9 -0
- package/dist/commands/gen.d.ts +2 -0
- package/dist/commands/gen.js +3 -1
- package/dist/commands/new.js +13 -0
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +8 -4
- package/dist/db/journal.js +58 -14
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +14 -14
- package/dist/db/replay.js +3 -3
- package/dist/db/resolve.d.ts +11 -1
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +17 -4
- package/dist/db.js +26 -5
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +9 -4
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/locale-parity.js +4 -1
- package/dist/doctor/render-return.d.ts +11 -0
- package/dist/doctor/render-return.js +143 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.js +16 -5
- package/dist/generate.d.ts +20 -1
- package/dist/generate.js +120 -21
- package/dist/hub.js +2 -0
- package/dist/i18n-config.d.ts +32 -0
- package/dist/i18n-config.js +170 -0
- package/dist/index.js +141 -29
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +13 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +16 -5
- package/dist/templates/auth/Dashboard.vue.tpl +3 -2
- package/dist/templates/auth/Login.vue.tpl +3 -5
- package/dist/templates/auth/Signup.vue.tpl +3 -5
- package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
- package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
- package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
- package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/Dockerfile.tpl +11 -1
- package/dist/templates/project/agents/async.md.tpl +70 -14
- package/dist/templates/project/agents/data.md.tpl +173 -52
- package/dist/templates/project/agents/frontend.md.tpl +19 -4
- package/dist/templates/project/agents/i18n.md.tpl +20 -2
- package/dist/templates/project/agents/mail.md.tpl +8 -1
- package/dist/templates/project/agents/realtime.md.tpl +25 -6
- package/dist/templates/project/agents/seal.md.tpl +8 -3
- package/dist/templates/project/agents/security.md.tpl +50 -19
- package/dist/templates/project/agents/storage.md.tpl +39 -6
- package/dist/templates/project/agents/web.md.tpl +54 -22
- package/dist/work.d.ts +3 -0
- package/dist/work.js +4 -0
- package/package.json +7 -7
|
@@ -92,7 +92,17 @@ await SendWelcomeMail.at(someDate, user.id) // 특정 시각 실행
|
|
|
92
92
|
- **옵션** — `queue`(기본 `'default'`) · `retries`(기본 3) ·
|
|
93
93
|
`curve`(백오프 곡선 ms) · `jitter` · `concurrency`.
|
|
94
94
|
- **실패** — 재시도를 소진하면 DLQ 로 간다. `gaon jobs list --failed` ·
|
|
95
|
-
`gaon jobs retry <id>` 로
|
|
95
|
+
`gaon jobs retry <id>` 로 조회·재적재한다(조회는 전량 배치 스캔 — 옛 레코드도
|
|
96
|
+
상한 없이 찾아 재적재할 수 있다 · 결정 351).
|
|
97
|
+
- **네이티브 재전달 소진도 DLQ 로 간다(결정 347).** 크래시 루프·미등록 잡(워커에
|
|
98
|
+
`domain/jobs/` 파일이 배포되지 않음)이 재전달 상한(기본 25 · `maxDeliver`)을
|
|
99
|
+
소진하면, 워커가 MAX_DELIVERIES advisory 를 받아 그 잡을 DLQ 로 이관한다 —
|
|
100
|
+
이전엔 스트림에 무신호로 영구 잔류했다. 미등록 잡의 재전달 지연은 지수
|
|
101
|
+
(1s→2s→…30s 포화)이고 잡 이름당 1회 경고를 남긴다(정상 롤링 배포 창은 통과).
|
|
102
|
+
advisory 는 비영속이라 백스톱은 best-effort 다(소진 순간 워커가 전무하면 다음
|
|
103
|
+
소진 때 회수).
|
|
104
|
+
- **큐 동시성은 큐별로 정확히 적용된다(결정 348)** — 다른 큐의 긴 잡이 이 큐의
|
|
105
|
+
처리량을 깎지 않는다(잡별 `concurrency` 선언 = 그 큐의 실제 동시 처리 수).
|
|
96
106
|
- **워커 복원력(결정 258)** — 재시도 재적재나 DLQ 이관을 하는 도중 NATS 가
|
|
97
107
|
순단해 발행 자체가 실패해도, 워커의 큐 소비 루프는 **멈추지 않는다**. 그 잡은
|
|
98
108
|
ack/DLQ 하지 않고 되돌려(재전달 백스톱) 유실을 막고, 실패는 로그로 남긴다 —
|
|
@@ -181,7 +191,10 @@ await OrderPlaced.emit({ orderId: 1n })
|
|
|
181
191
|
- 실패하면 백오프(잡과 같은 곡선 `[1s, 5s, 30s, 5m, 1h]`)로 재전달되고, 최대
|
|
182
192
|
재전달(기본 6 · 최초 포함) 소진 시 **영구 폐기**된다. 즉 계속 실패하는 이벤트는
|
|
183
193
|
약 **1시간 36분** 뒤 사라진다 — `gaon work` 가 `✗ 이벤트 폐기` 로 신호한다
|
|
184
|
-
(결정 308 · 이전엔 human 모드 무신호).
|
|
194
|
+
(결정 308 · 이전엔 human 모드 무신호). **크래시 루프**(핸들러 throw 가 아니라
|
|
195
|
+
프로세스가 ack 전에 반복 사망)로 소진돼도 MAX_DELIVERIES advisory 백스톱이
|
|
196
|
+
같은 `✗ 이벤트 폐기` 신호를 낸다(결정 398 · 잡의 결정 347 동형 · advisory 는
|
|
197
|
+
비영속이라 best-effort). 놓치면 안 되는 처리는 리스너에서 잡을
|
|
185
198
|
발행해(`.later()`) 잡의 재시도·DLQ 배터리로 넘긴다.
|
|
186
199
|
- **리스너별 순차 처리는 보장되지 않는다** — 재시도 대기 중 다음 이벤트가 먼저
|
|
187
200
|
처리될 수 있고, 드물게 같은 리스너의 두 이벤트가 겹칠 수 있다. 순서·중복에
|
|
@@ -216,22 +229,34 @@ export const PlaceOrder = service(async (input: { name: string }) => {
|
|
|
216
229
|
- 대부분의 도메인 코드는 `service()` 본문에서 emit 하거나, "커밋 후 즉시
|
|
217
230
|
발행"이면 `afterCommit(fn)`(§서비스)을 쓴다. 저수준 원시 `runInTransaction(db, fn)`
|
|
218
231
|
(커넥션을 직접 넘긴다)은 프레임웍 밖에서 트랜잭션을 손수 열 때만 쓰는 탈출구다.
|
|
219
|
-
- 릴레이(`gaon work` 내장)가
|
|
220
|
-
|
|
232
|
+
- 릴레이(`gaon work` 내장)가 아웃박스를 폴링해 발행한다(기본 폴 1000ms ·
|
|
233
|
+
배치 100). **2단계 publish(결정 346)**: 짧은 트랜잭션에서 행을 선점(`SKIP
|
|
234
|
+
LOCKED` + `claimed_at`)하고 커밋해 락을 즉시 놓은 뒤, 트랜잭션 **밖**에서
|
|
235
|
+
NATS 로 발행하고 성공 행만 `published_at` 을 찍는다 — NATS 지연·순단이 DB
|
|
236
|
+
행 락/커넥션 점유로 전파되지 않는다. 선점 리스(claim · 기본 60s ·
|
|
237
|
+
`claimTimeoutMs`)가 지나면(크래시·발행 실패) 재클레임된다 — 미발행 행은
|
|
238
|
+
어떤 경로로도 삭제되지 않으므로 유실이 없다.
|
|
221
239
|
- at-least-once — 발행 후 표시하므로 중복 가능성이 있고, dedup(msgID)이
|
|
222
|
-
|
|
240
|
+
흡수한다(claim 리스 60s < dedupe 창 120s 라 "발행 후 표시 전 크래시"
|
|
241
|
+
재발행도 창 안에서 접힌다). **"claim 리스 < dedupe 창" 은 부팅이 강제한다
|
|
242
|
+
(결정 396)** — `dedupeWindowMs` 를 claim 아래로 줄이는 오설정은 조용한 이중
|
|
243
|
+
배달이 되므로 `gaon work` 가 수리 안내와 함께 fail-loud 한다. claim 리스는
|
|
244
|
+
env `GAON_OUTBOX_CLAIM_TIMEOUT_MS`(또는 `runWork` 의 `outboxClaimTimeoutMs`)
|
|
245
|
+
로 조정한다.
|
|
223
246
|
- 아웃박스 테이블(`_gaon_outbox`)은 코어 내장이며 `gaon serve`·`gaon work`
|
|
224
247
|
기동 시 보장된다(결정 144 · nats 설정이 있을 때).
|
|
225
248
|
- 발행 완료 행은 릴레이가 **자동 정리(purge)** 한다 — 기본 7일 보존 후 삭제
|
|
226
|
-
(결정 78). 수동 cleanup 코드를 쓰지 말 것. 보존 기간·간격·폴링
|
|
227
|
-
`GAON_OUTBOX_RETENTION_MS`·`GAON_OUTBOX_PURGE_INTERVAL_MS`·`GAON_OUTBOX_RELAY_POLL_MS
|
|
249
|
+
(결정 78). 수동 cleanup 코드를 쓰지 말 것. 보존 기간·간격·폴링 주기·claim 리스는 env
|
|
250
|
+
`GAON_OUTBOX_RETENTION_MS`·`GAON_OUTBOX_PURGE_INTERVAL_MS`·`GAON_OUTBOX_RELAY_POLL_MS`·
|
|
251
|
+
`GAON_OUTBOX_CLAIM_TIMEOUT_MS`(결정 396)
|
|
228
252
|
로 조정한다(결정 312 · `GAON_WORKER_*` 와 대칭 · `gaon work` 가 읽는다). 프로그래매틱
|
|
229
|
-
경로는 `runWork()` 의 `outboxRetentionMs`·`outboxPurgeIntervalMs`·`relayPollMs
|
|
230
|
-
미발행 행은 절대 삭제되지 않는다.
|
|
231
|
-
- **발행 실패는 행 단위로 격리된다(결정 306).** 한 행의 publish 가 실패해도
|
|
232
|
-
페이로드가 NATS `max_payload` 1MiB 초과) 그 행만
|
|
233
|
-
뒤 행들은 정상 발행된다 — 한 행이 아웃박스 전체를
|
|
234
|
-
|
|
253
|
+
경로는 `runWork()` 의 `outboxRetentionMs`·`outboxPurgeIntervalMs`·`relayPollMs`·
|
|
254
|
+
`outboxClaimTimeoutMs`. 미발행 행은 절대 삭제되지 않는다.
|
|
255
|
+
- **발행 실패는 행 단위로 격리된다(결정 306 · 346).** 한 행의 publish 가 실패해도
|
|
256
|
+
(예: 페이로드가 NATS `max_payload` 1MiB 초과) 그 행만 claim 된 채 남아 리스
|
|
257
|
+
만료(60s) 후 재시도되고, 뒤 행들은 정상 발행된다 — 한 행이 아웃박스 전체를
|
|
258
|
+
조용히 세우지 않고, 실패 행 재시도도 폴링(1s) 해머링이 아니라 60s 로 자연
|
|
259
|
+
스로틀된다. 실패는 `gaon work` 에 `⚠ 아웃박스 릴레이 오류` 로 신호된다.
|
|
235
260
|
이벤트 페이로드는 1MiB 미만으로 유지한다 — 큰 데이터는 본문 대신 id 를 실어
|
|
236
261
|
리스너가 조회하게 한다.
|
|
237
262
|
|
|
@@ -284,9 +309,22 @@ export default schedule((s) => {
|
|
|
284
309
|
워커에 로드밸런싱된다. 발행은 1인, 처리는 N인.
|
|
285
310
|
- **exactly-once(발행 기준)** — 한 스케줄 틱은 리더 1인이 한 번만 발행한다.
|
|
286
311
|
리더 교체(페일오버) 순간 구·신 리더가 같은 틱을 겹쳐 발행해도, 결정론적
|
|
287
|
-
dedupe 키 + JetStream 중복 윈도우가 이를 1회로 수렴시킨다
|
|
312
|
+
dedupe 키 + JetStream 중복 윈도우가 이를 1회로 수렴시킨다 — cron 은 발화
|
|
313
|
+
분(minute) 키(결정 233), **every 는 위상 슬롯 키**(결정 394 · 아래)로 접힌다.
|
|
288
314
|
잡 자체는 재시도(백오프)가 있으니 **핸들러는 멱등**하게 짠다(같은 잡이 두 번
|
|
289
315
|
처리돼도 안전하게).
|
|
316
|
+
- **`s.every` 위상은 리더 교체를 가로질러 보존된다(결정 349).** 마지막 발화
|
|
317
|
+
시각이 KV(`gaon_scheduler`)에 남아, 새 리더는 밀렸으면 즉시 1회 발화하고
|
|
318
|
+
아니면 잔여 시간만 기다린다 — 재선출마다 타이머가 리셋돼 리스 플래핑이
|
|
319
|
+
interval 보다 잦으면 every 잡이 영영 안 돌던 기아가 없다. 리스 갱신도 순단
|
|
320
|
+
1~2회는 재시도 후에만 리더를 내려놓는다(불필요한 failover·리셋 억제).
|
|
321
|
+
- **`s.every` 의 failover 이중발화도 접힌다(결정 394).** 구 리더가 발화 직후
|
|
322
|
+
위상 기록(KV put · best-effort)을 못 남기고 죽으면 신 리더가 같은 슬롯을
|
|
323
|
+
즉시 재발화하는데, every 발화가 위상 슬롯 기반 결정적 dedupe 키
|
|
324
|
+
(`enqueueScheduled`)를 쓰므로 같은 슬롯의 재발화는 JetStream 중복 윈도우
|
|
325
|
+
안에서 1건으로 수렴한다 — 결정 349 이후 every 만 랜덤 id(`later()`)라
|
|
326
|
+
안 접히던 창을 닫았다. 사임(stop) 시 리스 키 삭제도 CAS 라 stale 리더의
|
|
327
|
+
종료가 신 리더의 키를 지우지 않는다(결정 395).
|
|
290
328
|
- **`gaon serve` 는 스케줄러를 돌리지 않는다** — 스케줄·리더 선출·아웃박스
|
|
291
329
|
릴레이는 **`gaon work` 전용**이다. 웹 프로세스는 잡을 **발행**만 할 수 있고
|
|
292
330
|
(`.later()`), 처리·스케줄은 워커가 한다. **운영에서** 스케줄이 안 도는 흔한
|
|
@@ -330,6 +368,9 @@ drain — 스케줄러 리더를 반납하고 진행 중인 잡을 완료한 뒤
|
|
|
330
368
|
- **`gaon work`** — 신규 잡 pull 을 멈추고 **진행 중 잡을 완료**한 뒤 종료한다
|
|
331
369
|
(`drainTimeoutMs` 상한 · 기본 30s). 스케줄러 리더는 즉시 반납해 다른 인스턴스가
|
|
332
370
|
승계한다. drain 상한을 넘긴 잡은 ack 되지 않아 재전달(크래시 복구)된다.
|
|
371
|
+
상한은 **큐가 동시성 포화 상태로 긴 잡을 물고 있어도** 지켜진다(결정 397 —
|
|
372
|
+
종전엔 이 경우 소비 루프 종료 대기가 상한 밖이라 stop 이 잡 완료까지 무기한
|
|
373
|
+
붙들렸다).
|
|
333
374
|
- **컨테이너 기본 워커 1** — `gaon serve` 클러스터(`--workers`)도 SIGTERM 에
|
|
334
375
|
워커들을 graceful drain 후 종료한다(결정 84).
|
|
335
376
|
|
|
@@ -441,6 +482,10 @@ async create() {
|
|
|
441
482
|
발행은 `afterCommit()` 또는 아웃박스로.
|
|
442
483
|
- **테스트에서 NATS 목업 금지** (§9) — 실 JetStream 에 접속한다
|
|
443
484
|
(`agents/testing.md`).
|
|
485
|
+
- **MySQL 은 8.0+(`explicit_defaults_for_timestamp=ON`) 전제** (결정 400) —
|
|
486
|
+
구식 설정(OFF · 5.7 기본)에서는 아웃박스 테이블의 timestamp 컬럼이 NOT NULL
|
|
487
|
+
+ zero-date 기본으로 생성돼 strict `NO_ZERO_DATE` 와 충돌할 수 있다. 관리형
|
|
488
|
+
MySQL 에서 해당 플래그가 OFF 면 ON 으로 바꿔라.
|
|
444
489
|
- **동시 실행 방지에 로컬 뮤텍스·플래그 금지** (결정 147) — `let running = false`
|
|
445
490
|
같은 프로세스 로컬 가드는 멀티 인스턴스에서 안 먹는다. `lock(key, fn)` 을
|
|
446
491
|
쓴다. 운영에서 `redis` 미설정이면 `lock()` 이 수리 안내로 throw 하니 조용한
|
|
@@ -466,4 +511,15 @@ async create() {
|
|
|
466
511
|
| 결정 308 | `gaon work` human 신호 확장(§3) — 리스너 폐기(`✗ 이벤트 폐기`)·워커 인프라 오류·재시도가 기본 모드에서 무신호이던 갭 봉합 + 리스너 재시도·폐기 계약(DLQ 없음·~1h36m) 명문화 |
|
|
467
512
|
| 결정 310 | 스케줄 대상 잡 무인자 가드(§5) — 인자 필수 잡 등록을 컴파일 타임 거부(메서드 bivariance 로 통과해 `undefined` 인자 발화하던 구멍 차단) |
|
|
468
513
|
| 결정 312 | 아웃박스 릴레이 env 튜닝(§4) — `GAON_OUTBOX_RETENTION_MS`·`GAON_OUTBOX_PURGE_INTERVAL_MS`·`GAON_OUTBOX_RELAY_POLL_MS`(`GAON_WORKER_*` 대칭) |
|
|
514
|
+
| 결정 346 | 아웃박스 2단계 publish(§4) — claim(`claimed_at`+SKIP LOCKED 짧은 tx) → 커밋 → tx 밖 publish → 표시 · NATS 지연의 DB 락 전파 제거 · claim 리스 60s(< dedupe 창) · 유실 0 |
|
|
515
|
+
| 결정 347 | max_deliver 소진 DLQ 백스톱(§1) — MAX_DELIVERIES advisory → DLQ 이관(무신호 영구 잔류 봉합) · 미등록 잡 지수 nak(1s→30s 포화)+이름당 1회 경고 · `maxDeliver` 옵션 |
|
|
516
|
+
| 결정 348 | 워커 큐별 동시성 게이트(§1) — 전역 inflight 비교가 낳던 교차 큐 간섭 제거(선언 `concurrency` = 실제 동시 처리) |
|
|
517
|
+
| 결정 349 | 리스 갱신 순단 재시도 + every 위상 KV 보존(§5) — 키가 내 것이면 revision 동기화 재시도 후에만 revoke · `gaon_scheduler` KV 로 위상 이어받기(플래핑 기아 봉합) |
|
|
518
|
+
| 결정 351 | DLQ 조회 배치 스캔(§1) — ordered 컨슈머 fetch 로 삭제 갭 서버 스킵 · findDlq 1000건 상한 제거(옛 레코드 retry 복원) |
|
|
519
|
+
| 결정 394 | every failover 이중발화 dedupe(§5) — 위상 슬롯 기반 결정적 키(`enqueueScheduled`)로 구·신 리더의 같은 슬롯 재발화가 1건으로 수렴(결정 349 가 연 창 봉합 · cron 결정 233 동형) |
|
|
520
|
+
| 결정 395 | 리스 사임 CAS 삭제(§5) — stop() 이 `previousSeq` 로 자기 revision 에서만 키 삭제 · stale 리더 종료가 신 리더 키를 지우던 재선출 순단 봉합(허브 endpoint 결정 259 동형) |
|
|
521
|
+
| 결정 396 | 아웃박스 claim<dedupe 불변식 부팅 강제(§4) — 위반 시 fail-loud(조용한 이중 배달 봉합) · claim 리스 운영 표면 `GAON_OUTBOX_CLAIM_TIMEOUT_MS`/`outboxClaimTimeoutMs` 신설 |
|
|
522
|
+
| 결정 397 | graceful drain 상한 유계화(§6) — 포화 큐의 긴 잡이 소비 루프 종료를 붙들어 `drainTimeoutMs` 가 무효이던 우회 봉합(closers 도 같은 deadline 공유 · 워커·리스너 공통) |
|
|
523
|
+
| 결정 398 | 리스너 크래시 루프 소진 신호(§3) — EVENTS 스트림 MAX_DELIVERIES advisory 백스톱으로 `dropped` 신호(잡 결정 347 동형 · 공유 스트림이라 메시지 삭제는 안 함) |
|
|
524
|
+
| 결정 400 | P3 청소 묶음 — 허브 KV 복원 오염 키 방어(try/continue) · 라인 디코더 완결 초과 라인도 onOverflow(무신호 명령 소실 금지) · MySQL timestamp 모드 함정 문서화 · listDlq 롤링 버퍼(O(limit) 메모리) · advisory dead 는 삭제 성공 워커만 신호 + 삭제 실패 잔류 관측 |
|
|
469
525
|
| §7 | 비동기 배터리 원문 (백오프 기본값 = M7 벤치마크 확정) |
|
|
@@ -25,6 +25,10 @@ export const posts = table('posts', {
|
|
|
25
25
|
**컬럼 정의가 곧 DB 타입 + TS 타입 + 검증 규칙의 단일 원천**이다
|
|
26
26
|
(v0.15 §4.2 line 388 원문). 세 곳에 따로 쓰지 않는다.
|
|
27
27
|
|
|
28
|
+
**PK 컬럼 키는 언제나 `id`** 다(The One Way · 결정 322) — `t.id()`/`t.uuidPk()` 를 다른
|
|
29
|
+
키(`userId: t.uuidPk()` 등)에 달면 `model()` 정의 시점에 수리 안내로 throw 한다.
|
|
30
|
+
단건 경로(`find`·`rec.update`/`delete`·원자 프리미티브)가 `id` 컬럼을 사용한다.
|
|
31
|
+
|
|
28
32
|
### 1.1 관계 선언 (M2D · 결정 33)
|
|
29
33
|
|
|
30
34
|
관계는 **두 자리**에 나뉘어 산다 — FK 컬럼을 **가진 쪽**은 컬럼으로,
|
|
@@ -180,8 +184,15 @@ export const logs = table('logs', {
|
|
|
180
184
|
```
|
|
181
185
|
|
|
182
186
|
- 인덱스 원소가 **문자열 배열**이면 기존과 100% 동일(btree). **객체**면 `cols`·`expr`·`using`·`where`·`name`.
|
|
187
|
+
- 이름 규약 `idx_<table>_<cols>` 는 method/partial 을 구분하지 않는다 — **같은 컬럼에 두 인덱스**
|
|
188
|
+
(예: 컬럼 `.index({where})` + 테이블 레벨 `[['col']]`)를 선언하면 이름이 충돌해 **정의 시점에
|
|
189
|
+
throw** 한다(결정 329). 테이블 레벨 객체의 `name` 으로 구분한다.
|
|
183
190
|
- 프레임웍이 만든 인덱스는 **`idx_` 접두**만 관리한다 — raw(수동 튜닝) 인덱스는 diff 가 건드리지
|
|
184
|
-
않으니(drop 계획 안 함), 손수 만든 특수 인덱스는 `idx_` 아닌 이름으로 둔다.
|
|
191
|
+
않으니(drop 계획 안 함), 손수 만든 특수 인덱스는 `idx_` 아닌 이름으로 둔다. 같은 이유로
|
|
192
|
+
**테이블 레벨 객체의 명시 `name` 은 `idx_` 접두 필수**다 — 접두 밖 이름은 introspection 에
|
|
193
|
+
안 잡혀 매 diff 마다 재생성 대상이 되므로 **정의 시점에 throw** 한다(결정 373). 인덱스
|
|
194
|
+
이름은 **커넥션(스키마) 전역 유일**이라 서로 다른 테이블의 같은 명시 name 도 diff 진입에서
|
|
195
|
+
throw 한다(결정 382).
|
|
185
196
|
- **MySQL/MariaDB(legacy §4.5) 커넥션은 gin/brin/gist·partial·표현식 인덱스가 없다** — 선언하면
|
|
186
197
|
`gaon db migrate` 가 **명확히 실패**한다(조용히 btree 로 떨구지 않음). 이런 인덱스는 main(postgres)에.
|
|
187
198
|
|
|
@@ -209,12 +220,19 @@ export const logs = table('logs', {
|
|
|
209
220
|
`dropPartition`·`listPartitions`. **retention(오래된 파티션 파기)은 절대 자동으로 하지 않는다** —
|
|
210
221
|
`keep` 을 명시하고 그 잡을 실행할 때만 지운다(결정 39 자동 DROP 금지 정신).
|
|
211
222
|
- MySQL/MariaDB 커넥션은 선언적 파티셔닝을 지원하지 않는다(migrate 시 fail-loud).
|
|
223
|
+
- **기존 테이블의 `partitionBy` 변경(추가·제거·전략/키 변경)은 diff/migrate 가 즉시 throw**
|
|
224
|
+
한다(결정 321) — PostgreSQL 은 기존 테이블을 ALTER 로 파티션 테이블로 못 바꾼다. 전환은
|
|
225
|
+
손작성 마이그로 "새 파티션 테이블 생성 → `INSERT ... SELECT` 이관 → rename 교체" 순서다.
|
|
212
226
|
|
|
213
227
|
**`t.timestamps()` 와 `update()`** (결정 276): `update()` 는 `updatedAt` 을 **자동으로 현재 시각으로
|
|
214
228
|
갱신**한다(patch 에 `updatedAt` 을 직접 주면 그 값을 존중). 값 변경 없이 시각만 올리려면 `touch()`.
|
|
215
|
-
|
|
216
|
-
충돌-갱신
|
|
217
|
-
|
|
229
|
+
**벌크·upsert 도 자동 갱신한다**(결정 323 · 286 유보 해소) — `updateAll()` 과 `upsert()` 의
|
|
230
|
+
충돌-갱신 경로가 `updatedAt` 을 현재 시각으로 함께 갱신한다. "update 하면 updatedAt 이
|
|
231
|
+
바뀐다" 가 단건/벌크 구분 없이 하나다(The One Way). patch/update 에 `updatedAt` 을 명시하면
|
|
232
|
+
그 값을 존중한다. **원자 프리미티브는 단건/벌크 모두 대상 밖**(결정 375 — 323 이 벌크 증감만
|
|
233
|
+
포함하던 비대칭 해소): `rec.increment`/`decrement`/`toggle` 은 물론 `incrementAll`/
|
|
234
|
+
`decrementAll` 도 카운터·플래그 노이즈가 updatedAt 의 "콘텐츠 갱신" 신호를 덮지 않게
|
|
235
|
+
갱신하지 않는다(필요하면 `touch()` 병행).
|
|
218
236
|
|
|
219
237
|
### 4. 체이닝 전체 (`packages/data/src/model.ts`)
|
|
220
238
|
|
|
@@ -281,7 +299,7 @@ patch 에 `updatedAt: new Date()` 를 직접 넣는다(결정 286 유보 — 벌
|
|
|
281
299
|
| 메서드 | 시그니처 | 반환 | 비고 |
|
|
282
300
|
|---|---|---|---|
|
|
283
301
|
| `create` | `(data)` | `Promise<Rec>` | `t.id()`·`t.timestamps()`·`.default()` 컬럼은 입력에서 선택적 (`InsertOf`) |
|
|
284
|
-
| `batchInsert` | `(rows)` | `Promise<BulkResult>` | **벌크 삽입** (M2F · 결정 35 → BulkResult 결정 90) — 여러 row 를 **단일 INSERT 문**(원자적)으로 · `{ count }`(삽입 행 수) 반환. 빈 배열은 DB 무접촉 `{ count: 0 }`. beforeCreate 훅은 각 row 에
|
|
302
|
+
| `batchInsert` | `(rows)` | `Promise<BulkResult>` | **벌크 삽입** (M2F · 결정 35 → BulkResult 결정 90) — 여러 row 를 **단일 INSERT 문**(원자적)으로 · `{ count }`(삽입 행 수) 반환. 빈 배열은 DB 무접촉 `{ count: 0 }`. beforeCreate 훅은 각 row 에 적용. **프리페어드 파라미터 상한(65,535)을 넘는 대량 배치는 자동 분할**되고 전체가 한 트랜잭션(원자성 유지 · 결정 325) |
|
|
285
303
|
| `insertOrIgnore` | `(row \| rows)` | `Promise<BulkResult>` | **멱등 삽입** (M2F) — 충돌 행은 건너뛰고 `count` = **실제로 삽입된 행 수**(PG=`ON CONFLICT DO NOTHING` · MySQL=`INSERT IGNORE`). 충돌 대상 인자 없음(어떤 UNIQUE/PK 든 충돌 시 건너뜀) |
|
|
286
304
|
| `upsert` | `(row \| rows, { onConflict?, update? })` | `Promise<BulkResult>` | **있으면 갱신 없으면 삽입** (M2F) — PG=`ON CONFLICT DO UPDATE` · MySQL=`ON DUPLICATE KEY UPDATE`. `count` = 처리된 입력 행 수(삽입+갱신 · 갱신 대상 없으면 실 삽입 수). `onConflict` 생략 = 기본 키(`id`) 자동(없으면 throw) · `update` 생략(`'exclude'`) = 입력 컬럼에서 충돌 기준·`id` 뺀 나머지를 덮음 |
|
|
287
305
|
| `find` | `(id)` | `Promise<Rec>` | 없으면 **throw** — undefined 를 허용하려면 `where('id', '=', id).first()` |
|
|
@@ -454,7 +472,7 @@ export const PlaceOrder = service(async (input: { userId: string; total: number
|
|
|
454
472
|
- **왜 `afterCommit`** — main 이 롤백되면 analytics 기록도 일어나지 않아야 한다.
|
|
455
473
|
`afterCommit` 은 커밋이 성공한 경우에만 콜백을 돈다(§9 · `agents/async.md`).
|
|
456
474
|
|
|
457
|
-
### 7.1 문서형 컬렉션 — `collection()` (v1.1 · `@gaonjs/adapter-mongo` · 결정 278~282)
|
|
475
|
+
### 7.1 문서형 컬렉션 — `collection()` (v1.1 · `@gaonjs/adapter-mongo` · 결정 278~282 · 288~292 · 331~336)
|
|
458
476
|
|
|
459
477
|
SQL `model()`(Kysely) 옆에 문서형 동사 `collection()` 을 **Mongoose** 위에 둔다.
|
|
460
478
|
로그·이벤트·감사·분석처럼 **문서·유연 스키마·대량 append** 용도다. **`model()` 은
|
|
@@ -468,33 +486,54 @@ npm i @gaonjs/adapter-mongo mongoose
|
|
|
468
486
|
|
|
469
487
|
```ts
|
|
470
488
|
// domain/schema/auditLog.ts — mongoSchema() 는 진짜 Mongoose Schema 를 반환한다.
|
|
471
|
-
// 정본
|
|
489
|
+
// 정본 패턴(결정 288·332): Doc + Methods + Model 인터페이스를 선언하고 제네릭 3개를
|
|
490
|
+
// 명시한다(mongoose 공식 타입드 statics 패턴). statics/methods 는 **옵션으로** 선언 —
|
|
491
|
+
// AuditLog.recent(50)·doc.summary() 가 캐스트 없이 컴파일된다.
|
|
472
492
|
import { collection, mongoSchema } from 'gaonjs/data'
|
|
493
|
+
import type { Model } from 'mongoose'
|
|
473
494
|
|
|
474
495
|
interface AuditDoc {
|
|
475
496
|
actorId: string
|
|
476
497
|
action: string
|
|
477
498
|
ip?: string // hidden 이어도 서버 코드는 투명하게 읽는다
|
|
499
|
+
createdAt?: Date // { timestamps: true } 산출
|
|
500
|
+
updatedAt?: Date
|
|
501
|
+
}
|
|
502
|
+
interface AuditMethods {
|
|
503
|
+
summary(): string
|
|
504
|
+
}
|
|
505
|
+
interface AuditModel extends Model<AuditDoc, {}, AuditMethods> {
|
|
506
|
+
recent(limit: number): Promise<AuditDoc[]>
|
|
478
507
|
}
|
|
479
508
|
|
|
480
|
-
const auditLogSchema = mongoSchema<AuditDoc>({
|
|
509
|
+
const auditLogSchema = mongoSchema<AuditDoc, AuditModel, AuditMethods>({
|
|
481
510
|
actorId: { type: String, required: true, index: true },
|
|
482
511
|
action: { type: String, required: true },
|
|
483
512
|
ip: { type: String, hidden: true }, // 직렬화 경계 제외 (SQL .hidden() 과 동일 의미)
|
|
484
|
-
}, {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
}
|
|
513
|
+
}, {
|
|
514
|
+
timestamps: true,
|
|
515
|
+
statics: {
|
|
516
|
+
async recent(limit: number) {
|
|
517
|
+
return (await this.find().sort({ createdAt: -1 }).limit(limit).lean()) as AuditDoc[]
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
methods: {
|
|
521
|
+
summary() { return `${this.actorId}:${this.action}` },
|
|
522
|
+
},
|
|
523
|
+
})
|
|
490
524
|
|
|
491
525
|
// db: 'logs' — 몽고 커넥션 바인딩 (SQL 의 { db: 'legacy' } 와 대칭)
|
|
492
526
|
export const AuditLog = collection('audit_logs', auditLogSchema, { db: 'logs' })
|
|
527
|
+
|
|
528
|
+
// 사용감 — 전부 캐스트 없이 타입이 흐른다(결정 332).
|
|
529
|
+
// const logs = await AuditLog.recent(50)
|
|
530
|
+
// const doc = await AuditLog.create({ actorId: 'u1', action: 'login' }); doc.summary()
|
|
493
531
|
```
|
|
494
532
|
|
|
495
|
-
|
|
496
|
-
결정 288 이
|
|
497
|
-
|
|
533
|
+
statics/methods 가 없으면 제네릭 없이 `mongoSchema({...})` 만으로 정의 지점 추론이
|
|
534
|
+
성립한다(`new mongoose.Schema` 와 동일 · 결정 288 이 생성자 제네릭 복제로 고정).
|
|
535
|
+
statics 가 필요하면 **반드시 위 3-제네릭 패턴** — 제네릭 없이 statics 만 옵션에 넣는
|
|
536
|
+
추론 조합은 mongoose 업스트림 타이핑이 깨져 있어(바닐라도 동일 실측) 지원하지 않는다.
|
|
498
537
|
|
|
499
538
|
```ts
|
|
500
539
|
// gaon.config.ts — 문서형 커넥션은 adapter: 'mongodb' 로 SQL 과 나란히 선언한다.
|
|
@@ -512,11 +551,14 @@ export default defineConfig({
|
|
|
512
551
|
`_id`(ObjectId)는 응답 경계에서 자동으로 string 이 된다(결정 282 · 결정 37 문서형
|
|
513
552
|
대응). `create()` 반환 문서·`doc.toObject()` 를 그대로 넘겨도 직렬화 경계가
|
|
514
553
|
방어한다(결정 289 — save/insertMany 훅 + toObject/toJSON transform 마커 + 응답
|
|
515
|
-
경계의 문서 → POJO 정규화).
|
|
516
|
-
|
|
554
|
+
경계의 문서 → POJO 정규화). **중첩 경로도 지원**한다(결정 336 · dot-path 마커):
|
|
555
|
+
`profile: { ssn: { type: String, hidden: true } }` → `profile.ssn` 만 제외(형제
|
|
556
|
+
보존), 서브도큐먼트 배열 `items: [{ secret: {...hidden} }]` → 요소별 `secret` 제외,
|
|
557
|
+
배열 요소 def 자체의 hidden(`tags: [{ type: String, hidden: true }]`) → 배열 통째
|
|
558
|
+
제외. 지원 밖 위치(옵션 객체의 형제 키 아래 등)는 조용히 무시하지 않고 fail-loud.
|
|
517
559
|
- **몽고 쓰기를 SQL `service()` tx 안에서 하지 말 것** — 몽고는 v1 에서 트랜잭션이
|
|
518
560
|
없어, tx 안 몽고 쓰기는 `MongoCrossConnectionWriteError` 로 **막힌다**(조용한 부분
|
|
519
|
-
커밋 방지 · 결정 281). "커밋 후 로그" 는 `afterCommit` 으로 잇는다(§7 크로스커넥션과 동일 규율):
|
|
561
|
+
커밋 방지 · 결정 281 · 331). "커밋 후 로그" 는 `afterCommit` 으로 잇는다(§7 크로스커넥션과 동일 규율):
|
|
520
562
|
```ts
|
|
521
563
|
export const SignUp = service(async (input) => {
|
|
522
564
|
const user = await Users.create(input) // main(SQL) 트랜잭션
|
|
@@ -526,35 +568,61 @@ export default defineConfig({
|
|
|
526
568
|
```
|
|
527
569
|
- **SQL ↔ 문서형 관계 금지** — `belongsTo`/`hasMany` 는 SQL 전용이다. 몽고 컬렉션끼리의
|
|
528
570
|
참조는 Mongoose `ref`/`populate` 로 사용자가 직접 한다(Gaon doctor 가 강제하지 않음).
|
|
529
|
-
- **인덱스 = Mongoose 스키마 선언**(`index: true` / `schema.index()`) —
|
|
530
|
-
`autoIndex`
|
|
531
|
-
|
|
532
|
-
절차에서 `
|
|
571
|
+
- **인덱스 = Mongoose 스키마 선언**(`index: true` / `schema.index()`) — dev·test 는
|
|
572
|
+
mongoose `autoIndex` 기본값이 첫 사용 시 인덱스를 만든다. **운영(NODE_ENV=production)
|
|
573
|
+
은 autoIndex 기본 false**(결정 333 · §8-3) — 부팅·첫 사용 시점의 암묵 인덱스 빌드가
|
|
574
|
+
없다. 운영 인덱스는 배포 절차에서 `syncMongoIndexes()`(gaonjs/data 재수출)로 명시
|
|
575
|
+
동기화한다(커넥션 키 지정 가능 · 스키마에 없는 인덱스는 드랍). `collection()`
|
|
576
|
+
정의는 자동 등록되므로 **아직 한 번도 안 쓴 컬렉션도 sync 가 강제 컴파일해
|
|
577
|
+
포함**한다(결정 365 — 이전엔 이미 쓴 모델만 순회해 부팅 직후 sync 가 무신호
|
|
578
|
+
no-op 였다 · 컴파일된 컬렉션이 0개면 경고). 커넥션 설정
|
|
579
|
+
`autoIndex: true/false` 명시가 항상 우선. 몽고는 마이그레이션이 없다 —
|
|
533
580
|
`gaon db diff/migrate`(키 생략 = 전 커넥션 순회)는 mongodb 커넥션을 **건너뛰고
|
|
534
581
|
skipped 로 보고**하며(결정 292), `--db <mongo키>` 명시 지정만 fail-loud 다.
|
|
582
|
+
- **문서형 커넥션은 DB 명이 필수** — `url` 에 경로(`mongodb://…/myapp`)를 넣거나
|
|
583
|
+
`database` 를 지정한다. 둘 다 없으면 부팅이 수리 안내로 throw 한다(결정 335 —
|
|
584
|
+
드라이버 기본 DB `test` 로 조용히 붙던 무신호 오배선 봉합). **경로 없는 url**
|
|
585
|
+
(`mongodb://host:27017`·`mongodb+srv://cluster`·`…/?authSource=admin`)도 단독으로는
|
|
586
|
+
같은 이유로 throw 하며, `database` 를 병기하면 그 값이 `dbName` 으로 우선한다
|
|
587
|
+
(결정 364 — url 경로와 `database` 병기 시에도 `database` 가 이긴다).
|
|
535
588
|
|
|
536
589
|
**알려진 함정**:
|
|
537
590
|
- `aggregate` 결과는 임의 projection(그룹·계산)이라 `hidden` 마커를 심지 **않는다** —
|
|
538
591
|
SQL `Post.query()`(Kysely 원본) raw 탈출구가 hidden 을 우회하는 것과 동형. 문서를
|
|
539
592
|
안전하게 렌더에 흘리려면 `find()/findOne().lean()` 을 쓴다.
|
|
540
|
-
- **크로스커넥션 쓰기
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
593
|
+
- **크로스커넥션 쓰기 가드는 allowlist 반전이다(결정 331 · 281 확장)** — SQL tx 안에서
|
|
594
|
+
는 읽기 전용으로 확인된 내장(find/findOne/countDocuments/aggregate 읽기 등)만
|
|
595
|
+
통과하고, **그 밖의 모든 static 호출은 fail-closed** 로 막힌다. Query 체이닝 쓰기
|
|
596
|
+
(`find(f).updateMany()`·`where(f).deleteMany()`)와 `aggregate` 의 `$out`/`$merge` 도
|
|
597
|
+
실행 지점 pre 훅이 막는다. **읽기 전용이어도 커스텀 static 은 tx 안에서 막힌다**
|
|
598
|
+
(쓰기 여부 판정 불가 — 내장 읽기를 직접 쓰거나 호출을 tx 밖으로). **네이티브
|
|
599
|
+
탈출구 프로퍼티(`Model.collection`·`Model.db`·`Model.base`)도 tx 안 접근 자체가
|
|
600
|
+
막힌다**(결정 372 — 함수 가드를 우회해 `collection.insertOne` 무가드 쓰기가
|
|
601
|
+
가능하던 구멍 봉합). 유일한 예외는
|
|
602
|
+
인스턴스 `doc.save()`(정본 예외 · 미가드). tx 안에서 쓸 일이면 `afterCommit`.
|
|
603
|
+
- **같은 커넥션·같은 컬렉션명에 다른 스키마를 선언하면 첫 접근에서 throw**(결정 334)
|
|
604
|
+
— 조용히 첫 스키마를 재사용하지 않는다. 같은 컬렉션이면 mongoSchema 산출을 한
|
|
605
|
+
모듈에서 export 해 재사용하라(스키마 객체 하나).
|
|
551
606
|
- **`service()` 는 SQL 전용** — `service(fn, { db: '<mongo키>' })` 는 SQL 레지스트리
|
|
552
607
|
조회라 "커넥션 미등록" 에러가 난다(안내 문구도 SQL 기준). 몽고 쓰기에는 서비스
|
|
553
608
|
트랜잭션 개념이 없다(v1 몽고 tx 없음) — 그냥 static 으로 쓰거나 SQL 서비스의
|
|
554
609
|
`afterCommit` 에서 쓴다.
|
|
555
610
|
- **mongoSchema 제네릭**: v1.21(adapter-mongo 0.1.0) 이하에서는 제네릭을 생략하면
|
|
556
611
|
문서 타입이 `{ actorId: StringConstructor }` 로 **조용히 붕괴**했다 — 0.2.0(결정
|
|
557
|
-
288)부터 정의 지점 추론이
|
|
612
|
+
288)부터 정의 지점 추론이 성립한다. statics/methods 타입이 필요하면 3-제네릭 정본
|
|
613
|
+
패턴(결정 332)만 쓴다 — 제네릭 없는 statics 옵션 추론은 mongoose 업스트림 타이핑이
|
|
614
|
+
깨져 있다(바닐라 동일 실측 · 지원 안 함).
|
|
615
|
+
- **중첩 hidden 은 v1.22(adapter-mongo 0.2.0) 이하에서 fail-loud throw 였다**(결정
|
|
616
|
+
290) — 0.3.0(결정 336)부터 dot-path 로 실지원된다. 지원 밖 위치(옵션 객체의 형제 키
|
|
617
|
+
아래 등)의 hidden 은 여전히 fail-loud(조용한 유출 금지). **서브스키마 인스턴스
|
|
618
|
+
(`mongoSchema` 산출을 필드/`type:`/배열에 임베드)의 내부 hidden 도 부모가 경로
|
|
619
|
+
접두로 승계한다**(결정 387 — 이전엔 미수집이라 `.lean()` 경로 무신호 유출이었다).
|
|
620
|
+
순정 `new mongoose.Schema` 임베드는 마커가 없어 승계 대상이 아니다 — hidden 이
|
|
621
|
+
필요한 서브스키마는 mongoSchema 로 만들라.
|
|
622
|
+
- **`schema.set('toObject', {...})` 는 hidden 마커 transform 을 통째로 대체한다**
|
|
623
|
+
(mongoose set 의 wholesale 교체) — 응답 경계가 문서 인스턴스 마커를 상속 스레딩해
|
|
624
|
+
hidden 유출은 막지만(결정 371 방어), virtuals 등 옵션이 필요하면 mongoSchema 의
|
|
625
|
+
`toObject` **옵션 인자로** 주는 쪽이 안전하다(transform 이 체이닝된다 · 결정 289).
|
|
558
626
|
|
|
559
627
|
### 8. 모델 정의 (`model()`) (`packages/data/src/model.ts`)
|
|
560
628
|
|
|
@@ -728,9 +796,12 @@ const top = await Post.published().latest().limit(5).withCache(30).all()
|
|
|
728
796
|
신선도가 중요하면 **짧은 TTL** 을 쓰거나 `cache.forget(key)` 로 명시
|
|
729
797
|
무효화한다. `.withCache` 결과는 TTL 만료로만 갱신된다(자동 퍼지 없음).
|
|
730
798
|
- **적용 지점은 `Chain` 의 `all()`/`first()` 뿐이다** — `.withCache()` 뒤에
|
|
731
|
-
`include()`/`select()`/`distinct(col)
|
|
732
|
-
캐시가
|
|
733
|
-
|
|
799
|
+
`include()`/`select()`/`distinct(col)`/`groupBy()`/`withCount()`/`join()`/`paginate()`
|
|
800
|
+
가 오면 캐시가 적용될 수 없어 **즉시 throw** 한다(수리 안내 포함 · 결정 330 — 이전엔
|
|
801
|
+
조용히 미적용). **종단 집계(`count()`/`exists()`/`sum()`/`avg()`/`min()`/`max()`/
|
|
802
|
+
`pluck()`)와 벌크 쓰기(`updateAll()` 류)도 같은 이유로 throw** 한다(결정 370 · 330
|
|
803
|
+
완결). 캐시가 필요하면 include/select 없는 형태로 `all()`/`first()` 마감하거나,
|
|
804
|
+
결과 조립 전체를 `cache.remember` 로 감싼다.
|
|
734
805
|
- **키는 호출자가 정한다** — 로케일·사용자 등 변이 축은 **키에 직접 넣는다**
|
|
735
806
|
(`` `page:${locale}` ``). 프레임웍이 변이 축을 자동으로 섞지 않는다.
|
|
736
807
|
- 백엔드는 Redis 가 기본(설정에 `redis` 있으면 자동), 메모리는 dev/테스트/폴백.
|
|
@@ -859,7 +930,10 @@ diff/migrate/status/seed 는 `--db` 를 생략하면 **등록된 전 커넥션
|
|
|
859
930
|
migrate 가 자동 적용**한다(예: `email` 에 `.unique()` 추가 → `ADD CONSTRAINT
|
|
860
931
|
… UNIQUE` 생성). **제거(제약·인덱스·기본값을 스키마에서 뗌)는 자동 적용하지
|
|
861
932
|
않고** dropTable 처럼 크게 알린다 — 제거는 `gaon db diff` 의 down SQL 을 보고
|
|
862
|
-
손작성 마이그로 적용한다(수동/외부 제약 보호 · 결정 39 정합).
|
|
933
|
+
손작성 마이그로 적용한다(수동/외부 제약 보호 · 결정 39 정합). 단 **재생성
|
|
934
|
+
쌍**(인덱스 method 변경·enum 값 변경이 내는 같은 이름의 drop→add)은 제거가
|
|
935
|
+
아니라 교체이므로 migrate 가 **한 단위로 자동 적용**한다(결정 366 — 이전엔
|
|
936
|
+
drop 반쪽만 걸러져 add 가 "already exists" 로 매번 실패했다). **한계**: 임의
|
|
863
937
|
`.check(expr)` 의 **표현식만** 바꾸는 변경(같은 컬럼·같은 제약, 식만 수정)은
|
|
864
938
|
Postgres 가 표현식을 정규화해 신뢰 비교가 불가능하므로 **감지하지 못한다** —
|
|
865
939
|
이 경우 컬럼을 갈거나 손작성 마이그로 CHECK 를 drop→add 한다. (enum 값 변경과
|
|
@@ -870,7 +944,9 @@ diff/migrate/status/seed 는 `--db` 를 생략하면 **등록된 전 커넥션
|
|
|
870
944
|
미검출(undershoot)이 안전하다는 원칙 — 값 변경이 필요하면 손작성 마이그로 `ALTER
|
|
871
945
|
COLUMN … SET DEFAULT` 를 쓴다. (기본값 **추가/제거**·스칼라(bool·정수·문자열·enum)
|
|
872
946
|
값 변경은 정상 감지된다.) **FK(belongsTo 의 REFERENCES)는 diff 축이 아니다** — FK 는
|
|
873
|
-
createTable/addColumn 시점에만
|
|
947
|
+
createTable/addColumn 시점에만 생성되므로(mysql 도 addColumn 이 FK 를 결정적 이름
|
|
948
|
+
`fk_<table>_<col>` 로 동반하고, `migrate down` 롤백이 그 FK 를 선-drop 한다 · 결정 327·369),
|
|
949
|
+
**기존 컬럼**을 `t.belongsTo()` 로 바꾸거나
|
|
874
950
|
belongsTo 를 떼도 컬럼 shape(bigint)가 같아 감지되지 않는다(FK 가 조용히 미생성/잔존).
|
|
875
951
|
기존 컬럼에 FK 를 걸려면 손작성 마이그로 `ADD CONSTRAINT … FOREIGN KEY` 를 쓴다.
|
|
876
952
|
이 제약·기본값 diff 는 postgres 커넥션 기준이다
|
|
@@ -888,7 +964,9 @@ diff/migrate/status/seed 는 `--db` 를 생략하면 **등록된 전 커넥션
|
|
|
888
964
|
|
|
889
965
|
- **시그니처**: `seed(fn: () => Promise<void> | void): SeedDef` — `gaonjs/data`
|
|
890
966
|
에서 import 한다. 본문(`fn`)은 **모델을 그대로** 쓴다 — 모델이 커넥션을 자동
|
|
891
|
-
바인딩하므로(§4.5·§7) 시드는 커넥션을 몰라도 된다.
|
|
967
|
+
바인딩하므로(§4.5·§7) 시드는 커넥션을 몰라도 된다. `gaon db seed` 는 선언된
|
|
968
|
+
**전 SQL 커넥션을 등록하고 시드를 1회 실행**한다(결정 367 — 여러 커넥션의
|
|
969
|
+
모델을 한 시드에서 섞어 써도 된다 · 문서형(mongodb) 커넥션은 열지 않는다).
|
|
892
970
|
- **멱등하게 짠다** — 시드는 재적재에 자주 쓰이므로 여러 번 돌려도 안전해야
|
|
893
971
|
한다(예: `upsert`/존재 확인 후 생성).
|
|
894
972
|
- `gaon db seed` 는 모델 레이어를 거치므로 **프로젝트 로컬로 실행**하는 것이
|
|
@@ -1034,13 +1112,18 @@ await Post.upsert({ id, title, body }) // onConflict 생략 = 기
|
|
|
1034
1112
|
안전(오타 방지)이고, 값은 op 에 맞는 타입이다. `whereAny` 로 표현 못 하는 복합 논리(컬럼별
|
|
1035
1113
|
다른 op·중첩 그룹)는 `Post.query()` Kysely 탈출구(§5)로 내려간다 — `whereGroup` 같은 범용
|
|
1036
1114
|
그룹핑 API 는 없다(선택지 증식 회피 · 결정 118).
|
|
1037
|
-
-
|
|
1038
|
-
`updateAll
|
|
1039
|
-
|
|
1040
|
-
`
|
|
1041
|
-
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1115
|
+
- **원자 프리미티브(단건·벌크)는 `updatedAt` 을 갱신하지 않는다** (결정 276·323·375) —
|
|
1116
|
+
`update`/`updateAll`/`upsert` 충돌-갱신은 자동 갱신하지만, `rec.increment`/`decrement`/
|
|
1117
|
+
`toggle` 과 `incrementAll`/`decrementAll` 은 카운터 축이라 대상 밖이다. 갱신 시각까지
|
|
1118
|
+
남기려면 `touch()` 를 병행하거나 patch 로 갱신한다.
|
|
1119
|
+
- **hidden 컬럼을 `pluck()`/`groupBy()` 로 뽑은 값은 render props 에 싣지 말 것** (결정 379) —
|
|
1120
|
+
스칼라 배열·그룹 행은 런타임 마커(HIDDEN_COLUMNS)를 실을 수 없어 serializeProps 가 못
|
|
1121
|
+
떨군다(결정 283 은 행 단위). 타입 축은 `Serialized` 가 그 값을 **never** 로 접어 신호한다 —
|
|
1122
|
+
서버측 내부 검증·비교 용도로만 쓴다.
|
|
1123
|
+
- **`.withCache()` 는 `Chain` 의 `all()`/`first()` 에만 적용된다** (결정 148·330·370 · §8.2) —
|
|
1124
|
+
뒤에 `include()`/`select()`/`distinct(col)`/`paginate()` 류 전이는 물론 **종단 집계
|
|
1125
|
+
(`count()`/`pluck()` 등)·벌크 쓰기(`updateAll()` 류)도 즉시 throw** 한다(조용한
|
|
1126
|
+
미적용 대신 수리 안내). include/select 없는 형태로 마감하거나
|
|
1044
1127
|
`cache.remember` 로 감싼다.
|
|
1045
1128
|
- **캐시를 "쓰면 자동으로 지워진다"고 기대하면 함정** (결정 148) — `cache`·`.withCache`
|
|
1046
1129
|
는 **자동 무효화가 없다**. `create` 후에도 같은 `cache.remember`/`.withCache` 키는 TTL
|
|
@@ -1077,13 +1160,51 @@ await Post.upsert({ id, title, body }) // onConflict 생략 = 기
|
|
|
1077
1160
|
| 결정 278 | 문서형 동사 `collection()`(Mongoose) — SQL `model()` 과 분리 · `mongoSchema()` 얇은 래퍼(판별 태그+hidden 전개) · `@gaonjs/adapter-mongo`(mongoose optional peer)(§7.1) |
|
|
1078
1161
|
| 결정 283 | `select()`/`distinct(col)` 좁은 행에도 hidden 마커 부착 — 명시 select 한 hidden 컬럼의 조용한 직렬화 유출 봉합(§3 `.hidden()`) |
|
|
1079
1162
|
| 결정 285 | nullable FK 의 belongsTo 관계는 `Row \| null` — 지연·include 모두 null 정규화(타입 green NPE 봉합 · §1.1) |
|
|
1080
|
-
| 결정 286 | `updateAll()` 도 mysql jsonb 직렬화(F3 확장) · 벌크 `updatedAt` 자동
|
|
1163
|
+
| 결정 286 | `updateAll()` 도 mysql jsonb 직렬화(F3 확장) · 벌크 `updatedAt` 자동 갱신 유보 → **결정 323 으로 해소** |
|
|
1164
|
+
| 결정 321 | 기존 테이블 `partitionBy` 변경은 diff/migrate 즉시 throw(조용한 no-op 제거 · 손작성 이관 안내 · §3) |
|
|
1165
|
+
| 결정 322 | PK 컬럼 키 `id` 강제 — `model()` 정의 시점 수리 안내 throw(§1) |
|
|
1166
|
+
| 결정 323 | `updateAll`·`upsert` 충돌-갱신도 `updatedAt` 자동 갱신 — 벌크 증감 포함은 **결정 375 로 제외 재결정**(§3) |
|
|
1167
|
+
| 결정 325 | `batchInsert` 파라미터 상한(65,535) 초과 자동 분할 — 전체 한 트랜잭션(원자성 유지 · §4) |
|
|
1168
|
+
| 결정 327 | mysql `addColumn` 이 belongsTo FK 를 같은 ALTER 문으로 동반(§10) |
|
|
1169
|
+
| 결정 328 | `distinct(col).paginate` 의 total = 선택 컬럼 distinct 축(§4) |
|
|
1170
|
+
| 결정 329 | 인덱스 이름 충돌(같은 컬럼 method/partial 중복) 정의 시점 throw — `name` 으로 구분(§3) |
|
|
1171
|
+
| 결정 330 | `.withCache()` 무효 전이(include/select/paginate 류) 즉시 throw(§8.2·함정) |
|
|
1172
|
+
| 결정 364 | 문서형 url 에 DB 경로 없으면 throw · url+`database` 병기 시 `database`(dbName) 우선(335 완결 · §7.1) |
|
|
1173
|
+
| 결정 365 | `collection()` 정의 자동 등록 — `syncMongoIndexes()` 가 미사용 컬렉션도 강제 컴파일 후 동기화(§7.1) |
|
|
1174
|
+
| 결정 366 | 재생성 쌍(인덱스 method·enum 값 변경의 같은 이름 drop→add)은 defer 예외 — migrate 가 한 단위 자동 적용(§10) |
|
|
1175
|
+
| 결정 367 | `gaon db seed` = 전 SQL 커넥션 등록 + 1회 실행 — 멀티커넥션 시드의 '미등록' 실패 봉합(§10.1) |
|
|
1176
|
+
| 결정 368 | 캐시 코덱 bytea(Uint8Array·Buffer) 태그 왕복 — `.withCache` 히트의 조용한 값 손상 봉합(§8.2) |
|
|
1177
|
+
| 결정 369 | mysql `addColumn` FK 결정적 이름(`fk_<table>_<col>`) + `migrate down` 이 FK 선-drop(327 의 down 대칭 · §10) |
|
|
1178
|
+
| 결정 370 | `.withCache()` 뒤 종단 집계(count/pluck 류)·벌크 쓰기도 throw(330 완결 · §8.2·함정) |
|
|
1179
|
+
| 결정 371 | 응답 경계가 문서 인스턴스 마커를 상속 스레딩 — 사용자 `schema.set('toObject')` transform 대체에도 hidden 유출 없음(§7.1 함정) |
|
|
1180
|
+
| 결정 372 | 네이티브 탈출구 프로퍼티(`Model.collection`/`db`/`base`)도 tx 안 접근 fail-closed(331 완결 · §7.1) |
|
|
1181
|
+
| 결정 373 | 테이블 레벨 명시 인덱스 `name` 은 `idx_` 접두 필수 — 정의 시점 throw(329 완결 · §3) |
|
|
1182
|
+
| 결정 374 | `gaon db migrate` 가 applyStatements 경유 — 비호환 타입 변경 실패에 USING 수리 안내(F5 정본 경로 배선 · §10) |
|
|
1183
|
+
| 결정 375 | 원자 증감은 단건/벌크 모두 `updatedAt` 대상 밖(323 비대칭 해소 — 카운터 축 · §3) |
|
|
1184
|
+
| 결정 376 | 몽고 allowlist 읽기 4종(applyVirtuals·applyTimestamps·createSearchIndexes·clientEncryption) 편입 · 죽은 count 제거(§7.1) |
|
|
1185
|
+
| 결정 377 | mysql MODIFY 가 대상 기본값을 재선언 — 기존 DEFAULT 조용한 소거 봉합(§10) |
|
|
1186
|
+
| 결정 378 | eager hasOne 빈 값 = undefined 통일(타입 계약 정합 · §6) |
|
|
1187
|
+
| 결정 379 | 객체 키 밖 hidden 브랜드 값(pluck/groupBy hiddenCol)은 `Serialized` 가 never 로 신호(§4 함정) |
|
|
1188
|
+
| 결정 380 | journal applied_at 프로세스 내 단조 증가 + mysql 원장 timestamp(6)·mediumtext 승격 — 롤백 대상 오선정·64KB 절단 봉합(§10) |
|
|
1189
|
+
| 결정 381 | 파티션 헬퍼 스키마 한정(to_regclass·자식 스키마 반환·한정 drop) + 월 bound UTC 명시(§3) |
|
|
1190
|
+
| 결정 382 | 인덱스 이름 교차 테이블 전역 유일 검사 — diff 진입 fail-loud(329·373 완결 · §3) |
|
|
1191
|
+
| 결정 383 | DDL 식별자 인용 `"` 이스케이프 위생(§10) |
|
|
1192
|
+
| 결정 384 | partitionBy 불일치·mongo 키 안내문 방언/실동작 정정(321·333 문구 · §3·§7.1) |
|
|
1193
|
+
| 결정 385 | 단일 값 enum CHECK(`= 'a'::text` 정규화형)도 값 diff — 1개→N개 확장 감지(§10) |
|
|
1194
|
+
| 결정 386 | collection() Proxy 함수 identity 캐시 — `Coll.on === Coll.on`(off 해제 패턴 성립 · §7.1) |
|
|
1195
|
+
| 결정 387 | 서브스키마 임베드 내부 hidden 을 부모가 경로 접두로 승계 — `.lean()` 무신호 유출 봉합(§7.1) |
|
|
1081
1196
|
| 결정 279 | 문서형 커넥션 `adapter: 'mongodb'`(config `db` 맵) · `isTableDef` 가 collection 제외(tables.d.ts·doctor) · `gaon db` mongo 마이그 fail-loud(§7.1) |
|
|
1082
1197
|
| 결정 281 | 몽고 쓰기를 SQL `service()` tx 안에서 하면 `MongoCrossConnectionWriteError` 로 막힘 — "커밋 후 로그" 는 `afterCommit`(§7.1 · 결정 221 동형) |
|
|
1083
1198
|
| 결정 282 | ObjectId(`_id` 포함) → string 직렬화 정규화(응답 경계 · 결정 37 문서형 대응 · `.lean()` 권장)(§7.1) |
|
|
1084
1199
|
| 결정 288 | `mongoSchema()` 제네릭 = Mongoose Schema 생성자 복제 — 자작 시그니처의 문서 타입 조용한 붕괴(StringConstructor) 봉합 · 정본 = 명시 인터페이스(§7.1) |
|
|
1085
1200
|
| 결정 289 | 문서 인스턴스 경로 hidden 마커 전파 — save/insertMany 훅 + toObject/toJSON transform + 응답 경계 문서→POJO 정규화(§7.1) |
|
|
1086
|
-
| 결정 290 | 중첩 경로 `hidden: true` 는 fail-loud throw — top-level 전용
|
|
1087
|
-
| 결정 291 | 쓰기 가드에 `insertOne`(mongoose 8.16+)·`bulkSave` 편입
|
|
1201
|
+
| 결정 290 | 중첩 경로 `hidden: true` 는 fail-loud throw — top-level 전용 → **결정 336 이 실지원으로 대체**(§7.1) |
|
|
1202
|
+
| 결정 291 | 쓰기 가드에 `insertOne`(mongoose 8.16+)·`bulkSave` 편입 → **결정 331 allowlist 반전으로 흡수**(§7.1 · 결정 281 확장) |
|
|
1088
1203
|
| 결정 292 | `gaon db` 전 커넥션 순회는 mongodb 를 skip + skipped 보고(exit 0) — `--db <mongo키>` 명시만 fail-loud(§7.1) |
|
|
1204
|
+
| 결정 331 | 쓰기 가드 allowlist 반전 — 읽기 전용 내장만 SQL tx 통과 · Query 체이닝/aggregate $out·$merge 는 실행 지점 pre 훅 · 커스텀 static 도 tx 안 fail-closed · 예외 = doc.save()(§7.1) |
|
|
1205
|
+
| 결정 332 | statics/methods 타입 통로 — Doc+Methods+Model 인터페이스 3-제네릭 정본 · collection() 반환이 mongoose.model() 오버로드 복제(캐스트 0)(§7.1) |
|
|
1206
|
+
| 결정 333 | 운영(NODE_ENV=production) autoIndex 기본 false + `syncMongoIndexes()` 명시 동기화 표면(§7.1 · §8-3) |
|
|
1207
|
+
| 결정 334 | 같은 커넥션·같은 이름·다른 스키마 재선언은 첫 접근 throw — 조용한 첫 스키마 재사용 봉합(§7.1) |
|
|
1208
|
+
| 결정 335 | 문서형 커넥션 url·database 둘 다 없으면 부팅 fail-loud — 드라이버 기본 `test` DB 무신호 접속 봉합(§7.1) |
|
|
1209
|
+
| 결정 336 | 중첩 hidden dot-path 실지원(290 대체) — 중첩 객체·type:{} 서브도큐먼트·서브도큐먼트 배열 · 직렬화 경계 상속 스레딩 · 지원 밖 위치는 fail-loud(§7.1) |
|
|
1089
1210
|
| E-4 | 컬럼 타입·수식어·체이닝 확장 · `Post.query()` 정정 · Serialized 명명 |
|
|
@@ -138,10 +138,19 @@ async function search(q: string) {
|
|
|
138
138
|
}
|
|
139
139
|
```
|
|
140
140
|
- **CSRF** — 세션 앱은 상태 변경 요청(POST/PUT/PATCH/DELETE)에 CSRF 토큰을
|
|
141
|
-
자동으로 `X-CSRF-Token` 헤더에 붙인다(손수 넘길 필요 없음 · 결정 166).
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
자동으로 `X-CSRF-Token` 헤더에 붙인다(손수 넘길 필요 없음 · 결정 166·341). 출처는
|
|
142
|
+
**라이브 Inertia 페이지 props 의 csrf**(결정 341 · `useShared().csrf` 와 같은 단일
|
|
143
|
+
출처 · `packages/vue/src/csrf.ts`) — 로그인(세션 재생성 · 결정 254) 후에도 항상
|
|
144
|
+
최신이다. 최초 문서 data-page 와 `<meta name="csrf-token">` 은 부팅 전 폴백.
|
|
145
|
+
`useForm`/`router` 제출도 같은 자동 부착을 받는다(결정 342 — 페이지가 `_csrf`
|
|
146
|
+
바디·수동 헤더를 싣지 않는다 · `agents/security.md`). **명시 헤더는 존중된다
|
|
147
|
+
(탈출구 · 결정 388)**: `api(key, params, { headers: { 'X-CSRF-Token': ... } })` 나
|
|
148
|
+
`useForm().post(url, { headers: ... })` 로 토큰을 직접 실으면(대소문자 무관)
|
|
149
|
+
자동 부착이 그 값을 덮지 않는다.
|
|
150
|
+
- **파라미터 배열(결정 343)** — `api('web:posts#index', { tags: ['a', 'b'] })` 처럼
|
|
151
|
+
배열 값을 넘길 수 있다. GET 은 중복 키(`tags=a&tags=b` · 서버 `this.params` 의
|
|
152
|
+
"중복 키 = 배열" 규칙과 대칭), 본문은 문자열화된 JSON 배열로 실린다. `:param`
|
|
153
|
+
자리표시자엔 스칼라만(배열이면 수리 안내 throw).
|
|
145
154
|
|
|
146
155
|
### 3. bigint PK 식별자 — 컨트롤러에서 `String()` 정규화 (결정 37)
|
|
147
156
|
|
|
@@ -523,6 +532,12 @@ async function runSearch(q: string) {
|
|
|
523
532
|
| 결정 303 | `useChannel` 계약 3정비 — `send()` OPEN 아니면 `false` · 컴포넌트 밖 = 즉시 접속(정리는 호출자) · `maxMessages` 상한 (`agents/realtime.md` §4) |
|
|
524
533
|
| 결정 304 | `api()` 실패 타입 공개 — `ApiError`·`isApiError`·`ApiValidationBody`(422 issues 타입드) (§2) |
|
|
525
534
|
| 결정 305 | `createGaonApp` 마운트 대상(`#app`) 부재 = 수리 안내 throw(조용한 빈 화면 봉합) |
|
|
535
|
+
| 결정 341 | CSRF 토큰 출처 = 라이브 Inertia 페이지 props(`csrf.ts` · 최초 문서·meta 는 부팅 전 폴백) — 로그인 세션 재생성 후 api() 403 stale 봉합(§2) |
|
|
536
|
+
| 결정 342 | `useForm`/`router` 상태 변경에 `X-CSRF-Token` 자동 부착(라이브 출처 · api() 와 단일화) — `_csrf` 수동 보일러플레이트 제거 · 명시 헤더 존중 · 우회 전송은 `readCsrfToken()` 탈출구 |
|
|
537
|
+
| 결정 343 | `ApiParams` 배열 지원 — GET 중복 키·본문 JSON 배열(서버 중복 키=배열 규칙 대칭) · `:param` 은 스칼라만(§2) |
|
|
538
|
+
| 결정 344 | `useChannel` 함수형 `params` — 접속·재접속 시점마다 평가(JWT access_token 회전 반영 · `agents/realtime.md` §4) |
|
|
539
|
+
| 결정 345 | `PageMap` 항목 타입 의도 문서화(`PageMapEntry` — 지연 로더·eager 모듈 · vite glob unknown 제약 명기) |
|
|
540
|
+
| 결정 388 | `api()` 명시 `X-CSRF-Token` 헤더 존중(대소문자 무관 · 인터셉터와 대칭 — 덮어쓰기·콤마 병합 403 봉합) · `:param` 누락 에러 예시를 라우트 키 형태로 정정(§2) |
|
|
526
541
|
| E-3 §C | 타입드 `api()` 클라이언트 (routes.d.ts 브리지 재사용) |
|
|
527
542
|
|
|
528
543
|
## `@gaonjs/seal` 켠 앱의 프론트
|
|
@@ -71,7 +71,7 @@ export default defineConfig({
|
|
|
71
71
|
i18n: {
|
|
72
72
|
fallbackLng: 'ko',
|
|
73
73
|
supportedLngs: ['ko', 'en', 'ja'], // 생략 시 locales/ 폴더 하위 언어들
|
|
74
|
-
// dir: 'locales', // 생략 시 'locales'
|
|
74
|
+
// dir: 'locales', // 생략 시 'locales' — 타입 축·doctor 도 이 값을 따른다(결정 352)
|
|
75
75
|
// detect: { cookieName: 'gaon_locale', priority: ['session', 'cookie', 'header'] }, // 기본값
|
|
76
76
|
},
|
|
77
77
|
})
|
|
@@ -200,7 +200,8 @@ CSS 가 올바른 언어를 안다). 비-i18n 프로젝트는 템플릿 정적
|
|
|
200
200
|
|
|
201
201
|
### 7. 로케일 커버리지 — `locale-parity` 경고 (결정 216)
|
|
202
202
|
|
|
203
|
-
`messages.d.ts` 의 키 유니온은 **기준 로케일 하나**에서 나온다(§4
|
|
203
|
+
`messages.d.ts` 의 키 유니온은 **기준 로케일 하나**에서 나온다(§4 · 기준 = `fallbackLng` — 결정 352 ·
|
|
204
|
+
이전엔 알파벳순 첫 로케일이라 컴파일 보증이 fallback 체인과 다른 로케일에 정박했다). 그래서 어떤 키를
|
|
204
205
|
`ko.json`·`en.json` 에는 넣고 `ja.json` 에만 빠뜨리면 **컴파일은 통과**하고, 런타임에
|
|
205
206
|
일본어 사용자만 fallback(대개 다른 언어) 번역을 조용히 본다 — 타입도 화면도 못 잡는
|
|
206
207
|
사각이다. `gaon doctor`/`gaon check` 의 `locale-parity`(§2.2 27번)가 `locales/` 의
|
|
@@ -236,6 +237,18 @@ export const SendDigest = job(async ({ userId, locale }: { userId: bigint; local
|
|
|
236
237
|
|
|
237
238
|
## 알려진 함정
|
|
238
239
|
|
|
240
|
+
- **`i18n.dir`·`fallbackLng` 는 문자열 리터럴로 쓴다**(결정 412) — 변수 참조·env 표현식은
|
|
241
|
+
타입 축(`.gaon/messages.d.ts`)과 doctor 가 정적으로 못 읽어 기본값(`locales` · 정렬 첫
|
|
242
|
+
로케일)으로 떨어진다. 못 읽으면 `gaon gen`·`dev`·`check` 가 한 줄 경고를 낸다(무소음 아님).
|
|
243
|
+
치환 없는 템플릿 리터럴(`` `locales` ``)·`satisfies`/`as` 래핑은 읽는다.
|
|
244
|
+
- **절대 경로 `dir` 도 지원된다**(결정 412) — 런타임과 CLI 축이 같은 규약으로 해석한다.
|
|
245
|
+
- **`gaon check` 가 부팅 조건을 먼저 본다**(결정 413) — 카탈로그가 비었거나
|
|
246
|
+
`fallbackLng`/`supportedLngs` 가 지목한 파일이 없으면 check 가 실패한다(종전엔 check 는
|
|
247
|
+
통과하고 `gaon serve` 만 죽었다 · 결정 353).
|
|
248
|
+
- **`supportedLngs: []`(빈 배열)은 "미지정" 과 같다**(결정 414) — 종전엔 협상이 조용히
|
|
249
|
+
꺼져 항상 fallback 이 나갔다. 실제로 제한하려면 언어를 채우고, 제한이 없으면 키를 뺀다.
|
|
250
|
+
|
|
251
|
+
|
|
239
252
|
- **`i18n` 설정이 없으면 `t()` 는 항상 fallback** — 요청별 로케일 협상은
|
|
240
253
|
`gaon.config.ts` 에 `i18n` 이 있어야 배선된다(결정 159). 설정만 하면 자동.
|
|
241
254
|
- **키를 손으로 `string` 으로 넓히지 말 것** — `.gaon/messages.d.ts`(결정 158)가
|
|
@@ -264,3 +277,8 @@ export const SendDigest = job(async ({ userId, locale }: { userId: bigint; local
|
|
|
264
277
|
| 결정 213 | Vue 클라 소비 = 서버 주도 render props/sharedProps 만(§5) · `t()`·`useT()` 클라 미노출(서버 ALS 전용) · sharedProps 는 결정 150 동형 |
|
|
265
278
|
| 결정 214 (13차 W2) | 최초 문서 셸 `<html lang>` 이 요청 협상 로케일 자동 추종(§6) · 템플릿 정적값은 기본값 · 비-i18n 무회귀 |
|
|
266
279
|
| 결정 216 (13차 W4) | `locale-parity` doctor 경고(§7) — 로케일 간 키 부분 누락 = fallback 조용 노출 · 기준 로케일 유니온의 사각 |
|
|
280
|
+
| 결정 352 | messages.d.ts 기준 로케일 = `fallbackLng`(정적 분석 전달) · `i18n.dir` 을 타입 축(gen/dev/check)·locale-parity 가 존중(하드코딩 'locales' 제거) |
|
|
281
|
+
| 결정 353 | 부팅 fail-loud — i18n 설정 + 빈 카탈로그(전 화면 raw 키 방지) · `fallbackLng`/`supportedLngs` 가 지목한 로케일 파일 통째 부재(조용한 fallback 언어 대체 방지) 는 부팅 에러 + 수리 안내 |
|
|
282
|
+
| 결정 412 | i18n config 정적 분석 범위 확대(템플릿 리터럴·satisfies/as·shorthand) + **미해석 경고**(무소음 제거) · 절대 경로 `dir` 을 런타임과 같은 규약으로 해석 |
|
|
283
|
+
| 결정 413 | 결정 353(부팅 fail-loud) 조건을 `gaon check` 가 정적으로 미리 검사(check green → serve 크래시 사각 제거) |
|
|
284
|
+
| 결정 414 | messages 축을 gen/check 보고에 포함 · 생성 키 이스케이프 · stale `messages.d.ts` 정리 · `supportedLngs: []` 를 미지정과 일원화 · 협상 언어 순서 결정론화 |
|