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
@@ -0,0 +1,285 @@
1
+ // @ts-check
2
+ /**
3
+ * MegaModel 공통 CRUD — **mongo(document) 경로** (ADR-212 P3). SQL 경로(model-crud.js)와 같은 표면·
4
+ * 의미를 mongo 도큐먼트 연산(`this.db.collection(table).find/insertOne/updateOne/...`)으로 구현한다.
5
+ *
6
+ * SQL 과 동일하게 **`static schema`(ADR-209 $jsonSchema) 게이트**로 식별자 화이트리스트를 강제하고,
7
+ * filter 는 등호 AND + 배열(`$in`) + null 까지만(체인 쿼리빌더 없음). PK 는 mongo 네이티브 `_id`
8
+ * (스키마의 `_id: t.objectId().primary()` 에서 도출) — `findById` 는 `_id` 로 조회한다.
9
+ *
10
+ * # SQL 과 다른 의미차(정직 명시)
11
+ * - **트랜잭션 자동 참여 없음**: SQL 은 `Model.query` 가 ALS txContext 로 자동 라우팅되지만, mongo CRUD 는
12
+ * 글로벌 `Db` 핸들의 collection 을 쓴다 → `withTransaction((db, session) => …)` 의 session 에 자동으로
13
+ * 묶이지 않는다. 트랜잭션 안 도큐먼트 연산은 raw `collection.op(doc, { session })` 를 직접 써야 한다.
14
+ * - **updateOne/deleteOne "정확히 하나"**: mongo updateOne 은 본래 첫 건만 바꾸고 에러가 없다 → SQL 의
15
+ * >1 throw 를 맞추려 **사전 countDocuments** 로 매칭 수를 센다. count→연산 사이는 비원자(데모 수준 보장).
16
+ *
17
+ * @module models/mongo-crud
18
+ */
19
+ import { MegaInternalError } from '../errors/http-errors.js'
20
+ import { getModelMeta } from './model-crud.js'
21
+ import { assertColumn, assertNonNegInt } from './crud-sql-builder.js'
22
+
23
+ /** mongo 컬렉션 핸들 + schema 화이트리스트(+`_id`)·PK 메타. @param {any} Model @returns {{ col: any, cols: Set<string>, pk: string }} */
24
+ function mongoCtx(Model) {
25
+ const meta = getModelMeta(Model) // schema 없으면 model.crud_requires_schema (SQL 과 동일 게이트)
26
+ const cols = new Set(meta.cols)
27
+ cols.add('_id') // mongo PK 는 항상 허용(스키마 미선언이어도 필터/조회 가능)
28
+ return { col: Model.db.collection(meta.table), cols, pk: meta.pk ?? '_id' }
29
+ }
30
+
31
+ /**
32
+ * filter → mongo 쿼리 도큐먼트. 등호 + 배열(`$in`) + null 만(ADR-212 경계). `undefined` 는 throw.
33
+ * @param {Record<string, any>} filter @param {Set<string>} cols @param {string} model @returns {Record<string, any>}
34
+ */
35
+ function toQuery(filter, cols, model) {
36
+ /** @type {Record<string, any>} */
37
+ const q = {}
38
+ for (const [k, v] of Object.entries(filter)) {
39
+ assertColumn(k, cols, model, 'filter')
40
+ if (v === undefined) {
41
+ throw new MegaInternalError(
42
+ 'model.invalid_filter',
43
+ `${model}: filter['${k}'] 가 undefined 입니다(null 과 구분 — 의도 모호).`,
44
+ { details: { model, column: k } },
45
+ )
46
+ }
47
+ q[k] = Array.isArray(v) ? { $in: v } : v // 빈 배열 → {$in: []} 는 매칭 0(안전). null → 그대로(null 매칭).
48
+ }
49
+ return q
50
+ }
51
+
52
+ /**
53
+ * select → mongo projection. 미지정이면 전체(undefined). 지정 시 inclusion + `_id` 제외(요청 안 했을 때).
54
+ * @param {string[]|undefined} select @param {Set<string>} cols @param {string} model @returns {Record<string, 0|1>|undefined}
55
+ */
56
+ function toProjection(select, cols, model) {
57
+ if (!Array.isArray(select) || select.length === 0) return undefined
58
+ /** @type {Record<string, 0|1>} */
59
+ const p = {}
60
+ for (const c of select) {
61
+ assertColumn(c, cols, model, 'select')
62
+ p[c] = 1
63
+ }
64
+ if (!select.includes('_id')) p._id = 0 // 도메인 형태 — 명시 안 하면 _id 숨김(inclusion + _id 제외만 mongo 허용 조합).
65
+ return p
66
+ }
67
+
68
+ /** orderBy → mongo sort 도큐먼트. @param {any} orderBy @param {Set<string>} cols @param {string} model @returns {Record<string, 1|-1>|undefined} */
69
+ function toSort(orderBy, cols, model) {
70
+ if (orderBy === undefined) return undefined
71
+ const list = typeof orderBy === 'string' ? [{ column: orderBy }] : orderBy
72
+ if (!Array.isArray(list) || list.length === 0) return undefined
73
+ /** @type {Record<string, 1|-1>} */
74
+ const s = {}
75
+ for (const o of list) {
76
+ const col = typeof o === 'string' ? o : o.column
77
+ assertColumn(col, cols, model, 'orderBy')
78
+ const dir = typeof o === 'string' ? 'asc' : (o.dir ?? 'asc')
79
+ if (dir !== 'asc' && dir !== 'desc') {
80
+ throw new MegaInternalError(
81
+ 'model.invalid_pagination',
82
+ `${model}: orderBy dir 은 'asc'|'desc' 만 — got '${dir}'.`,
83
+ { details: { model, dir } },
84
+ )
85
+ }
86
+ s[col] = dir === 'desc' ? -1 : 1
87
+ }
88
+ return s
89
+ }
90
+
91
+ /** @param {any} Model @param {string} method @param {number} n */
92
+ function multipleMatches(Model, method, n) {
93
+ return new MegaInternalError(
94
+ 'model.multiple_matches',
95
+ `${Model.name}.${method}: filter 가 ${n} 건에 매칭됐습니다("정확히 하나" 위반). _id/unique filter 를 쓰거나 ${method.replace('One', 'Many')} 를 쓰세요.`,
96
+ {
97
+ details: { model: Model.name, method, matched: n },
98
+ },
99
+ )
100
+ }
101
+
102
+ // ── 읽기 ───────────────────────────────────────────────────────────────────
103
+
104
+ /** @param {any} Model @param {Record<string, any>} filter @param {{ select?: string[] }} [opts] @returns {Promise<any|null>} */
105
+ export async function findOne(Model, filter, opts = {}) {
106
+ const { col, cols } = mongoCtx(Model)
107
+ const projection = toProjection(opts.select, cols, Model.name)
108
+ return col.findOne(toQuery(filter ?? {}, cols, Model.name), projection ? { projection } : {})
109
+ }
110
+
111
+ /** @param {any} Model @param {Record<string, any>} filter @param {{ select?: string[], orderBy?: any, limit?: number, offset?: number }} [opts] @returns {Promise<any[]>} */
112
+ export async function findMany(Model, filter, opts = {}) {
113
+ const { col, cols } = mongoCtx(Model)
114
+ const projection = toProjection(opts.select, cols, Model.name)
115
+ let cur = col.find(toQuery(filter ?? {}, cols, Model.name), projection ? { projection } : {})
116
+ const sort = toSort(opts.orderBy, cols, Model.name)
117
+ if (sort) cur = cur.sort(sort)
118
+ if (opts.offset !== undefined) cur = cur.skip(assertNonNegInt(opts.offset, 'offset', Model.name))
119
+ if (opts.limit !== undefined) cur = cur.limit(assertNonNegInt(opts.limit, 'limit', Model.name))
120
+ return cur.toArray()
121
+ }
122
+
123
+ /** @param {any} Model @param {any} id @param {{ select?: string[] }} [opts] @returns {Promise<any|null>} */
124
+ export async function findById(Model, id, opts = {}) {
125
+ const { pk } = mongoCtx(Model)
126
+ return findOne(Model, { [pk]: id }, opts)
127
+ }
128
+
129
+ /** @param {any} Model @param {Record<string, any>} [filter] @returns {Promise<number>} */
130
+ export async function count(Model, filter = {}) {
131
+ const { col, cols } = mongoCtx(Model)
132
+ return col.countDocuments(toQuery(filter, cols, Model.name))
133
+ }
134
+
135
+ /** @param {any} Model @param {Record<string, any>} filter @returns {Promise<boolean>} */
136
+ export async function exists(Model, filter) {
137
+ const { col, cols } = mongoCtx(Model)
138
+ return (await col.countDocuments(toQuery(filter ?? {}, cols, Model.name), { limit: 1 })) > 0
139
+ }
140
+
141
+ /** @param {any} Model @param {Record<string, any>} filter @param {{ select?: string[], orderBy?: any, limit: number, offset?: number, withTotal?: boolean }} opts @returns {Promise<{ rows: any[], limit: number, offset: number, total?: number }>} */
142
+ export async function paginate(Model, filter, opts) {
143
+ const limit = assertNonNegInt(opts?.limit, 'limit', Model.name)
144
+ const offset = opts?.offset !== undefined ? assertNonNegInt(opts.offset, 'offset', Model.name) : 0
145
+ const rows = await findMany(Model, filter, {
146
+ select: opts?.select,
147
+ orderBy: opts?.orderBy,
148
+ limit,
149
+ offset,
150
+ })
151
+ /** @type {{ rows: any[], limit: number, offset: number, total?: number }} */
152
+ const out = { rows, limit, offset }
153
+ if (opts?.withTotal === true) out.total = await count(Model, filter ?? {})
154
+ return out
155
+ }
156
+
157
+ // ── 쓰기 ───────────────────────────────────────────────────────────────────
158
+
159
+ /** 삽입 도큐먼트 컬럼 검증. @param {any} Model @param {Record<string, any>} data @param {Set<string>} cols */
160
+ function assertInsert(Model, data, cols) {
161
+ const keys = Object.keys(data ?? {})
162
+ if (keys.length === 0)
163
+ throw new MegaInternalError('model.empty_insert', `${Model.name}: 삽입할 필드가 없습니다.`, {
164
+ details: { model: Model.name },
165
+ })
166
+ keys.forEach((k) => assertColumn(k, cols, Model.name, 'data'))
167
+ }
168
+
169
+ /** @param {any} Model @param {Record<string, any>} data @param {{ returning?: boolean }} [opts] @returns {Promise<any>} */
170
+ export async function insertOne(Model, data, opts = {}) {
171
+ const { col, cols } = mongoCtx(Model)
172
+ assertInsert(Model, data, cols)
173
+ const res = await col.insertOne({ ...data })
174
+ if (opts.returning === true) return col.findOne({ _id: res.insertedId })
175
+ return res.insertedId // 기본 — 새 _id
176
+ }
177
+
178
+ /** @param {any} Model @param {Record<string, any>[]} rows @param {{ returning?: boolean }} [opts] @returns {Promise<{ count: number } | any[]>} */
179
+ export async function insertMany(Model, rows, opts = {}) {
180
+ if (!Array.isArray(rows) || rows.length === 0) return opts.returning === true ? [] : { count: 0 }
181
+ const { col, cols } = mongoCtx(Model)
182
+ rows.forEach((r) => assertInsert(Model, r, cols))
183
+ const res = await col.insertMany(rows.map((r) => ({ ...r })))
184
+ if (opts.returning === true) {
185
+ const ids = Object.values(res.insertedIds)
186
+ return col.find({ _id: { $in: ids } }).toArray()
187
+ }
188
+ return { count: res.insertedCount }
189
+ }
190
+
191
+ /** patch 검증(빈 patch throw). @param {any} Model @param {Record<string, any>} patch @param {Set<string>} cols @returns {string[]} */
192
+ function assertPatch(Model, patch, cols) {
193
+ const keys = Object.keys(patch ?? {})
194
+ if (keys.length === 0)
195
+ throw new MegaInternalError(
196
+ 'model.empty_patch',
197
+ `${Model.name}: 갱신할 필드(patch)가 없습니다.`,
198
+ { details: { model: Model.name } },
199
+ )
200
+ keys.forEach((k) => assertColumn(k, cols, Model.name, 'patch'))
201
+ return keys
202
+ }
203
+
204
+ /** @param {any} Model @param {Record<string, any>} filter @param {Record<string, any>} patch @returns {Promise<number>} */
205
+ export async function updateOne(Model, filter, patch) {
206
+ const { col, cols } = mongoCtx(Model)
207
+ if (Object.keys(filter ?? {}).length === 0) {
208
+ throw new MegaInternalError(
209
+ 'model.invalid_filter',
210
+ `${Model.name}: updateOne 은 대상을 특정하는 filter 가 필요합니다(빈 filter 금지).`,
211
+ { details: { model: Model.name } },
212
+ )
213
+ }
214
+ assertPatch(Model, patch, cols)
215
+ const q = toQuery(filter, cols, Model.name)
216
+ // "정확히 하나" — 사전 count(비원자, 데모 수준). >1 이면 변경 없이 throw.
217
+ const n = await col.countDocuments(q, { limit: 2 })
218
+ if (n > 1) throw multipleMatches(Model, 'updateOne', n)
219
+ return (await col.updateOne(q, { $set: patch })).matchedCount
220
+ }
221
+
222
+ /** @param {any} Model @param {Record<string, any>} filter @param {Record<string, any>} patch @param {{ all?: boolean }} [opts] @returns {Promise<number>} */
223
+ export async function updateMany(Model, filter, patch, opts = {}) {
224
+ const { col, cols } = mongoCtx(Model)
225
+ assertPatch(Model, patch, cols)
226
+ if (Object.keys(filter ?? {}).length === 0 && opts.all !== true) {
227
+ throw new MegaInternalError(
228
+ 'model.unbounded_write',
229
+ `${Model.name}: 빈 filter 의 전체 갱신은 막혀 있습니다. 의도면 { all: true } 를 쓰세요.`,
230
+ { details: { model: Model.name } },
231
+ )
232
+ }
233
+ return (await col.updateMany(toQuery(filter ?? {}, cols, Model.name), { $set: patch }))
234
+ .matchedCount
235
+ }
236
+
237
+ /** @param {any} Model @param {Record<string, any>} filter @returns {Promise<number>} */
238
+ export async function deleteOne(Model, filter) {
239
+ const { col, cols } = mongoCtx(Model)
240
+ if (Object.keys(filter ?? {}).length === 0) {
241
+ throw new MegaInternalError(
242
+ 'model.invalid_filter',
243
+ `${Model.name}: deleteOne 은 대상을 특정하는 filter 가 필요합니다(빈 filter 금지).`,
244
+ { details: { model: Model.name } },
245
+ )
246
+ }
247
+ const q = toQuery(filter, cols, Model.name)
248
+ const n = await col.countDocuments(q, { limit: 2 })
249
+ if (n > 1) throw multipleMatches(Model, 'deleteOne', n)
250
+ return (await col.deleteOne(q)).deletedCount
251
+ }
252
+
253
+ /** @param {any} Model @param {Record<string, any>} filter @param {{ all?: boolean }} [opts] @returns {Promise<number>} */
254
+ export async function deleteMany(Model, filter, opts = {}) {
255
+ const { col, cols } = mongoCtx(Model)
256
+ if (Object.keys(filter ?? {}).length === 0 && opts.all !== true) {
257
+ throw new MegaInternalError(
258
+ 'model.unbounded_write',
259
+ `${Model.name}: 빈 filter 의 전체 삭제는 막혀 있습니다. 의도면 { all: true } 를 쓰세요.`,
260
+ { details: { model: Model.name } },
261
+ )
262
+ }
263
+ return (await col.deleteMany(toQuery(filter ?? {}, cols, Model.name))).deletedCount
264
+ }
265
+
266
+ /** @param {any} Model @param {Record<string, any>} data @param {{ conflict: string[], returning?: boolean }} opts @returns {Promise<any>} */
267
+ export async function upsert(Model, data, opts) {
268
+ const { col, cols } = mongoCtx(Model)
269
+ assertInsert(Model, data, cols)
270
+ const conflict = opts?.conflict
271
+ if (!Array.isArray(conflict) || conflict.length === 0) {
272
+ throw new MegaInternalError(
273
+ 'model.invalid_conflict_target',
274
+ `${Model.name}: upsert 는 opts.conflict(충돌 필드 배열)가 필요합니다.`,
275
+ { details: { model: Model.name } },
276
+ )
277
+ }
278
+ conflict.forEach((c) => assertColumn(c, cols, Model.name, 'conflict'))
279
+ /** @type {Record<string, any>} */
280
+ const q = {}
281
+ conflict.forEach((c) => (q[c] = data[c]))
282
+ await col.updateOne(q, { $set: { ...data } }, { upsert: true })
283
+ if (opts.returning === true) return col.findOne(q)
284
+ return undefined
285
+ }
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import { MegaModel } from 'mega-framework'
3
+
4
+ /**
5
+ * {{Name}} 모델(mongodb) — `static table` 이 collection 이름이다(SQL table 과 통일, ADR-081).
6
+ * `static adapter` 는 mega.config.js 의 services.databases 키를 가리킨다(ADR-061).
7
+ *
8
+ * `static schema`(빌더)를 선언하면 자동 마이그레이션 트랙에 합류한다(ADR-204/209 옵트인) —
9
+ * `mega migrate:generate` 가 collection validator($jsonSchema)·인덱스를 생성/갱신한다.
10
+ * mongo 차이(ADR-209/210): `_id` 는 자동 ObjectId(명시는 `_id: t.objectId().primary()` 만),
11
+ * `.default()` 미지원(기본값은 도큐먼트 생성 시 앱 레벨), FK/`.check()` 없음(cross-field 는
12
+ * 생성 파일 raw 편집의 `$expr`), 명시 null 을 저장하는 필드는 `.nullable()`, 중첩 도큐먼트는
13
+ * `t.object({...})`. 블록 삭제 시 raw 마이그레이션 트랙으로 돌아간다(호환).
14
+ */
15
+ export class {{Name}} extends MegaModel {
16
+ static adapter = '{{adapter}}'
17
+ static table = '{{table}}'
18
+
19
+ // 자동 마이그레이션 스키마(ADR-209) — 빌더 API 는 docs/guide/03-service-model-db.md §5 참조.
20
+ static schema = (t) => ({
21
+ name: t.varchar(100).notNull(),
22
+ createdAt: t.timestamptz().notNull(), // 생성 시 앱에서 new Date() 로 채운다(mongo 는 default 미지원)
23
+ })
24
+
25
+ // 인덱스(선택): static indexes = (t) => [t.index(['name'], { unique: true })]
26
+
27
+ /**
28
+ * _id 로 1건 조회 — `this.db` 는 native mongodb `Db`(ADR-009), 도큐먼트 API 직접 사용.
29
+ * @param {import('mongodb').ObjectId} id
30
+ * @returns {Promise<object|null>}
31
+ */
32
+ static async findById(id) {
33
+ return this.db.collection(this.table).findOne({ _id: id })
34
+ }
35
+ }
@@ -4,11 +4,25 @@ import { MegaModel } from 'mega-framework'
4
4
  /**
5
5
  * {{Name}} 모델 — 데이터 소스는 `static table` 로 통일(SQL table / Mongo collection, ADR-081).
6
6
  * `static adapter` 는 mega.config.js 의 services.databases 키를 가리킨다(ADR-061).
7
+ *
8
+ * `static schema`(빌더)를 선언하면 자동 마이그레이션 트랙에 합류한다(ADR-204 옵트인) —
9
+ * 모델 변경 후 `mega migrate:generate` 가 diff 로 마이그레이션 파일을 만들고 `mega migrate` 로
10
+ * 적용한다. 이 블록을 삭제하면 raw SQL 마이그레이션(`mega g migration <name>`)만 쓰는 트랙으로
11
+ * 돌아간다(기존 모델과 동일 — 호환).
7
12
  */
