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,19 +1,24 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* ๋ง์ด๊ทธ๋ ์ด์
๋ฌ๋ (ADR-149) โ `apps/<app>/migrations/<ts>-<name>.js` ์ up/down ์
|
|
3
|
+
* ๋ง์ด๊ทธ๋ ์ด์
๋ฌ๋ (ADR-149/184) โ `apps/<app>/migrations/<ts>-<name>.js` ์ up/down ์
|
|
4
4
|
* ๋์ DB ์ ์ ์ฉยท๋กค๋ฐฑํ๊ณ , ์ ์ฉ ์ด๋ ฅ์ ๋์ DB ์ `mega_migrations` ํ
์ด๋ธ๋ก ์ถ์ ํ๋ค.
|
|
5
|
+
* ์ด๋ ฅ ํ์๋ ํ์ผ ๋ด์ฉ์ sha-256 `checksum` ์ ํจ๊ป ๊ธฐ๋กํด, ์ ์ฉ ํ ํ์ผ์ด ์์ ๋ ๋๋ฆฌํํธ๋ฅผ
|
|
6
|
+
* `migrate:status`(modified)ยท`migrate`(๊ฒฝ๊ณ ๋ก๊ทธ)์์ ๊ฐ์งํ๋ค(ADR-190).
|
|
5
7
|
*
|
|
6
8
|
* ๋ฌ๋๋ **์ด๋ํฐ์ ๋น์์กด์ **์ด๋ค โ ํธ์ถ์(`runMigrateHost`)๊ฐ ์ฐ๊ฒฐ๋ DB ์ด๋ํฐ(`{ query, withTransaction }`)๋ฅผ
|
|
7
9
|
* `db` ๋ก ๋๊ธด๋ค. ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ์ `up(db)/down(db)` ๋ ์ด ์ด๋ํฐ๋ฅผ ๋ฐ์ SQL ์ ์คํํ๋ค. ์ด๋ํฐ๊ฐ
|
|
8
10
|
* `withTransaction` ์ ์ง์ํ๋ฉด ๊ฐ ๋ง์ด๊ทธ๋ ์ด์
์ ํธ๋์ญ์
์ผ๋ก ๊ฐ์ธ ๋ถ๋ถ ์คํจ๋ฅผ ๋กค๋ฐฑํ๋ค(postgres DDL-in-tx).
|
|
11
|
+
* ๋์ ์คํ ๋ฝ์ ๋ฌ๋๊ฐ ์๋๋ผ **ํธ์คํธ ๋ ๋ฒจ**({@link module:core/migration-lock}) ์ฑ
์์ด๋ค โ
|
|
12
|
+
* driver ๋ณ ๋ฝ ๋ฉ์ปค๋์ฆ์ด ๋ฌ๋ผ ์ด๋ํฐ ๋น์์กด์ฑ์ ๊นจ์ง ์๊ธฐ ์ํจ(ADR-190).
|
|
9
13
|
*
|
|
10
|
-
* ๋ถ๊ธฐ SQL(ํ
์ด๋ธ ์์ฑยทSELECTยทINSERTยทDELETE)์ ํ์ค SQL ๋ง ์ฐ๊ณ , ๊ฐ(๋ง์ด๊ทธ๋ ์ด์
nameยทapplied_at)์
|
|
11
|
-
* **framework-controlled ๋ผ ๋ฐ์ดํ๊ฐ ๋ถ๊ฐ๋ฅ**(name ์ {@link MIGRATION_FILE_RE} ๊ฒ์ฆ, applied_at ์ ISO
|
|
12
|
-
* placeholder(`$1` vs `?`) dialect ๋ถ๊ธฐ ์์ด ์ธ๋ผ์ธํด๋ ์ธ์ ์
-์์ ํ๋ค.
|
|
14
|
+
* ๋ถ๊ธฐ SQL(ํ
์ด๋ธ ์์ฑยทSELECTยทINSERTยทDELETE)์ ํ์ค SQL ๋ง ์ฐ๊ณ , ๊ฐ(๋ง์ด๊ทธ๋ ์ด์
nameยทapplied_atยทchecksum)์
|
|
15
|
+
* **framework-controlled ๋ผ ๋ฐ์ดํ๊ฐ ๋ถ๊ฐ๋ฅ**(name ์ {@link MIGRATION_FILE_RE} ๊ฒ์ฆ, applied_at ์ ISO,
|
|
16
|
+
* checksum ์ sha-256 hex)ํ๋ฏ๋ก placeholder(`$1` vs `?`) dialect ๋ถ๊ธฐ ์์ด ์ธ๋ผ์ธํด๋ ์ธ์ ์
-์์ ํ๋ค.
|
|
13
17
|
*
|
|
14
18
|
* @module core/migration-runner
|
|
15
19
|
*/
|
|
16
|
-
import {
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
17
22
|
import { join, resolve as pathResolve } from 'node:path'
|
|
18
23
|
import { pathToFileURL } from 'node:url'
|
|
19
24
|
import { MegaConfigError } from '../errors/config-error.js'
|
|
@@ -62,9 +67,11 @@ export function collectMigrationFiles({ projectRoot, appNames }) {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/**
|
|
65
|
-
* ๋ง์ด๊ทธ๋ ์ด์
๋ชจ๋ ๋ก๋ + up/down ๊ฒ์ฆ.
|
|
70
|
+
* ๋ง์ด๊ทธ๋ ์ด์
๋ชจ๋ ๋ก๋ + up/down ๊ฒ์ฆ. `export const transaction = false`(์ตํธ์์, ADR-205)๋ฉด
|
|
71
|
+
* ๋ฌ๋๊ฐ ํธ๋์ญ์
๋ํ์ ๋๋ค โ `CREATE INDEX CONCURRENTLY` ์ฒ๋ผ ํธ๋์ญ์
์์์ ์คํ ๋ถ๊ฐํ
|
|
72
|
+
* ๋ฌธ์ฅ์ ์ํ escape hatch(๋ถ๋ถ ์คํจ ์ ๋กค๋ฐฑ ์์์ ํ์ผ ์์ฑ์ ์ฑ
์ โ ์์ฑ๊ธฐ๊ฐ ํค๋์ ๋ช
์).
|
|
66
73
|
* @param {string} absPath
|
|
67
|
-
* @returns {Promise<{ up: Function, down: Function }>}
|
|
74
|
+
* @returns {Promise<{ up: Function, down: Function, useTransaction: boolean }>}
|
|
68
75
|
* @throws {MegaConfigError} import ์คํจ / upยทdown ๋๋ฝ.
|
|
69
76
|
*/
|
|
70
77
|
export async function loadMigration(absPath) {
|
|
@@ -77,35 +84,55 @@ export async function loadMigration(absPath) {
|
|
|
77
84
|
if (typeof mod.up !== 'function' || typeof mod.down !== 'function') {
|
|
78
85
|
throw new MegaConfigError('migration.invalid', `Migration '${absPath}' must export async function up(db) and down(db).`)
|
|
79
86
|
}
|
|
80
|
-
return { up: mod.up, down: mod.down }
|
|
87
|
+
return { up: mod.up, down: mod.down, useTransaction: mod.transaction !== false }
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
/**
|
|
84
91
|
* ์ด๋ ฅ ํ
์ด๋ธ ๋ณด์ฅ(idempotent). ํ์ค SQL โ postgres/maria/sqlite ๊ณตํต.
|
|
92
|
+
* v1(checksum ์๋) ๊ธฐ์กด ํ
์ด๋ธ์ ์ปฌ๋ผ ๋ถ์ฌ๋ฅผ ๊ฐ์งํด ADD COLUMN ์ผ๋ก ๋ณด๊ฐํ๋ค(ADR-190) โ
|
|
93
|
+
* `ADD COLUMN IF NOT EXISTS` ๊ฐ sqlite ๋ฏธ์ง์์ด๋ผ SELECT ์๋๋ก ๋ถ์ฌ๋ฅผ ํ๋ณํ๋ค.
|
|
85
94
|
* @param {MigrationDb} db
|
|
86
95
|
* @returns {Promise<void>}
|
|
87
96
|
*/
|
|
88
97
|
async function ensureTable(db) {
|
|
89
|
-
await db.query(
|
|
98
|
+
await db.query(
|
|
99
|
+
`CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (name VARCHAR(255) PRIMARY KEY, applied_at VARCHAR(64) NOT NULL, checksum VARCHAR(64))`,
|
|
100
|
+
)
|
|
101
|
+
try {
|
|
102
|
+
await db.query(`SELECT checksum FROM ${MIGRATIONS_TABLE} LIMIT 1`)
|
|
103
|
+
} catch {
|
|
104
|
+
// checksum ์ปฌ๋ผ ๋ถ์ฌ(v1 ํ
์ด๋ธ) โ ๋ณด๊ฐํ๋ค. SELECT ์คํจ๊ฐ ๋ค๋ฅธ ์์ธ์ด์๋ค๋ฉด ์๋ ALTER ๋
|
|
105
|
+
// ๊ฐ์ ์์ธ์ผ๋ก throw ํ๋ฏ๋ก ์๋ฌ๊ฐ ๋ฌปํ์ง ์๋๋ค(P4 โ silent ๋ฌด์ ์๋).
|
|
106
|
+
await db.query(`ALTER TABLE ${MIGRATIONS_TABLE} ADD COLUMN checksum VARCHAR(64)`)
|
|
107
|
+
}
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
/**
|
|
93
|
-
* ์ ์ฉ๋ ๋ง์ด๊ทธ๋ ์ด์
name
|
|
111
|
+
* ์ ์ฉ๋ ๋ง์ด๊ทธ๋ ์ด์
์ด๋ ฅ โ name โ checksum(checksum ๋์
์ v1 ํ์ null).
|
|
94
112
|
* @param {MigrationDb} db
|
|
95
|
-
* @returns {Promise<
|
|
113
|
+
* @returns {Promise<Map<string, string | null>>}
|
|
96
114
|
*/
|
|
97
|
-
async function
|
|
98
|
-
const res = await db.query(`SELECT name FROM ${MIGRATIONS_TABLE}`)
|
|
115
|
+
async function appliedRecords(db) {
|
|
116
|
+
const res = await db.query(`SELECT name, checksum FROM ${MIGRATIONS_TABLE}`)
|
|
99
117
|
const rows = res?.rows ?? (Array.isArray(res) ? res : [])
|
|
100
|
-
return new
|
|
118
|
+
return new Map(rows.map((/** @type {any} */ r) => [r.name, r.checksum ?? null]))
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
/**
|
|
104
|
-
*
|
|
105
|
-
* @param {
|
|
122
|
+
* ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ๋ด์ฉ์ sha-256 hex digest โ ์ ์ฉ ํ ํ์ผ ์์ (๋๋ฆฌํํธ) ๊ฐ์ง์ฉ(ADR-190).
|
|
123
|
+
* @param {string} absPath @returns {string}
|
|
106
124
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
function fileChecksum(absPath) {
|
|
126
|
+
return createHash('sha256').update(readFileSync(absPath)).digest('hex')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* ์ ์ฉ ๊ธฐ๋ก INSERT. name(๊ฒ์ฆ๋ [\d a-z -])ยทiso(ISO)ยทchecksum(sha-256 hex)์ ๋ฐ์ดํ ๋ถ๊ฐ๋ผ ์ธ๋ผ์ธ
|
|
131
|
+
* ์์ (placeholder dialect ํํผ).
|
|
132
|
+
* @param {MigrationDb} db @param {string} name @param {string} appliedAtIso @param {string} checksum
|
|
133
|
+
*/
|
|
134
|
+
async function recordApplied(db, name, appliedAtIso, checksum) {
|
|
135
|
+
await db.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at, checksum) VALUES ('${name}', '${appliedAtIso}', '${checksum}')`)
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
/**
|
|
@@ -120,38 +147,111 @@ async function removeApplied(db, name) {
|
|
|
120
147
|
* ์ด๋ํฐ๊ฐ ํธ๋์ญ์
์ ์ง์ํ๋ฉด fn ์ ํธ๋์ญ์
์ผ๋ก ๊ฐ์ธ ์คํํ๋ค. postgres ๋ AsyncLocalStorage ๋ก tx
|
|
121
148
|
* ์ปจํ
์คํธ๋ฅผ ์ถ์ ํ๋ฏ๋ก fn ์์ `db.query` ๊ฐ ๊ฐ์ ํธ๋์ญ์
ํด๋ผ์ด์ธํธ๋ก ๋ผ์ฐํ
๋๋ค(ADR-106).
|
|
122
149
|
* @param {MigrationDb} db @param {() => Promise<void>} fn
|
|
150
|
+
* @param {{ info?: Function, warn?: Function, debug?: Function }} [log]
|
|
123
151
|
*/
|
|
124
|
-
async function runInTransaction(db, fn) {
|
|
152
|
+
async function runInTransaction(db, fn, log) {
|
|
125
153
|
if (typeof db.withTransaction === 'function') {
|
|
126
154
|
await db.withTransaction(async () => {
|
|
127
155
|
await fn()
|
|
128
156
|
})
|
|
129
157
|
} else {
|
|
158
|
+
// ํธ๋์ญ์
์ ๊ธฐ๋(transaction ๋ฏธ์ ์ธ = ๊ธฐ๋ณธ true)ํ๋๋ฐ ์ด๋ํฐ๊ฐ ๋ฏธ์ง์(mongo ๋ฑ)์ด๋ฉด
|
|
159
|
+
// ์กฐ์ฉํ ํด์ค๋ฃจ๊ฐ "tx ๋ณดํธ๋ฅผ ๋ฐ๋๋ค" ๋ ์ค์ธ์ ๋ง๋ ๋ค(ADR-210 L-4) โ ๋ช
์ ๊ฒฝ๊ณ 1์ค.
|
|
160
|
+
log?.warn?.('migrate: adapter has no withTransaction โ running without transaction (rollback unavailable on failure)')
|
|
130
161
|
await fn()
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
166
|
+
* ์ ์ฉ๋ ํ์ผ ์ค ์ ์ฉ ํ ๋ด์ฉ์ด ์์ ๋(๋๋ฆฌํํธ) ๋ง์ด๊ทธ๋ ์ด์
์ด๋ฆ ๋ชฉ๋ก โ ์ ์ฅ๋ checksum ๊ณผ ํ์ฌ
|
|
167
|
+
* ํ์ผ digest ๋ถ์ผ์น. checksum ์ด null(v1 ์ด๋ ฅ)์ด๋ฉด ๋น๊ต ๋ถ๊ฐ๋ผ ์ ์ธํ๋ค(ADR-190).
|
|
168
|
+
* @param {MigrationFile[]} files @param {Map<string, string | null>} applied
|
|
169
|
+
* @returns {string[]}
|
|
170
|
+
*/
|
|
171
|
+
function findModified(files, applied) {
|
|
172
|
+
return files
|
|
173
|
+
.filter((m) => {
|
|
174
|
+
const stored = applied.get(m.name)
|
|
175
|
+
return stored != null && stored !== fileChecksum(m.absPath)
|
|
176
|
+
})
|
|
177
|
+
.map((m) => m.name)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* ๋ฏธ์ ์ฉ(pending) ๋ง์ด๊ทธ๋ ์ด์
์ ํ์์คํฌํ ์์ผ๋ก ๋ชจ๋ ์ ์ฉํ๋ค. ๊ฐ ๋ง์ด๊ทธ๋ ์ด์
์ up + ์ด๋ ฅ ๊ธฐ๋ก
|
|
182
|
+
* (sha-256 checksum ํฌํจ, ADR-190)์ ํ ํธ๋์ญ์
์ผ๋ก ๋ฌถ์ด ์์์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค. ์ด๋ฏธ ์ ์ฉ๋ ํ์ผ์ด
|
|
183
|
+
* ์ ์ฉ ํ ์์ ๋์ผ๋ฉด(checksum ๋ถ์ผ์น) ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค โ ์คํ๋ ์คํค๋ง์ ํ์ผ์ด ์ด๊ธ๋ ์ํ.
|
|
137
184
|
*
|
|
138
|
-
* @param {{ db: MigrationDb, projectRoot: string, appNames: string[], now?: () => string, log?: { info?: Function } }} opts
|
|
185
|
+
* @param {{ db: MigrationDb, projectRoot: string, appNames: string[], now?: () => string, log?: { info?: Function, warn?: Function, debug?: Function } }} opts
|
|
139
186
|
* - now: ์ ์ฉ ์๊ฐ ISO ๋ฌธ์์ด ๊ณต๊ธ์(ํ
์คํธ ์ฃผ์
์ฉ, ๊ธฐ๋ณธ `new Date().toISOString()`).
|
|
140
187
|
* @returns {Promise<{ applied: string[] }>}
|
|
141
188
|
*/
|
|
142
189
|
export async function migrateUp({ db, projectRoot, appNames, now = () => new Date().toISOString(), log }) {
|
|
143
190
|
await ensureTable(db)
|
|
144
|
-
const applied = await
|
|
145
|
-
const
|
|
191
|
+
const applied = await appliedRecords(db)
|
|
192
|
+
const all = collectMigrationFiles({ projectRoot, appNames })
|
|
193
|
+
for (const name of findModified(all, applied)) {
|
|
194
|
+
log?.warn?.({ migration: name }, 'migrate.up checksum mismatch โ file modified after apply')
|
|
195
|
+
}
|
|
196
|
+
const pending = all.filter((m) => !applied.has(m.name))
|
|
146
197
|
/** @type {string[]} */
|
|
147
198
|
const done = []
|
|
148
199
|
for (const m of pending) {
|
|
149
|
-
const { up } = await loadMigration(m.absPath)
|
|
200
|
+
const { up, useTransaction } = await loadMigration(m.absPath)
|
|
201
|
+
const checksum = fileChecksum(m.absPath)
|
|
150
202
|
const appliedAt = now()
|
|
151
|
-
|
|
203
|
+
let skipped = false
|
|
204
|
+
const apply = async () => {
|
|
205
|
+
// ์ ์ฉ ์ง์ ์ด๋ ฅ ์ฌํ์ธ โ pending ์ฐ์ถ(์์ ์ 1ํ)๊ณผ ์ค์ ์ ์ฉ ์ฌ์ด์ ๋์ ๋ฌ๋๊ฐ ๋ผ์ด๋ค ์
|
|
206
|
+
// ์๋ค. ๋ฝ์ด ์ฝํ dialect(sqlite โ ํ์ผ ๋ฝ์ read-check-apply ์์์ฑ์ ์ ์ค)์์ ํจ์๊ฐ
|
|
207
|
+
// ๊ฐ์ DDL ์ ์ฌ์คํํด ์ค๋์ฑ ์๋ฌ(duplicate column ๋ฑ)๋ก ์ฃฝ๋ ๊ฒ์, ๋ช
์ skip ์ผ๋ก ๋ฐ๊พผ๋ค.
|
|
208
|
+
const recheck = await db.query(`SELECT name FROM ${MIGRATIONS_TABLE} WHERE name = '${m.name}'`)
|
|
209
|
+
const recheckRows = recheck?.rows ?? (Array.isArray(recheck) ? recheck : [])
|
|
210
|
+
if (recheckRows.length > 0) {
|
|
211
|
+
skipped = true
|
|
212
|
+
log?.warn?.({ migration: m.name }, 'migrate.up skipped โ already applied by a concurrent runner')
|
|
213
|
+
return
|
|
214
|
+
}
|
|
152
215
|
await up(db)
|
|
153
|
-
await recordApplied(db, m.name, appliedAt)
|
|
154
|
-
}
|
|
216
|
+
await recordApplied(db, m.name, appliedAt, checksum)
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
if (useTransaction) {
|
|
220
|
+
await runInTransaction(db, apply, log)
|
|
221
|
+
} else {
|
|
222
|
+
// `export const transaction = false` ์ตํธ์์(ADR-205 โ CONCURRENTLY ๋ฑ) โ ๋ถ๋ถ ์คํจ ์
|
|
223
|
+
// ๋กค๋ฐฑ๋์ง ์์์ ๋ก๊ทธ๋ก ํ๋ฉดํํ๋ค(ํ์ผ ํค๋์๋ ๋ช
์๋จ).
|
|
224
|
+
log?.warn?.({ migration: m.name }, 'migrate.up no-transaction migration (rollback unavailable on partial failure)')
|
|
225
|
+
await apply()
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// ์คํจ ํ ์ด๋ ฅ ์ฌํ์ธ โ ์ด๋ ฅ ๊ธฐ๋ก์ ์ ์ฉ ์ฑ๊ณต์ ์ต์ข
๋จ๊ณ๋ผ, ์ด๋ฆ์ด ์ด๋ ฅ์ ์์ผ๋ฉด ๋์
|
|
229
|
+
// ๋ฌ๋๊ฐ ์ด๋ฏธ ์ ์ฉ์ ์๋ฃํ ๊ฒ์ด๋ค(๊ฐ์ ํ์ผ์ ์ ํํ ๋์์ ์์ํ ํจ์๋ ์ง์ ์ฌํ์ธ
|
|
230
|
+
// ์ฐฝ์ ์ง๋ DDL ์ถฉ๋ยท์ด๋ ฅ UNIQUE ๋ก ์ฃฝ๋๋ค โ ์ค์ธก). ์ค๋์ฑ ์๋ฌ ๋์ ๋ช
์ skip(ADR-208 M-2).
|
|
231
|
+
let appliedByOther = false
|
|
232
|
+
try {
|
|
233
|
+
const after = await db.query(`SELECT name FROM ${MIGRATIONS_TABLE} WHERE name = '${m.name}'`)
|
|
234
|
+
const afterRows = after?.rows ?? (Array.isArray(after) ? after : [])
|
|
235
|
+
appliedByOther = afterRows.length > 0
|
|
236
|
+
} catch (recheckErr) {
|
|
237
|
+
// ์ฌํ์ธ ์์ฒด๊ฐ ์คํจ(์ฐ๊ฒฐ ๋จ์ ๋ฑ)ํ๋ฉด ์์ธ ํ๋ณ ๋ถ๊ฐ โ ์๋ณธ ์๋ฌ๋ฅผ ๊ทธ๋๋ก ๋ณด๊ณ ํ๋ค.
|
|
238
|
+
log?.debug?.({ err: recheckErr, migration: m.name }, 'migrate.up post-failure history recheck failed')
|
|
239
|
+
}
|
|
240
|
+
if (appliedByOther) {
|
|
241
|
+
log?.warn?.({ migration: m.name }, 'migrate.up skipped โ already applied by a concurrent runner')
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
// ์ด๋ ํ์ผ์์ ์ฃฝ์๋์ง ์์ด driver ์๋ฌธ๋ง ๋์ง๋ฉด ๋ค๋จ ์ ์ฉ์์ ์ถ์ ์ด ์ด๋ ต๋ค โ ํ์ผ ์ปจํ
์คํธ wrap.
|
|
245
|
+
// no-tx ํ์ผ์ ๋ถ๋ถ ์ ์ฉ์ด ๋จ์ ์ ์๋ค โ ๋ฉฑ๋ฑ ๋ ๋(maria IF EXISTSยทmongo not-found ํ์ฉ)
|
|
246
|
+
// ๋์ ์์ธ ์ ๊ฑฐ ํ ๊ฐ์ ํ์ผ ์ฌ์คํ์ด 1์ฐจ ๋ณต๊ตฌ ์๋จ์์ ์๋ฌ์ ์ง์ ์๋ดํ๋ค(ADR-208/210).
|
|
247
|
+
const noTxHint = useTransaction ? '' : ' [no-transaction โ ๋ถ๋ถ ์ ์ฉ์ด ๋จ์์ ์ ์์ต๋๋ค. ์์ธ ์ ๊ฑฐ ํ ์ฌ์คํํ๋ฉด ๋ฉฑ๋ฑ ๋ฌธ์ฅ์ ๊ฑด๋๋๋๋ค]'
|
|
248
|
+
throw new MegaConfigError(
|
|
249
|
+
'migration.apply_failed',
|
|
250
|
+
`Migration '${m.name}' failed during up(): ${/** @type {any} */ (err).message}${noTxHint} (file: ${m.absPath})`,
|
|
251
|
+
{ cause: err, details: { migration: m.name, file: m.absPath, direction: 'up' } },
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
if (skipped) continue
|
|
155
255
|
log?.info?.({ migration: m.name }, 'migrate.up applied')
|
|
156
256
|
done.push(m.name)
|
|
157
257
|
}
|
|
@@ -160,37 +260,65 @@ export async function migrateUp({ db, projectRoot, appNames, now = () => new Dat
|
|
|
160
260
|
|
|
161
261
|
/**
|
|
162
262
|
* ๊ฐ์ฅ ์ต๊ทผ ์ ์ฉ๋ ๋ง์ด๊ทธ๋ ์ด์
1๊ฐ๋ฅผ ๋กค๋ฐฑํ๋ค(down + ์ด๋ ฅ ์ญ์ , ํ ํธ๋์ญ์
). ์ ์ฉ๋ถ์ด ์์ผ๋ฉด no-op.
|
|
163
|
-
* ์ ์ฉ ์ด๋ ฅ์ ์์ผ๋ ํ์ผ์ด ์ฌ๋ผ์ง ํญ๋ชฉ์ down ์ ์คํํ ์ ์์ด ๊ฑด๋๋ด๋ค.
|
|
164
263
|
*
|
|
165
|
-
*
|
|
264
|
+
* ์ด๋ ฅ์ ์ต์ ์ ์ฉ๋ถ์ **ํ์ผ์ด ์ฌ๋ผ์ง ๊ฒฝ์ฐ fail-fast** ํ๋ค(`migration.history_file_missing`, ADR-190) โ
|
|
265
|
+
* ์กฐ์ฉํ ๊ฑด๋๋ฐ๋ฉด ๋ ์ค๋๋ ๋ง์ด๊ทธ๋ ์ด์
์ด ๋กค๋ฐฑ๋์ด(์์ ์ญ์ ) ์ด๋ ฅ๊ณผ ์ค์ ์คํค๋ง๊ฐ ์ด๊ธ๋๋ค.
|
|
266
|
+
*
|
|
267
|
+
* @param {{ db: MigrationDb, projectRoot: string, appNames: string[], log?: { info?: Function, warn?: Function } }} opts
|
|
166
268
|
* @returns {Promise<{ rolledBack: string | null }>}
|
|
269
|
+
* @throws {MegaConfigError} `migration.history_file_missing` - ์ต์ ์ ์ฉ ์ด๋ ฅ์ ํ์ผ ๋ถ์ฌ.
|
|
167
270
|
*/
|
|
168
271
|
export async function migrateDown({ db, projectRoot, appNames, log }) {
|
|
169
272
|
await ensureTable(db)
|
|
170
|
-
const applied = await
|
|
273
|
+
const applied = await appliedRecords(db)
|
|
274
|
+
if (applied.size === 0) return { rolledBack: null }
|
|
275
|
+
// ์ด๋ ฅ์ ์ต์ ์ ์ฉ๋ถ โ name ์ `<ts>-<kebab>` ๋ผ ์ฌ์ ์ ์ ๋ ฌ = ์๊ฐ์.
|
|
276
|
+
const newest = [...applied.keys()].sort()[applied.size - 1]
|
|
171
277
|
const appliedFiles = collectMigrationFiles({ projectRoot, appNames }).filter((m) => applied.has(m.name))
|
|
172
278
|
const last = appliedFiles[appliedFiles.length - 1]
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
279
|
+
if (last === undefined || last.name !== newest) {
|
|
280
|
+
throw new MegaConfigError(
|
|
281
|
+
'migration.history_file_missing',
|
|
282
|
+
`Cannot roll back: migration '${newest}' is recorded in ${MIGRATIONS_TABLE} but its file is missing under apps/*/migrations. ` +
|
|
283
|
+
'Restore the file, or remove the history row manually after verifying the schema.',
|
|
284
|
+
{ details: { newestApplied: newest, lastFileFound: last?.name ?? null } },
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
const { down, useTransaction } = await loadMigration(last.absPath)
|
|
288
|
+
const rollback = async () => {
|
|
176
289
|
await down(db)
|
|
177
290
|
await removeApplied(db, last.name)
|
|
178
|
-
}
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
if (useTransaction) {
|
|
294
|
+
await runInTransaction(db, rollback)
|
|
295
|
+
} else {
|
|
296
|
+
log?.warn?.({ migration: last.name }, 'migrate.down no-transaction migration (rollback unavailable on partial failure)')
|
|
297
|
+
await rollback()
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
throw new MegaConfigError(
|
|
301
|
+
'migration.apply_failed',
|
|
302
|
+
`Migration '${last.name}' failed during down(): ${/** @type {any} */ (err).message} (file: ${last.absPath})`,
|
|
303
|
+
{ cause: err, details: { migration: last.name, file: last.absPath, direction: 'down' } },
|
|
304
|
+
)
|
|
305
|
+
}
|
|
179
306
|
log?.info?.({ migration: last.name }, 'migrate.down rolled back')
|
|
180
307
|
return { rolledBack: last.name }
|
|
181
308
|
}
|
|
182
309
|
|
|
183
310
|
/**
|
|
184
|
-
*
|
|
311
|
+
* ์ ์ฉ/๋ฏธ์ ์ฉ/๋๋ฆฌํํธ(์ ์ฉ ํ ์์ ) ๋ง์ด๊ทธ๋ ์ด์
๋ชฉ๋ก(ํ์์คํฌํ ์, ADR-190).
|
|
185
312
|
* @param {{ db: MigrationDb, projectRoot: string, appNames: string[] }} opts
|
|
186
|
-
* @returns {Promise<{ applied: string[], pending: string[] }>}
|
|
313
|
+
* @returns {Promise<{ applied: string[], pending: string[], modified: string[] }>}
|
|
187
314
|
*/
|
|
188
315
|
export async function migrateStatus({ db, projectRoot, appNames }) {
|
|
189
316
|
await ensureTable(db)
|
|
190
|
-
const applied = await
|
|
317
|
+
const applied = await appliedRecords(db)
|
|
191
318
|
const all = collectMigrationFiles({ projectRoot, appNames })
|
|
192
319
|
return {
|
|
193
320
|
applied: all.filter((m) => applied.has(m.name)).map((m) => m.name),
|
|
194
321
|
pending: all.filter((m) => !applied.has(m.name)).map((m) => m.name),
|
|
322
|
+
modified: findModified(all, applied),
|
|
195
323
|
}
|
|
196
324
|
}
|
package/src/core/multipart.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* ํ๋ฌ๊ทธ์ธ ๋ด์ฅ ๊ธฐ๋ฅ์ด ์๋๋ผ ๋ณธ ๋ชจ๋์ด ๊ฒ์ดํธ๋ก ๊ฐ์ โ ๋นํ์ฉ 415(`MegaUnsupportedMediaTypeError`).
|
|
24
24
|
* - **๊ฒฝ๋ก ํ์(path traversal) ์ฐจ๋จ**: ์ ์ฅ ์ `sanitizeFilename`(basename + ์ ํ ์ ์ ๊ฑฐ)์ผ๋ก ํ์ผ๋ช
์์
|
|
25
25
|
* ๋๋ ํฐ๋ฆฌ ์ฑ๋ถ์ ์ ๊ฑฐํ๊ณ , ์ต์ข
๊ฒฝ๋ก๊ฐ ๋์ ๋๋ ํฐ๋ฆฌ ๋ด๋ถ์ธ์ง ํ ๋ฒ ๋ ๊ฒ์ฆ(์ด์ค ๋ฐฉ์ด).
|
|
26
|
+
* - **ํ์ผ๋ช
์ถฉ๋ ์ฐจ๋จ**: ์ ์ฅ ํ์ผ๋ช
์ `uniquifyFilename`(ํ์์คํฌํ+๋๋ค ์ ๋ฏธ)์ผ๋ก ์ ์ผํ โ ๋์ผ๋ช
|
|
27
|
+
* ๋์/์ฐ์ ์
๋ก๋์ silent ๋ฎ์ด์ฐ๊ธฐยท์คํธ๋ฆผ race ๋ฅผ ๋ง๋๋ค. ํ์๋ช
(meta.filename)์ ์ด๊ท ์๋ณธ ์ ์ง.
|
|
26
28
|
* - **MIME ์คํธํ ํ๊ณ**: ๊ฒ์ดํธ๋ ํด๋ผ๊ฐ ์ ์ธํ `Content-Type`(part header) ๊ธฐ์ค์ด๋ค. ๋งค์ง๋ฐ์ดํธ ์ค๋ํ์
|
|
27
29
|
* ํ์ง ์๋๋ค(์คํธ๋ฆผ 1ํจ์ค ๋น์ฉ ํํผ). ์ ์ธ MIME ์์กฐ ๊ฐ๋ฅ์ฑ์ ๋ฌธ์ํ๋ ํ๊ณ์ด๋ฉฐ, ์ง์ง ์ฝํ
์ธ ๊ฒ์ฆ์ด
|
|
28
30
|
* ํ์ํ๋ฉด ํธ๋ค๋ฌ์์ `toBuffer()` ํ ๋ณ๋ ๊ฒ์ฌํ๋ค.
|
|
@@ -47,7 +49,8 @@ import { mkdir, unlink } from 'node:fs/promises'
|
|
|
47
49
|
import { createWriteStream } from 'node:fs'
|
|
48
50
|
import { stat } from 'node:fs/promises'
|
|
49
51
|
import { pipeline } from 'node:stream/promises'
|
|
50
|
-
import {
|
|
52
|
+
import { randomBytes } from 'node:crypto'
|
|
53
|
+
import { basename, extname, resolve, sep } from 'node:path'
|
|
51
54
|
import fastifyMultipart from '@fastify/multipart'
|
|
52
55
|
import { MegaUnsupportedMediaTypeError, MegaPayloadTooLargeError } from '../errors/http-errors.js'
|
|
53
56
|
import * as MegaTracing from '../lib/mega-tracing.js'
|
|
@@ -88,6 +91,23 @@ export function sanitizeFilename(name) {
|
|
|
88
91
|
return cleaned.length > 0 ? cleaned : 'upload'
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* ์ด๊ท ๋ ํ์ผ๋ช
์ ํ์์คํฌํ+๋๋ค ์ ๋ฏธ๋ฅผ ๋ถ์ฌ ์ ์ฅ ํ์ผ๋ช
์ ์ ์ผํํ๋ค.
|
|
96
|
+
*
|
|
97
|
+
* ์ด๊ท ๋ง์ผ๋ก๋ **๊ฐ์ ์ด๋ฆ์ ๋์/์ฐ์ ์
๋ก๋๊ฐ silent ๋ฎ์ด์ฐ๊ธฐ**๋๋ค(๋ ์ฌ์ฉ์๊ฐ 'photo.jpg' ๋ฅผ ์ฌ๋ฆฌ๋ฉด
|
|
98
|
+
* ๋์ค ๊ฒ์ด ์ด๊น, ๋์๋ฉด ์คํธ๋ฆผ ๊ต์ฐจ ๊ฐ๋ฅ). ์ ์ฅ ํ์ผ๋ช
์ `-<ts36>-<rand8hex>` ๋ฅผ ๋ถ์ฌ ์ถฉ๋์ ๋ง๋๋ค โ
|
|
99
|
+
* ๋ฐํ ๋ฉํ์ `filename`(ํ์๋ช
)์ ์ด๊ท ์๋ณธ ๊ทธ๋๋ก ์ ์งํ๊ณ `savedAs`(์ค์ ๊ฒฝ๋ก)๋ง ์ ์ผ๋ช
์ ๊ฐ๋๋ค.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} name - {@link sanitizeFilename} ์ ํต๊ณผํ ํ์ผ๋ช
.
|
|
102
|
+
* @returns {string} ์: 'photo.jpg' โ 'photo-mbz3k1x2-9f2ac01b.jpg'.
|
|
103
|
+
* @example uniquifyFilename('report.pdf') // 'report-<ts36>-<rand>.pdf'
|
|
104
|
+
*/
|
|
105
|
+
export function uniquifyFilename(name) {
|
|
106
|
+
const ext = extname(name)
|
|
107
|
+
const stem = name.slice(0, name.length - ext.length)
|
|
108
|
+
return `${stem}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}${ext}`
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
/**
|
|
92
112
|
* MIME ํ์
์ด ํ์ดํธ๋ฆฌ์คํธ์ ํ์ฉ๋๋์ง (Boolean โ `is*`). ๋น ๋ชฉ๋ก/๋ฏธ์ง์ ์ด๋ฉด **์ ๋ถ ํ์ฉ**(๊ฒ์ดํธ ๋นํ์ฑ).
|
|
93
113
|
* ์ ํ ๋งค์น + `type/*` ์์ผ๋์นด๋(์: `'image/*'` ๋ `'image/png'` ํ์ฉ)๋ฅผ ์ง์ํ๋ค.
|
|
@@ -210,9 +230,10 @@ export function registerMultipart(fastify, { upload, appName = '(unknown)', logg
|
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
/**
|
|
213
|
-
* ์
๋ก๋๋ ๋ชจ๋ ํ์ผ์ `destDir` ์ ์์ ํ๊ฒ ์ ์ฅํ๋ค(`req.saveUploads` ๊ตฌํ๋ถ). ํ์ผ๋ช
์
|
|
214
|
-
* ๊ฒฝ๋ก๊ฐ ๋์ ๋๋ ํฐ๋ฆฌ ๋ด๋ถ์ธ์ง ๊ฒ์ฆํ ๋ค ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๋์คํฌ์ ์ด๋ค. `mega.upload` span +
|
|
215
|
-
* ๋ฉํธ๋ฆญ์ ๊ธฐ๋กํ๋ค. MIME ๊ฒ์ดํธ๋ ๋ํ๋ `req.files()` ๊ฐ ์ ์ฉํ๋ค(๋นํ์ฉ 415 ์ ํ).
|
|
233
|
+
* ์
๋ก๋๋ ๋ชจ๋ ํ์ผ์ `destDir` ์ ์์ ํ๊ฒ ์ ์ฅํ๋ค(`req.saveUploads` ๊ตฌํ๋ถ). ํ์ผ๋ช
์ ์ด๊ท ยท์ ์ผํํ๊ณ
|
|
234
|
+
* ์ต์ข
๊ฒฝ๋ก๊ฐ ๋์ ๋๋ ํฐ๋ฆฌ ๋ด๋ถ์ธ์ง ๊ฒ์ฆํ ๋ค ์คํธ๋ฆฌ๋ฐ์ผ๋ก ๋์คํฌ์ ์ด๋ค. `mega.upload` span +
|
|
235
|
+
* `mega_upload_*` ๋ฉํธ๋ฆญ์ ๊ธฐ๋กํ๋ค. MIME ๊ฒ์ดํธ๋ ๋ํ๋ `req.files()` ๊ฐ ์ ์ฉํ๋ค(๋นํ์ฉ 415 ์ ํ).
|
|
236
|
+
* ๋ฐํ meta ์ `filename` ์ ์ด๊ท ํ์๋ช
, `savedAs` ๋ ์ ์ผํ๋ ์ค์ ์ ์ฅ ์ ๋๊ฒฝ๋ก๋ค.
|
|
216
237
|
*
|
|
217
238
|
* @param {object} args
|
|
218
239
|
* @param {any} args.req - Fastify ์์ฒญ(๋ํ๋ `files()` ๋ณด์ ).
|
|
@@ -236,7 +257,9 @@ async function saveUploads({ req, destDir, appName, saveOpts = {} }) {
|
|
|
236
257
|
try {
|
|
237
258
|
for await (const part of req.files(saveOpts.filesOptions)) {
|
|
238
259
|
const safeName = sanitizeFilename(part.filename)
|
|
239
|
-
|
|
260
|
+
// ์ ์ฅ ํ์ผ๋ช
์ ์ ์ผํ(๋์ผ๋ช
๋์/์ฐ์ ์
๋ก๋ silent ๋ฎ์ด์ฐ๊ธฐยท์คํธ๋ฆผ race ์ฐจ๋จ). ํ์๋ช
|
|
261
|
+
// (๋ฐํ meta.filename)์ ์ด๊ท ์๋ณธ ์ ์ง.
|
|
262
|
+
const abs = resolve(root, uniquifyFilename(safeName))
|
|
240
263
|
// ์ด์ค ๋ฐฉ์ด โ ์ด๊ท ํ์ด๋ ์ต์ข
๊ฒฝ๋ก๊ฐ root ๋ด๋ถ(root/<ํ์ผ>)๊ฐ ์๋๋ฉด ๊ฑฐ๋ถ.
|
|
241
264
|
if (!abs.startsWith(root + sep)) {
|
|
242
265
|
throw new Error(`upload.unsafe_path: resolved path escapes destDir (name='${part.filename}')`)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* HTTP ๋ผ์ฐํธ ๋ผ์ดํ์ฌ์ดํด Pipeline โ before/transform/after ํฉ์ฑ์ ๋จ์ผ ์๋ ด์ (ADR-185).
|
|
4
|
+
*
|
|
5
|
+
* ๋ฐฐ๊ฒฝ: ๋ฏธ๋ค์จ์ด wiring(arity ํก์ยทctx ์ฃผ์
ยทafter ์๋ฌ ์ ์ฑ
)์ด router.js(๋ผ์ฐํธ ๋ฑ๋ก)์
|
|
6
|
+
* mega-app.js(๊ธ๋ก๋ฒ ๋ฏธ๋ค์จ์ด)์ ๋น์ทํ์ง๋ง ๋ฏธ์ธํ๊ฒ ๋ค๋ฅธ ๋ณต์ฌ๋ณธ์ผ๋ก ํฉ์ด์ ธ ์์๊ณ , ๊ทธ๋์
|
|
7
|
+
* `router.use` ์ arity ๋ถํ
๊ฑฐ๋ถ(ADR-184 H2) ๊ฐ์ ์ฌ๊ฐ์ด ์๊ฒผ๋ค. ์ด ๋ชจ๋์ด ํฉ์ฑ ๋ก์ง์
|
|
8
|
+
* ์ ๋ณธ์ด๋ค โ ์ ๊ท ๋ฑ๋ก ๊ฒฝ๋ก๋ ๋ฐ๋์ ์ฌ๊ธฐ๋ฅผ ๊ฑฐ์น๋ค(๋ณต์ฌ๋ณธ ๊ธ์ง).
|
|
9
|
+
*
|
|
10
|
+
* ํฉ์ฑ ๊ท์น (ADR-091/134/156/184):
|
|
11
|
+
* - before: ๊ฐ๊ฐ arity-2 async ๋ํผ + canonical ctx ์ฃผ์
โ Fastify preHandler ๋ฐฐ์ด.
|
|
12
|
+
* (Fastify ๋ async hook ์ arity 3 ์ด์์ done ์ฝ๋ฐฑ์ผ๋ก ์ค์ธํด ๋ฑ๋ก์ ๊ฑฐ๋ถํ๋ฏ๋ก ๋ํผ ํ์.
|
|
13
|
+
* ๋ํผ๊ฐ ๋
๋ฆฝ preHandler ๋ผ ์์ยทreply ๋จ๋ฝ ์๋ฏธ๋ ๋ณด์กด๋๋ค.)
|
|
14
|
+
* - transform: ์์ฐจ await ์ฒด์ธ(payload ๋ณํ) โ ๋จ์ผ preSerialization. envelope wrap ์
|
|
15
|
+
* MegaApp onRoute ๊ฐ ์ฒด์ธ ๋งจ ๋์ ๋ณ๋ append(ADR-076 โ transform โ wrap ์์ ๋ณด์ฅ).
|
|
16
|
+
* - after: onResponse(์๋ต ์ ์ก ํ). throw ๋ warn ๋ก๊ทธ ํ ๋ฌด์ โ ์๋ต์ ์ํฅ ์์,
|
|
17
|
+
* silent ๊ธ์ง(P4, ADR-091).
|
|
18
|
+
*
|
|
19
|
+
* ํ์ผ/์ฑ/์ ์ญ ๋ ๋ฒจ transformยทafter ์ฌ๋กฏ(ADR-021 ์ ์ ์ฒด ์ฒด์ธ)์ Stage 2 โ ๋ฑ๋ก API ์ ๋ณธ
|
|
20
|
+
* ์ค๊ณ ํ ์ด ๋ชจ๋์ ์ถ๊ฐํ๋ค(ADR-185).
|
|
21
|
+
*
|
|
22
|
+
* @module core/pipeline
|
|
23
|
+
*/
|
|
24
|
+
import { getHttpCtx } from './ctx-builder.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ๋ฏธ๋ค์จ์ด๋ฅผ Fastify ๊ฐ async preHandler ๋ก ์ธ์ํ๋ arity-2 ๋ํผ๋ก ๊ฐ์ธ๊ณ , canonical ctx ๋ฅผ
|
|
28
|
+
* 3๋ฒ์งธ ์ธ์๋ก ์ฃผ์
ํ๋ค โ ํธ๋ค๋ฌยท๊ธ๋ก๋ฒ ๋ฏธ๋ค์จ์ด์ ๋์ผํ `(req, reply, ctx)` ๊ณ์ฝ(ADR-134/184).
|
|
29
|
+
* getHttpCtx ๋ ์์ฒญ๋น ์บ์ฑ์ด๋ผ ๊ฐ์ ์์ฒญ์ ๋ชจ๋ ๋ฏธ๋ค์จ์ดยทํธ๋ค๋ฌ๊ฐ ๋์ผ ctx ๊ฐ์ฒด๋ฅผ ๊ณต์ ํ๋ค.
|
|
30
|
+
*
|
|
31
|
+
* @param {Function} fn - `(req, reply, ctx?)` ๋ฏธ๋ค์จ์ด (arity-2 ๋ ํ์ ํธํ โ 3๋ฒ์งธ ์ธ์ ๋ฌด์).
|
|
32
|
+
* @param {import('./mega-app.js').MegaApp | null} [app] - ctx ์ ์ด๋ํฐยท์๋น์ค ์ ๊ทผ์ ์ถ์ฒ(์์ผ๋ฉด null).
|
|
33
|
+
* @returns {(req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => Promise<any>}
|
|
34
|
+
*/
|
|
35
|
+
export function wrapPreHandler(fn, app) {
|
|
36
|
+
return async (req, reply) => fn(req, reply, getHttpCtx({ app: app ?? null, req, reply }))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* transform ๋ฐฐ์ด์ ๋จ์ผ preSerialization ํจ์๋ก ํฉ์ฑํ๋ค โ ์์ฐจ await, ๊ฐ ๋ณํ์ ๋ฐํ๊ฐ์ด
|
|
41
|
+
* ๋ค์ ๋ณํ์ payload ๊ฐ ๋๋ค(raw data ๋ง ๋ค๋ฃธ โ envelope ๋ ์ดํ ๋จ๊ณ, ADR-091).
|
|
42
|
+
*
|
|
43
|
+
* @param {Function[]} transforms
|
|
44
|
+
* @returns {(req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, payload: any) => Promise<any>}
|
|
45
|
+
*/
|
|
46
|
+
export function composeTransform(transforms) {
|
|
47
|
+
return async (req, reply, payload) => {
|
|
48
|
+
let current = payload
|
|
49
|
+
for (const fn of transforms) {
|
|
50
|
+
current = await fn(req, reply, current)
|
|
51
|
+
}
|
|
52
|
+
return current
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* after ๋ฐฐ์ด์ ๋จ์ผ onResponse ํจ์๋ก ํฉ์ฑํ๋ค โ ์๋ต ์ ์ก ํ side-effect ์ ์ฉ.
|
|
58
|
+
* ๊ฐ๋ณ after ์ throw ๋ warn ๋ก๊ทธ ํ ๋ค์ after ๋ก ์งํ(์๋ต ์ํฅ ์์, ADR-091 / P4).
|
|
59
|
+
*
|
|
60
|
+
* @param {Function[]} afters
|
|
61
|
+
* @param {{ method: string, path: string }} route - warn ๋ก๊ทธ ์๋ณ์ฉ.
|
|
62
|
+
* @returns {(req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
export function composeAfter(afters, { method, path }) {
|
|
65
|
+
return async (req, reply) => {
|
|
66
|
+
for (const fn of afters) {
|
|
67
|
+
try {
|
|
68
|
+
await fn(req, reply)
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// ADR-091: silent fallback ๊ธ์ง โ warn ๋ก๊ทธ.
|
|
71
|
+
const log = req.log ?? console
|
|
72
|
+
log.warn?.({ err, hook: 'after', method, path }, `after middleware threw โ ignored (response already sent)`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {object} HttpPipeline
|
|
80
|
+
* @property {(req: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply) => Promise<any>} handler -
|
|
81
|
+
* canonical `(req, reply, ctx)` ์ฃผ์
์ด ์ ์ฉ๋ Fastify ํธ๋ค๋ฌ.
|
|
82
|
+
* @property {Function[]} [preHandler] - before ๋ํผ ๋ฐฐ์ด (before ์์ผ๋ฉด ์๋ต).
|
|
83
|
+
* @property {Function} [preSerialization] - transform ํฉ์ฑ (transform ์์ผ๋ฉด ์๋ต).
|
|
84
|
+
* @property {Function} [onResponse] - after ํฉ์ฑ (after ์์ผ๋ฉด ์๋ต).
|
|
85
|
+
* @property {() => { method: string, path: string, before: string[], transform: string[], after: string[] }} describe -
|
|
86
|
+
* ์ฒด์ธ introspection โ ๋จ๊ณ๋ณ ๋ฏธ๋ค์จ์ด ์ด๋ฆ ๋ชฉ๋ก(์ต๋ช
์ `(anonymous)`).
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ๋ผ์ฐํธ 1๊ฐ์ HTTP ๋ผ์ดํ์ฌ์ดํด์ ํฉ์ฑํ๋ค. Router._registerHttp ๊ฐ Fastify routeOpts ๋ก ์ฎ๊ธด๋ค.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} args
|
|
93
|
+
* @param {import('./mega-app.js').MegaApp | null} args.app - ctx ์ถ์ฒ (standalone Router ๋ฉด null).
|
|
94
|
+
* @param {string} args.method - HTTP ๋ฉ์๋ (์๋ฌธ์).
|
|
95
|
+
* @param {string} args.path - ๋ผ์ฐํธ ๊ฒฝ๋ก.
|
|
96
|
+
* @param {Function} args.handler - ์ฌ์ฉ์ ํธ๋ค๋ฌ (inline ๋๋ static method ref, ADR-074).
|
|
97
|
+
* @param {Function[]} [args.before]
|
|
98
|
+
* @param {Function[]} [args.transform]
|
|
99
|
+
* @param {Function[]} [args.after]
|
|
100
|
+
* @returns {HttpPipeline}
|
|
101
|
+
*/
|
|
102
|
+
export function buildHttpPipeline({ app = null, method, path, handler, before = [], transform = [], after = [] }) {
|
|
103
|
+
/** @type {HttpPipeline} */
|
|
104
|
+
const pipeline = {
|
|
105
|
+
// canonical ํธ๋ค๋ฌ ์๊ทธ๋์ฒ (req, res, ctx) (ADR-074, docs/03 ยง581). getHttpCtx ๋ ์์ฒญ๋น 1ํ
|
|
106
|
+
// ์บ์ฑ์ด๋ผ ๊ธ๋ก๋ฒยทbefore ๋ฏธ๋ค์จ์ด๊ฐ ๋จผ์ ๋ง๋ ctx ๋ฅผ ๊ทธ๋๋ก ์ด์ด๋ฐ๋๋ค(ADR-134).
|
|
107
|
+
handler: async (req, reply) => handler(req, reply, getHttpCtx({ app, req, reply })),
|
|
108
|
+
describe: () => ({
|
|
109
|
+
method,
|
|
110
|
+
path,
|
|
111
|
+
before: before.map(fnName),
|
|
112
|
+
transform: transform.map(fnName),
|
|
113
|
+
after: after.map(fnName),
|
|
114
|
+
}),
|
|
115
|
+
}
|
|
116
|
+
if (before.length > 0) pipeline.preHandler = before.map((fn) => wrapPreHandler(fn, app))
|
|
117
|
+
if (transform.length > 0) pipeline.preSerialization = composeTransform(transform)
|
|
118
|
+
if (after.length > 0) pipeline.onResponse = composeAfter(after, { method, path })
|
|
119
|
+
return pipeline
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ๋ฏธ๋ค์จ์ด ํจ์ ์ด๋ฆ (introspection ํ๊ธฐ์ฉ).
|
|
124
|
+
* @param {Function} fn
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function fnName(fn) {
|
|
128
|
+
return fn.name || '(anonymous)'
|
|
129
|
+
}
|