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
@@ -0,0 +1,134 @@
1
+ // Virtual fields at startup (D-151): each one's output, input and needs
2
+ // resolved, with the `run` or `runMany` its model's `.with` gives it.
3
+ import type { ModelDefinition, Output } from './definition.ts'
4
+ import { outputOf } from './definition.ts'
5
+ import { Refused } from './errors.ts'
6
+ import { isObject, systemFields } from './query-where.ts'
7
+ import type { Standard } from './validation.ts'
8
+ import { standardOf } from './validation.ts'
9
+
10
+ export type VirtualInfo = {
11
+ readonly name: string
12
+ readonly model: string
13
+ readonly output: Output
14
+ readonly input: Standard | undefined
15
+ /** The scalar fields its rule reads, `id` first. */
16
+ readonly needs: readonly string[]
17
+ /** `runMany` takes the rows at once, `run` one by one. */
18
+ readonly many: boolean
19
+ readonly run: (facts: object) => unknown
20
+ /** A value without input is in the row without `select`. */
21
+ readonly default: boolean
22
+ readonly description: string | undefined
23
+ }
24
+
25
+ /** The keys that shape a model output's rows, which no input property takes. */
26
+ const shaping = ['select', 'include', 'omit']
27
+
28
+ /** An input's properties, from its JSON Schema export when it has one. */
29
+ function propertiesOf(input: Standard): readonly string[] {
30
+ const exporter: unknown = Reflect.get(input.self, 'jsonSchema')
31
+ const exported = isObject(exporter) ? exporter['input'] : undefined
32
+ if (typeof exported !== 'function') return []
33
+ const node: unknown = Reflect.apply(exported, exporter, [
34
+ { target: 'draft-2020-12' },
35
+ ])
36
+ const properties = isObject(node) ? node['properties'] : undefined
37
+ return isObject(properties) ? Object.keys(properties) : []
38
+ }
39
+
40
+ function outputFor(value: unknown, where: string): Output {
41
+ const standard = standardOf(value)
42
+ if (standard !== undefined) return { kind: 'schema', schema: standard.schema }
43
+ if (typeof value !== 'function') {
44
+ throw new Refused(
45
+ `${where} is a Standard Schema, or a model or a type behind an arrow.`,
46
+ )
47
+ }
48
+ const returned: unknown = Reflect.apply(value, undefined, [])
49
+ return outputOf(returned, where)
50
+ }
51
+
52
+ function ruleOf(
53
+ given: { readonly [key: string]: unknown },
54
+ where: string,
55
+ ): { readonly many: boolean; readonly run: (facts: object) => unknown } {
56
+ const rule = given[where.slice(where.indexOf('.') + 1)]
57
+ if (!isObject(rule)) {
58
+ throw new Refused(
59
+ `${where} is virtual; give its run or runMany in .with({ virtuals }).`,
60
+ )
61
+ }
62
+ const { run, runMany } = rule
63
+ if ((typeof run === 'function') === (typeof runMany === 'function')) {
64
+ throw new Refused(`${where}: give run or runMany, one of them.`)
65
+ }
66
+ const chosen = typeof run === 'function' ? run : runMany
67
+ if (typeof chosen !== 'function') throw new TypeError('A rule is a function.')
68
+ return {
69
+ many: chosen === runMany,
70
+ run: (facts) => {
71
+ const value: unknown = Reflect.apply(chosen, rule, [facts])
72
+ return value
73
+ },
74
+ }
75
+ }
76
+
77
+ /** The virtual fields of a model, checked against its fields and models. */
78
+ export function virtualsOf(
79
+ model: string,
80
+ definition: ModelDefinition,
81
+ models: ReadonlyMap<object, string>,
82
+ ): ReadonlyMap<string, VirtualInfo> {
83
+ const rules = isObject(definition.rules)
84
+ ? definition.rules['virtuals']
85
+ : undefined
86
+ const given = isObject(rules) ? rules : {}
87
+ const virtuals = new Map<string, VirtualInfo>()
88
+ for (const name of Object.keys(given)) {
89
+ if (definition.fields.get(name)?.kind !== 'virtual') {
90
+ throw new Refused(`${model}.virtuals.${name} names no virtual field.`)
91
+ }
92
+ }
93
+ for (const [name, field] of definition.fields) {
94
+ if (field.kind !== 'virtual') continue
95
+ const where = `${model}.${name}`
96
+ const output = outputFor(field.output, where)
97
+ if (output.kind === 'model' && !models.has(output.value)) {
98
+ throw new Refused(`${where}: its output model is not in models.`)
99
+ }
100
+ const { input: schema, needs: listed, description } = field.options
101
+ const input = schema === undefined ? undefined : standardOf(schema)
102
+ if (schema !== undefined && input === undefined) {
103
+ throw new Refused(`${where}: input is a Standard Schema of an object.`)
104
+ }
105
+ for (const key of input === undefined ? [] : propertiesOf(input)) {
106
+ if (shaping.includes(key)) {
107
+ throw new Refused(
108
+ `${where}: no input property is named ${key}, which shapes a model's rows.`,
109
+ )
110
+ }
111
+ }
112
+ const needs: readonly unknown[] = Array.isArray(listed) ? listed : []
113
+ for (const need of needs) {
114
+ const scalar =
115
+ typeof need === 'string' &&
116
+ (systemFields.includes(need) ||
117
+ definition.fields.get(need)?.kind === 'scalar')
118
+ if (!scalar) {
119
+ throw new Refused(`${where} needs ${String(need)}, no scalar field.`)
120
+ }
121
+ }
122
+ virtuals.set(name, {
123
+ name,
124
+ model,
125
+ output,
126
+ input,
127
+ needs: [...new Set(['id', ...needs.map(String)])],
128
+ ...ruleOf(given, where),
129
+ default: output.kind === 'schema' && input === undefined,
130
+ description: typeof description === 'string' ? description : undefined,
131
+ })
132
+ }
133
+ return virtuals
134
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,367 @@
1
+ // The schema `start` resolves from the application's models: each relation
2
+ // gets its inverse and the side that stores its key (D-37, D-54, D-64).
3
+ import type { Schema, SchemaField, SchemaModel } from './adapter.ts'
4
+ import type {
5
+ Codec,
6
+ FieldDefinition,
7
+ FieldOptions,
8
+ ModelDefinition,
9
+ PluginCode,
10
+ PluginUse,
11
+ } from './definition.ts'
12
+ import type { Digits } from './decimal.ts'
13
+ import type { Enum } from './fields.ts'
14
+ import { modelDefinitions } from './definition.ts'
15
+ import { Refused } from './errors.ts'
16
+ import { installedUses } from './plugin-use.ts'
17
+ import { checkRelations, isFiltered, viewField } from './relations.ts'
18
+ import { checkIndexes, checkSearch } from './schema-indexes.ts'
19
+ import type { VirtualInfo } from './schema-virtuals.ts'
20
+ import { virtualsOf } from './schema-virtuals.ts'
21
+
22
+ /**
23
+ * A field as the kernel reads it: its schema, its builder's options, a
24
+ * plugin field type's codec (D-122) and its shared enum (D-150).
25
+ */
26
+ export type FieldInfo = SchemaField & {
27
+ readonly options: FieldOptions
28
+ readonly codec?: Codec
29
+ readonly enum?: Enum
30
+ }
31
+
32
+ export type ModelInfo = {
33
+ readonly name: string
34
+ readonly definition: ModelDefinition
35
+ readonly fields: ReadonlyMap<string, FieldInfo>
36
+ /** The unique targets (D-52): `id`, unique fields, unique indexes. */
37
+ readonly uniques: readonly (readonly string[])[]
38
+ /** The plugins of its `use`, in the order of `plugins` (D-29). */
39
+ readonly plugins: readonly PluginUse[]
40
+ /** The plugin whose own model it is, if one (D-110). */
41
+ readonly owner: string | undefined
42
+ /** Its fields computed on read, which the adapter never sees (D-151). */
43
+ readonly virtuals: ReadonlyMap<string, VirtualInfo>
44
+ }
45
+
46
+ export type Resolved = {
47
+ readonly schema: Schema
48
+ readonly models: ReadonlyMap<string, ModelInfo>
49
+ /** The model each `defineModel` value stands for. */
50
+ readonly byValue: ReadonlyMap<object, ModelInfo>
51
+ }
52
+
53
+ type Relation = Extract<FieldDefinition, { readonly kind: 'one' | 'many' }>
54
+ type Linked = {
55
+ readonly model: string
56
+ readonly name: string
57
+ readonly field: Relation
58
+ readonly target: string
59
+ }
60
+
61
+ function fail(message: string): never {
62
+ throw new Refused(message)
63
+ }
64
+
65
+ function inverseOf(link: Linked, links: readonly Linked[]): Linked {
66
+ const { model, name, target } = link
67
+ const where = `${model}.${name}`
68
+ const named = link.field.options['inverse']
69
+ const back = links.filter(
70
+ (other) =>
71
+ other.model === target &&
72
+ other.target === model &&
73
+ !(other.model === model && other.name === name),
74
+ )
75
+ if (typeof named === 'string') {
76
+ const found = links.find(
77
+ (other) => other.model === target && other.name === named,
78
+ )
79
+ if (found === undefined || found.target !== model) {
80
+ fail(
81
+ `${where}: inverse '${named}' is not a relation of ${target} to ${model}.`,
82
+ )
83
+ }
84
+ if (found.model === model && found.name === name) {
85
+ fail(`${where} cannot be its own inverse.`)
86
+ }
87
+ return found
88
+ }
89
+ const candidates = back.filter((other) => {
90
+ const theirs = other.field.options['inverse']
91
+ return typeof theirs !== 'string' || theirs === name
92
+ })
93
+ const [only, ...others] = candidates
94
+ if (only === undefined) {
95
+ fail(`${where}: ${target} has no relation to ${model}; add its side.`)
96
+ }
97
+ if (others.length > 0) {
98
+ fail(
99
+ `${where}: ${target} has several relations to ${model}; name one with \`inverse\`.`,
100
+ )
101
+ }
102
+ return only
103
+ }
104
+
105
+ function relationField(link: Linked, inverse: Linked): FieldInfo {
106
+ const { field } = link
107
+ const optional = field.options['optional'] === true
108
+ const where = `${link.model}.${link.name}`
109
+ if (field.kind === 'many') {
110
+ return {
111
+ kind: 'many',
112
+ name: link.name,
113
+ target: link.target,
114
+ inverse: inverse.name,
115
+ join: inverse.field.kind === 'many',
116
+ options: field.options,
117
+ }
118
+ }
119
+ let stored = inverse.field.kind === 'many'
120
+ if (inverse.field.kind === 'one') {
121
+ const theirs = inverse.field.options['optional'] === true
122
+ if (optional === theirs) {
123
+ fail(
124
+ `${where} and ${inverse.model}.${inverse.name}: make one side of a one-to-one relation optional.`,
125
+ )
126
+ }
127
+ stored = !optional
128
+ }
129
+ const asked = field.options['onDelete']
130
+ if (!stored && asked !== undefined) {
131
+ fail(
132
+ `${where}: onDelete belongs on ${inverse.model}.${inverse.name}, which stores the key.`,
133
+ )
134
+ }
135
+ if (asked === 'setNull' && !optional) {
136
+ throw new Refused(`${where}: setNull needs an optional relation.`)
137
+ }
138
+ const onDelete =
139
+ asked === 'cascade' || asked === 'restrict' || asked === 'setNull'
140
+ ? asked
141
+ : optional
142
+ ? 'setNull'
143
+ : 'restrict'
144
+ const dbName = field.options['dbName']
145
+ return {
146
+ kind: 'one',
147
+ name: link.name,
148
+ target: link.target,
149
+ inverse: inverse.name,
150
+ stored,
151
+ unique: stored && inverse.field.kind === 'one',
152
+ optional,
153
+ onDelete,
154
+ ...(typeof dbName === 'string' ? { dbName } : {}),
155
+ options: field.options,
156
+ }
157
+ }
158
+
159
+ /** A decimal's precision and scale, which its builder checked (D-145). */
160
+ const digitsOf = (options: FieldOptions): Digits => ({
161
+ precision: Number(options['precision']),
162
+ scale: Number(options['scale']),
163
+ })
164
+
165
+ function scalarField(name: string, field: FieldDefinition): FieldInfo {
166
+ if (field.kind !== 'scalar') throw new Error('Expected a scalar field.')
167
+ const dbName = field.options['dbName']
168
+ // A field type's default is a value code sees, never a column's; its
169
+ // stored field's fills the rows stored before it (D-122, D-152).
170
+ const fixed: unknown =
171
+ field.codec === undefined ? field.options['default'] : field.codec.filled
172
+ return {
173
+ kind: 'scalar',
174
+ name,
175
+ type: field.type,
176
+ ...(field.values === undefined ? {} : { values: field.values }),
177
+ list: field.options['list'] === true,
178
+ ...(field.type === 'number' && field.options['integer'] === true
179
+ ? { integer: true }
180
+ : {}),
181
+ ...(field.type === 'decimal' ? digitsOf(field.options) : {}),
182
+ optional: field.options['optional'] === true,
183
+ unique: field.options['unique'] === true,
184
+ ...(field.options['search'] === true ? { search: true } : {}),
185
+ ...(typeof dbName === 'string' ? { dbName } : {}),
186
+ // `null` is no value: a create without one stores none either.
187
+ ...(fixed === undefined || fixed === null || typeof fixed === 'function'
188
+ ? {}
189
+ : { default: fixed }),
190
+ options: field.options,
191
+ ...(field.codec === undefined ? {} : { codec: field.codec }),
192
+ ...(field.enum === undefined ? {} : { enum: field.enum }),
193
+ }
194
+ }
195
+
196
+ /** A field as the adapter reads it, without the builder's options. */
197
+ function schemaField(model: ModelInfo, field: FieldInfo): SchemaField {
198
+ const { options: _options, codec: _codec, enum: _enum, ...schema } = field
199
+ if (schema.kind !== 'many' || !isFiltered(model, field.name)) return schema
200
+ return { ...schema, filtered: true }
201
+ }
202
+
203
+ const whose = (owner: string | undefined): string =>
204
+ owner === undefined ? 'models lists' : `${owner}.models lists`
205
+
206
+ /** The models, the application's then each plugin's, by name (D-110). */
207
+ function definitionsOf(
208
+ values: readonly unknown[],
209
+ plugins: readonly PluginCode[],
210
+ ): {
211
+ readonly definitions: ReadonlyMap<string, ModelDefinition>
212
+ readonly names: ReadonlyMap<object, string>
213
+ readonly owners: ReadonlyMap<string, string | undefined>
214
+ } {
215
+ const definitions = new Map<string, ModelDefinition>()
216
+ const names = new Map<object, string>()
217
+ const owners = new Map<string, string | undefined>()
218
+ const lists: [string | undefined, readonly unknown[]][] = [
219
+ [undefined, values],
220
+ ...plugins.map((code): [string, readonly unknown[]] => [
221
+ code.name,
222
+ code.models,
223
+ ]),
224
+ ]
225
+ for (const [owner, list] of lists) {
226
+ for (const value of list) {
227
+ const definition =
228
+ typeof value === 'object' && value !== null
229
+ ? modelDefinitions.get(value)
230
+ : undefined
231
+ if (
232
+ definition === undefined ||
233
+ typeof value !== 'object' ||
234
+ value === null
235
+ ) {
236
+ throw new Refused(
237
+ `${whose(owner)} a value that defineModel did not return.`,
238
+ )
239
+ }
240
+ const { name } = definition
241
+ if (owners.has(name)) {
242
+ const other = owners.get(name)
243
+ throw new Refused(
244
+ owner === other
245
+ ? `${whose(owner)} two models named ${name}.`
246
+ : `${whose(owner)} a model named ${name}, as ${whose(other)}; name a plugin's models after it.`,
247
+ )
248
+ }
249
+ definitions.set(name, definition)
250
+ names.set(value, name)
251
+ owners.set(name, owner)
252
+ }
253
+ }
254
+ return { definitions, names, owners }
255
+ }
256
+
257
+ /** Resolves the application's models, or stops startup with a message. */
258
+ export function resolve(
259
+ values: readonly unknown[],
260
+ plugins: readonly PluginCode[] = [],
261
+ ): Resolved {
262
+ const { definitions, names, owners } = definitionsOf(values, plugins)
263
+ const links: Linked[] = []
264
+ const views: Linked[] = []
265
+ for (const [model, definition] of definitions) {
266
+ for (const [name, field] of definition.fields) {
267
+ if (field.kind === 'scalar' || field.kind === 'virtual') continue
268
+ const value = field.target()
269
+ const target =
270
+ typeof value === 'object' && value !== null
271
+ ? names.get(value)
272
+ : undefined
273
+ if (target === undefined) {
274
+ throw new Refused(`${model}.${name}: its target is not in models.`)
275
+ }
276
+ const owner = owners.get(model)
277
+ if (owner !== owners.get(target)) {
278
+ throw new Refused(
279
+ `${model}.${name}: a plugin's model relates only to models of the same plugin; keep the id as a scalar.`,
280
+ )
281
+ }
282
+ // A view reads the link of another relation (D-151).
283
+ const from = field.kind === 'many' ? field.options['from'] : undefined
284
+ const link = { model, name, field, target }
285
+ ;(typeof from === 'string' ? views : links).push(link)
286
+ }
287
+ }
288
+ const inverses = new Map(links.map((link) => [link, inverseOf(link, links)]))
289
+ for (const [link, inverse] of inverses) {
290
+ if (inverses.get(inverse) !== link) {
291
+ fail(
292
+ `${link.model}.${link.name} and ${inverse.model}.${inverse.name} do not name each other.`,
293
+ )
294
+ }
295
+ }
296
+ const linkOf = (list: readonly Linked[], model: string, name: string) =>
297
+ list.find((each) => each.model === model && each.name === name)
298
+ const relationOf = (model: string, name: string): FieldInfo | undefined => {
299
+ const link = linkOf(links, model, name)
300
+ const inverse = link && inverses.get(link)
301
+ return link && inverse ? relationField(link, inverse) : undefined
302
+ }
303
+ const fieldOf = (model: string, name: string, field: FieldDefinition) => {
304
+ const view = linkOf(views, model, name)
305
+ if (view === undefined) {
306
+ return relationOf(model, name) ?? scalarField(name, field)
307
+ }
308
+ const from = String(field.options['from'])
309
+ return viewField(
310
+ { model, name, target: view.target, options: field.options },
311
+ from,
312
+ linkOf(views, model, from) ? 'view' : relationOf(model, from),
313
+ )
314
+ }
315
+ const models = new Map<string, ModelInfo>()
316
+ const byValue = new Map<object, ModelInfo>()
317
+ const schemaModels: SchemaModel[] = []
318
+ for (const [model, definition] of definitions) {
319
+ const fields = new Map<string, FieldInfo>()
320
+ for (const [name, field] of definition.fields) {
321
+ // A virtual is no field of the adapter's (D-151).
322
+ if (field.kind === 'virtual') continue
323
+ const plugin = field.kind === 'scalar' ? field.codec?.plugin : undefined
324
+ if (plugin !== undefined && !plugins.some((p) => p.name === plugin)) {
325
+ fail(
326
+ `${model}.${name} is a ${plugin} field, and ${plugin} is not in plugins.`,
327
+ )
328
+ }
329
+ fields.set(name, fieldOf(model, name, field))
330
+ }
331
+ checkIndexes(model, fields, definition.indexes)
332
+ const uniques: (readonly string[])[] = [['id']]
333
+ for (const field of fields.values()) {
334
+ if (field.kind === 'scalar' && field.unique) uniques.push([field.name])
335
+ }
336
+ for (const index of definition.indexes) {
337
+ if (index.unique === true) uniques.push(index.fields)
338
+ }
339
+ const info: ModelInfo = {
340
+ name: model,
341
+ definition,
342
+ fields,
343
+ uniques,
344
+ plugins: installedUses(definition, plugins),
345
+ owner: owners.get(model),
346
+ virtuals: virtualsOf(model, definition, names),
347
+ }
348
+ checkSearch(info)
349
+ checkRelations(info)
350
+ models.set(model, info)
351
+ schemaModels.push({
352
+ name: model,
353
+ ...(definition.dbName === undefined ? {} : { dbName: definition.dbName }),
354
+ fields: [...fields.values()].map((field) => schemaField(info, field)),
355
+ indexes: definition.indexes.map((index) => ({
356
+ fields: index.fields,
357
+ unique: index.unique === true,
358
+ contains: index.contains === true,
359
+ })),
360
+ })
361
+ }
362
+ for (const [value, name] of names) {
363
+ const info = models.get(name)
364
+ if (info !== undefined) byValue.set(value, info)
365
+ }
366
+ return { schema: { models: schemaModels }, models, byValue }
367
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,123 @@
1
+ // The filters one query reads through (D-71, D-86, D-88): the caller's read
2
+ // rules and the `beforeFind` of each model the query reaches, run once per
3
+ // operation (D-124).
4
+ import type { Access } from './access.ts'
5
+ import { and } from './access.ts'
6
+ import type { Filter } from './adapter.ts'
7
+ import { beforeFind } from './hooks.ts'
8
+ import type { FindOperation } from './model.ts'
9
+ import type { Caller } from './rules.ts'
10
+ import { Relations } from './relations.ts'
11
+ import type { FieldInfo, ModelInfo } from './schema.ts'
12
+ import type { Models, Scope, Verb } from './query-where.ts'
13
+ import { modelOf } from './query-where.ts'
14
+
15
+ /**
16
+ * `readable` is synchronous, as the planning that calls it, but `beforeFind`
17
+ * may not be: a model read before its hook ran is recorded, and `ready` runs
18
+ * the hooks so the query can be planned again with their filters.
19
+ */
20
+ export class QueryScope implements Scope {
21
+ /** By `operation name`, as `read Post`. */
22
+ private readonly narrowed = new Map<string, Filter | undefined>()
23
+ private readonly wanted = new Map<
24
+ string,
25
+ { readonly name: string; readonly operation: FindOperation }
26
+ >()
27
+ readonly models: Models
28
+ readonly caller: Caller
29
+ /** `undefined` for Master, which skips access rules (D-48). */
30
+ readonly access: Access | undefined
31
+ private readonly relations: Relations
32
+
33
+ constructor(models: Models, caller: Caller, access: Access | undefined) {
34
+ this.models = models
35
+ this.caller = caller
36
+ this.access = access
37
+ this.relations = access?.relations ?? new Relations(models, caller)
38
+ }
39
+
40
+ check(model: ModelInfo, field: string, path: string, verb: Verb): void {
41
+ this.access?.check(model, field, path, verb)
42
+ }
43
+
44
+ ruled(model: ModelInfo, field: string): boolean {
45
+ return this.access?.ruled(model, field) ?? false
46
+ }
47
+
48
+ /**
49
+ * The rows of `name` the query reads, or the write `operation` targets:
50
+ * its read rule and `beforeFind`.
51
+ */
52
+ readable(
53
+ name: string,
54
+ operation: FindOperation = 'read',
55
+ ): Filter | undefined {
56
+ return and(this.narrowing(name, operation), this.access?.readable(name))
57
+ }
58
+
59
+ related(
60
+ model: ModelInfo,
61
+ field: Extract<FieldInfo, { readonly kind: 'many' }>,
62
+ ): Filter | undefined {
63
+ return and(this.relations.filter(model, field), this.readable(field.target))
64
+ }
65
+
66
+ /**
67
+ * Runs the `beforeFind` hooks of the models read since the last call;
68
+ * whether one of them narrows, so the query must be planned again.
69
+ */
70
+ async ready(): Promise<boolean> {
71
+ let narrowed = false
72
+ for (const [key, { name, operation }] of this.wanted) {
73
+ this.wanted.delete(key)
74
+ const model = modelOf(this.models, name)
75
+ const filter = await beforeFind(
76
+ this.models,
77
+ model,
78
+ this.caller,
79
+ operation,
80
+ this.relations,
81
+ )
82
+ this.narrowed.set(key, filter)
83
+ if (filter !== undefined) narrowed = true
84
+ }
85
+ return narrowed
86
+ }
87
+
88
+ /**
89
+ * The rows of `name` a write reaches; `trusted` for a value a hook or a
90
+ * default wrote, which skips access rules but not `beforeFind` (D-67).
91
+ */
92
+ async filter(
93
+ name: string,
94
+ trusted: boolean,
95
+ operation: FindOperation = 'read',
96
+ ): Promise<Filter | undefined> {
97
+ this.narrowing(name, operation)
98
+ await this.ready()
99
+ return trusted
100
+ ? this.narrowing(name, operation)
101
+ : this.readable(name, operation)
102
+ }
103
+
104
+ private narrowing(
105
+ name: string,
106
+ operation: FindOperation,
107
+ ): Filter | undefined {
108
+ const key = `${operation} ${name}`
109
+ if (this.narrowed.has(key)) return this.narrowed.get(key)
110
+ this.wanted.set(key, { name, operation })
111
+ return undefined
112
+ }
113
+ }
114
+
115
+ /** Plans a query, then again once the `beforeFind` hooks it reached ran. */
116
+ export async function planned<T>(
117
+ scope: QueryScope,
118
+ build: () => T,
119
+ ): Promise<T> {
120
+ let built = build()
121
+ while (await scope.ready()) built = build()
122
+ return built
123
+ }
package/src/scratch.ts ADDED
@@ -0,0 +1,28 @@
1
+ // A test file's own tables on a database that keeps them (D-137): the
2
+ // adapter says how it gives them, and `testApplication` asks for them.
3
+ import type { Adapter } from './adapter.ts'
4
+
5
+ /**
6
+ * The tables of one test file: until `drop`, each start of the adapter
7
+ * finds them empty, and no other table is read or written.
8
+ */
9
+ export type Scratch = {
10
+ /** Drops them; the adapter's next start is on its own tables again. */
11
+ readonly drop: () => Promise<void>
12
+ }
13
+
14
+ const scratches = new WeakMap<Adapter, () => Scratch>()
15
+
16
+ /** Remembers how `adapter` gives a test file its own tables. */
17
+ export function scratchable<T extends Adapter>(
18
+ adapter: T,
19
+ scratch: () => Scratch,
20
+ ): T {
21
+ scratches.set(adapter, scratch)
22
+ return adapter
23
+ }
24
+
25
+ /** How `adapter` gives a test file its own tables, if it can. */
26
+ export function scratchOf(adapter: Adapter): (() => Scratch) | undefined {
27
+ return scratches.get(adapter)
28
+ }
package/src/serve.ts ADDED
@@ -0,0 +1,5 @@
1
+ // What `ultimabase dev` runs under `node --watch` (D-105): the application
2
+ // of the config file it is given.
3
+ import { serve } from './cli-main.ts'
4
+
5
+ process.exitCode = await serve(process.argv[2])
package/src/symbols.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Keys that carry types only. They never exist at runtime and stay out of
2
+ // autocompletion on models and fields.
3
+ export declare const modelFields: unique symbol
4
+ export declare const fieldType: unique symbol
5
+ export declare const declaredRecord: unique symbol
6
+ export declare const pluginTypes: unique symbol
7
+ export declare const readFilter: unique symbol
8
+ export declare const typeFields: unique symbol
9
+ export declare const nullableOf: unique symbol
10
+ /** Marks a calendar day's options, a string compared in order (D-150). */
11
+ export declare const calendarDay: unique symbol