8
13
  export class {{Name}} extends MegaModel {
9
- static adapter = 'primary'
14
+ static adapter = '{{adapter}}'
10
15
  static table = '{{table}}'
11
16
 
17
+ // 자동 마이그레이션 스키마(ADR-204) — 빌더 API 는 docs/guide/03-service-model-db.md §5 참조.
18
+ static schema = (t) => ({
19
+ id: t.serial().primary(),
20
+ name: t.varchar(100).notNull(),
21
+ createdAt: t.timestamptz().defaultNow(),
22
+ })
23
+
24
+ // 인덱스(선택): static indexes = (t) => [t.index(['name'], { unique: true })]
25
+
12
26
  /**
13
27
  * id 로 1건 조회. `this.query` 는 계측된 어댑터 query 위임(ADR-138).
14
28
  * @param {string|number} id
@@ -0,0 +1,38 @@
1
+ // @ts-check
2
+ import { describe, test, expect } from 'vitest'
3
+ import { MegaModel } from 'mega-framework'
4
+ import { {{Name}} } from '{{importPath}}'
5
+
6
+ describe('{{Name}} model (mongodb)', () => {
7
+ test('MegaModel 을 상속하고 static table(collection)/adapter 설정됨', () => {
8
+ expect(Object.getPrototypeOf({{Name}})).toBe(MegaModel)
9
+ expect({{Name}}.table).toBe('{{table}}')
10
+ expect(typeof {{Name}}.adapter).toBe('string')
11
+ })
12
+
13
+ test('schema 빌더 선언 — 자동 마이그레이션 트랙 옵트인(ADR-209)', () => {
14
+ expect(typeof {{Name}}.schema).toBe('function')
15
+ })
16
+
17
+ test('findById — native Db 의 도큐먼트 API 에 위임', async () => {
18
+ /** @type {any[]} */
19
+ const calls = []
20
+ const fakeDb = {
21
+ collection: (/** @type {string} */ name) => ({
22
+ findOne: async (/** @type {any} */ filter) => {
23
+ calls.push({ name, filter })
24
+ return { _id: filter._id }
25
+ },
26
+ }),
27
+ }
28
+ // MegaModel.db 는 getter 라 서브클래스 own property 로 가린다(테스트 한정).
29
+ Object.defineProperty({{Name}}, 'db', { value: fakeDb, configurable: true })
30
+ try {
31
+ const doc = await {{Name}}.findById(/** @type {any} */ ('id-1'))
32
+ expect(doc).toEqual({ _id: 'id-1' })
33
+ expect(calls[0].name).toBe('{{table}}')
34
+ } finally {
35
+ delete /** @type {any} */ ({{Name}}).db
36
+ }
37
+ })
38
+ })
@@ -10,6 +10,10 @@ describe('{{Name}} model', () => {
10
10
  expect(typeof {{Name}}.adapter).toBe('string')
11
11
  })
