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.
Files changed (233) hide show
  1. package/bin/mega-ws-hub.js +2 -2
  2. package/package.json +32 -8
  3. package/sample/crud/.env +1 -1
  4. package/sample/crud/.env.example +1 -1
  5. package/sample/crud/.mega/journal/history/20260612092543-create-users.json +261 -0
  6. package/sample/crud/.mega/journal/snapshot.json +261 -0
  7. package/sample/crud/apps/main/controllers/auth-controller.js +22 -14
  8. package/sample/crud/apps/main/controllers/web-controller.js +7 -5
  9. package/sample/crud/apps/main/migrations/20260606000001-create-users.js +91 -13
  10. package/sample/crud/apps/main/migrations/20260606000002-create-boards.js +165 -0
  11. package/sample/crud/apps/main/migrations/20260606000003-create-logs.js +107 -0
  12. package/sample/crud/apps/main/models/log-partition-model.js +105 -0
  13. package/sample/crud/apps/main/models/note-model.js +79 -0
  14. package/sample/crud/apps/main/models/user-level-model.js +24 -0
  15. package/sample/crud/apps/main/models/user-model.js +146 -0
  16. package/sample/crud/apps/main/models/user-type-model.js +21 -0
  17. package/sample/crud/apps/main/models/wallet-model.js +24 -0
  18. package/sample/crud/apps/main/routes/users.js +55 -10
  19. package/sample/crud/apps/main/schedules/log-partition-schedule.js +33 -0
  20. package/sample/crud/apps/main/services/auth-service.js +39 -24
  21. package/sample/crud/apps/main/services/log-partition-service.js +101 -0
  22. package/sample/crud/apps/main/services/note-service.js +6 -6
  23. package/sample/crud/apps/main/services/redis-demo-service.js +3 -3
  24. package/sample/crud/apps/main/services/user-service.js +62 -21
  25. package/sample/crud/apps/main/views/auth/login.ejs +6 -6
  26. package/sample/crud/apps/main/views/auth/register.ejs +46 -5
  27. package/sample/crud/apps/main/views/users/edit.ejs +42 -5
  28. package/sample/crud/apps/main/views/users/list.ejs +6 -2
  29. package/sample/crud/apps/main/views/users/new.ejs +56 -4
  30. package/sample/crud/docs/log_partition_design.mm.md +23 -0
  31. package/sample/crud/mega.config.js +3 -2
  32. package/sample/crud/package.json +1 -1
  33. package/sample/crud/scripts/start-ws-hub.sh +2 -2
  34. package/sample/simple/package.json +2 -2
  35. package/src/adapters/adapter-manager.js +2 -1
  36. package/src/adapters/adapter-options.js +30 -0
  37. package/src/adapters/maria-adapter.js +26 -3
  38. package/src/adapters/mega-db-adapter.js +7 -1
  39. package/src/adapters/mongo-adapter.js +19 -1
  40. package/src/adapters/postgres-adapter.js +25 -2
  41. package/src/adapters/sqlite-adapter.js +20 -1
  42. package/src/cli/commands/new.js +13 -3
  43. package/src/cli/commands/scaffold.js +137 -33
  44. package/src/cli/generators/index.js +82 -2
  45. package/src/cli/index.js +353 -100
  46. package/src/core/ajv-mapper.js +27 -2
  47. package/src/core/boot.js +464 -245
  48. package/src/core/cluster-metrics.js +13 -4
  49. package/src/core/ctx-builder.js +6 -2
  50. package/src/core/envelope.js +112 -12
  51. package/src/core/hub-link.js +65 -4
  52. package/src/core/i18n.js +11 -1
  53. package/src/core/index.js +6 -2
  54. package/src/core/mega-app.js +201 -463
  55. package/src/core/mega-cluster.js +4 -1
  56. package/src/core/mega-server.js +40 -9
  57. package/src/core/migration/dialect-registry.js +107 -0
  58. package/src/core/migration/dialects/README.md +62 -0
  59. package/src/core/migration/dialects/maria.js +496 -0
  60. package/src/core/migration/dialects/mongo.js +824 -0
  61. package/src/core/migration/dialects/postgres.js +563 -0
  62. package/src/core/migration/dialects/sqlite.js +476 -0
  63. package/src/core/migration/differ.js +456 -0
  64. package/src/core/migration/generate.js +508 -0
  65. package/src/core/migration/journal.js +167 -0
  66. package/src/core/migration/model-scan.js +84 -0
  67. package/src/core/migration/mongo-migration-db.js +97 -0
  68. package/src/core/migration/schema-builder.js +400 -0
  69. package/src/core/migration/schema-validator.js +315 -0
  70. package/src/core/migration-lock.js +205 -0
  71. package/src/core/migration-runner.js +166 -38
  72. package/src/core/multipart.js +28 -5
  73. package/src/core/pipeline.js +129 -0
  74. package/src/core/router.js +70 -65
  75. package/src/core/security.js +67 -9
  76. package/src/core/workers-manager.js +12 -1
  77. package/src/core/ws-cluster.js +10 -3
  78. package/src/core/ws-message.js +48 -4
  79. package/src/core/ws-presence.js +624 -0
  80. package/src/core/ws-roster.js +4 -1
  81. package/src/core/ws-upgrade.js +118 -12
  82. package/src/index.js +1 -1
  83. package/src/lib/hub-protocol.js +29 -0
  84. package/src/lib/mega-health.js +25 -4
  85. package/src/lib/mega-job-queue.js +98 -21
  86. package/src/lib/mega-job.js +29 -0
  87. package/src/lib/mega-metrics.js +3 -12
  88. package/src/lib/mega-plugin.js +34 -3
  89. package/src/lib/mega-schedule.js +40 -22
  90. package/src/lib/mega-shutdown.js +114 -39
  91. package/src/lib/mega-tracing.js +66 -19
  92. package/src/lib/mega-worker.js +5 -1
  93. package/src/lib/otel-resource.js +36 -0
  94. package/src/{cli โ†’ lib}/ws-hub.js +51 -8
  95. package/src/models/crud-sql-builder.js +133 -0
  96. package/src/models/mega-model.js +82 -2
  97. package/src/models/model-crud.js +483 -0
  98. package/src/models/mongo-crud.js +285 -0
  99. package/templates/model/code-mongo.tpl +35 -0
  100. package/templates/model/code.tpl +15 -1
  101. package/templates/model/test-mongo.tpl +38 -0
  102. package/templates/model/test.tpl +4 -0
  103. package/types/adapters/adapter-manager.d.ts +95 -0
  104. package/types/adapters/adapter-options.d.ts +91 -0
  105. package/types/adapters/file-adapter.d.ts +94 -0
  106. package/types/adapters/file-session-adapter.d.ts +101 -0
  107. package/types/adapters/index.d.ts +20 -0
  108. package/types/adapters/maria-adapter.d.ts +115 -0
  109. package/types/adapters/mega-adapter.d.ts +215 -0
  110. package/types/adapters/mega-bus-adapter.d.ts +45 -0
  111. package/types/adapters/mega-cache-adapter.d.ts +47 -0
  112. package/types/adapters/mega-db-adapter.d.ts +47 -0
  113. package/types/adapters/mega-lock-adapter.d.ts +62 -0
  114. package/types/adapters/mega-log-sink-adapter.d.ts +15 -0
  115. package/types/adapters/mega-session-adapter.d.ts +32 -0
  116. package/types/adapters/mongo-adapter.d.ts +139 -0
  117. package/types/adapters/nats-adapter.d.ts +108 -0
  118. package/types/adapters/postgres-adapter.d.ts +139 -0
  119. package/types/adapters/redis-adapter.d.ts +70 -0
  120. package/types/adapters/redis-session-adapter.d.ts +82 -0
  121. package/types/adapters/redlock-adapter.d.ts +149 -0
  122. package/types/adapters/registry.d.ts +46 -0
  123. package/types/adapters/sqlite-adapter.d.ts +106 -0
  124. package/types/auth/index.d.ts +24 -0
  125. package/types/cli/commands/console-cmd.d.ts +37 -0
  126. package/types/cli/commands/new.d.ts +16 -0
  127. package/types/cli/commands/routes.d.ts +36 -0
  128. package/types/cli/commands/scaffold.d.ts +78 -0
  129. package/types/cli/commands/test-cmd.d.ts +14 -0
  130. package/types/cli/generators/index.d.ts +112 -0
  131. package/types/cli/index.d.ts +249 -0
  132. package/types/cli/template-engine.d.ts +40 -0
  133. package/types/core/ajv-mapper.d.ts +27 -0
  134. package/types/core/boot.d.ts +233 -0
  135. package/types/core/cluster-metrics.d.ts +52 -0
  136. package/types/core/config-loader.d.ts +13 -0
  137. package/types/core/config-validator.d.ts +30 -0
  138. package/types/core/ctx-builder.d.ts +80 -0
  139. package/types/core/envelope.d.ts +79 -0
  140. package/types/core/error-mapper.d.ts +17 -0
  141. package/types/core/formbody.d.ts +41 -0
  142. package/types/core/hub-link.d.ts +264 -0
  143. package/types/core/i18n.d.ts +178 -0
  144. package/types/core/index.d.ts +28 -0
  145. package/types/core/mega-app.d.ts +529 -0
  146. package/types/core/mega-cluster.d.ts +104 -0
  147. package/types/core/mega-server.d.ts +91 -0
  148. package/types/core/mega-service.d.ts +31 -0
  149. package/types/core/migration/dialect-registry.d.ts +22 -0
  150. package/types/core/migration/dialects/maria.d.ts +99 -0
  151. package/types/core/migration/dialects/mongo.d.ts +89 -0
  152. package/types/core/migration/dialects/postgres.d.ts +117 -0
  153. package/types/core/migration/dialects/sqlite.d.ts +111 -0
  154. package/types/core/migration/differ.d.ts +47 -0
  155. package/types/core/migration/generate.d.ts +56 -0
  156. package/types/core/migration/journal.d.ts +52 -0
  157. package/types/core/migration/model-scan.d.ts +19 -0
  158. package/types/core/migration/mongo-migration-db.d.ts +7 -0
  159. package/types/core/migration/schema-builder.d.ts +197 -0
  160. package/types/core/migration/schema-validator.d.ts +20 -0
  161. package/types/core/migration-lock.d.ts +33 -0
  162. package/types/core/migration-runner.d.ts +101 -0
  163. package/types/core/multipart.d.ts +86 -0
  164. package/types/core/openapi.d.ts +62 -0
  165. package/types/core/pipeline.d.ts +92 -0
  166. package/types/core/router.d.ts +159 -0
  167. package/types/core/routes-loader.d.ts +21 -0
  168. package/types/core/scope-registry.d.ts +14 -0
  169. package/types/core/security.d.ts +77 -0
  170. package/types/core/services-loader.d.ts +27 -0
  171. package/types/core/session-cleanup-schedule.d.ts +19 -0
  172. package/types/core/session-store.d.ts +18 -0
  173. package/types/core/session.d.ts +77 -0
  174. package/types/core/static-assets.d.ts +73 -0
  175. package/types/core/template.d.ts +106 -0
  176. package/types/core/workers-manager.d.ts +79 -0
  177. package/types/core/ws-cluster.d.ts +208 -0
  178. package/types/core/ws-compression.d.ts +112 -0
  179. package/types/core/ws-controller.d.ts +65 -0
  180. package/types/core/ws-message.d.ts +106 -0
  181. package/types/core/ws-presence.d.ts +273 -0
  182. package/types/core/ws-roster.d.ts +96 -0
  183. package/types/core/ws-upgrade.d.ts +231 -0
  184. package/types/errors/config-error.d.ts +10 -0
  185. package/types/errors/http-errors.d.ts +120 -0
  186. package/types/errors/index.d.ts +3 -0
  187. package/types/errors/mega-error.d.ts +32 -0
  188. package/types/index.d.ts +39 -0
  189. package/types/lib/asp/config.d.ts +49 -0
  190. package/types/lib/asp/crypto.d.ts +43 -0
  191. package/types/lib/asp/errors.d.ts +30 -0
  192. package/types/lib/asp/nonce-cache.d.ts +52 -0
  193. package/types/lib/asp/plugin.d.ts +30 -0
  194. package/types/lib/asp/ws-terminator.d.ts +45 -0
  195. package/types/lib/env-mapper.d.ts +14 -0
  196. package/types/lib/hub-protocol.d.ts +106 -0
  197. package/types/lib/index.d.ts +22 -0
  198. package/types/lib/logger/telegram-core.d.ts +104 -0
  199. package/types/lib/logger/telegram-transport.d.ts +45 -0
  200. package/types/lib/mega-brute-force.d.ts +66 -0
  201. package/types/lib/mega-circuit-breaker.d.ts +241 -0
  202. package/types/lib/mega-cron.d.ts +66 -0
  203. package/types/lib/mega-hash.d.ts +32 -0
  204. package/types/lib/mega-health.d.ts +41 -0
  205. package/types/lib/mega-job-queue.d.ts +176 -0
  206. package/types/lib/mega-job-worker.d.ts +130 -0
  207. package/types/lib/mega-job.d.ts +138 -0
  208. package/types/lib/mega-logger.d.ts +45 -0
  209. package/types/lib/mega-metrics.d.ts +285 -0
  210. package/types/lib/mega-plugin.d.ts +245 -0
  211. package/types/lib/mega-retry.d.ts +85 -0
  212. package/types/lib/mega-schedule.d.ts +260 -0
  213. package/types/lib/mega-shutdown.d.ts +135 -0
  214. package/types/lib/mega-tracing.d.ts +224 -0
  215. package/types/lib/mega-worker.d.ts +127 -0
  216. package/types/lib/otel-resource.d.ts +16 -0
  217. package/types/lib/worker-runner/process-entry.d.ts +1 -0
  218. package/types/lib/worker-runner/task-dispatch.d.ts +28 -0
  219. package/types/lib/worker-runner/thread-entry.d.ts +1 -0
  220. package/types/lib/ws-hub.d.ts +234 -0
  221. package/types/models/crud-sql-builder.d.ts +48 -0
  222. package/types/models/index.d.ts +1 -0
  223. package/types/models/mega-model.d.ts +138 -0
  224. package/types/models/model-crud.d.ts +82 -0
  225. package/types/models/mongo-crud.d.ts +59 -0
  226. package/types/test/index.d.ts +84 -0
  227. package/.env +0 -127
  228. package/sample/crud/apps/main/migrations/20260606000002-add-auth-to-users.js +0 -30
  229. package/sample/crud/apps/main/models/note.js +0 -71
  230. package/sample/crud/apps/main/models/user.js +0 -86
  231. package/sample/crud/package-lock.json +0 -5665
  232. package/sample/crud/yarn.lock +0 -2142
  233. 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 { readdirSync } from 'node:fs'
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(`CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (name VARCHAR(255) PRIMARY KEY, applied_at VARCHAR(64) NOT NULL)`)
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<Set<string>>}
113
+ * @returns {Promise<Map<string, string | null>>}
96
114
  */
