mega-framework 0.1.6 → 0.1.7
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/bin/mega-ws-hub.js +2 -2
- package/package.json +32 -8
- package/sample/crud/.env +1 -1
- package/sample/crud/.env.example +1 -1
- package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
- package/sample/crud/.mega/journal/snapshot.json +261 -0
- package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
- package/sample/crud/apps/main/controllers/web-controller.js +7 -5
- package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
- package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
- package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
- package/sample/crud/apps/main/models/log-partition-model.js +105 -0
- package/sample/crud/apps/main/models/note-model.js +79 -0
- package/sample/crud/apps/main/models/user-level-model.js +24 -0
- package/sample/crud/apps/main/models/user-model.js +146 -0
- package/sample/crud/apps/main/models/user-type-model.js +21 -0
- package/sample/crud/apps/main/models/wallet-model.js +24 -0
- package/sample/crud/apps/main/routes/users.js +55 -10
- package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
- package/sample/crud/apps/main/services/auth-service.js +39 -24
- package/sample/crud/apps/main/services/log-partition-service.js +101 -0
- package/sample/crud/apps/main/services/note-service.js +6 -6
- package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
- package/sample/crud/apps/main/services/user-service.js +62 -21
- package/sample/crud/apps/main/views/auth/login.ejs +6 -6
- package/sample/crud/apps/main/views/auth/register.ejs +46 -5
- package/sample/crud/apps/main/views/users/edit.ejs +42 -5
- package/sample/crud/apps/main/views/users/list.ejs +6 -2
- package/sample/crud/apps/main/views/users/new.ejs +56 -4
- package/sample/crud/docs/log_partition_design.mm.md +23 -0
- package/sample/crud/mega.config.js +3 -2
- package/sample/crud/package.json +1 -1
- package/sample/crud/scripts/start-ws-hub.sh +2 -2
- package/sample/simple/package.json +2 -2
- package/src/adapters/adapter-manager.js +2 -1
- package/src/adapters/adapter-options.js +30 -0
- package/src/adapters/maria-adapter.js +26 -3
- package/src/adapters/mega-db-adapter.js +7 -1
- package/src/adapters/mongo-adapter.js +19 -1
- package/src/adapters/postgres-adapter.js +25 -2
- package/src/adapters/sqlite-adapter.js +20 -1
- package/src/cli/commands/new.js +13 -3
- package/src/cli/commands/scaffold.js +137 -33
- package/src/cli/generators/index.js +82 -2
- package/src/cli/index.js +353 -100
- package/src/core/ajv-mapper.js +27 -2
- package/src/core/boot.js +464 -245
- package/src/core/cluster-metrics.js +13 -4
- package/src/core/ctx-builder.js +6 -2
- package/src/core/envelope.js +112 -12
- package/src/core/hub-link.js +65 -4
- package/src/core/i18n.js +11 -1
- package/src/core/index.js +6 -2
- package/src/core/mega-app.js +201 -463
- package/src/core/mega-cluster.js +4 -1
- package/src/core/mega-server.js +40 -9
- package/src/core/migration/dialect-registry.js +107 -0
- package/src/core/migration/dialects/README.md +62 -0
- package/src/core/migration/dialects/maria.js +496 -0
- package/src/core/migration/dialects/mongo.js +824 -0
- package/src/core/migration/dialects/postgres.js +563 -0
- package/src/core/migration/dialects/sqlite.js +476 -0
- package/src/core/migration/differ.js +456 -0
- package/src/core/migration/generate.js +508 -0
- package/src/core/migration/journal.js +167 -0
- package/src/core/migration/model-scan.js +84 -0
- package/src/core/migration/mongo-migration-db.js +97 -0
- package/src/core/migration/schema-builder.js +400 -0
- package/src/core/migration/schema-validator.js +315 -0
- package/src/core/migration-lock.js +205 -0
- package/src/core/migration-runner.js +166 -38
- package/src/core/multipart.js +28 -5
- package/src/core/pipeline.js +129 -0
- package/src/core/router.js +70 -65
- package/src/core/security.js +67 -9
- package/src/core/workers-manager.js +12 -1
- package/src/core/ws-cluster.js +10 -3
- package/src/core/ws-message.js +48 -4
- package/src/core/ws-presence.js +624 -0
- package/src/core/ws-roster.js +4 -1
- package/src/core/ws-upgrade.js +118 -12
- package/src/index.js +1 -1
- package/src/lib/hub-protocol.js +29 -0
- package/src/lib/mega-health.js +25 -4
- package/src/lib/mega-job-queue.js +98 -21
- package/src/lib/mega-job.js +29 -0
- package/src/lib/mega-metrics.js +3 -12
- package/src/lib/mega-plugin.js +34 -3
- package/src/lib/mega-schedule.js +40 -22
- package/src/lib/mega-shutdown.js +114 -39
- package/src/lib/mega-tracing.js +66 -19
- package/src/lib/mega-worker.js +5 -1
- package/src/lib/otel-resource.js +36 -0
- package/src/{cli → lib}/ws-hub.js +51 -8
- package/src/models/crud-sql-builder.js +133 -0
- package/src/models/mega-model.js +82 -2
- package/src/models/model-crud.js +483 -0
- package/src/models/mongo-crud.js +285 -0
- package/templates/model/code-mongo.tpl +35 -0
- package/templates/model/code.tpl +15 -1
- package/templates/model/test-mongo.tpl +38 -0
- package/templates/model/test.tpl +4 -0
- package/types/adapters/adapter-manager.d.ts +95 -0
- package/types/adapters/adapter-options.d.ts +91 -0
- package/types/adapters/file-adapter.d.ts +94 -0
- package/types/adapters/file-session-adapter.d.ts +101 -0
- package/types/adapters/index.d.ts +20 -0
- package/types/adapters/maria-adapter.d.ts +115 -0
- package/types/adapters/mega-adapter.d.ts +215 -0
- package/types/adapters/mega-bus-adapter.d.ts +45 -0
- package/types/adapters/mega-cache-adapter.d.ts +47 -0
- package/types/adapters/mega-db-adapter.d.ts +47 -0
- package/types/adapters/mega-lock-adapter.d.ts +62 -0
- package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
- package/types/adapters/mega-session-adapter.d.ts +32 -0
- package/types/adapters/mongo-adapter.d.ts +139 -0
- package/types/adapters/nats-adapter.d.ts +108 -0
- package/types/adapters/postgres-adapter.d.ts +139 -0
- package/types/adapters/redis-adapter.d.ts +70 -0
- package/types/adapters/redis-session-adapter.d.ts +82 -0
- package/types/adapters/redlock-adapter.d.ts +149 -0
- package/types/adapters/registry.d.ts +46 -0
- package/types/adapters/sqlite-adapter.d.ts +106 -0
- package/types/auth/index.d.ts +24 -0
- package/types/cli/commands/console-cmd.d.ts +37 -0
- package/types/cli/commands/new.d.ts +16 -0
- package/types/cli/commands/routes.d.ts +36 -0
- package/types/cli/commands/scaffold.d.ts +78 -0
- package/types/cli/commands/test-cmd.d.ts +14 -0
- package/types/cli/generators/index.d.ts +112 -0
- package/types/cli/index.d.ts +249 -0
- package/types/cli/template-engine.d.ts +40 -0
- package/types/core/ajv-mapper.d.ts +27 -0
- package/types/core/boot.d.ts +233 -0
- package/types/core/cluster-metrics.d.ts +52 -0
- package/types/core/config-loader.d.ts +13 -0
- package/types/core/config-validator.d.ts +30 -0
- package/types/core/ctx-builder.d.ts +80 -0
- package/types/core/envelope.d.ts +79 -0
- package/types/core/error-mapper.d.ts +17 -0
- package/types/core/formbody.d.ts +41 -0
- package/types/core/hub-link.d.ts +264 -0
- package/types/core/i18n.d.ts +178 -0
- package/types/core/index.d.ts +28 -0
- package/types/core/mega-app.d.ts +529 -0
- package/types/core/mega-cluster.d.ts +104 -0
- package/types/core/mega-server.d.ts +91 -0
- package/types/core/mega-service.d.ts +31 -0
- package/types/core/migration/dialect-registry.d.ts +22 -0
- package/types/core/migration/dialects/maria.d.ts +99 -0
- package/types/core/migration/dialects/mongo.d.ts +89 -0
- package/types/core/migration/dialects/postgres.d.ts +117 -0
- package/types/core/migration/dialects/sqlite.d.ts +111 -0
- package/types/core/migration/differ.d.ts +47 -0
- package/types/core/migration/generate.d.ts +56 -0
- package/types/core/migration/journal.d.ts +52 -0
- package/types/core/migration/model-scan.d.ts +19 -0
- package/types/core/migration/mongo-migration-db.d.ts +7 -0
- package/types/core/migration/schema-builder.d.ts +197 -0
- package/types/core/migration/schema-validator.d.ts +20 -0
- package/types/core/migration-lock.d.ts +33 -0
- package/types/core/migration-runner.d.ts +101 -0
- package/types/core/multipart.d.ts +86 -0
- package/types/core/openapi.d.ts +62 -0
- package/types/core/pipeline.d.ts +92 -0
- package/types/core/router.d.ts +159 -0
- package/types/core/routes-loader.d.ts +21 -0
- package/types/core/scope-registry.d.ts +14 -0
- package/types/core/security.d.ts +77 -0
- package/types/core/services-loader.d.ts +27 -0
- package/types/core/session-cleanup-schedule.d.ts +19 -0
- package/types/core/session-store.d.ts +18 -0
- package/types/core/session.d.ts +77 -0
- package/types/core/static-assets.d.ts +73 -0
- package/types/core/template.d.ts +106 -0
- package/types/core/workers-manager.d.ts +79 -0
- package/types/core/ws-cluster.d.ts +208 -0
- package/types/core/ws-compression.d.ts +112 -0
- package/types/core/ws-controller.d.ts +65 -0
- package/types/core/ws-message.d.ts +106 -0
- package/types/core/ws-presence.d.ts +273 -0
- package/types/core/ws-roster.d.ts +96 -0
- package/types/core/ws-upgrade.d.ts +231 -0
- package/types/errors/config-error.d.ts +10 -0
- package/types/errors/http-errors.d.ts +120 -0
- package/types/errors/index.d.ts +3 -0
- package/types/errors/mega-error.d.ts +32 -0
- package/types/index.d.ts +39 -0
- package/types/lib/asp/config.d.ts +49 -0
- package/types/lib/asp/crypto.d.ts +43 -0
- package/types/lib/asp/errors.d.ts +30 -0
- package/types/lib/asp/nonce-cache.d.ts +52 -0
- package/types/lib/asp/plugin.d.ts +30 -0
- package/types/lib/asp/ws-terminator.d.ts +45 -0
- package/types/lib/env-mapper.d.ts +14 -0
- package/types/lib/hub-protocol.d.ts +106 -0
- package/types/lib/index.d.ts +22 -0
- package/types/lib/logger/telegram-core.d.ts +104 -0
- package/types/lib/logger/telegram-transport.d.ts +45 -0
- package/types/lib/mega-brute-force.d.ts +66 -0
- package/types/lib/mega-circuit-breaker.d.ts +241 -0
- package/types/lib/mega-cron.d.ts +66 -0
- package/types/lib/mega-hash.d.ts +32 -0
- package/types/lib/mega-health.d.ts +41 -0
- package/types/lib/mega-job-queue.d.ts +176 -0
- package/types/lib/mega-job-worker.d.ts +130 -0
- package/types/lib/mega-job.d.ts +138 -0
- package/types/lib/mega-logger.d.ts +45 -0
- package/types/lib/mega-metrics.d.ts +285 -0
- package/types/lib/mega-plugin.d.ts +245 -0
- package/types/lib/mega-retry.d.ts +85 -0
- package/types/lib/mega-schedule.d.ts +260 -0
- package/types/lib/mega-shutdown.d.ts +135 -0
- package/types/lib/mega-tracing.d.ts +224 -0
- package/types/lib/mega-worker.d.ts +127 -0
- package/types/lib/otel-resource.d.ts +16 -0
- package/types/lib/worker-runner/process-entry.d.ts +1 -0
- package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
- package/types/lib/worker-runner/thread-entry.d.ts +1 -0
- package/types/lib/ws-hub.d.ts +234 -0
- package/types/models/crud-sql-builder.d.ts +48 -0
- package/types/models/index.d.ts +1 -0
- package/types/models/mega-model.d.ts +138 -0
- package/types/models/model-crud.d.ts +82 -0
- package/types/models/mongo-crud.d.ts +59 -0
- package/types/test/index.d.ts +84 -0
- package/.env +0 -127
- package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
- package/sample/crud/apps/main/models/note.js +0 -71
- package/sample/crud/apps/main/models/user.js +0 -86
- package/sample/crud/package-lock.json +0 -5665
- package/sample/crud/yarn.lock +0 -2142
- package/sample/simple/package-lock.json +0 -1851
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { MegaService } from 'mega-framework'
|
|
3
3
|
import { MegaNotFoundError, MegaValidationError } from 'mega-framework/errors'
|
|
4
|
-
import {
|
|
4
|
+
import { NoteModel } from '../models/note-model.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* NoteService — note 도메인 비즈니스 로직(ADR-022). 컨트롤러는 모델을 직접 만지지 않고 이 서비스를 거친다.
|
|
@@ -16,12 +16,12 @@ export class NoteService extends MegaService {
|
|
|
16
16
|
/** @returns {Promise<object[]>} */
|
|
17
17
|
async list() {
|
|
18
18
|
this.log.debug?.('note.list')
|
|
19
|
-
return
|
|
19
|
+
return NoteModel.list()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** @param {string} id @returns {Promise<object>} @throws {MegaNotFoundError} */
|
|
23
23
|
async get(id) {
|
|
24
|
-
const note = await
|
|
24
|
+
const note = await NoteModel.findById(String(id))
|
|
25
25
|
if (!note) throw new MegaNotFoundError('note.not_found', `Note ${id} not found`, { details: { id } })
|
|
26
26
|
return note
|
|
27
27
|
}
|
|
@@ -34,7 +34,7 @@ export class NoteService extends MegaService {
|
|
|
34
34
|
async create(input) {
|
|
35
35
|
const { title, body } = NoteService.#normalize(input)
|
|
36
36
|
this.log.debug?.({ title }, 'note.create')
|
|
37
|
-
return
|
|
37
|
+
return NoteModel.create({ title, body })
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -44,14 +44,14 @@ export class NoteService extends MegaService {
|
|
|
44
44
|
*/
|
|
45
45
|
async update(id, patch) {
|
|
46
46
|
const { title, body } = NoteService.#normalize(patch)
|
|
47
|
-
const updated = await
|
|
47
|
+
const updated = await NoteModel.update(String(id), { title, body })
|
|
48
48
|
if (!updated) throw new MegaNotFoundError('note.not_found', `Note ${id} not found`, { details: { id } })
|
|
49
49
|
return updated
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** @param {string} id @returns {Promise<{ deleted: true }>} @throws {MegaNotFoundError} */
|
|
53
53
|
async remove(id) {
|
|
54
|
-
const ok = await
|
|
54
|
+
const ok = await NoteModel.remove(String(id))
|
|
55
55
|
if (!ok) throw new MegaNotFoundError('note.not_found', `Note ${id} not found`, { details: { id } })
|
|
56
56
|
return { deleted: true }
|
|
57
57
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { MegaService } from 'mega-framework'
|
|
3
|
-
import {
|
|
3
|
+
import { UserModel } from '../models/user-model.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* RedisDemoService — /demo/redis 데모 로직(ADR-157). 파일명 `redis-demo-service.js` → 자동 DI 이름
|
|
@@ -8,7 +8,7 @@ import { User } from '../models/user.js'
|
|
|
8
8
|
*
|
|
9
9
|
* 1) **방문 카운터** — 표준 캐시 표면(get/set)에 없는 원자적 INCR 가 필요하므로 `.native`(raw ioredis)로
|
|
10
10
|
* `INCR`/`EXPIRE` 를 직접 호출한다(ADR-009 escape hatch). 누적 카운터 + 일자별 카운터(TTL 2일).
|
|
11
|
-
* 2) **쿼리 결과 캐시** — 표준 표면 `get/set`(JSON 직렬화 + TTL)으로 SQL 카운트(
|
|
11
|
+
* 2) **쿼리 결과 캐시** — 표준 표면 `get/set`(JSON 직렬화 + TTL)으로 SQL 카운트(UserModel.count)를 30초 캐싱한다.
|
|
12
12
|
* hit/miss 와 남은 TTL(`.native.ttl`)을 함께 돌려줘 캐시 동작을 눈으로 보게 한다.
|
|
13
13
|
*
|
|
14
14
|
* 캐시 키는 'demo' 캐시 안에서 `demo:redis:*` 네임스페이스로 두어 다른 용도와 분리한다(같은 redis 인스턴스라도
|
|
@@ -56,7 +56,7 @@ export class RedisDemoService extends MegaService {
|
|
|
56
56
|
this.ctx.log?.debug?.({ value: cached, ttlSeconds }, 'redis-demo.cache hit')
|
|
57
57
|
return { value: cached, isHit: true, ttlSeconds }
|
|
58
58
|
}
|
|
59
|
-
const value = await
|
|
59
|
+
const value = await UserModel.count()
|
|
60
60
|
await cache.set(RedisDemoService.USER_COUNT_KEY, value, { ttl: RedisDemoService.USER_COUNT_TTL })
|
|
61
61
|
this.ctx.log?.debug?.({ value }, 'redis-demo.cache miss — recomputed from SQL')
|
|
62
62
|
return { value, isHit: false, ttlSeconds: RedisDemoService.USER_COUNT_TTL }
|
|
@@ -1,56 +1,81 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { MegaService } from 'mega-framework'
|
|
3
|
+
import { MegaHash } from 'mega-framework'
|
|
3
4
|
import { MegaNotFoundError, MegaValidationError, MegaConflictError } from 'mega-framework/errors'
|
|
4
|
-
import {
|
|
5
|
+
import { UserModel } from '../models/user-model.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* UserService — user 도메인 비즈니스 로직(ADR-022). 컨트롤러는 모델을 직접 만지지 않고 이 서비스를
|
|
8
9
|
* 거친다. 파일명 `user-service.js` → 자동 DI 이름 `user`(ctx.services.user, ADR-148). 모델 import 는
|
|
9
10
|
* 서비스에서만 허용(유일한 모델 접근 경로).
|
|
11
|
+
*
|
|
12
|
+
* `users` 테이블이 username·name·nickname·password_hash 를 NOT NULL 로 요구하므로(ADR-204 schema),
|
|
13
|
+
* 관리자 생성도 회원가입과 동일하게 이 4개를 필수로 받고 비밀번호를 해시해 저장한다. 로그인 식별자는
|
|
14
|
+
* username 이다(email 은 선택).
|
|
10
15
|
*/
|
|
11
16
|
export class UserService extends MegaService {
|
|
17
|
+
/** 비밀번호 최소 길이(auth-service 와 동일 정책). */
|
|
18
|
+
static MIN_PASSWORD = 8
|
|
19
|
+
|
|
12
20
|
/** @returns {Promise<object[]>} */
|
|
13
21
|
async list() {
|
|
14
22
|
this.log.debug?.('user.list')
|
|
15
|
-
return
|
|
23
|
+
return UserModel.list()
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/** @param {string|number} id @returns {Promise<object>} @throws {MegaNotFoundError} */
|
|
19
27
|
async get(id) {
|
|
20
|
-
const user = await
|
|
28
|
+
const user = await UserModel.findById(Number(id))
|
|
21
29
|
if (!user) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
22
30
|
return user
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* 관리자 사용자 생성. 필수: username·name·nickname·password. 선택: email·phone_number·type_code·level_code.
|
|
35
|
+
* 비밀번호는 해시해 저장(평문 미보관). 반환은 공개 컬럼(해시 미노출).
|
|
36
|
+
* @param {{ username?: unknown, name?: unknown, nickname?: unknown, email?: unknown, phone_number?: unknown, password?: unknown, type_code?: unknown, level_code?: unknown }} input
|
|
37
|
+
* @returns {Promise<object>}
|
|
38
|
+
* @throws {MegaValidationError} `user.invalid` @throws {MegaConflictError} `user.username_taken`|`user.nickname_taken`
|
|
39
|
+
*/
|
|
26
40
|
async create(input) {
|
|
41
|
+
const username = typeof input?.username === 'string' ? input.username.trim() : ''
|
|
27
42
|
const name = typeof input?.name === 'string' ? input.name.trim() : ''
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
const nickname = typeof input?.nickname === 'string' ? input.nickname.trim() : ''
|
|
44
|
+
const email = typeof input?.email === 'string' ? input.email.trim().toLowerCase() : ''
|
|
45
|
+
const phone = typeof input?.phone_number === 'string' ? input.phone_number.trim() : ''
|
|
46
|
+
const password = typeof input?.password === 'string' ? input.password : ''
|
|
47
|
+
const valid = { username: !!username, name: !!name, nickname: !!nickname, password: password.length >= UserService.MIN_PASSWORD }
|
|
48
|
+
if (!username || !name || !nickname || password.length < UserService.MIN_PASSWORD) {
|
|
49
|
+
throw new MegaValidationError('user.invalid', 'username, name, nickname and password(8+) are required', { details: valid })
|
|
31
50
|
}
|
|
51
|
+
const passwordHash = await MegaHash.password.hash(password)
|
|
32
52
|
try {
|
|
33
|
-
return await
|
|
53
|
+
return await UserModel.create({
|
|
54
|
+
username,
|
|
55
|
+
name,
|
|
56
|
+
nickname,
|
|
57
|
+
email: email || null,
|
|
58
|
+
phone_number: phone || null,
|
|
59
|
+
passwordHash,
|
|
60
|
+
type_code: typeof input?.type_code === 'string' && input.type_code ? input.type_code : undefined,
|
|
61
|
+
level_code: typeof input?.level_code === 'string' && input.level_code ? input.level_code : undefined,
|
|
62
|
+
})
|
|
34
63
|
} catch (err) {
|
|
35
|
-
|
|
36
|
-
if (/** @type {any} */ (err)?.code === '23505') {
|
|
37
|
-
throw new MegaConflictError('user.email_taken', `email '${email}' already exists`, { details: { email }, cause: err })
|
|
38
|
-
}
|
|
39
|
-
throw err
|
|
64
|
+
throw UserService.#mapConflict(err, { username, nickname })
|
|
40
65
|
}
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 부분 수정 — name·nickname·email·phone_number·type_code·level_code. username·password 는 안 바꾼다.
|
|
70
|
+
* @param {string|number} id @param {Record<string, any>} patch @returns {Promise<object>}
|
|
71
|
+
* @throws {MegaNotFoundError} @throws {MegaConflictError}
|
|
72
|
+
*/
|
|
44
73
|
async update(id, patch) {
|
|
45
74
|
let updated
|
|
46
75
|
try {
|
|
47
|
-
updated = await
|
|
76
|
+
updated = await UserModel.update(Number(id), patch ?? {})
|
|
48
77
|
} catch (err) {
|
|
49
|
-
|
|
50
|
-
if (/** @type {any} */ (err)?.code === '23505') {
|
|
51
|
-
throw new MegaConflictError('user.email_taken', `email '${patch?.email}' already exists`, { details: { email: patch?.email }, cause: err })
|
|
52
|
-
}
|
|
53
|
-
throw err
|
|
78
|
+
throw UserService.#mapConflict(err, { nickname: patch?.nickname })
|
|
54
79
|
}
|
|
55
80
|
if (!updated) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
56
81
|
return updated
|
|
@@ -58,8 +83,24 @@ export class UserService extends MegaService {
|
|
|
58
83
|
|
|
59
84
|
/** @param {string|number} id @returns {Promise<{ deleted: true }>} @throws {MegaNotFoundError} */
|
|
60
85
|
async remove(id) {
|
|
61
|
-
const ok = await
|
|
86
|
+
const ok = await UserModel.remove(Number(id))
|
|
62
87
|
if (!ok) throw new MegaNotFoundError('user.not_found', `User ${id} not found`, { details: { id } })
|
|
63
88
|
return { deleted: true }
|
|
64
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* postgres unique_violation(23505) → username/nickname 도메인 충돌(409) 매핑(P4 — 명시 처리 후 throw).
|
|
93
|
+
* 그 외 에러는 그대로 반환(상위에서 throw).
|
|
94
|
+
* @param {unknown} err @param {{ username?: string, nickname?: string }} ctx @returns {Error}
|
|
95
|
+
*/
|
|
96
|
+
static #mapConflict(err, ctx) {
|
|
97
|
+
if (/** @type {any} */ (err)?.code === '23505') {
|
|
98
|
+
const constraint = String(/** @type {any} */ (err)?.constraint ?? '')
|
|
99
|
+
if (constraint.includes('nickname')) {
|
|
100
|
+
return new MegaConflictError('user.nickname_taken', `nickname '${ctx.nickname}' already exists`, { details: { nickname: ctx.nickname }, cause: /** @type {any} */ (err) })
|
|
101
|
+
}
|
|
102
|
+
return new MegaConflictError('user.username_taken', `username '${ctx.username}' already exists`, { details: { username: ctx.username }, cause: /** @type {any} */ (err) })
|
|
103
|
+
}
|
|
104
|
+
return /** @type {Error} */ (err)
|
|
105
|
+
}
|
|
65
106
|
}
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
18
18
|
|
|
19
19
|
<div class="mb-3">
|
|
20
|
-
<label for="
|
|
20
|
+
<label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
|
|
21
21
|
<input
|
|
22
|
-
type="
|
|
22
|
+
type="text"
|
|
23
23
|
class="form-control"
|
|
24
|
-
id="
|
|
25
|
-
name="
|
|
26
|
-
value="<%= values && values.
|
|
27
|
-
placeholder="<%= t('
|
|
24
|
+
id="username"
|
|
25
|
+
name="username"
|
|
26
|
+
value="<%= values && values.username ? values.username : '' %>"
|
|
27
|
+
placeholder="<%= t('field_username_ph', '예: hong123') %>"
|
|
28
28
|
autocomplete="username"
|
|
29
29
|
required
|
|
30
30
|
/>
|
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
<form method="post" action="/register" novalidate>
|
|
14
14
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
15
15
|
|
|
16
|
+
<div class="mb-3">
|
|
17
|
+
<label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
|
|
18
|
+
<input
|
|
19
|
+
type="text"
|
|
20
|
+
class="form-control <%= invalid && invalid.username ? 'is-invalid' : '' %>"
|
|
21
|
+
id="username"
|
|
22
|
+
name="username"
|
|
23
|
+
value="<%= values && values.username ? values.username : '' %>"
|
|
24
|
+
placeholder="<%= t('field_username_ph', '예: hong123') %>"
|
|
25
|
+
autocomplete="username"
|
|
26
|
+
required
|
|
27
|
+
/>
|
|
28
|
+
<div class="invalid-feedback"><%= t('field_username_required', '아이디를 입력하세요.') %></div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
16
31
|
<div class="mb-3">
|
|
17
32
|
<label for="name" class="form-label"><%= t('field_name', '이름') %></label>
|
|
18
33
|
<input
|
|
@@ -29,18 +44,44 @@
|
|
|
29
44
|
</div>
|
|
30
45
|
|
|
31
46
|
<div class="mb-3">
|
|
32
|
-
<label for="
|
|
47
|
+
<label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
|
|
48
|
+
<input
|
|
49
|
+
type="text"
|
|
50
|
+
class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
|
|
51
|
+
id="nickname"
|
|
52
|
+
name="nickname"
|
|
53
|
+
value="<%= values && values.nickname ? values.nickname : '' %>"
|
|
54
|
+
placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
|
|
55
|
+
autocomplete="nickname"
|
|
56
|
+
required
|
|
57
|
+
/>
|
|
58
|
+
<div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="mb-3">
|
|
62
|
+
<label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
33
63
|
<input
|
|
34
64
|
type="email"
|
|
35
|
-
class="form-control
|
|
65
|
+
class="form-control"
|
|
36
66
|
id="email"
|
|
37
67
|
name="email"
|
|
38
68
|
value="<%= values && values.email ? values.email : '' %>"
|
|
39
69
|
placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
|
|
40
|
-
autocomplete="
|
|
41
|
-
|
|
70
|
+
autocomplete="email"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="mb-3">
|
|
75
|
+
<label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
76
|
+
<input
|
|
77
|
+
type="tel"
|
|
78
|
+
class="form-control"
|
|
79
|
+
id="phone_number"
|
|
80
|
+
name="phone_number"
|
|
81
|
+
value="<%= values && values.phone_number ? values.phone_number : '' %>"
|
|
82
|
+
placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
|
|
83
|
+
autocomplete="tel"
|
|
42
84
|
/>
|
|
43
|
-
<div class="invalid-feedback"><%= t('field_email_required', '올바른 이메일을 입력하세요.') %></div>
|
|
44
85
|
</div>
|
|
45
86
|
|
|
46
87
|
<div class="mb-4">
|
|
@@ -11,6 +11,19 @@
|
|
|
11
11
|
<form method="post" action="/admin/users/<%= values.id %>" novalidate>
|
|
12
12
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
13
|
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control"
|
|
19
|
+
id="username"
|
|
20
|
+
value="<%= values && values.username ? values.username : '' %>"
|
|
21
|
+
readonly
|
|
22
|
+
disabled
|
|
23
|
+
/>
|
|
24
|
+
<div class="form-text"><%= t('field_username_readonly', '아이디는 변경할 수 없습니다.') %></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
14
27
|
<div class="mb-3">
|
|
15
28
|
<label for="name" class="form-label"><%= t('field_name', '이름') %></label>
|
|
16
29
|
<input
|
|
@@ -25,18 +38,42 @@
|
|
|
25
38
|
<div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
|
|
26
39
|
</div>
|
|
27
40
|
|
|
28
|
-
<div class="mb-
|
|
29
|
-
<label for="
|
|
41
|
+
<div class="mb-3">
|
|
42
|
+
<label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
|
|
43
|
+
<input
|
|
44
|
+
type="text"
|
|
45
|
+
class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
|
|
46
|
+
id="nickname"
|
|
47
|
+
name="nickname"
|
|
48
|
+
value="<%= values && values.nickname ? values.nickname : '' %>"
|
|
49
|
+
placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
|
|
50
|
+
required
|
|
51
|
+
/>
|
|
52
|
+
<div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="mb-3">
|
|
56
|
+
<label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
30
57
|
<input
|
|
31
58
|
type="email"
|
|
32
|
-
class="form-control
|
|
59
|
+
class="form-control"
|
|
33
60
|
id="email"
|
|
34
61
|
name="email"
|
|
35
62
|
value="<%= values && values.email ? values.email : '' %>"
|
|
36
63
|
placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
|
|
37
|
-
required
|
|
38
64
|
/>
|
|
39
|
-
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="mb-4">
|
|
68
|
+
<label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
69
|
+
<input
|
|
70
|
+
type="tel"
|
|
71
|
+
class="form-control"
|
|
72
|
+
id="phone_number"
|
|
73
|
+
name="phone_number"
|
|
74
|
+
value="<%= values && values.phone_number ? values.phone_number : '' %>"
|
|
75
|
+
placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
|
|
76
|
+
/>
|
|
40
77
|
</div>
|
|
41
78
|
|
|
42
79
|
<div class="d-flex gap-2">
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
<thead>
|
|
25
25
|
<tr>
|
|
26
26
|
<th scope="col" class="text-end" style="width: 5rem"><%= t('col_id', 'ID') %></th>
|
|
27
|
+
<th scope="col"><%= t('col_username', '아이디') %></th>
|
|
27
28
|
<th scope="col"><%= t('col_name', '이름') %></th>
|
|
29
|
+
<th scope="col"><%= t('col_nickname', '별명') %></th>
|
|
28
30
|
<th scope="col"><%= t('col_email', '이메일') %></th>
|
|
29
31
|
<th scope="col"><%= t('col_created', '생성일') %></th>
|
|
30
32
|
<th scope="col" class="text-end"><%= t('col_actions', '관리') %></th>
|
|
@@ -34,8 +36,10 @@
|
|
|
34
36
|
<% users.forEach(function (u) { %>
|
|
35
37
|
<tr>
|
|
36
38
|
<td class="text-end text-body-secondary"><%= u.id %></td>
|
|
37
|
-
<td class="fw-medium"><%= u.
|
|
38
|
-
<td><%= u.
|
|
39
|
+
<td class="fw-medium"><%= u.username %></td>
|
|
40
|
+
<td><%= u.name %></td>
|
|
41
|
+
<td><%= u.nickname %></td>
|
|
42
|
+
<td class="text-body-secondary"><%= u.email || '-' %></td>
|
|
39
43
|
<td class="text-body-secondary small"><%= u.created_at %></td>
|
|
40
44
|
<td class="text-end text-nowrap">
|
|
41
45
|
<a href="/admin/users/<%= u.id %>/edit" class="btn btn-sm btn-outline-secondary"><%= t('action_edit', '수정') %></a>
|
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
<form method="post" action="/admin/users" novalidate>
|
|
12
12
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />
|
|
13
13
|
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label for="username" class="form-label"><%= t('field_username', '아이디') %></label>
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
class="form-control <%= invalid && invalid.username ? 'is-invalid' : '' %>"
|
|
19
|
+
id="username"
|
|
20
|
+
name="username"
|
|
21
|
+
value="<%= values && values.username ? values.username : '' %>"
|
|
22
|
+
placeholder="<%= t('field_username_ph', '예: hong123') %>"
|
|
23
|
+
autocomplete="off"
|
|
24
|
+
required
|
|
25
|
+
/>
|
|
26
|
+
<div class="invalid-feedback"><%= t('field_username_required', '아이디를 입력하세요.') %></div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
14
29
|
<div class="mb-3">
|
|
15
30
|
<label for="name" class="form-label"><%= t('field_name', '이름') %></label>
|
|
16
31
|
<input
|
|
@@ -25,18 +40,55 @@
|
|
|
25
40
|
<div class="invalid-feedback"><%= t('field_name_required', '이름을 입력하세요.') %></div>
|
|
26
41
|
</div>
|
|
27
42
|
|
|
28
|
-
<div class="mb-
|
|
29
|
-
<label for="
|
|
43
|
+
<div class="mb-3">
|
|
44
|
+
<label for="nickname" class="form-label"><%= t('field_nickname', '별명') %></label>
|
|
45
|
+
<input
|
|
46
|
+
type="text"
|
|
47
|
+
class="form-control <%= invalid && invalid.nickname ? 'is-invalid' : '' %>"
|
|
48
|
+
id="nickname"
|
|
49
|
+
name="nickname"
|
|
50
|
+
value="<%= values && values.nickname ? values.nickname : '' %>"
|
|
51
|
+
placeholder="<%= t('field_nickname_ph', '예: 길동이') %>"
|
|
52
|
+
required
|
|
53
|
+
/>
|
|
54
|
+
<div class="invalid-feedback"><%= t('field_nickname_required', '별명을 입력하세요.') %></div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="mb-3">
|
|
58
|
+
<label for="email" class="form-label"><%= t('field_email', '이메일') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
30
59
|
<input
|
|
31
60
|
type="email"
|
|
32
|
-
class="form-control
|
|
61
|
+
class="form-control"
|
|
33
62
|
id="email"
|
|
34
63
|
name="email"
|
|
35
64
|
value="<%= values && values.email ? values.email : '' %>"
|
|
36
65
|
placeholder="<%= t('field_email_ph', '예: hong@example.com') %>"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="mb-3">
|
|
70
|
+
<label for="phone_number" class="form-label"><%= t('field_phone', '전화번호') %> <span class="text-body-secondary small">(<%= t('optional', '선택') %>)</span></label>
|
|
71
|
+
<input
|
|
72
|
+
type="tel"
|
|
73
|
+
class="form-control"
|
|
74
|
+
id="phone_number"
|
|
75
|
+
name="phone_number"
|
|
76
|
+
value="<%= values && values.phone_number ? values.phone_number : '' %>"
|
|
77
|
+
placeholder="<%= t('field_phone_ph', '예: 010-1234-5678') %>"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="mb-4">
|
|
82
|
+
<label for="password" class="form-label"><%= t('field_password', '비밀번호') %></label>
|
|
83
|
+
<input
|
|
84
|
+
type="password"
|
|
85
|
+
class="form-control <%= invalid && invalid.password ? 'is-invalid' : '' %>"
|
|
86
|
+
id="password"
|
|
87
|
+
name="password"
|
|
88
|
+
autocomplete="new-password"
|
|
37
89
|
required
|
|
38
90
|
/>
|
|
39
|
-
<div class="invalid-
|
|
91
|
+
<div class="form-text <%= invalid && invalid.password ? 'text-danger' : '' %>"><%= t('field_password_hint', '최소 8자 이상 입력하세요.') %></div>
|
|
40
92
|
</div>
|
|
41
93
|
|
|
42
94
|
<div class="d-flex gap-2">
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 🏗️ Log Table Partitioning Architecture
|
|
2
|
+
|
|
3
|
+
## 📦 LogPartitionSchedule
|
|
4
|
+
- **Role**: 로그 테이블 파티션 자동 관리 스케줄러
|
|
5
|
+
- **Responsibilities**
|
|
6
|
+
- 매일 특정 시각(예: 새벽 2시)에 실행
|
|
7
|
+
- 미래(다음 달, 다다음 달) 파티션 테이블 자동 생성
|
|
8
|
+
- 과거(1개월 이전) 오래된 파티션 테이블 자동 분리(Detach) 및 삭제(Drop)
|
|
9
|
+
- **Concurrency Control**
|
|
10
|
+
- `static lock = { lock: 'log-partition', ttl: 60000 }` (Redlock을 활용하여 스케줄러 인스턴스 간 중복 실행 방지)
|
|
11
|
+
|
|
12
|
+
## 📦 Database Partitioned Tables
|
|
13
|
+
- **Role**: 시간 기반 범위 파티셔닝(Range Partitioning by `created_at`)을 통한 로그 데이터 관리
|
|
14
|
+
- **Properties**
|
|
15
|
+
- `action_logs` (PARTITION BY RANGE (created_at))
|
|
16
|
+
- `wallet_logs` (PARTITION BY RANGE (created_at))
|
|
17
|
+
- `detail_logs` (PARTITION BY RANGE (created_at))
|
|
18
|
+
- **Responsibilities**
|
|
19
|
+
- 대량 로그 유입 시 쓰기 성능 향상 및 Vacuum 오버헤드 완화
|
|
20
|
+
- 보관 주기(1개월) 만료 데이터의 물리적 삭제 성능 최적화 (DROP PARTITION)
|
|
21
|
+
- **Constraints**
|
|
22
|
+
- Primary Key에 파티션 키 `created_at` 포함 필수: `PRIMARY KEY (id, created_at)`
|
|
23
|
+
- Unique Index에 파티션 키 `created_at` 포함 필수: `UNIQUE (uuid, created_at)` 등
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* App-only 키(databases 별명 등)는 apps/<name>/app.config.js 로.
|
|
5
5
|
*/
|
|
6
6
|
import { CronCounterSchedule } from './apps/main/schedules/cron-counter-schedule.js'
|
|
7
|
+
import { LogPartitionSchedule } from './apps/main/schedules/log-partition-schedule.js'
|
|
7
8
|
import { EmailJob } from './apps/main/jobs/email-job.js'
|
|
8
9
|
import { HashWorker } from './apps/main/workers/hash-worker.js'
|
|
9
10
|
|
|
@@ -135,7 +136,7 @@ export default {
|
|
|
135
136
|
// },
|
|
136
137
|
|
|
137
138
|
// 정기 작업(ADR-028/118) — `mega scheduler` 프로세스가 등록·실행한다(config.schedules, ADR-123).
|
|
138
|
-
schedules: [CronCounterSchedule],
|
|
139
|
+
schedules: [CronCounterSchedule, LogPartitionSchedule],
|
|
139
140
|
|
|
140
141
|
// 영속 잡(ADR-028/119) — `mega worker` 프로세스(ecosystem instances:2)가 소비한다(config.jobs, ADR-123).
|
|
141
142
|
jobs: [EmailJob],
|
|
@@ -168,7 +169,7 @@ export default {
|
|
|
168
169
|
// (예시·미사용) liveness/readiness 경로 — 기본 '/health' · '/health/ready'.
|
|
169
170
|
// paths: { live: '/health', ready: '/health/ready' },
|
|
170
171
|
// (예시·미사용) 메트릭 resource service.name — 미지정 시 server.serviceName → MEGA_OTEL_SERVICE_NAME 폴백.
|
|
171
|
-
|
|
172
|
+
serviceName: 'sample-crud',
|
|
172
173
|
},
|
|
173
174
|
|
|
174
175
|
// OpenTelemetry 분산 트레이싱 — **config 블록이 아니라 .env 의 MEGA_OTEL_\*** 로 설정한다
|
package/sample/crud/package.json
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# 앱(`yarn dev` = `mega start`)이 app.config 의 `bridgeHub` 로 이 허브에 **자동 연결**한다(ADR-176).
|
|
7
7
|
#
|
|
8
8
|
# ⚠️ `mega-ws-hub` 는 `mega start` 와 달리 `.env` 를 자동 로드하지 않는다(직접 process.env 만 읽음,
|
|
9
|
-
# src/
|
|
10
|
-
# 읽는 env(src/
|
|
9
|
+
# src/lib/ws-hub.js). 그래서 Node 20.6+ 의 `--env-file` 로 .env 를 주입한다.
|
|
10
|
+
# 읽는 env(src/lib/ws-hub.js runWsHubCli): MEGA_WSHUB_TOKENS(필수, 콤마구분) /
|
|
11
11
|
# MEGA_WSHUB_PORT(기본 3100) / MEGA_WSHUB_HOST(기본 0.0.0.0) / MEGA_WSHUB_HEARTBEAT_MS.
|
|
12
12
|
#
|
|
13
13
|
# 사용:
|
|
@@ -127,7 +127,8 @@ export function buildFromGlobalConfig(globalConfig, { registerShutdownHook = tru
|
|
|
127
127
|
if (registerShutdownHook) {
|
|
128
128
|
// 재빌드 안전 — 항상 1개만 유지(MegaApp 의 hublink hook 패턴과 동일).
|
|
129
129
|
MegaShutdown.unregister(SHUTDOWN_HOOK)
|
|
130
|
-
|
|
130
|
+
// 'adapters' stage — 서버/잡/앱 정리(어댑터를 쓰는 정리)가 모두 끝난 뒤 disconnect 된다.
|
|
131
|
+
MegaShutdown.register(SHUTDOWN_HOOK, async () => disconnectAll(), { stage: 'adapters' })
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -47,6 +47,36 @@ function invalidOption(message, details) {
|
|
|
47
47
|
return new MegaValidationError('adapter.invalid_option', message, { details })
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* `withTransaction(fn, opts)` 의 isolation 값 → 표준 SQL 조각 (ADR-190).
|
|
52
|
+
* 화이트리스트 매핑이라 SQL 인젝션이 구조적으로 불가능하다(임의 문자열은 throw).
|
|
53
|
+
* @type {Record<string, string>}
|
|
54
|
+
*/
|
|
55
|
+
const TX_ISOLATION_SQL = {
|
|
56
|
+
'read uncommitted': 'READ UNCOMMITTED',
|
|
57
|
+
'read committed': 'READ COMMITTED',
|
|
58
|
+
'repeatable read': 'REPEATABLE READ',
|
|
59
|
+
serializable: 'SERIALIZABLE',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 트랜잭션 격리수준 옵션 검증 + SQL 조각 변환 (ADR-190). `undefined` 는 driver 디폴트 위임(undefined 반환).
|
|
64
|
+
* @param {unknown} isolation - `withTransaction` 의 `opts.isolation`.
|
|
65
|
+
* @param {string} driver - 에러 메시지용 driver 이름.
|
|
66
|
+
* @returns {string | undefined} `SET TRANSACTION ISOLATION LEVEL <조각>` 에 쓸 SQL 조각.
|
|
67
|
+
* @throws {MegaValidationError} `adapter.invalid_option` - 화이트리스트 외 값.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveTxIsolation(isolation, driver) {
|
|
70
|
+
if (isolation === undefined) return undefined
|
|
71
|
+
if (typeof isolation !== 'string' || !Object.prototype.hasOwnProperty.call(TX_ISOLATION_SQL, isolation)) {
|
|
72
|
+
throw invalidOption(
|
|
73
|
+
`${driver}: withTransaction "isolation" must be one of 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable'. Got ${JSON.stringify(isolation)}.`,
|
|
74
|
+
{ driver, option: 'isolation', value: isolation },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return TX_ISOLATION_SQL[isolation]
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
/**
|
|
51
81
|
* 양의 정수(> 0) 검증 (undefined 통과). 위반 시 `adapter.invalid_option` throw.
|
|
52
82
|
* @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
|