12
12
 
13
+ test('schema 빌더 선언 — 자동 마이그레이션 트랙 옵트인(ADR-204)', () => {
14
+ expect(typeof {{Name}}.schema).toBe('function')
15
+ })
16
+
13
17
  test('findById — 어댑터 query 에 위임', async () => {
14
18
  const orig = {{Name}}.query
15
19
  /** @type {any[]} */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 전역 config(`services.databases/caches/buses`)로부터 어댑터 인스턴스를 만든다 (lifecycle 1단계).
3
+ *
4
+ * 각 도메인의 `<globalKey>: { driver, ...opts }` 마다 `registry.resolve(driver)` → `new Cls(opts)`.
5
+ * driver 미등록(빌트인은 import 시 자기등록)·미지정·도메인 불일치는 즉시 throw (부팅 abort, fail-fast).
6
+ *
7
+ * # 종료 순서 (중요)
8
+ * 본 함수가 LIFO shutdown hook 을 등록하므로, **MegaApp 생성보다 먼저** 호출해야 한다. MegaShutdown
9
+ * 은 LIFO(나중 등록이 먼저 실행) — 어댑터 hook 을 앱 hook 보다 먼저 등록하면, 종료 시 앱(HTTP/WS)이
10
+ * 먼저 닫히고 그 다음 어댑터가 끊긴다(docs/10: ClosingHttp → … → DisconnectingAdapters).
11
+ *
12
+ * @param {{ services?: Record<string, any> }} [globalConfig] - mega.config.js (default export).
13
+ * @param {{ registerShutdownHook?: boolean }} [opts] - 테스트 격리용으로 hook 등록을 끌 수 있음(기본 on).
14
+ * @returns {void}
15
+ */
16
+ export function buildFromGlobalConfig(globalConfig?: {
17
+ services?: Record<string, any>;
18
+ }, { registerShutdownHook }?: {
19
+ registerShutdownHook?: boolean;
20
+ }): void;
21
+ /**
22
+ * 도메인+globalKey 로 공유 어댑터 인스턴스 조회. 없으면 `adapter.not_registered` throw (silent X).
23
+ *
24
+ * @param {'db'|'cache'|'bus'|'lock'} domain
25
+ * @param {string} key - globalKey.
26
+ * @returns {import('./mega-adapter.js').MegaAdapter}
27
+ */
28
+ export function get(domain: "db" | "cache" | "bus" | "lock", key: string): import("./mega-adapter.js").MegaAdapter;
29
+ /**
30
+ * 등록 여부 (Boolean — `has*`, ADR-036).
31
+ * @param {'db'|'cache'|'bus'|'lock'} domain @param {string} key @returns {boolean}
32
+ */
33
+ export function has(domain: "db" | "cache" | "bus" | "lock", key: string): boolean;
34
+ /**
35
+ * 전체 인스턴스 스냅샷 (디버그·테스트·헬스용). 등록 순서대로.
36
+ * @returns {Array<{ domain: 'db'|'cache'|'bus'|'lock', key: string, driver: string, state: string }>}
37
+ */
38
+ export function list(): Array<{
39
+ domain: "db" | "cache" | "bus" | "lock";
40
+ key: string;
41
+ driver: string;
42
+ state: string;
43
+ }>;
44
+ /**
45
+ * 전체 어댑터 엔트리(인스턴스 포함)를 등록 순서대로 반환 — 외부 부착자(`MegaTracing`)가
46
+ * 모든 공유 어댑터에 hook 리스너를 일괄 구독할 때 쓴다. {@link list} 와 달리 raw 어댑터 인스턴스를
47
+ * 노출하므로 디버그·트레이싱 부착 외 용도로는 신중히 사용.
48
+ *
49
+ * @returns {AdapterEntry[]}
50
+ */
51
+ export function entries(): AdapterEntry[];
52
+ /**
53
+ * 모든 어댑터 connect (부팅 6단계). 병렬 시도하되 **fail-fast + 자동 cleanup** (ADR-102 QA M-A).
54
+ * 하나라도 실패하면 부팅을 abort 하기 위해 첫 실패 에러를 throw 하는데, 그 전에 이미 connect 된
55
+ * 어댑터를 **등록 역순(LIFO)으로 best-effort disconnect** 해서 연결 풀 leak 을 막는다 — 호출자가
56
+ * catch 안 해도 매니저 레벨에서 자원을 회수한다(call-site CLI 책임에만 의존하지 않음).
57
+ *
58
+ * `ping:true` 면 connect 직후 `healthCheck()` 를 1회 호출해 ok=false 면 `adapter.health_failed`
59
+ * throw (헬스까지 검증). ping 실패 어댑터도 connect 자체는 성공했으므로 cleanup 대상에 포함된다.
60
+ *
61
+ * cleanup 중 disconnect 가 실패해도 원래 connect 실패 에러를 우선 전파한다 — cleanup 실패는
62
+ * 비치명적이라 warn 로그만 남기고 삼킨다(부팅 abort 정리 중 한 어댑터 정리 실패가
63
+ * 진짜 원인(connect 실패)을 덮으면 안 됨).
64
+ *
65
+ * @param {{ logger?: any, ping?: boolean }} [opts]
66
+ * @returns {Promise<void>}
67
+ */
68
+ export function connectAll({ logger, ping }?: {
69
+ logger?: any;
70
+ ping?: boolean;
71
+ }): Promise<void>;
72
+ /**
73
+ * 모든 어댑터 disconnect — 등록 **역순(LIFO)** (docs/10 §6, 07-sequence-diagrams §6). graceful
74
+ * shutdown 중이므로 개별 disconnect 실패는 비치명적: warn 후 다음 어댑터로 계속한다( * 한 어댑터 정리 실패가 나머지 정리를 막으면 안 됨).
75
+ *
76
+ * @param {{ logger?: any }} [opts]
77
+ * @returns {Promise<void>}
78
+ */
79
+ export function disconnectAll({ logger }?: {
80
+ logger?: any;
81
+ }): Promise<void>;
82
+ /**
83
+ * 테스트용 reset — 인스턴스 비우고 shutdown hook 해제.
84
+ * @returns {void}
85
+ */
86
+ export function _reset(): void;
87
+ export type AdapterEntry = {
88
+ domain: "db" | "cache" | "bus" | "lock";
89
+ /**
90
+ * - globalKey (services.<configKey>.<key>).
91
+ */
92
+ key: string;
93
+ driver: string;
94
+ adapter: import("./mega-adapter.js").MegaAdapter;
95
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * 트랜잭션 격리수준 옵션 검증 + SQL 조각 변환 (ADR-190). `undefined` 는 driver 디폴트 위임(undefined 반환).
3
+ * @param {unknown} isolation - `withTransaction` 의 `opts.isolation`.
4
+ * @param {string} driver - 에러 메시지용 driver 이름.
5
+ * @returns {string | undefined} `SET TRANSACTION ISOLATION LEVEL <조각>` 에 쓸 SQL 조각.
6
+ * @throws {MegaValidationError} `adapter.invalid_option` - 화이트리스트 외 값.
7
+ */
8
+ export function resolveTxIsolation(isolation: unknown, driver: string): string | undefined;
9
+ /**
10
+ * 양의 정수(> 0) 검증 (undefined 통과). 위반 시 `adapter.invalid_option` throw.
11
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
12
+ */
13
+ export function assertPositiveInt(name: string, value: unknown, extra?: Record<string, unknown>): void;
14
+ /**
15
+ * 음 아닌 정수(>= 0) 검증 (undefined 통과). 0 은 드라이버에서 "무제한/즉시/never" 의미로 허용.
16
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
17
+ */
18
+ export function assertNonNegativeInt(name: string, value: unknown, extra?: Record<string, unknown>): void;
19
+ /**
20
+ * 플레인 객체 검증 (undefined 통과). 배열·null·non-object 거부.
21
+ * @param {string} name @param {unknown} value @param {Record<string, unknown>} [extra] @returns {void}
22
+ */
23
+ export function assertPlainObject(name: string, value: unknown, extra?: Record<string, unknown>): void;
24
+ /**
25
+ * 연결 모드 결정(url XOR discrete) + 충돌·필수 검증.
26
+ *
27
+ * @param {Record<string, any>} config - 어댑터 생성자 config.
28
+ * @param {object} opts
29
+ * @param {string} opts.driver - 에러 details 용 드라이버명('postgres'|'mariadb'|'mongodb').
30
+ * @param {string} [opts.dbKey] - discrete db 키 이름(SQL='database', 기본값). Mongo 는 'dbName' 을
31
+ * url 과 충돌시키지 않으므로 별도 처리(아래 dbConflictsWithUrl 참조).
32
+ * @param {boolean} [opts.dbConflictsWithUrl] - db 키가 url 과 상호 배타인지(SQL=true). Mongo 의 dbName 은
33
+ * url(=connection)과 별개(=db 선택)라 false.
34
+ * @returns {{ url?: string, host?: string, port?: number, user?: string, password?: string, database?: string }}
35
+ * url 모드면 `{ url }`, discrete 모드면 존재하는 연결필드만. (database 는 dbConflictsWithUrl=true 일 때만 포함.)
36
+ * @throws {MegaValidationError} `adapter.connection_conflict` / `adapter.connection_required` / `adapter.invalid_option`
37
+ */
38
+ export function resolveConnection(config: Record<string, any>, { driver, dbKey, dbConflictsWithUrl }: {
39
+ driver: string;
40
+ dbKey?: string;
41
+ dbConflictsWithUrl?: boolean;
42
+ }): {
43
+ url?: string;
44
+ host?: string;
45
+ port?: number;
46
+ user?: string;
47
+ password?: string;
48
+ database?: string;
49
+ };
50
+ /**
51
+ * 공통 풀 인터페이스(`{ min, max, idleTimeoutMs, acquireTimeoutMs, maxLifetimeMs }`)를 드라이버
52
+ * 풀 옵션 객체로 변환·검증한다.
53
+ *
54
+ * - 알 수 없는 풀 키 → `adapter.invalid_option`(오타 fail-fast).
55
+ * - 드라이버 미지원 키(spec 값 null, 예: mariadb/mongo 의 maxLifetimeMs) → `adapter.invalid_option`(silent 무시 X).
56
+ * - `max` 는 양의 정수, 그 외(min·*Ms)는 음 아닌 정수.
57
+ * - `divideBy` 가 있으면 ms→초 변환(floor).
58
+ *
59
+ * @param {unknown} pool - config.pool.
60
+ * @param {Record<string, { key: string, divideBy?: number } | null>} spec - 드라이버 풀 매핑.
61
+ * @param {string} driver - 에러 details 용 드라이버명.
62
+ * @returns {Record<string, number>} 드라이버 풀 옵션 객체(빈 객체 가능).
63
+ */
64
+ export function normalizePool(pool: unknown, spec: Record<string, {
65
+ key: string;
66
+ divideBy?: number;
67
+ } | null>, driver: string): Record<string, number>;
68
+ /**
69
+ * pg 풀 매핑 — 값이 null 이면 미지원(throw), `{ key, divideBy? }` 면 키 이름 변경(+단위 변환).
70
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
71
+ */
72
+ export const PG_POOL_SPEC: Record<string, {
73
+ key: string;
74
+ divideBy?: number;
75
+ } | null>;
76
+ /**
77
+ * mariadb 풀 매핑 — idleTimeout 은 초 단위(ms÷1000). maxLifetime 동등물 없음(미지원).
78
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
79
+ */
80
+ export const MARIA_POOL_SPEC: Record<string, {
81
+ key: string;
82
+ divideBy?: number;
83
+ } | null>;
84
+ /**
85
+ * mongodb 풀 매핑 — 전부 ms. maxLifetime 동등물 없음(미지원).
86
+ * @type {Record<string, { key: string, divideBy?: number } | null>}
87
+ */
88
+ export const MONGO_POOL_SPEC: Record<string, {
89
+ key: string;
90
+ divideBy?: number;
91
+ } | null>;