ultimabase 0.0.0-stage → 0.1.0

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 +7 -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 +16 -0
  37. package/dist/cli-main.d.ts.map +1 -0
  38. package/dist/cli-main.js +151 -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 +165 -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
package/src/cascade.ts ADDED
@@ -0,0 +1,297 @@
1
+ // What a `delete` does to the rows that reference the row it deletes
2
+ // (D-54, D-90): `cascade` deletes them, `setNull` empties their key,
3
+ // `restrict` refuses the delete.
4
+ import type { Change, Filter, Row } from './adapter.ts'
5
+ import { PublicError } from './errors.ts'
6
+ import { hasHook, redirectOf } from './hooks.ts'
7
+ import type { Models } from './query-where.ts'
8
+ import type { ModelInfo } from './schema.ts'
9
+ import type { Checked, Reads } from './write-plan.ts'
10
+ import { updated } from './write-plan.ts'
11
+
12
+ /** A row the delete removes, or redirects (D-89). */
13
+ export type Doomed = {
14
+ readonly model: ModelInfo
15
+ readonly row: Row
16
+ readonly redirected: boolean
17
+ }
18
+
19
+ /** Rows of `model` whose key `field` the delete empties. */
20
+ export type Nulled = {
21
+ readonly model: ModelInfo
22
+ readonly field: string
23
+ readonly rows: readonly Row[]
24
+ }
25
+
26
+ export type Cascade = {
27
+ /** The deepest first, the rows the caller deletes last. */
28
+ readonly deleted: readonly Doomed[]
29
+ readonly nulled: readonly Nulled[]
30
+ }
31
+
32
+ type Reference = { readonly model: ModelInfo; readonly field: string }
33
+
34
+ /** A row's key the cascade leaves: the write empties it itself (D-138). */
35
+ export type Except = {
36
+ readonly model: string
37
+ readonly field: string
38
+ readonly id: string
39
+ }
40
+
41
+ /** The stored `one` relations to `target`, each with its `onDelete`. */
42
+ function referencesTo(
43
+ models: Models,
44
+ target: string,
45
+ ): readonly (Reference & { readonly onDelete: string })[] {
46
+ const references: (Reference & { readonly onDelete: string })[] = []
47
+ for (const model of models.values()) {
48
+ for (const field of model.fields.values()) {
49
+ if (field.kind !== 'one' || !field.stored || field.target !== target) {
50
+ continue
51
+ }
52
+ references.push({ model, field: field.name, onDelete: field.onDelete })
53
+ }
54
+ }
55
+ return references
56
+ }
57
+
58
+ const keyOf = (model: ModelInfo, id: string): string => `${model.name}:${id}`
59
+
60
+ /**
61
+ * Loads every row deleting `deleting` reaches, level by level, as stored: no
62
+ * `beforeFind`, no access rules (D-54, D-90). Fails with
63
+ * `RELATION_RESTRICTED` when a row the delete leaves references a deleted one.
64
+ * A redirected row stays: it cascades nothing (D-89, D-90).
65
+ */
66
+ export async function cascadeOf(
67
+ models: Models,
68
+ model: ModelInfo,
69
+ deleting: readonly Row[],
70
+ store: Reads,
71
+ except?: Except,
72
+ ): Promise<Cascade> {
73
+ const redirected = redirectOf(model) !== undefined
74
+ const roots = deleting.map((row) => ({ model, row, redirected }))
75
+ if (redirected) return { deleted: roots, nulled: [] }
76
+ const deleted = new Map<string, Doomed>()
77
+ for (const root of roots) deleted.set(keyOf(model, root.row.id), root)
78
+ const restricted: (Reference & {
79
+ readonly id: string
80
+ readonly to: string
81
+ })[] = []
82
+ const nulled = new Map<string, Reference & { readonly rows: Row[] }>()
83
+ let level: readonly Doomed[] = roots
84
+ while (level.length > 0) {
85
+ const next: Doomed[] = []
86
+ const ids = new Map<ModelInfo, string[]>()
87
+ for (const doomed of level) {
88
+ ids.set(doomed.model, [...(ids.get(doomed.model) ?? []), doomed.row.id])
89
+ }
90
+ for (const [target, targets] of ids) {
91
+ for (const reference of referencesTo(models, target.name)) {
92
+ const { model: from, field, onDelete } = reference
93
+ const rows = await store.find({
94
+ model: from.name,
95
+ where: { [field]: { is: { id: { in: targets } } } },
96
+ lock: true,
97
+ })
98
+ for (const found of rows) {
99
+ const left =
100
+ except?.model === from.name &&
101
+ except.field === field &&
102
+ except.id === found.id
103
+ if (left) continue
104
+ if (onDelete === 'restrict') {
105
+ restricted.push({
106
+ model: from,
107
+ field,
108
+ id: found.id,
109
+ to: target.name,
110
+ })
111
+ } else if (onDelete === 'setNull') {
112
+ const key = `${from.name}.${field}`
113
+ const group = nulled.get(key) ?? { model: from, field, rows: [] }
114
+ // A key holds one target, deleted once: no row comes twice.
115
+ group.rows.push(found)
116
+ nulled.set(key, group)
117
+ } else if (!deleted.has(keyOf(from, found.id))) {
118
+ // Not redirected: a row that stayed would keep a key to a
119
+ // deleted one (D-90).
120
+ const doomed = { model: from, row: found, redirected: false }
121
+ deleted.set(keyOf(from, found.id), doomed)
122
+ next.push(doomed)
123
+ }
124
+ }
125
+ }
126
+ }
127
+ level = next
128
+ }
129
+ // A row the cascade removes no longer references anything.
130
+ const stays = (from: ModelInfo, id: string): boolean =>
131
+ !deleted.has(keyOf(from, id))
132
+ for (const { model: from, field, id, to } of restricted) {
133
+ if (!stays(from, id)) continue
134
+ throw new PublicError(
135
+ 'RELATION_RESTRICTED',
136
+ `${from.name}.${field} references a ${to} the delete removes.`,
137
+ { details: { model: from.name, field } },
138
+ )
139
+ }
140
+ return {
141
+ // The roots last, in the order they were read.
142
+ deleted: [...deleted.values()]
143
+ .slice(roots.length)
144
+ .toReversed()
145
+ .concat(roots),
146
+ nulled: [...nulled.values()].map((group) => ({
147
+ ...group,
148
+ rows: group.rows.filter((found) => stays(group.model, found.id)),
149
+ })),
150
+ }
151
+ }
152
+
153
+ /** Whether deleting a row of `model` runs code of its own. */
154
+ const deleteRuns = (model: ModelInfo): boolean =>
155
+ redirectOf(model) !== undefined ||
156
+ hasHook(model, 'beforeDelete') ||
157
+ hasHook(model, 'afterDelete') ||
158
+ hasHook(model, 'afterDeleteCommit')
159
+
160
+ const anyOf = (filters: readonly Filter[]): Filter =>
161
+ filters.length === 1 && filters[0] !== undefined
162
+ ? filters[0]
163
+ : { OR: filters }
164
+
165
+ /**
166
+ * The changes and checks of a delete whose reach runs no code, so no row is
167
+ * loaded (D-102): the root goes by `where`, each level by the rows the level
168
+ * above removed, and `setNull` empties the keys naming them. `undefined` when
169
+ * a reached model has delete hooks or a redirect, a `setNull` one update
170
+ * hooks, a cascade comes back to a model it left, or a model is both deleted
171
+ * and emptied, or emptied twice.
172
+ */
173
+ export function cascadeWhere(
174
+ models: Models,
175
+ model: ModelInfo,
176
+ where: Filter,
177
+ ): { readonly changes: Change[]; readonly checks: Checked[] } | undefined {
178
+ /**
179
+ * Each deleted model's filters from the root, its longest path from the
180
+ * root, and the relations to the deleted models that reach it.
181
+ */
182
+ type Level = {
183
+ readonly filters: Filter[]
184
+ depth: number
185
+ readonly parents: Map<string, Reference>
186
+ }
187
+ const deleted = new Map<string, Level>()
188
+ /** The rows each `setNull` or `restrict` relation reaches, by relation. */
189
+ const reached = new Map<
190
+ string,
191
+ Reference & {
192
+ readonly to: string
193
+ readonly restricts: boolean
194
+ readonly filters: Filter[]
195
+ }
196
+ >()
197
+ const reach = (
198
+ reference: Reference & { readonly to: string; readonly restricts: boolean },
199
+ filter: Filter,
200
+ ): void => {
201
+ const key = `${reference.model.name}.${reference.field}`
202
+ const group = reached.get(key) ?? { ...reference, filters: [] }
203
+ group.filters.push(filter)
204
+ reached.set(key, group)
205
+ }
206
+ const visit = (
207
+ at: ModelInfo,
208
+ filter: Filter,
209
+ path: string[],
210
+ parent?: Reference,
211
+ ): boolean => {
212
+ if (deleteRuns(at)) return false
213
+ const entry: Level = deleted.get(at.name) ?? {
214
+ filters: [],
215
+ depth: 0,
216
+ parents: new Map(),
217
+ }
218
+ entry.filters.push(filter)
219
+ entry.depth = Math.max(entry.depth, path.length)
220
+ if (parent !== undefined) {
221
+ entry.parents.set(`${parent.model.name}.${parent.field}`, parent)
222
+ }
223
+ deleted.set(at.name, entry)
224
+ const below = [...path, at.name]
225
+ for (const { model: from, field, onDelete } of referencesTo(
226
+ models,
227
+ at.name,
228
+ )) {
229
+ const child = { [field]: { is: filter } }
230
+ if (onDelete === 'cascade') {
231
+ const edge = { model: at, field }
232
+ if (below.includes(from.name) || !visit(from, child, below, edge)) {
233
+ return false
234
+ }
235
+ } else if (onDelete === 'setNull' && updated(from)) {
236
+ return false
237
+ } else {
238
+ const restricts = onDelete === 'restrict'
239
+ reach({ model: from, field, to: at.name, restricts }, child)
240
+ }
241
+ }
242
+ return true
243
+ }
244
+ if (!visit(model, where, [])) return undefined
245
+ const groups = [...reached.values()]
246
+ const nulls = groups.filter((group) => !group.restricts)
247
+ const emptied = new Set(nulls.map((group) => group.model.name))
248
+ // PostgreSQL gives two changes of the same rows in one statement no order.
249
+ const twice =
250
+ emptied.size < nulls.length ||
251
+ [...emptied].some((name) => deleted.has(name))
252
+ if (twice) return undefined
253
+ // A row the cascade removes is not refusing.
254
+ const left = (from: ModelInfo, filters: readonly Filter[]): Filter => {
255
+ const gone = deleted.get(from.name)
256
+ return gone === undefined
257
+ ? anyOf(filters)
258
+ : { AND: [anyOf(filters), { NOT: [anyOf(gone.filters)] }] }
259
+ }
260
+ const checks = groups
261
+ .filter((group) => group.restricts)
262
+ .map((group) => ({
263
+ check: {
264
+ model: group.model.name,
265
+ where: left(group.model, group.filters),
266
+ count: 0,
267
+ },
268
+ message: `${group.model.name}.${group.field} references a ${group.to} the delete removes.`,
269
+ restricted: { model: group.model.name, field: group.field },
270
+ }))
271
+ // A level after the levels above it: its parents have a lower depth.
272
+ const levels = [...deleted].toSorted(([, a], [, b]) => a.depth - b.depth)
273
+ const removedBy = (field: string, name: string): Filter => ({
274
+ [field]: { removedBy: levels.findIndex(([each]) => each === name) },
275
+ })
276
+ const changes: Change[] = [
277
+ ...levels.map(([name, { parents }]) => ({
278
+ kind: 'delete' as const,
279
+ model: name,
280
+ where:
281
+ parents.size === 0
282
+ ? where
283
+ : anyOf(
284
+ [...parents.values()].map((edge) =>
285
+ removedBy(edge.field, edge.model.name),
286
+ ),
287
+ ),
288
+ })),
289
+ ...nulls.map((group) => ({
290
+ kind: 'update' as const,
291
+ model: group.model.name,
292
+ where: removedBy(group.field, group.to),
293
+ set: { [group.field]: null, updatedAt: new Date(Date.now()) },
294
+ })),
295
+ ]
296
+ return { changes, checks }
297
+ }
@@ -0,0 +1,45 @@
1
+ // `ultimabase.config.ts` (D-105): where the commands find the application.
2
+ import { existsSync } from 'node:fs'
3
+ import { basename, join, resolve } from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+ import type { Internals } from './app-internals.ts'
6
+ import { internalsOf } from './app-internals.ts'
7
+ import { Refused } from './errors.ts'
8
+
9
+ const names = [
10
+ 'ultimabase.config.ts',
11
+ 'ultimabase.config.mts',
12
+ 'ultimabase.config.js',
13
+ 'ultimabase.config.mjs',
14
+ ]
15
+
16
+ /** The config file `--config` names, or the first one in `cwd`. */
17
+ export function configPath(cwd: string, given: string | undefined): string {
18
+ if (given !== undefined) {
19
+ const path = resolve(cwd, given)
20
+ if (!existsSync(path)) throw new Refused(`There is no ${path}.`)
21
+ return path
22
+ }
23
+ const found = names.map((name) => join(cwd, name)).find(existsSync)
24
+ if (found === undefined) {
25
+ throw new Refused(
26
+ `There is no ultimabase.config.ts in ${cwd}: write one that default-exports defineApp(...), or pass --config <path>.`,
27
+ )
28
+ }
29
+ return found
30
+ }
31
+
32
+ /** The application the config file default-exports. */
33
+ export async function load(path: string): Promise<Internals> {
34
+ const module: unknown = await import(pathToFileURL(path).href)
35
+ const app =
36
+ typeof module === 'object' && module !== null
37
+ ? internalsOf(Reflect.get(module, 'default'))
38
+ : undefined
39
+ if (app === undefined) {
40
+ throw new Refused(
41
+ `${basename(path)} does not default-export defineApp(...).`,
42
+ )
43
+ }
44
+ return app
45
+ }
@@ -0,0 +1,235 @@
1
+ // `migrate deploy`, `migrate status` and `reset` (D-105): the migration
2
+ // files against those the database applied.
3
+ import type { Migration, Migrations, Stored } from './adapter.ts'
4
+ import { MigrationFailed } from './adapter.ts'
5
+ import type { Internals } from './app-internals.ts'
6
+ import { create, read } from './cli-migrate.ts'
7
+ import { kernelLogger, schemaOf } from './lifecycle.ts'
8
+ import { Refused } from './errors.ts'
9
+ import { ahead, hashOf, held, migrationsOf } from './migrate.ts'
10
+
11
+ const upToDate = 'The database is up to date.'
12
+
13
+ const production = (): boolean => process.env['NODE_ENV'] === 'production'
14
+
15
+ /** How a development database gets back to what the files build. */
16
+ const rebuild = (): readonly string[] =>
17
+ production()
18
+ ? []
19
+ : [
20
+ 'Restore the dump you test on, or rebuild this database from the migrations with `npx ultimabase reset`.',
21
+ ]
22
+
23
+ const refused = (...lines: readonly string[]): Refused =>
24
+ new Refused([...lines, ...rebuild()].join('\n'))
25
+
26
+ /**
27
+ * The files the database has not applied, and the files it applied after
28
+ * the last one, which newer code wrote; refuses a file edited or removed.
29
+ */
30
+ function planOf(
31
+ stored: Stored | undefined,
32
+ files: readonly Migration[],
33
+ ): { readonly pending: readonly Migration[]; readonly later: string[] } {
34
+ const applied = stored?.applied ?? []
35
+ const last = files.at(-1)?.name ?? ''
36
+ const later: string[] = []
37
+ for (const { name, checksum } of applied) {
38
+ const file = files.find((given) => given.name === name)
39
+ if (file === undefined && name > last) {
40
+ later.push(name)
41
+ } else if (file === undefined) {
42
+ throw refused(
43
+ `${name} was applied to the database, and migrations/ no longer holds it: restore it from version control.`,
44
+ )
45
+ } else if (file.checksum !== checksum) {
46
+ throw refused(
47
+ `${name} was edited after it was applied to the database: restore it, and write the change with ${create}.`,
48
+ )
49
+ }
50
+ }
51
+ const pending = files.filter(
52
+ (file) => !applied.some((given) => given.name === file.name),
53
+ )
54
+ const [first] = pending
55
+ if (first !== undefined && later.length > 0) {
56
+ throw refused(
57
+ `${first.name} is pending, and the database applied ${later.join(', ')} after it, which migrations/ does not hold: deploy the code that holds them.`,
58
+ )
59
+ }
60
+ return { pending, later }
61
+ }
62
+
63
+ /** Why the files do not lead to the models whose hash is `hash`, if so. */
64
+ function behind(files: readonly Migration[], hash: string): string | undefined {
65
+ const last = files.at(-1)
66
+ if (last === undefined) {
67
+ return `migrations/ holds no migration: run ${create} first.`
68
+ }
69
+ if (last.schema === hash) return undefined
70
+ return `The models changed after ${last.name} was written: run ${create}, then deploy again.`
71
+ }
72
+
73
+ const syncedSince = (): Refused =>
74
+ refused(
75
+ 'Development sync changed this database since its last deploy: deploy applies migrations only to a database they built.',
76
+ )
77
+
78
+ /** Applies `files`; a refused one names those applied before it. */
79
+ async function deployed(
80
+ migrations: Migrations,
81
+ files: readonly Migration[],
82
+ to: unknown,
83
+ again: string,
84
+ ): Promise<readonly string[]> {
85
+ const lines = files.map((file) => `Applied ${file.name}.`)
86
+ try {
87
+ await migrations.apply({ kind: 'deploy', files, to })
88
+ } catch (error) {
89
+ if (!(error instanceof MigrationFailed)) throw error
90
+ const done = files.findIndex((file) => file.name === error.at)
91
+ throw new Refused(
92
+ [
93
+ ...lines.slice(0, done),
94
+ `${error.at} was not applied: ${error.reason}`,
95
+ files[done]?.transaction === false
96
+ ? `It ran without a transaction, so nothing was rolled back: undo what it left, such as an invalid index, then ${again}.`
97
+ : `The database rolled it back: fix it, then ${again}.`,
98
+ ].join('\n'),
99
+ )
100
+ }
101
+ return lines
102
+ }
103
+
104
+ /** Applies the pending migrations, then stores the models' snapshot. */
105
+ export async function migrateDeploy(
106
+ { options }: Internals,
107
+ dir: string,
108
+ ): Promise<readonly string[]> {
109
+ const { database } = options
110
+ const migrations = migrationsOf(database)
111
+ const schema = schemaOf(options)
112
+ const to = migrations.snapshot(schema)
113
+ const hash = hashOf(to)
114
+ const { files } = await read(dir)
115
+ const late = behind(files, hash)
116
+ if (late !== undefined) throw new Refused(late)
117
+ await database.start(schema, kernelLogger(options))
118
+ try {
119
+ const stored = await migrations.stored()
120
+ if (stored?.by === 'sync') throw syncedSince()
121
+ const { pending, later } = planOf(stored, files)
122
+ // The code was rolled back: its migrations are applied, and more.
123
+ if (later.length > 0) return [ahead(later)]
124
+ const current =
125
+ stored?.snapshot !== undefined && hashOf(stored.snapshot) === hash
126
+ if (pending.length === 0 && current) return [upToDate]
127
+ const lines = await deployed(migrations, pending, to, 'deploy again')
128
+ return lines.length === 0 ? [upToDate] : lines
129
+ } finally {
130
+ await database.stop()
131
+ }
132
+ }
133
+
134
+ /** What a deploy would do or refuse; `ok` when it has nothing to do. */
135
+ export async function migrateStatus(
136
+ { options }: Internals,
137
+ dir: string,
138
+ ): Promise<{ readonly lines: readonly string[]; readonly ok: boolean }> {
139
+ const { database } = options
140
+ const migrations = migrationsOf(database)
141
+ const schema = schemaOf(options)
142
+ const hash = hashOf(migrations.snapshot(schema))
143
+ const { files } = await read(dir)
144
+ await database.start(schema, kernelLogger(options))
145
+ try {
146
+ const stored = await migrations.stored()
147
+ const late = behind(files, hash)
148
+ const { pending, later } = planOf(stored, files)
149
+ const lines = [
150
+ ...(late === undefined ? [] : [late]),
151
+ ...(stored?.by === 'sync' ? [syncedSince().message] : []),
152
+ ...pending.map((file) => `${file.name} is pending.`),
153
+ ]
154
+ if (lines.length > 0) return { lines, ok: false }
155
+ if (later.length > 0) return { lines: [ahead(later)], ok: true }
156
+ if (stored?.snapshot !== undefined && hashOf(stored.snapshot) === hash) {
157
+ return { lines: [upToDate], ok: true }
158
+ }
159
+ return {
160
+ lines: [
161
+ "The database's snapshot is not the models': run `npx ultimabase migrate deploy`.",
162
+ ],
163
+ ok: false,
164
+ }
165
+ } finally {
166
+ await database.stop()
167
+ }
168
+ }
169
+
170
+ /** Drops every table; one another session holds stops the reset. */
171
+ async function dropAll(migrations: Migrations): Promise<readonly string[]> {
172
+ try {
173
+ return await migrations.reset()
174
+ } catch (error) {
175
+ if (!(error instanceof MigrationFailed && error.locked)) throw error
176
+ throw new Refused(
177
+ [
178
+ 'UltimaBase did not reset the database: another session held a table.',
179
+ ` ${error.at};`,
180
+ ` -- ${error.reason}`,
181
+ held,
182
+ ].join('\n'),
183
+ )
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Drops a development database's tables, then applies every migration; one
189
+ * on another machine only with `force`.
190
+ */
191
+ export async function reset(
192
+ { options }: Internals,
193
+ dir: string,
194
+ force: boolean,
195
+ ): Promise<readonly string[]> {
196
+ if (production()) {
197
+ throw new Refused(
198
+ 'reset rebuilds only a development database: it is refused in production.',
199
+ )
200
+ }
201
+ const { database } = options
202
+ const migrations = migrationsOf(database)
203
+ if (migrations.remote !== undefined && !force) {
204
+ throw new Refused(
205
+ `reset drops every table, and ${migrations.remote} is not this machine: pass --force if it holds a development database.`,
206
+ )
207
+ }
208
+ const schema = schemaOf(options)
209
+ const { files, snapshot } = await read(dir)
210
+ await database.start(schema, kernelLogger(options))
211
+ try {
212
+ const dropped = await dropAll(migrations)
213
+ const lines = [
214
+ dropped.length === 0
215
+ ? 'The database held no table.'
216
+ : `Dropped ${dropped.join(', ')}.`,
217
+ ]
218
+ const last = files.at(-1)
219
+ if (last === undefined) {
220
+ return [
221
+ ...lines,
222
+ 'migrations/ holds no migration: the next start builds the tables.',
223
+ ]
224
+ }
225
+ lines.push(...(await deployed(migrations, files, snapshot, 'reset again')))
226
+ if (behind(files, hashOf(migrations.snapshot(schema))) !== undefined) {
227
+ lines.push(
228
+ `The models changed after ${last.name} was written: the next start syncs them.`,
229
+ )
230
+ }
231
+ return lines
232
+ } finally {
233
+ await database.stop()
234
+ }
235
+ }