ultimabase 0.0.0-stage → 0.1.1

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 (411) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -2
  3. package/dist/access.d.ts +62 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +258 -0
  6. package/dist/adapter-errors.d.ts +43 -0
  7. package/dist/adapter-errors.d.ts.map +1 -0
  8. package/dist/adapter-errors.js +72 -0
  9. package/dist/adapter-json.d.ts +20 -0
  10. package/dist/adapter-json.d.ts.map +1 -0
  11. package/dist/adapter-json.js +2 -0
  12. package/dist/adapter-migrations.d.ts +90 -0
  13. package/dist/adapter-migrations.d.ts.map +1 -0
  14. package/dist/adapter-migrations.js +1 -0
  15. package/dist/adapter.d.ts +333 -0
  16. package/dist/adapter.d.ts.map +1 -0
  17. package/dist/adapter.js +5 -0
  18. package/dist/aggregate.d.ts +31 -0
  19. package/dist/aggregate.d.ts.map +1 -0
  20. package/dist/aggregate.js +99 -0
  21. package/dist/app-internals.d.ts +10 -0
  22. package/dist/app-internals.d.ts.map +1 -0
  23. package/dist/app-internals.js +12 -0
  24. package/dist/app-options.d.ts +22 -0
  25. package/dist/app-options.d.ts.map +1 -0
  26. package/dist/app-options.js +3 -0
  27. package/dist/cascade.d.ts +47 -0
  28. package/dist/cascade.d.ts.map +1 -0
  29. package/dist/cascade.js +209 -0
  30. package/dist/cli-config.d.ts +6 -0
  31. package/dist/cli-config.d.ts.map +1 -0
  32. package/dist/cli-config.js +45 -0
  33. package/dist/cli-deploy.d.ts +14 -0
  34. package/dist/cli-deploy.d.ts.map +1 -0
  35. package/dist/cli-deploy.js +193 -0
  36. package/dist/cli-main.d.ts +15 -0
  37. package/dist/cli-main.d.ts.map +1 -0
  38. package/dist/cli-main.js +155 -0
  39. package/dist/cli-migrate.d.ts +20 -0
  40. package/dist/cli-migrate.d.ts.map +1 -0
  41. package/dist/cli-migrate.js +105 -0
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +4 -0
  45. package/dist/complete.d.ts +13 -0
  46. package/dist/complete.d.ts.map +1 -0
  47. package/dist/complete.js +60 -0
  48. package/dist/console-logger.d.ts +3 -0
  49. package/dist/console-logger.d.ts.map +1 -0
  50. package/dist/console-logger.js +62 -0
  51. package/dist/create-many.d.ts +7 -0
  52. package/dist/create-many.d.ts.map +1 -0
  53. package/dist/create-many.js +88 -0
  54. package/dist/decimal.d.ts +23 -0
  55. package/dist/decimal.d.ts.map +1 -0
  56. package/dist/decimal.js +50 -0
  57. package/dist/define-app.d.ts +59 -0
  58. package/dist/define-app.d.ts.map +1 -0
  59. package/dist/define-app.js +5 -0
  60. package/dist/define-enum.d.ts +6 -0
  61. package/dist/define-enum.d.ts.map +1 -0
  62. package/dist/define-enum.js +18 -0
  63. package/dist/define-function.d.ts +25 -0
  64. package/dist/define-function.d.ts.map +1 -0
  65. package/dist/define-function.js +111 -0
  66. package/dist/define-model.d.ts +48 -0
  67. package/dist/define-model.d.ts.map +1 -0
  68. package/dist/define-model.js +85 -0
  69. package/dist/definition.d.ts +179 -0
  70. package/dist/definition.d.ts.map +1 -0
  71. package/dist/definition.js +165 -0
  72. package/dist/delete-many.d.ts +13 -0
  73. package/dist/delete-many.d.ts.map +1 -0
  74. package/dist/delete-many.js +58 -0
  75. package/dist/engine.d.ts +16 -0
  76. package/dist/engine.d.ts.map +1 -0
  77. package/dist/engine.js +174 -0
  78. package/dist/errors.d.ts +50 -0
  79. package/dist/errors.d.ts.map +1 -0
  80. package/dist/errors.js +43 -0
  81. package/dist/exact.d.ts +20 -0
  82. package/dist/exact.d.ts.map +1 -0
  83. package/dist/exact.js +1 -0
  84. package/dist/feed.d.ts +6 -0
  85. package/dist/feed.d.ts.map +1 -0
  86. package/dist/feed.js +33 -0
  87. package/dist/field-codec.d.ts +11 -0
  88. package/dist/field-codec.d.ts.map +1 -0
  89. package/dist/field-codec.js +48 -0
  90. package/dist/field-rules.d.ts +57 -0
  91. package/dist/field-rules.d.ts.map +1 -0
  92. package/dist/field-rules.js +1 -0
  93. package/dist/field-type.d.ts +51 -0
  94. package/dist/field-type.d.ts.map +1 -0
  95. package/dist/field-type.js +106 -0
  96. package/dist/fields.d.ts +295 -0
  97. package/dist/fields.d.ts.map +1 -0
  98. package/dist/fields.js +1 -0
  99. package/dist/function.d.ts +126 -0
  100. package/dist/function.d.ts.map +1 -0
  101. package/dist/function.js +1 -0
  102. package/dist/functions.d.ts +18 -0
  103. package/dist/functions.d.ts.map +1 -0
  104. package/dist/functions.js +99 -0
  105. package/dist/group-by.d.ts +10 -0
  106. package/dist/group-by.d.ts.map +1 -0
  107. package/dist/group-by.js +145 -0
  108. package/dist/group-having.d.ts +8 -0
  109. package/dist/group-having.d.ts.map +1 -0
  110. package/dist/group-having.js +97 -0
  111. package/dist/hooks.d.ts +41 -0
  112. package/dist/hooks.d.ts.map +1 -0
  113. package/dist/hooks.js +149 -0
  114. package/dist/hosts.d.ts +3 -0
  115. package/dist/hosts.d.ts.map +1 -0
  116. package/dist/hosts.js +8 -0
  117. package/dist/identify.d.ts +15 -0
  118. package/dist/identify.d.ts.map +1 -0
  119. package/dist/identify.js +71 -0
  120. package/dist/index.d.ts +25 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +12 -0
  123. package/dist/internal.d.ts +28 -0
  124. package/dist/internal.d.ts.map +1 -0
  125. package/dist/internal.js +39 -0
  126. package/dist/lifecycle.d.ts +21 -0
  127. package/dist/lifecycle.d.ts.map +1 -0
  128. package/dist/lifecycle.js +149 -0
  129. package/dist/list-update.d.ts +12 -0
  130. package/dist/list-update.d.ts.map +1 -0
  131. package/dist/list-update.js +52 -0
  132. package/dist/live.d.ts +37 -0
  133. package/dist/live.d.ts.map +1 -0
  134. package/dist/live.js +139 -0
  135. package/dist/manifest.d.ts +11 -0
  136. package/dist/manifest.d.ts.map +1 -0
  137. package/dist/manifest.js +60 -0
  138. package/dist/memory-aggregate.d.ts +7 -0
  139. package/dist/memory-aggregate.d.ts.map +1 -0
  140. package/dist/memory-aggregate.js +107 -0
  141. package/dist/memory-conditions.d.ts +8 -0
  142. package/dist/memory-conditions.d.ts.map +1 -0
  143. package/dist/memory-conditions.js +82 -0
  144. package/dist/memory-draft.d.ts +34 -0
  145. package/dist/memory-draft.d.ts.map +1 -0
  146. package/dist/memory-draft.js +255 -0
  147. package/dist/memory-json.d.ts +6 -0
  148. package/dist/memory-json.d.ts.map +1 -0
  149. package/dist/memory-json.js +86 -0
  150. package/dist/memory-read.d.ts +45 -0
  151. package/dist/memory-read.d.ts.map +1 -0
  152. package/dist/memory-read.js +244 -0
  153. package/dist/memory-search.d.ts +16 -0
  154. package/dist/memory-search.d.ts.map +1 -0
  155. package/dist/memory-search.js +110 -0
  156. package/dist/memory-values.d.ts +23 -0
  157. package/dist/memory-values.d.ts.map +1 -0
  158. package/dist/memory-values.js +82 -0
  159. package/dist/memory.d.ts +4 -0
  160. package/dist/memory.d.ts.map +1 -0
  161. package/dist/memory.js +157 -0
  162. package/dist/migrate.d.ts +22 -0
  163. package/dist/migrate.d.ts.map +1 -0
  164. package/dist/migrate.js +131 -0
  165. package/dist/model.d.ts +239 -0
  166. package/dist/model.d.ts.map +1 -0
  167. package/dist/model.js +1 -0
  168. package/dist/nested-write.d.ts +64 -0
  169. package/dist/nested-write.d.ts.map +1 -0
  170. package/dist/nested-write.js +274 -0
  171. package/dist/number-update.d.ts +18 -0
  172. package/dist/number-update.d.ts.map +1 -0
  173. package/dist/number-update.js +96 -0
  174. package/dist/plugin-app.d.ts +48 -0
  175. package/dist/plugin-app.d.ts.map +1 -0
  176. package/dist/plugin-app.js +230 -0
  177. package/dist/plugin-code.d.ts +17 -0
  178. package/dist/plugin-code.d.ts.map +1 -0
  179. package/dist/plugin-code.js +125 -0
  180. package/dist/plugin-use.d.ts +22 -0
  181. package/dist/plugin-use.d.ts.map +1 -0
  182. package/dist/plugin-use.js +161 -0
  183. package/dist/plugin.d.ts +357 -0
  184. package/dist/plugin.d.ts.map +1 -0
  185. package/dist/plugin.js +17 -0
  186. package/dist/query-aggregate.d.ts +82 -0
  187. package/dist/query-aggregate.d.ts.map +1 -0
  188. package/dist/query-aggregate.js +1 -0
  189. package/dist/query-args.d.ts +41 -0
  190. package/dist/query-args.d.ts.map +1 -0
  191. package/dist/query-args.js +263 -0
  192. package/dist/query-bulk.d.ts +35 -0
  193. package/dist/query-bulk.d.ts.map +1 -0
  194. package/dist/query-bulk.js +1 -0
  195. package/dist/query-conditions.d.ts +9 -0
  196. package/dist/query-conditions.d.ts.map +1 -0
  197. package/dist/query-conditions.js +46 -0
  198. package/dist/query-context.d.ts +32 -0
  199. package/dist/query-context.d.ts.map +1 -0
  200. package/dist/query-context.js +55 -0
  201. package/dist/query-filter.d.ts +46 -0
  202. package/dist/query-filter.d.ts.map +1 -0
  203. package/dist/query-filter.js +1 -0
  204. package/dist/query-group.d.ts +82 -0
  205. package/dist/query-group.d.ts.map +1 -0
  206. package/dist/query-group.js +1 -0
  207. package/dist/query-json.d.ts +9 -0
  208. package/dist/query-json.d.ts.map +1 -0
  209. package/dist/query-json.js +66 -0
  210. package/dist/query-order.d.ts +15 -0
  211. package/dist/query-order.d.ts.map +1 -0
  212. package/dist/query-order.js +115 -0
  213. package/dist/query-project.d.ts +15 -0
  214. package/dist/query-project.d.ts.map +1 -0
  215. package/dist/query-project.js +45 -0
  216. package/dist/query-search.d.ts +24 -0
  217. package/dist/query-search.d.ts.map +1 -0
  218. package/dist/query-search.js +2 -0
  219. package/dist/query-values.d.ts +28 -0
  220. package/dist/query-values.d.ts.map +1 -0
  221. package/dist/query-values.js +91 -0
  222. package/dist/query-where.d.ts +46 -0
  223. package/dist/query-where.d.ts.map +1 -0
  224. package/dist/query-where.js +200 -0
  225. package/dist/query-write.d.ts +14 -0
  226. package/dist/query-write.d.ts.map +1 -0
  227. package/dist/query-write.js +260 -0
  228. package/dist/query.d.ts +254 -0
  229. package/dist/query.d.ts.map +1 -0
  230. package/dist/query.js +1 -0
  231. package/dist/record.d.ts +257 -0
  232. package/dist/record.d.ts.map +1 -0
  233. package/dist/record.js +1 -0
  234. package/dist/recorder.d.ts +32 -0
  235. package/dist/recorder.d.ts.map +1 -0
  236. package/dist/recorder.js +152 -0
  237. package/dist/relation-write.d.ts +41 -0
  238. package/dist/relation-write.d.ts.map +1 -0
  239. package/dist/relation-write.js +112 -0
  240. package/dist/relations.d.ts +42 -0
  241. package/dist/relations.d.ts.map +1 -0
  242. package/dist/relations.js +119 -0
  243. package/dist/rules.d.ts +97 -0
  244. package/dist/rules.d.ts.map +1 -0
  245. package/dist/rules.js +71 -0
  246. package/dist/running.d.ts +29 -0
  247. package/dist/running.d.ts.map +1 -0
  248. package/dist/running.js +39 -0
  249. package/dist/schema-indexes.d.ts +11 -0
  250. package/dist/schema-indexes.d.ts.map +1 -0
  251. package/dist/schema-indexes.js +59 -0
  252. package/dist/schema-virtuals.d.ts +19 -0
  253. package/dist/schema-virtuals.d.ts.map +1 -0
  254. package/dist/schema-virtuals.js +100 -0
  255. package/dist/schema.d.ts +35 -0
  256. package/dist/schema.d.ts.map +1 -0
  257. package/dist/schema.js +267 -0
  258. package/dist/scope.d.ts +46 -0
  259. package/dist/scope.d.ts.map +1 -0
  260. package/dist/scope.js +82 -0
  261. package/dist/scratch.d.ts +14 -0
  262. package/dist/scratch.d.ts.map +1 -0
  263. package/dist/scratch.js +10 -0
  264. package/dist/serve.d.ts +2 -0
  265. package/dist/serve.d.ts.map +1 -0
  266. package/dist/serve.js +4 -0
  267. package/dist/symbols.d.ts +10 -0
  268. package/dist/symbols.d.ts.map +1 -0
  269. package/dist/symbols.js +1 -0
  270. package/dist/transaction.d.ts +28 -0
  271. package/dist/transaction.d.ts.map +1 -0
  272. package/dist/transaction.js +156 -0
  273. package/dist/types.d.ts +58 -0
  274. package/dist/types.d.ts.map +1 -0
  275. package/dist/types.js +1 -0
  276. package/dist/update-many.d.ts +7 -0
  277. package/dist/update-many.d.ts.map +1 -0
  278. package/dist/update-many.js +182 -0
  279. package/dist/uuid.d.ts +7 -0
  280. package/dist/uuid.d.ts.map +1 -0
  281. package/dist/uuid.js +48 -0
  282. package/dist/validation.d.ts +24 -0
  283. package/dist/validation.d.ts.map +1 -0
  284. package/dist/validation.js +72 -0
  285. package/dist/virtual-read.d.ts +9 -0
  286. package/dist/virtual-read.d.ts.map +1 -0
  287. package/dist/virtual-read.js +145 -0
  288. package/dist/virtual.d.ts +142 -0
  289. package/dist/virtual.d.ts.map +1 -0
  290. package/dist/virtual.js +1 -0
  291. package/dist/write-context.d.ts +46 -0
  292. package/dist/write-context.d.ts.map +1 -0
  293. package/dist/write-context.js +102 -0
  294. package/dist/write-data.d.ts +26 -0
  295. package/dist/write-data.d.ts.map +1 -0
  296. package/dist/write-data.js +144 -0
  297. package/dist/write-plan.d.ts +107 -0
  298. package/dist/write-plan.d.ts.map +1 -0
  299. package/dist/write-plan.js +170 -0
  300. package/dist/write-target.d.ts +17 -0
  301. package/dist/write-target.d.ts.map +1 -0
  302. package/dist/write-target.js +59 -0
  303. package/dist/writes.d.ts +23 -0
  304. package/dist/writes.d.ts.map +1 -0
  305. package/dist/writes.js +246 -0
  306. package/dist/writing.d.ts +10 -0
  307. package/dist/writing.d.ts.map +1 -0
  308. package/dist/writing.js +66 -0
  309. package/package.json +42 -3
  310. package/src/access.ts +329 -0
  311. package/src/adapter-errors.ts +89 -0
  312. package/src/adapter-json.ts +21 -0
  313. package/src/adapter-migrations.ts +98 -0
  314. package/src/adapter.ts +363 -0
  315. package/src/aggregate.ts +136 -0
  316. package/src/app-internals.ts +26 -0
  317. package/src/app-options.ts +24 -0
  318. package/src/cascade.ts +297 -0
  319. package/src/cli-config.ts +45 -0
  320. package/src/cli-deploy.ts +235 -0
  321. package/src/cli-main.ts +166 -0
  322. package/src/cli-migrate.ts +143 -0
  323. package/src/cli.ts +5 -0
  324. package/src/complete.ts +73 -0
  325. package/src/console-logger.ts +87 -0
  326. package/src/create-many.ts +126 -0
  327. package/src/decimal.ts +74 -0
  328. package/src/define-app.ts +108 -0
  329. package/src/define-enum.ts +26 -0
  330. package/src/define-function.ts +188 -0
  331. package/src/define-model.ts +220 -0
  332. package/src/definition.ts +352 -0
  333. package/src/delete-many.ts +76 -0
  334. package/src/engine.ts +281 -0
  335. package/src/errors.ts +97 -0
  336. package/src/exact.ts +49 -0
  337. package/src/feed.ts +39 -0
  338. package/src/field-codec.ts +72 -0
  339. package/src/field-rules.ts +77 -0
  340. package/src/field-type.ts +219 -0
  341. package/src/fields.ts +383 -0
  342. package/src/function.ts +143 -0
  343. package/src/functions.ts +154 -0
  344. package/src/group-by.ts +186 -0
  345. package/src/group-having.ts +143 -0
  346. package/src/hooks.ts +215 -0
  347. package/src/hosts.ts +11 -0
  348. package/src/identify.ts +95 -0
  349. package/src/index.ts +125 -0
  350. package/src/internal.ts +68 -0
  351. package/src/lifecycle.ts +214 -0
  352. package/src/list-update.ts +71 -0
  353. package/src/live.ts +199 -0
  354. package/src/manifest.ts +71 -0
  355. package/src/memory-aggregate.ts +162 -0
  356. package/src/memory-conditions.ts +88 -0
  357. package/src/memory-draft.ts +319 -0
  358. package/src/memory-json.ts +91 -0
  359. package/src/memory-read.ts +311 -0
  360. package/src/memory-search.ts +149 -0
  361. package/src/memory-values.ts +106 -0
  362. package/src/memory.ts +173 -0
  363. package/src/migrate.ts +152 -0
  364. package/src/model.ts +312 -0
  365. package/src/nested-write.ts +390 -0
  366. package/src/number-update.ts +142 -0
  367. package/src/plugin-app.ts +308 -0
  368. package/src/plugin-code.ts +158 -0
  369. package/src/plugin-use.ts +217 -0
  370. package/src/plugin.ts +400 -0
  371. package/src/query-aggregate.ts +96 -0
  372. package/src/query-args.ts +375 -0
  373. package/src/query-bulk.ts +53 -0
  374. package/src/query-conditions.ts +53 -0
  375. package/src/query-context.ts +104 -0
  376. package/src/query-filter.ts +54 -0
  377. package/src/query-group.ts +137 -0
  378. package/src/query-json.ts +90 -0
  379. package/src/query-order.ts +158 -0
  380. package/src/query-project.ts +73 -0
  381. package/src/query-search.ts +31 -0
  382. package/src/query-values.ts +131 -0
  383. package/src/query-where.ts +318 -0
  384. package/src/query-write.ts +360 -0
  385. package/src/query.ts +400 -0
  386. package/src/record.ts +379 -0
  387. package/src/recorder.ts +212 -0
  388. package/src/relation-write.ts +176 -0
  389. package/src/relations.ts +165 -0
  390. package/src/rules.ts +188 -0
  391. package/src/running.ts +62 -0
  392. package/src/schema-indexes.ts +88 -0
  393. package/src/schema-virtuals.ts +134 -0
  394. package/src/schema.ts +367 -0
  395. package/src/scope.ts +123 -0
  396. package/src/scratch.ts +28 -0
  397. package/src/serve.ts +5 -0
  398. package/src/symbols.ts +11 -0
  399. package/src/transaction.ts +211 -0
  400. package/src/types.ts +79 -0
  401. package/src/update-many.ts +268 -0
  402. package/src/uuid.ts +51 -0
  403. package/src/validation.ts +107 -0
  404. package/src/virtual-read.ts +198 -0
  405. package/src/virtual.ts +187 -0
  406. package/src/write-context.ts +170 -0
  407. package/src/write-data.ts +188 -0
  408. package/src/write-plan.ts +273 -0
  409. package/src/write-target.ts +92 -0
  410. package/src/writes.ts +361 -0
  411. package/src/writing.ts +93 -0
