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/memory.ts ADDED
@@ -0,0 +1,173 @@
1
+ // `memory()`: the in-memory adapter, for tests and examples (D-02). Each write
2
+ // runs on a copy of the store that replaces it once whole, so a failed write
3
+ // changes nothing; a transaction holds the write lock until it ends.
4
+ import type { Adapter, Changed, Feed, Schema, Transaction } from './adapter.ts'
5
+ import { feedFields, feedIds } from './adapter.ts'
6
+ import { Draft } from './memory-draft.ts'
7
+ import type { Fields, Store, StoredRow } from './memory-read.ts'
8
+ import { clone } from './memory-read.ts'
9
+
10
+ /** The reads of the draft `get` gives, each a tick later, as a database's. */
11
+ function readsOf(
12
+ get: () => Draft,
13
+ ): Pick<Transaction, 'find' | 'count' | 'aggregate'> {
14
+ const later = async <R>(read: (draft: Draft) => R): Promise<R> => {
15
+ await Promise.resolve()
16
+ return read(get())
17
+ }
18
+ return {
19
+ find: async (read) => later((draft) => draft.find(read)),
20
+ count: async (read) => later((draft) => draft.count(read)),
21
+ aggregate: async (read) => later((draft) => draft.aggregate(read)),
22
+ }
23
+ }
24
+
25
+ /**
26
+ * `changes` as the feed gives them: one entry for the same rows' changes,
27
+ * without the lists past its limits (D-154).
28
+ */
29
+ function fed(changes: readonly Changed[]): Changed[] {
30
+ type Entry = {
31
+ readonly change: Changed
32
+ readonly ids: string[]
33
+ readonly pairs: (readonly [string, string])[]
34
+ }
35
+ const entries = new Map<string, Entry>()
36
+ for (const change of changes) {
37
+ const linked = 'field' in change
38
+ const key = JSON.stringify([
39
+ change.kind,
40
+ change.model,
41
+ linked ? change.field : change.fields,
42
+ ])
43
+ const entry = entries.get(key) ?? { change, ids: [], pairs: [] }
44
+ if (linked) entry.pairs.push(...(change.pairs ?? []))
45
+ else entry.ids.push(...(change.ids ?? []))
46
+ entries.set(key, entry)
47
+ }
48
+ return [...entries.values()].map(({ change, ids, pairs }): Changed => {
49
+ if ('field' in change) {
50
+ const { kind, model, field } = change
51
+ const listed = pairs.length > feedIds ? {} : { pairs }
52
+ return { kind, model, field, ...listed }
53
+ }
54
+ const { kind, model, fields } = change
55
+ const listed = ids.length > feedIds ? {} : { ids }
56
+ const some =
57
+ fields !== undefined && JSON.stringify(fields).length <= feedFields
58
+ return { kind, model, ...listed, ...(some ? { fields } : {}) }
59
+ })
60
+ }
61
+
62
+ /** The in-memory adapter (D-02); its state lives as long as the value. */
63
+ export function memory(): Adapter {
64
+ let schema: Schema | undefined
65
+ let fields: Fields = new Map()
66
+ let store: Store = { tables: new Map(), joins: new Map() }
67
+ let lock: Promise<void> = Promise.resolve()
68
+ let feed: Feed | undefined
69
+
70
+ // After the commit, as a database's notifications come.
71
+ const deliver = (changes: readonly Changed[]): void => {
72
+ const to = feed
73
+ if (to === undefined || changes.length === 0) return
74
+ setImmediate(() => to.changed(fed(changes)))
75
+ }
76
+
77
+ const draft = (from: Store): Draft => {
78
+ if (schema === undefined) throw new Error('memory() is not started.')
79
+ return new Draft(from, fields, schema)
80
+ }
81
+ // Writes queue behind each other and behind an open transaction.
82
+ const acquire = async (): Promise<() => void> => {
83
+ const previous = lock
84
+ const { promise, resolve } = Promise.withResolvers<void>()
85
+ lock = promise
86
+ await previous
87
+ return resolve
88
+ }
89
+
90
+ return {
91
+ start: async (started, _logger, given) => {
92
+ schema = started
93
+ feed = given
94
+ fields = new Map(
95
+ started.models.map((model) => [
96
+ model.name,
97
+ new Map(model.fields.map((field) => [field.name, field])),
98
+ ]),
99
+ )
100
+ store = {
101
+ tables: new Map(
102
+ started.models.map((model) => [
103
+ model.name,
104
+ new Map<string, StoredRow>(),
105
+ ]),
106
+ ),
107
+ joins: new Map(),
108
+ }
109
+ await Promise.resolve()
110
+ },
111
+ stop: async () => {
112
+ await lock
113
+ },
114
+ ...readsOf(() => draft(store)),
115
+ write: async (write) => {
116
+ const release = await acquire()
117
+ try {
118
+ const next = clone(store)
119
+ const writing = draft(next)
120
+ const rows = writing.write(write)
121
+ store = next
122
+ deliver(writing.changed)
123
+ return rows
124
+ } finally {
125
+ release()
126
+ }
127
+ },
128
+ begin: async (): Promise<Transaction> => {
129
+ const release = await acquire()
130
+ const next = clone(store)
131
+ const changed: Changed[] = []
132
+ let open = true
133
+ const active = (): Draft => {
134
+ if (!open) throw new Error('The transaction has ended.')
135
+ return draft(next)
136
+ }
137
+ const end = (): void => {
138
+ open = false
139
+ release()
140
+ }
141
+ return {
142
+ ...readsOf(active),
143
+ write: async (write) => {
144
+ await Promise.resolve()
145
+ active()
146
+ // A failed write leaves the transaction as it was.
147
+ const attempt = clone(next)
148
+ const writing = draft(attempt)
149
+ const rows = writing.write(write)
150
+ changed.push(...writing.changed)
151
+ next.tables.clear()
152
+ next.joins.clear()
153
+ for (const [name, table] of attempt.tables)
154
+ next.tables.set(name, table)
155
+ for (const [name, side] of attempt.joins) next.joins.set(name, side)
156
+ return rows
157
+ },
158
+ commit: async () => {
159
+ active()
160
+ store = next
161
+ end()
162
+ deliver(changed)
163
+ await Promise.resolve()
164
+ },
165
+ rollback: async () => {
166
+ active()
167
+ end()
168
+ await Promise.resolve()
169
+ },
170
+ }
171
+ },
172
+ }
173
+ }
package/src/migrate.ts ADDED
@@ -0,0 +1,152 @@
1
+ // Development sync and the production check (D-105): the models' snapshot
2
+ // against the database's, through the adapter's `migrations`.
3
+ import { createHash } from 'node:crypto'
4
+ import type { Adapter, Migrations, Schema, Statement } from './adapter.ts'
5
+ import { MigrationFailed } from './adapter.ts'
6
+ import type { Logger } from './app-options.ts'
7
+ import { Refused } from './errors.ts'
8
+
9
+ /** The value with its keys sorted, so equal snapshots serialize alike. */
10
+ function canonical(value: unknown): unknown {
11
+ if (Array.isArray(value)) return value.map(canonical)
12
+ if (typeof value !== 'object' || value === null) return value
13
+ const keys = Object.keys(value).toSorted()
14
+ return Object.fromEntries(
15
+ keys.map((key) => [key, canonical(Reflect.get(value, key))]),
16
+ )
17
+ }
18
+
19
+ /** The SHA-256 of a snapshot's canonical JSON. */
20
+ export function hashOf(snapshot: unknown): string {
21
+ return createHash('sha256')
22
+ .update(JSON.stringify(canonical(snapshot)))
23
+ .digest('hex')
24
+ }
25
+
26
+ /** The database's `migrations`, or a refusal naming what has none. */
27
+ export function migrationsOf(database: Adapter): Migrations {
28
+ if (database.migrations === undefined) {
29
+ throw new Refused(
30
+ 'This database starts empty every time: it has no tables to migrate.',
31
+ )
32
+ }
33
+ return database.migrations
34
+ }
35
+
36
+ function refusal(losing: readonly Statement[]): string {
37
+ const models = [...new Set(losing.map((statement) => statement.model))]
38
+ const why = losing.map((statement) => statement.loses).join('; ')
39
+ return [
40
+ `UltimaBase did not sync ${models.join(', ')}: ${why}.`,
41
+ ...losing.flatMap(({ sql, rename }) => [
42
+ ...(rename === undefined
43
+ ? []
44
+ : [
45
+ ` -- a rename? keep its data: give ${rename.to} dbName: '${rename.dbName}'.`,
46
+ ]),
47
+ ` ${sql};`,
48
+ ]),
49
+ 'Run `npx ultimabase sync --force` to apply it to this development database,',
50
+ 'or `npx ultimabase migrate create <name>` to write it as a migration.',
51
+ ].join('\n')
52
+ }
53
+
54
+ /** What to do about a table another session held too long. */
55
+ export const held =
56
+ "End that session's open transaction, in a database tool perhaps, then try again."
57
+
58
+ /** Why a tried statement failed, and how to fix the rows. */
59
+ function failure(
60
+ statements: readonly Statement[],
61
+ { at, reason, locked }: MigrationFailed,
62
+ ): string {
63
+ const statement = statements.find((given) => given.sql === at)
64
+ const model = statement?.model ?? 'the models'
65
+ const why = locked
66
+ ? 'another session held its table'
67
+ : (statement?.tries ?? 'the database refused it')
68
+ return [
69
+ `UltimaBase did not sync ${model}: ${why}.`,
70
+ ` ${at};`,
71
+ ` -- ${reason}`,
72
+ ...(locked
73
+ ? [held]
74
+ : [
75
+ 'Fix those rows in this development database, or rebuild it from the migrations with `npx ultimabase reset`.',
76
+ 'In production, a migration fixes them: write it with `npx ultimabase migrate create <name>`.',
77
+ ]),
78
+ ].join('\n')
79
+ }
80
+
81
+ /**
82
+ * Brings a development database to the models, refusing what loses data
83
+ * unless `force`, and logs what changed. Returns the statements it ran, or
84
+ * none when the database already matched.
85
+ */
86
+ export async function synced(
87
+ database: Adapter,
88
+ schema: Schema,
89
+ logger: Logger,
90
+ force: boolean,
91
+ ): Promise<readonly Statement[] | undefined> {
92
+ const migrations = database.migrations
93
+ if (migrations === undefined) return undefined
94
+ const stored = await migrations.stored()
95
+ const from = stored?.snapshot
96
+ const to = migrations.snapshot(schema)
97
+ if (from !== undefined && hashOf(from) === hashOf(to)) return undefined
98
+ const statements = migrations.plan(from, schema)
99
+ const losing = statements.filter((statement) => statement.loses !== undefined)
100
+ if (losing.length > 0 && !force) throw new Refused(refusal(losing))
101
+ try {
102
+ await migrations.apply({ kind: 'sync', from, statements, to })
103
+ } catch (error) {
104
+ if (error instanceof MigrationFailed) {
105
+ throw new Refused(failure(statements, error))
106
+ }
107
+ throw error
108
+ }
109
+ if (from === undefined) {
110
+ logger.info("Created the database's tables.")
111
+ return statements
112
+ }
113
+ if (statements.length === 0) {
114
+ logger.info("Synced the models' names: no table changed.")
115
+ }
116
+ const changes = Map.groupBy(statements, (statement) => statement.model)
117
+ for (const [model, made] of changes) {
118
+ const does = made.map((statement) => statement.does).join(', ')
119
+ logger.info(`Synced ${model}: ${does}.`)
120
+ }
121
+ return statements
122
+ }
123
+
124
+ /**
125
+ * Refuses a production database whose snapshot is not the models', unless
126
+ * a deploy applied migrations after them: the code was rolled back.
127
+ */
128
+ export async function checked(
129
+ database: Adapter,
130
+ schema: Schema,
131
+ logger: Logger,
132
+ ): Promise<void> {
133
+ const migrations = database.migrations
134
+ if (migrations === undefined) return
135
+ const stored = await migrations.stored()
136
+ const deploy = 'run `npx ultimabase migrate deploy`, then start again.'
137
+ if (stored?.snapshot === undefined) {
138
+ throw new Refused(`The database has no UltimaBase schema: ${deploy}`)
139
+ }
140
+ const hash = hashOf(migrations.snapshot(schema))
141
+ if (hashOf(stored.snapshot) === hash) return
142
+ const at = stored.applied.findIndex((file) => file.schema === hash)
143
+ const later = stored.applied.slice(at + 1).map((file) => file.name)
144
+ if (at === -1 || later.length === 0) {
145
+ throw new Refused(`The database's tables are not the models': ${deploy}`)
146
+ }
147
+ logger.warn(ahead(later))
148
+ }
149
+
150
+ /** What a database whose migrations follow the models' says. */
151
+ export const ahead = (later: readonly string[]): string =>
152
+ `The database is ahead of the models: ${later.join(', ')} ${later.length === 1 ? 'was' : 'were'} applied after them.`
package/src/model.ts ADDED
@@ -0,0 +1,312 @@
1
+ // The model `defineModel(...).with(...)` returns (D-07, D-24) and the
2
+ // application `defineApp` assembles.
3
+ import type { Nullable } from './function.ts'
4
+ import type { FindWhere, Queries, Where } from './query.ts'
5
+ import type {
6
+ CreateData,
7
+ RecordOf,
8
+ StoredRecord,
9
+ TargetFields,
10
+ UpdateData,
11
+ } from './record.ts'
12
+ import type { Caller } from './rules.ts'
13
+ import type { VirtualRules } from './virtual.ts'
14
+ import type {
15
+ declaredRecord,
16
+ fieldType,
17
+ modelFields,
18
+ readFilter,
19
+ } from './symbols.ts'
20
+
21
+ type ReadFacts = { readonly caller: Caller; readonly operation: 'read' }
22
+ type CreateFacts<F, N extends string> = {
23
+ readonly caller: Caller
24
+ readonly operation: 'create'
25
+ readonly value: CreateData<F, N>
26
+ }
27
+ type UpdateFacts<F, N extends string> = {
28
+ readonly caller: Caller
29
+ readonly operation: 'update'
30
+ readonly record: StoredRecord<F, N>
31
+ readonly value: UpdateData<F, N, false>
32
+ }
33
+ type DeleteFacts<F, N extends string> = {
34
+ readonly caller: Caller
35
+ readonly operation: 'delete'
36
+ readonly record: StoredRecord<F, N>
37
+ }
38
+
39
+ /** A read filter; only the `where` a read rule receives builds one (D-10). */
40
+ type ReadFilter = { readonly [readFilter]: true }
41
+ /**
42
+ * A model read rule cannot see the record: it may return a filter (D-10).
43
+ * TypeScript never checks a returned object for unknown keys, but it checks
44
+ * an argument: `where({ ... })` refuses a misspelled key on its line.
45
+ */
46
+ type ReadRule<F> = (
47
+ facts: ReadFacts & { readonly where: (filter: Where<F>) => ReadFilter },
48
+ ) => boolean | ReadFilter
49
+
50
+ /** One rule, or one rule per operation; `write` covers the three writes. */
51
+ export type ModelAccess<F, N extends string> =
52
+ | ((
53
+ facts:
54
+ ReadFacts | CreateFacts<F, N> | UpdateFacts<F, N> | DeleteFacts<F, N>,
55
+ ) => boolean)
56
+ | {
57
+ readonly read?: ReadRule<F>
58
+ readonly create?: (facts: CreateFacts<F, N>) => boolean
59
+ readonly update?: (facts: UpdateFacts<F, N>) => boolean
60
+ readonly delete?: (facts: DeleteFacts<F, N>) => boolean
61
+ readonly write?: (
62
+ facts: CreateFacts<F, N> | UpdateFacts<F, N> | DeleteFacts<F, N>,
63
+ ) => boolean
64
+ }
65
+
66
+ type Hook<Facts> = (
67
+ facts: Facts & { readonly caller: Caller },
68
+ ) => void | Promise<void>
69
+ type RecordHook<F, N extends string> = Hook<{
70
+ readonly record: StoredRecord<F, N>
71
+ }>
72
+ type UpdatedHook<F, N extends string> = Hook<{
73
+ readonly record: StoredRecord<F, N>
74
+ readonly previous: StoredRecord<F, N>
75
+ }>
76
+ /** The data a `before*` hook changes in place (D-71). */
77
+ type Draft<D> = { -readonly [K in keyof D]: D[K] }
78
+
79
+ /**
80
+ * What the rows a `beforeFind` narrows are for: an update or a delete of the
81
+ * rows a write targets, a read everywhere else (D-124).
82
+ */
83
+ export type FindOperation = 'read' | 'update' | 'delete'
84
+
85
+ /** The fixed hooks of D-15, written by the application (D-43, D-71). */
86
+ export type ModelHooks<F, N extends string = string> = {
87
+ /**
88
+ * Narrows every read of the model, at every level, Master included;
89
+ * `operation` says whether the rows are read or written (D-124).
90
+ */
91
+ readonly beforeFind?: (facts: {
92
+ readonly caller: Caller
93
+ readonly operation: FindOperation
94
+ readonly where: (filter: FindWhere<F>) => ReadFilter
95
+ }) => ReadFilter | void | Promise<ReadFilter | void>
96
+ readonly beforeCreate?: Hook<{ readonly data: Draft<CreateData<F, N>> }>
97
+ readonly beforeUpdate?: Hook<{
98
+ readonly record: StoredRecord<F, N>
99
+ readonly data: Draft<UpdateData<F, N, false>>
100
+ }>
101
+ readonly beforeDelete?: RecordHook<F, N>
102
+ readonly afterCreate?: RecordHook<F, N>
103
+ readonly afterUpdate?: UpdatedHook<F, N>
104
+ readonly afterDelete?: RecordHook<F, N>
105
+ readonly afterCreateCommit?: RecordHook<F, N>
106
+ readonly afterUpdateCommit?: UpdatedHook<F, N>
107
+ readonly afterDeleteCommit?: RecordHook<F, N>
108
+ }
109
+
110
+ /**
111
+ * What a `many` relation's reads see, Master's too (D-151): a function
112
+ * shaped like a read rule, typed from the target.
113
+ */
114
+ type RelationFilter<T> = {
115
+ readonly where: (facts: {
116
+ readonly caller: Caller
117
+ readonly where: (filter: Where<T>) => ReadFilter
118
+ }) => boolean | ReadFilter
119
+ }
120
+
121
+ /** A filter for each `many` relation that has one (D-151). */
122
+ export type RelationFilters<F> = {
123
+ readonly [
124
+ K in keyof F as F[K] extends {
125
+ readonly [fieldType]: { readonly kind: 'many' }
126
+ }
127
+ ? K
128
+ : never
129
+ ]?: F[K] extends { readonly [fieldType]: { readonly target: infer T } }
130
+ ? RelationFilter<TargetFields<T>>
131
+ : never
132
+ }
133
+
134
+ /** The kernel's keys; `W` holds one key per plugin (D-42, D-151). */
135
+ export type ModelWith<F, W, N extends string> = {
136
+ readonly access?: ModelAccess<F, N>
137
+ readonly hooks?: ModelHooks<F, N>
138
+ readonly relations?: RelationFilters<F>
139
+ readonly virtuals?: VirtualRules<F>
140
+ } & W
141
+
142
+ /** `X` lists the fields of each unique index (D-52). */
143
+ export interface Model<N extends string, F, P, W, X = never> extends Queries<
144
+ F,
145
+ X,
146
+ N
147
+ > {
148
+ readonly name: N
149
+ readonly [modelFields]: F
150
+ /** The plugins it uses, checked against the application's (D-24). */
151
+ readonly plugins?: P
152
+ with(rules: ModelWith<F, W, N>): this
153
+ /** The model, or `null`, as a function's output (D-109). */
154
+ nullable(): Nullable<this>
155
+ }
156
+
157
+ export type FieldShape = {
158
+ readonly [fieldType]: {
159
+ readonly kind: 'scalar' | 'one' | 'many' | 'virtual'
160
+ readonly options: object
161
+ }
162
+ }
163
+
164
+ // Exact type equality, which `any` and optional keys do not pass.
165
+ type Same<A, B> =
166
+ // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- the test needs a free `T`
167
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
168
+ ? true
169
+ : false
170
+
171
+ type Exact<V> = (value: V) => V
172
+
173
+ // The record type a field's rules declare (D-44) lists some of the model's
174
+ // fields, each with the model's exact type, required. Checked here, where the
175
+ // fields are known; a mismatch fails against the model's type, so the error
176
+ // shows it. `any` fits both ways and gets a message instead.
177
+ type Expected<D, F> = {
178
+ readonly [P in keyof D]-?: P extends keyof RecordOf<F>
179
+ ? Same<D[P], Exact<RecordOf<F>[P]>> extends true
180
+ ? D[P]
181
+ : D[P] extends Exact<RecordOf<F>[P]>
182
+ ? `'${P & string}' does not have the model's exact type`
183
+ : Exact<RecordOf<F>[P]>
184
+ : `'${P & string}' is not a scalar field of the model`
185
+ }
186
+
187
+ // A field typed before the model cannot check its record against it.
188
+ export type RecordCheck<F> = {
189
+ readonly [K in keyof F]: F[K] extends { readonly [declaredRecord]: infer D }
190
+ ? { readonly [declaredRecord]: Expected<D, F> }
191
+ : unknown
192
+ }
193
+
194
+ /**
195
+ * The plugins that provide the model's field types (D-32). Without
196
+ * `exactOptionalPropertyTypes`, a field no plugin provides reads `undefined`.
197
+ */
198
+ export type FieldTypePlugins<F> = {
199
+ [K in keyof F]: F[K] extends {
200
+ readonly [fieldType]: { readonly plugin?: infer P }
201
+ }
202
+ ? P extends string
203
+ ? P
204
+ : never
205
+ : never
206
+ }[keyof F]
207
+
208
+ /** An index of `defineModel`, over one or several fields (D-52). */
209
+ export type IndexShape = {
210
+ readonly fields: readonly string[]
211
+ readonly unique?: boolean
212
+ /** A trigram index, for `contains` and `mode: 'insensitive'` (D-141). */
213
+ readonly contains?: true
214
+ }
215
+
216
+ /** The fields of each unique index. */
217
+ export type UniqueIndexFields<X> = X extends readonly (infer I)[]
218
+ ? I extends { readonly unique: true; readonly fields: infer K }
219
+ ? K
220
+ : never
221
+ : never
222
+
223
+ // An index covers scalar fields and `one` relations, the model's columns.
224
+ type IndexableKeys<F> = {
225
+ [K in keyof F]: F[K] extends {
226
+ readonly [fieldType]: { readonly kind: 'scalar' | 'one' }
227
+ }
228
+ ? K
229
+ : never
230
+ }[keyof F]
231
+ // A `contains` index covers one string field that is not a list (D-141).
232
+ type TextKeys<F> = {
233
+ [K in keyof F]: F[K] extends {
234
+ readonly [fieldType]: { readonly kind: 'scalar'; readonly value: infer V }
235
+ }
236
+ ? [V] extends [string]
237
+ ? string extends V
238
+ ? K
239
+ : never
240
+ : never
241
+ : never
242
+ }[keyof F]
243
+ type ContainsCheck<I, F> = I extends { readonly contains: true }
244
+ ? I extends {
245
+ readonly fields: readonly [TextKeys<F>]
246
+ readonly unique?: false | undefined
247
+ }
248
+ ? unknown
249
+ : {
250
+ readonly contains: 'a contains index covers one string field that is not a list, and is not unique'
251
+ }
252
+ : unknown
253
+ type NotIndexable<I, F> = I extends { readonly fields: readonly (infer K)[] }
254
+ ? Exclude<K, IndexableKeys<F>>
255
+ : never
256
+
257
+ /**
258
+ * Each index names fields of the model. TypeScript first checks the call
259
+ * before reading `fields`, while `F` is still any field name: the check
260
+ * waits for the real fields.
261
+ */
262
+ export type IndexCheck<X, F> = string extends keyof F
263
+ ? unknown
264
+ : {
265
+ readonly [I in keyof X]: ([NotIndexable<X[I], F>] extends [never]
266
+ ? unknown
267
+ : {
268
+ readonly fields: `'${NotIndexable<X[I], F> & string}' is not a scalar field or a one relation of the model`
269
+ }) &
270
+ ContainsCheck<X[I], F>
271
+ }
272
+
273
+ export type ModelShape = {
274
+ readonly name: string
275
+ readonly [modelFields]: object
276
+ }
277
+
278
+ // An `inverse` names a field of the target (D-37). It is checked here, where
279
+ // every model is typed, since reading the target while declaring is a cycle.
280
+ type InverseErrors<N extends string, F> = {
281
+ [K in keyof F]: F[K] extends {
282
+ readonly [fieldType]: {
283
+ readonly target: infer T
284
+ readonly options: { readonly inverse: infer I extends string }
285
+ }
286
+ }
287
+ ? I extends keyof TargetFields<T>
288
+ ? never
289
+ : `${N}.${K & string}: inverse '${I}' is not a field of the target`
290
+ : never
291
+ }[keyof F]
292
+
293
+ type AppInverseErrors<M> = {
294
+ [I in keyof M]: M[I] extends {
295
+ readonly name: infer N extends string
296
+ readonly [modelFields]: infer F
297
+ }
298
+ ? InverseErrors<N, F>
299
+ : never
300
+ }[number & keyof M]
301
+
302
+ /** Checked on the whole options object, as `ModelCheck` is. */
303
+ export type InverseCheck<M> = [AppInverseErrors<M>] extends [never]
304
+ ? unknown
305
+ : { readonly inverseError: AppInverseErrors<M> }
306
+
307
+ /** The application `defineApp` returns (D-24, D-59). */
308
+ export interface App<M> {
309
+ readonly models: M
310
+ start(): Promise<void>
311
+ stop(): Promise<void>
312
+ }