97
- async function appliedSet(db) {
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 Set(rows.map((/** @type {any} */ r) => r.name))
118
+ return new Map(rows.map((/** @type {any} */ r) => [r.name, r.checksum ?? null]))
101
119
  }
102
120
 
103
121
  /**
104
- * ์ ์šฉ ๊ธฐ๋ก INSERT. name(๊ฒ€์ฆ๋œ [\d a-z -])ยทiso(ISO)๋Š” ๋”ฐ์˜ดํ‘œ ๋ถˆ๊ฐ€๋ผ ์ธ๋ผ์ธ ์•ˆ์ „(placeholder dialect ํšŒํ”ผ).
105
- * @param {MigrationDb} db @param {string} name @param {string} appliedAtIso
122
+ * ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋‚ด์šฉ์˜ sha-256 hex digest โ€” ์ ์šฉ ํ›„ ํŒŒ์ผ ์ˆ˜์ •(๋“œ๋ฆฌํ”„ํŠธ) ๊ฐ์ง€์šฉ(ADR-190).
123
+ * @param {string} absPath @returns {string}
106
124
  */
107
- async function recordApplied(db, name, appliedAtIso) {
108
- await db.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ('${name}', '${appliedAtIso}')`)
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
- * ๋ฏธ์ ์šฉ(pending) ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ํƒ€์ž„์Šคํƒฌํ”„ ์ˆœ์œผ๋กœ ๋ชจ๋‘ ์ ์šฉํ•œ๋‹ค. ๊ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์˜ up + ์ด๋ ฅ ๊ธฐ๋ก์„
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 appliedSet(db)
145
- const pending = collectMigrationFiles({ projectRoot, appNames }).filter((m) => !applied.has(m.name))
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
- await runInTransaction(db, async () => {
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
- * @param {{ db: MigrationDb, projectRoot: string, appNames: string[], log?: { info?: Function } }} opts
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 appliedSet(db)
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 (!last) return { rolledBack: null }
174
- const { down } = await loadMigration(last.absPath)
175
- await runInTransaction(db, async () => {
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 appliedSet(db)
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
  }
@@ -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 { basename, resolve, sep } from 'node:path'
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 + `mega_upload_*`
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
- const abs = resolve(root, safeName)
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
+ }