@@ -0,0 +1,166 @@
1
+ // The `ultimabase` command (D-105): `dev`, `start`, `sync`, `reset` and
2
+ // `migrate`.
3
+ import { spawn } from 'node:child_process'
4
+ import { dirname, extname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { parseArgs } from 'node:util'
7
+ import { configPath, load } from './cli-config.ts'
8
+ import { migrateDeploy, migrateStatus, reset } from './cli-deploy.ts'
9
+ import { migrateCreate, sync } from './cli-migrate.ts'
10
+ import { Refused } from './errors.ts'
11
+
12
+ const usage = `Usage: ultimabase <command> [--config <path>]
13
+
14
+ dev Start the application, sync its development
15
+ database, and restart it when a file changes.
16
+ start Start the application in production.
17
+ sync [--force] Sync the development database to the models.
18
+ reset [--force] Rebuild the development database from the
19
+ migrations: every table is dropped. --force
20
+ allows a database on another machine.
21
+ migrate create <name> Write the models' changes as a migration, or an
22
+ empty one for your SQL.
23
+ migrate deploy Apply the pending migrations.
24
+ migrate status Say what a deploy would do; exit 1 if anything.`
25
+
26
+ export type Io = {
27
+ readonly cwd: string
28
+ readonly out: (text: string) => void
29
+ readonly err: (text: string) => void
30
+ readonly now: () => Date
31
+ /** Runs Node with `args` until it exits, and returns its exit code. */
32
+ readonly node: (args: readonly string[]) => Promise<number>
33
+ }
34
+
35
+ /**
36
+ * The file `dev` runs under `node --watch`: `serve.ts` beside the sources,
37
+ * `serve.js` in the published `dist`.
38
+ */
39
+ const here = fileURLToPath(import.meta.url)
40
+ export const serveFile: string = join(dirname(here), `serve${extname(here)}`)
41
+
42
+ function runNode(args: readonly string[]): Promise<number> {
43
+ return new Promise((done, fail) => {
44
+ const child = spawn(process.execPath, args, { stdio: 'inherit' })
45
+ const forward = () => child.kill('SIGTERM')
46
+ process.on('SIGTERM', forward)
47
+ child.on('error', fail)
48
+ child.on('exit', (code) => {
49
+ process.off('SIGTERM', forward)
50
+ done(code ?? 1)
51
+ })
52
+ })
53
+ }
54
+
55
+ export const processIo: Io = {
56
+ cwd: process.cwd(),
57
+ out: (text) => process.stdout.write(`${text}\n`),
58
+ err: (text) => process.stderr.write(`${text}\n`),
59
+ now: () => new Date(),
60
+ node: runNode,
61
+ }
62
+
63
+ /**
64
+ * An expected failure, a refusal or an error with a `code` (from Node,
65
+ * PostgreSQL or the kernel), prints its message; any other, a bug, its
66
+ * stack (D-105).
67
+ */
68
+ function report(error: unknown, io: Io): number {
69
+ if (!(error instanceof Error)) io.err(String(error))
70
+ else if (
71
+ error.name === 'Refused' ||
72
+ typeof Reflect.get(error, 'code') === 'string'
73
+ )
74
+ io.err(error.message)
75
+ else io.err(error.stack ?? error.message)
76
+ return 1
77
+ }
78
+
79
+ /** Reads `args`, refusing an option it does not know with its first sentence. */
80
+ function read(args: readonly string[]) {
81
+ try {
82
+ return parseArgs({
83
+ args: [...args],
84
+ allowPositionals: true,
85
+ options: {
86
+ config: { type: 'string' },
87
+ force: { type: 'boolean', default: false },
88
+ help: { type: 'boolean', default: false },
89
+ },
90
+ })
91
+ } catch (error) {
92
+ const message: unknown = Reflect.get(Object(error), 'message')
93
+ const first = String(message).replace(/\.( .*)?$/s, '')
94
+ throw new Refused(`${first}. Run \`npx ultimabase --help\`.`)
95
+ }
96
+ }
97
+
98
+ async function run(args: readonly string[], io: Io): Promise<number> {
99
+ const { values, positionals } = read(args)
100
+ const [command, sub, name, ...rest] = positionals
101
+ const known =
102
+ rest.length === 0 &&
103
+ (command === 'migrate'
104
+ ? (sub === 'create' && name !== undefined) ||
105
+ (['deploy', 'status'].includes(sub ?? '') && name === undefined)
106
+ : ['dev', 'start', 'sync', 'reset'].includes(command ?? '') &&
107
+ sub === undefined)
108
+ if (values.help) {
109
+ io.out(usage)
110
+ return 0
111
+ }
112
+ if (!known) {
113
+ if (command !== undefined)
114
+ io.err(`Unknown command '${positionals.join(' ')}'.`)
115
+ io.err(usage)
116
+ return 1
117
+ }
118
+ const path = configPath(io.cwd, values.config)
119
+ if (command === 'dev') {
120
+ process.env['NODE_ENV'] ??= 'development'
121
+ return io.node(['--watch', serveFile, path])
122
+ }
123
+ if (command === 'start') process.env['NODE_ENV'] ??= 'production'
124
+ const app = await load(path)
125
+ const dir = join(dirname(path), 'migrations')
126
+ if (command === 'start') await app.app.start()
127
+ else if (command === 'sync') {
128
+ const said = await sync(app, values.force)
129
+ if (said !== undefined) io.out(said)
130
+ } else if (command === 'reset') {
131
+ for (const line of await reset(app, dir, values.force)) io.out(line)
132
+ } else if (sub === 'deploy') {
133
+ for (const line of await migrateDeploy(app, dir)) io.out(line)
134
+ } else if (sub === 'status') {
135
+ const { lines, ok } = await migrateStatus(app, dir)
136
+ for (const line of lines) io.out(line)
137
+ return ok ? 0 : 1
138
+ } else io.out(await migrateCreate(app, dir, name ?? '', io.now()))
139
+ return 0
140
+ }
141
+
142
+ /** Runs the command `args` name, and returns its exit code. */
143
+ export async function main(
144
+ args: readonly string[],
145
+ io: Io = processIo,
146
+ ): Promise<number> {
147
+ try {
148
+ return await run(args, io)
149
+ } catch (error) {
150
+ return report(error, io)
151
+ }
152
+ }
153
+
154
+ /** Starts the application of `path`, for `dev` (D-105). */
155
+ export async function serve(
156
+ path: string | undefined,
157
+ io: Io = processIo,
158
+ ): Promise<number> {
159
+ try {
160
+ const { app } = await load(configPath(io.cwd, path))
161
+ await app.start()
162
+ return 0
163
+ } catch (error) {
164
+ return report(error, io)
165
+ }
166
+ }
@@ -0,0 +1,143 @@
1
+ // `migrate create` and `sync` (D-105), and the migration files both
2
+ // `migrate` commands read.
3
+ import { createHash } from 'node:crypto'
4
+ import { existsSync } from 'node:fs'
5
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
6
+ import { join } from 'node:path'
7
+ import type { Migration, Statement } from './adapter.ts'
8
+ import type { Internals } from './app-internals.ts'
9
+ import { kernelLogger, schemaOf } from './lifecycle.ts'
10
+ import { Refused } from './errors.ts'
11
+ import { hashOf, migrationsOf, synced } from './migrate.ts'
12
+
13
+ export const header = '-- ultimabase schema '
14
+
15
+ /** For a statement a transaction refuses: `create index concurrently`. */
16
+ export const noTransaction = '-- ultimabase no transaction'
17
+ export const create = '`npx ultimabase migrate create <name>`'
18
+
19
+ export type Folder = {
20
+ readonly files: readonly Migration[]
21
+ readonly snapshot: unknown
22
+ }
23
+
24
+ /** The `.sql` files of `dir` in name order, and its `schema.json`. */
25
+ export async function read(dir: string): Promise<Folder> {
26
+ if (!existsSync(dir)) return { files: [], snapshot: undefined }
27
+ const names = (await readdir(dir))
28
+ .filter((name) => name.endsWith('.sql'))
29
+ .toSorted()
30
+ const files = await Promise.all(
31
+ names.map(async (name) => {
32
+ // A Windows checkout's line endings change neither checksum nor header.
33
+ const text = await readFile(join(dir, name), 'utf8')
34
+ const sql = text.replaceAll('\r\n', '\n')
35
+ const checksum = createHash('sha256').update(sql).digest('hex')
36
+ const [first = '', second] = sql.split('\n', 2)
37
+ const schema = first.startsWith(header)
38
+ ? first.slice(header.length)
39
+ : undefined
40
+ return {
41
+ name,
42
+ sql,
43
+ checksum,
44
+ ...(schema === undefined ? {} : { schema }),
45
+ transaction: second !== noTransaction,
46
+ }
47
+ }),
48
+ )
49
+ const json = join(dir, 'schema.json')
50
+ if (!existsSync(json)) {
51
+ if (files.length > 0) {
52
+ throw new Refused(
53
+ 'migrations/ holds SQL files but no schema.json: restore it from version control.',
54
+ )
55
+ }
56
+ return { files, snapshot: undefined }
57
+ }
58
+ const snapshot: unknown = JSON.parse(await readFile(json, 'utf8'))
59
+ return { files, snapshot }
60
+ }
61
+
62
+ /** `yyyyMMddHHmmss`, in UTC. */
63
+ const stamp = (now: Date): string =>
64
+ now.toISOString().replaceAll(/[-:T]/g, '').slice(0, 'yyyyMMddHHmmss'.length)
65
+
66
+ function body(
67
+ hash: string,
68
+ statements: readonly Statement[],
69
+ none: string,
70
+ ): string {
71
+ const blocks = statements.map(({ sql, loses, tries, rename }) =>
72
+ [
73
+ ...(loses === undefined ? [] : [`-- ${loses}.`]),
74
+ ...(tries === undefined ? [] : [`-- ${tries}.`]),
75
+ ...(rename === undefined ? [] : [`-- a rename? write: ${rename.sql};`]),
76
+ `${sql};`,
77
+ ].join('\n'),
78
+ )
79
+ const parts = blocks.length > 0 ? blocks : [none]
80
+ return `${header}${hash}\n\n${parts.join('\n\n')}\n`
81
+ }
82
+
83
+ /**
84
+ * Writes the migration from `schema.json` to the models; with no model
85
+ * changed, a file for the developer's SQL, as a data migration.
86
+ */
87
+ export async function migrateCreate(
88
+ { options }: Internals,
89
+ dir: string,
90
+ name: string,
91
+ now: Date,
92
+ ): Promise<string> {
93
+ if (!/^[a-z0-9_]+$/.test(name)) {
94
+ throw new Refused(
95
+ `"${name}" is not a migration name: use lowercase letters, digits and _.`,
96
+ )
97
+ }
98
+ const migrations = migrationsOf(options.database)
99
+ const schema = schemaOf(options)
100
+ const to = migrations.snapshot(schema)
101
+ const { snapshot: from } = await read(dir)
102
+ const hash = hashOf(to)
103
+ const file = `${stamp(now)}_${name}.sql`
104
+ await mkdir(dir, { recursive: true })
105
+ if (from !== undefined && hashOf(from) === hash) {
106
+ const sql = body(hash, [], '-- No model changed: write your SQL here.')
107
+ await writeFile(join(dir, file), sql, { flag: 'wx' })
108
+ return `Wrote migrations/${file}: no model changed, write your SQL in it.`
109
+ }
110
+ const statements = migrations.plan(from, schema)
111
+ const sql = body(hash, statements, '-- No statement: only names changed.')
112
+ await writeFile(join(dir, file), sql, { flag: 'wx' })
113
+ await writeFile(
114
+ join(dir, 'schema.json'),
115
+ `${JSON.stringify(to, null, '\t')}\n`,
116
+ )
117
+ return `Wrote migrations/${file}.`
118
+ }
119
+
120
+ /** Syncs a development database, losing data only with `force`. */
121
+ export async function sync(
122
+ { options }: Internals,
123
+ force: boolean,
124
+ ): Promise<string | undefined> {
125
+ if (process.env['NODE_ENV'] === 'production') {
126
+ throw new Refused(
127
+ 'sync changes only a development database: run `npx ultimabase migrate deploy` in production.',
128
+ )
129
+ }
130
+ const { database } = options
131
+ migrationsOf(database)
132
+ const schema = schemaOf(options)
133
+ const logger = kernelLogger(options)
134
+ await database.start(schema, logger)
135
+ try {
136
+ const ran = await synced(database, schema, logger, force)
137
+ return ran === undefined
138
+ ? 'The database already matches the models.'
139
+ : undefined
140
+ } finally {
141
+ await database.stop()
142
+ }
143
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // The `ultimabase` bin (D-105).
3
+ import { main } from './cli-main.ts'
4
+
5
+ process.exitCode = await main(process.argv.slice(2))
@@ -0,0 +1,73 @@
1
+ // A user caller completed with its plugins' keys (D-121): once per object,
2
+ // each plugin's `caller` in the order of `plugins`, a key given by one only,
3
+ // a symbol key as a string one (D-126).
4
+ import { isObject } from './query-where.ts'
5
+ import type { Caller, Identity } from './rules.ts'
6
+
7
+ /** A plugin's `caller`, bound to its context. */
8
+ export type PluginCaller = {
9
+ readonly plugin: string
10
+ readonly caller: (identity: Identity) => Promise<unknown>
11
+ }
12
+
13
+ export type Complete = (identity: Identity) => Promise<Caller>
14
+
15
+ // The keys every caller holds, which no plugin gives.
16
+ const own = new Set<PropertyKey>(['kind', 'user'])
17
+
18
+ async function completed(
19
+ identity: Identity,
20
+ plugins: readonly PluginCaller[],
21
+ ): Promise<Caller> {
22
+ const keys: { [key: PropertyKey]: unknown } = {}
23
+ const givers = new Map<PropertyKey, string>()
24
+ for (const { plugin, caller } of plugins) {
25
+ const given = await caller(identity)
26
+ if (!isObject(given)) {
27
+ throw new TypeError(`${plugin}.caller returns an object of keys.`)
28
+ }
29
+ for (const key of Reflect.ownKeys(given)) {
30
+ if (!Object.prototype.propertyIsEnumerable.call(given, key)) continue
31
+ const other = own.has(key) ? 'the kernel' : givers.get(key)
32
+ if (other !== undefined) {
33
+ throw new Error(
34
+ `${plugin}.caller gives the caller ${String(key)}, which ${other} gives; a key has one plugin.`,
35
+ )
36
+ }
37
+ givers.set(key, plugin)
38
+ const value: unknown = Reflect.get(given, key)
39
+ keys[key] = value
40
+ }
41
+ }
42
+ // Unnecessary only while no plugin declares a key: once one does, the
43
+ // kernel's types cannot see it given (D-121). caller-keys-typing proves it.
44
+ // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- see above.
45
+ return Object.freeze({ ...identity, ...keys }) as Caller
46
+ }
47
+
48
+ /**
49
+ * Completes a user caller the first time the kernel receives it; `plugins`
50
+ * is read on each call, filled once their contexts exist.
51
+ */
52
+ export function completer(plugins: readonly PluginCaller[]): Complete {
53
+ const pending = new WeakMap<object, Promise<Caller>>()
54
+ const done = new WeakSet<object>()
55
+ // A completed caller, or a user one when no plugin gives a key.
56
+ const kept = (identity: Identity): identity is Caller & Identity =>
57
+ done.has(identity)
58
+ return async (identity) => {
59
+ if (identity.kind !== 'user') return identity
60
+ if (plugins.length === 0) done.add(identity)
61
+ if (kept(identity)) return identity
62
+ const known = pending.get(identity)
63
+ if (known !== undefined) return known
64
+ const caller = completed(identity, plugins).then((result) => {
65
+ done.add(result)
66
+ return result
67
+ })
68
+ pending.set(identity, caller)
69
+ // A failure is not kept: the next request tries again.
70
+ caller.catch(() => pending.delete(identity))
71
+ return caller
72
+ }
73
+ }
@@ -0,0 +1,87 @@
1
+ // The logger the kernel writes to when the application gives none (D-58,
2
+ // D-88, D-93): a readable line in a terminal, one JSON line with pino's
3
+ // fields everywhere else.
4
+ import { styleText } from 'node:util'
5
+ import type { Logger } from './app-options.ts'
6
+
7
+ type Level = 'info' | 'warn' | 'error'
8
+
9
+ const colors = { info: 'green', warn: 'yellow', error: 'red' } as const
10
+
11
+ /** An error as pino's `err` serializer writes it. */
12
+ function serialized(value: unknown): unknown {
13
+ if (!(value instanceof Error)) return value
14
+ return { type: value.name, message: value.message, stack: value.stack }
15
+ }
16
+
17
+ function json(level: Level, fields: object, msg: string | undefined): string {
18
+ return JSON.stringify({
19
+ level,
20
+ time: Date.now(),
21
+ ...Object.fromEntries(
22
+ Object.entries(fields).map(([key, value]) => [key, serialized(value)]),
23
+ ),
24
+ ...(msg === undefined ? {} : { msg }),
25
+ })
26
+ }
27
+
28
+ /** A value as `key=value` shows it: quoted when it would read ambiguously. */
29
+ function shown(value: unknown): string {
30
+ if (typeof value === 'string' && /^[^\s"=]+$/u.test(value)) return value
31
+ return JSON.stringify(value) ?? String(value)
32
+ }
33
+
34
+ /** `10:42:07 INFO [cache] Warm. size=2`, then each error's stack, indented. */
35
+ function readable(
36
+ level: Level,
37
+ fields: object,
38
+ msg: string | undefined,
39
+ stream: NodeJS.WriteStream,
40
+ ): string {
41
+ const { name, ...rest }: { readonly name?: unknown } = fields
42
+ const errors = Object.values(rest).filter((value) => value instanceof Error)
43
+ const details = Object.entries(rest)
44
+ .filter(([, value]) => !(value instanceof Error))
45
+ .map(([key, value]) => `${key}=${shown(value)}`)
46
+ const line = [
47
+ styleText('dim', new Date().toTimeString().slice(0, 8), { stream }),
48
+ styleText(colors[level], level.toUpperCase().padEnd(5), { stream }),
49
+ name === undefined ? undefined : `[${shown(name)}]`,
50
+ msg,
51
+ ...details,
52
+ ]
53
+ const stacks = errors.map((error) =>
54
+ (error.stack ?? `${error.name}: ${error.message}`).replaceAll(
55
+ /^/gmu,
56
+ ' ',
57
+ ),
58
+ )
59
+ return [line.filter((part) => part !== undefined).join(' '), ...stacks].join(
60
+ '\n',
61
+ )
62
+ }
63
+
64
+ export function consoleLogger(bindings: object = {}): Logger {
65
+ const log =
66
+ (level: Level) =>
67
+ (first: string | object, message?: string): void => {
68
+ const fields = {
69
+ ...bindings,
70
+ ...(typeof first === 'string' ? {} : first),
71
+ }
72
+ const msg = typeof first === 'string' ? first : message
73
+ // console.info writes to stdout, console.warn and console.error to stderr.
74
+ const stream = level === 'info' ? process.stdout : process.stderr
75
+ console[level](
76
+ stream.isTTY
77
+ ? readable(level, fields, msg, stream)
78
+ : json(level, fields, msg),
79
+ )
80
+ }
81
+ return {
82
+ info: log('info'),
83
+ warn: log('warn'),
84
+ error: log('error'),
85
+ child: (more) => consoleLogger({ ...bindings, ...more }),
86
+ }
87
+ }
@@ -0,0 +1,126 @@
1
+ // `createMany` (D-135): what a `create` runs, on each row of `data`, all or
2
+ // nothing; the rows go in one write (D-99). `createManyAndReturn` returns
3
+ // them in `data`'s order, each as `create` does (D-149).
4
+ import type { Change } from './adapter.ts'
5
+ import { InvalidQuery } from './errors.ts'
6
+ import type { Event } from './hooks.ts'
7
+ import { rereads, runHook, settle, settles } from './hooks.ts'
8
+ import type { Plan as Shaped } from './query-args.ts'
9
+ import { oneKeys } from './query-args.ts'
10
+ import type { Context } from './query-context.ts'
11
+ import { given, shapedRows } from './query-context.ts'
12
+ import { byIds, entries, isObject } from './query-where.ts'
13
+ import { createPlan } from './query-write.ts'
14
+ import type { Commit, Store } from './transaction.ts'
15
+ import type { Draft } from './write-data.ts'
16
+ import { prepared, readData } from './write-data.ts'
17
+ import { guarded } from './write-target.ts'
18
+ import { writingOf } from './writing.ts'
19
+ import { returned } from './writes.ts'
20
+
21
+ /** A row's data, as `create` reads it, with no write but `connect` on a `one`. */
22
+ function rowData(context: Context, data: unknown, at: string): Draft {
23
+ const { models, model } = context
24
+ for (const [name, value] of entries(data, at)) {
25
+ const field = model.fields.get(name)
26
+ if (field === undefined || field.kind === 'scalar') continue
27
+ const path = `${at}.${name}`
28
+ if (field.kind === 'many') {
29
+ throw new InvalidQuery(
30
+ path,
31
+ `${path} is a many relation, which createMany does not write.`,
32
+ )
33
+ }
34
+ if (
35
+ !isObject(value) ||
36
+ Object.keys(value).some((key) => key !== 'connect')
37
+ ) {
38
+ throw new InvalidQuery(path, `${path} takes only connect in createMany.`)
39
+ }
40
+ }
41
+ return readData(models, model, data, true, at)
42
+ }
43
+
44
+ /**
45
+ * Creates the rows of `data`: their ids, or with `at` the rows as it shapes
46
+ * them, in `data`'s order. The rows come back even when a read rule would
47
+ * hide them, as `create`'s does (D-86).
48
+ */
49
+ async function created(
50
+ context: Context,
51
+ store: Store,
52
+ commits: Commit[],
53
+ data: unknown,
54
+ at?: Shaped,
55
+ ): Promise<readonly unknown[]> {
56
+ const { model, caller, scope } = context
57
+ const drafts = Array.isArray(data)
58
+ ? data.map((row: unknown, index) => rowData(context, row, `data.${index}`))
59
+ : [rowData(context, data, 'data')]
60
+ const sent: readonly Readonly<Draft>[] = drafts.map((draft) => ({ ...draft }))
61
+ // One row refused, none written (D-111).
62
+ for (const each of sent) scope.access?.allow(model, 'create', { data: each })
63
+ if (drafts.length === 0) return []
64
+ for (const draft of drafts) {
65
+ await prepared(model, draft, caller, true)
66
+ await runHook(model, 'beforeCreate', { caller, data: draft })
67
+ }
68
+ const events: Event[] = []
69
+ const writing = writingOf(scope, store, events)
70
+ const changes: Change[] = []
71
+ const ids: string[] = []
72
+ for (const [index, draft] of drafts.entries()) {
73
+ const { id, plan } = await createPlan(
74
+ scope,
75
+ model,
76
+ draft,
77
+ sent[index] ?? {},
78
+ store,
79
+ )
80
+ ids.push(id)
81
+ events.push({ model, operation: 'create', id }, ...plan.events)
82
+ await plan.check(writing)
83
+ changes.push(...(await plan.changes(writing)))
84
+ }
85
+ const read = at && { model: model.name, ...at.read, where: byIds(ids) }
86
+ // An `after*` hook may change the rows: they are read once it ran (D-88).
87
+ const again = rereads(events)
88
+ const write = {
89
+ checks: writing.checks.map(({ check }) => check),
90
+ changes,
91
+ ...(read && !again && { read }),
92
+ }
93
+ // Nothing follows a write without hooks: it may need no transaction.
94
+ const run = settles(events) ? store.write(write) : store.last(write)
95
+ const rows = await guarded(run, writing.checks)
96
+ await settle(store, caller, events, commits)
97
+ if (at === undefined || read === undefined) return ids
98
+ const found = new Map(
99
+ (again ? await store.find(read) : rows).map((row) => [row.id, row]),
100
+ )
101
+ const ordered = ids.flatMap((id) => found.get(id) ?? [])
102
+ return shapedRows(context, store, ordered, at.shape, scope.access)
103
+ }
104
+
105
+ export async function createMany(
106
+ context: Context,
107
+ store: Store,
108
+ commits: Commit[],
109
+ args: unknown,
110
+ ): Promise<{ readonly count: number }> {
111
+ const found = given(args, context.model.name, ['data'], ['data'])
112
+ const ids = await created(context, store, commits, found.get('data'))
113
+ return { count: ids.length }
114
+ }
115
+
116
+ export async function createManyAndReturn(
117
+ context: Context,
118
+ store: Store,
119
+ commits: Commit[],
120
+ args: unknown,
121
+ ): Promise<readonly unknown[]> {
122
+ const keys = ['data', ...oneKeys]
123
+ const found = given(args, context.model.name, keys, ['data'])
124
+ const at = await returned(context, found)
125
+ return created(context, store, commits, found.get('data'), at)
126
+ }
package/src/decimal.ts ADDED
@@ -0,0 +1,74 @@
1
+ // Decimal values (D-145): exact numbers as strings, normalized to their
2
+ // field's scale, compared and summed through `BigInt`.
3
+
4
+ /** A decimal string's shape, as transports describe it. */
5
+ export const decimalPattern = '^-?\\d+(\\.\\d+)?$'
6
+ const shape = new RegExp(decimalPattern)
7
+
8
+ /** A decimal's digits as a whole number, and how many follow its point. */
9
+ type Exact = { readonly units: bigint; readonly scale: number }
10
+
11
+ function exact(value: string): Exact {
12
+ const [whole = '', fraction = ''] = value.split('.')
13
+ return { units: BigInt(`${whole}${fraction}`), scale: fraction.length }
14
+ }
15
+
16
+ /** `value`'s units at `to` digits after the point; a cut digit is a 0. */
17
+ const scaled = ({ units, scale }: Exact, to: number): bigint =>
18
+ to >= scale
19
+ ? units * 10n ** BigInt(to - scale)
20
+ : units / 10n ** BigInt(scale - to)
21
+
22
+ function text(units: bigint, scale: number): string {
23
+ const digits = (units < 0n ? -units : units)
24
+ .toString()
25
+ .padStart(scale + 1, '0')
26
+ const whole = digits.slice(0, digits.length - scale)
27
+ const sign = units < 0n ? '-' : ''
28
+ return scale === 0
29
+ ? `${sign}${whole}`
30
+ : `${sign}${whole}.${digits.slice(-scale)}`
31
+ }
32
+
33
+ /** A decimal field's precision and scale. */
34
+ export type Digits = { readonly precision: number; readonly scale: number }
35
+
36
+ /** A field, which has digits if it is a decimal. */
37
+ type Digited = { readonly type: string } & Partial<Digits>
38
+
39
+ /** The message of a value a decimal `field` cannot hold, if it cannot. */
40
+ export function decimalIssue(
41
+ value: unknown,
42
+ field: Digited,
43
+ ): string | undefined {
44
+ const digits = { precision: field.precision ?? 0, scale: field.scale ?? 0 }
45
+ const expected = `Expected a decimal string of at most ${digits.precision} digits, ${digits.scale} after the point, such as '12.50'.`
46
+ if (typeof value !== 'string' || !shape.test(value)) return expected
47
+ const [whole = '', fraction = ''] = value.replace('-', '').split('.')
48
+ const significant = whole.replace(/^0+/, '').length
49
+ const decimals = fraction.replace(/0+$/, '').length
50
+ return significant > digits.precision - digits.scale ||
51
+ decimals > digits.scale
52
+ ? expected
53
+ : undefined
54
+ }
55
+
56
+ /** `value`, which fits, at `scale`: `'012.5'` is `'12.50'`. */
57
+ export const normalized = (value: string, scale: number): string =>
58
+ text(scaled(exact(value), scale), scale)
59
+
60
+ /** A value that fits `field`, as stored: a decimal at its scale. */
61
+ export const storedValue = (value: unknown, field: Digited): unknown =>
62
+ field.type === 'decimal' && typeof value === 'string'
63
+ ? normalized(value, field.scale ?? 0)
64
+ : value
65
+
66
+ /** A decimal string's units at `scale`, which order decimals by value. */
67
+ export const unitsOf = (value: string, scale: number): bigint =>
68
+ scaled(exact(value), scale)
69
+
70
+ /** The exact sum of decimal strings, at `scale`. */
71
+ export function sumDecimals(values: readonly string[], scale: number): string {
72
+ const total = values.reduce((sum, value) => sum + unitsOf(value, scale), 0n)
73
+ return text(total, scale)
74
+ }