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.
- package/CHANGELOG.md +7 -0
- package/README.md +4 -2
- package/dist/access.d.ts +62 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +258 -0
- package/dist/adapter-errors.d.ts +43 -0
- package/dist/adapter-errors.d.ts.map +1 -0
- package/dist/adapter-errors.js +72 -0
- package/dist/adapter-json.d.ts +20 -0
- package/dist/adapter-json.d.ts.map +1 -0
- package/dist/adapter-json.js +2 -0
- package/dist/adapter-migrations.d.ts +90 -0
- package/dist/adapter-migrations.d.ts.map +1 -0
- package/dist/adapter-migrations.js +1 -0
- package/dist/adapter.d.ts +333 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +5 -0
- package/dist/aggregate.d.ts +31 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +99 -0
- package/dist/app-internals.d.ts +10 -0
- package/dist/app-internals.d.ts.map +1 -0
- package/dist/app-internals.js +12 -0
- package/dist/app-options.d.ts +22 -0
- package/dist/app-options.d.ts.map +1 -0
- package/dist/app-options.js +3 -0
- package/dist/cascade.d.ts +47 -0
- package/dist/cascade.d.ts.map +1 -0
- package/dist/cascade.js +209 -0
- package/dist/cli-config.d.ts +6 -0
- package/dist/cli-config.d.ts.map +1 -0
- package/dist/cli-config.js +45 -0
- package/dist/cli-deploy.d.ts +14 -0
- package/dist/cli-deploy.d.ts.map +1 -0
- package/dist/cli-deploy.js +193 -0
- package/dist/cli-main.d.ts +16 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +151 -0
- package/dist/cli-migrate.d.ts +20 -0
- package/dist/cli-migrate.d.ts.map +1 -0
- package/dist/cli-migrate.js +105 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/complete.d.ts +13 -0
- package/dist/complete.d.ts.map +1 -0
- package/dist/complete.js +60 -0
- package/dist/console-logger.d.ts +3 -0
- package/dist/console-logger.d.ts.map +1 -0
- package/dist/console-logger.js +62 -0
- package/dist/create-many.d.ts +7 -0
- package/dist/create-many.d.ts.map +1 -0
- package/dist/create-many.js +88 -0
- package/dist/decimal.d.ts +23 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +50 -0
- package/dist/define-app.d.ts +59 -0
- package/dist/define-app.d.ts.map +1 -0
- package/dist/define-app.js +5 -0
- package/dist/define-enum.d.ts +6 -0
- package/dist/define-enum.d.ts.map +1 -0
- package/dist/define-enum.js +18 -0
- package/dist/define-function.d.ts +25 -0
- package/dist/define-function.d.ts.map +1 -0
- package/dist/define-function.js +111 -0
- package/dist/define-model.d.ts +48 -0
- package/dist/define-model.d.ts.map +1 -0
- package/dist/define-model.js +85 -0
- package/dist/definition.d.ts +179 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +165 -0
- package/dist/delete-many.d.ts +13 -0
- package/dist/delete-many.d.ts.map +1 -0
- package/dist/delete-many.js +58 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +174 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +43 -0
- package/dist/exact.d.ts +20 -0
- package/dist/exact.d.ts.map +1 -0
- package/dist/exact.js +1 -0
- package/dist/feed.d.ts +6 -0
- package/dist/feed.d.ts.map +1 -0
- package/dist/feed.js +33 -0
- package/dist/field-codec.d.ts +11 -0
- package/dist/field-codec.d.ts.map +1 -0
- package/dist/field-codec.js +48 -0
- package/dist/field-rules.d.ts +57 -0
- package/dist/field-rules.d.ts.map +1 -0
- package/dist/field-rules.js +1 -0
- package/dist/field-type.d.ts +51 -0
- package/dist/field-type.d.ts.map +1 -0
- package/dist/field-type.js +106 -0
- package/dist/fields.d.ts +295 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +1 -0
- package/dist/function.d.ts +126 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +1 -0
- package/dist/functions.d.ts +18 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +99 -0
- package/dist/group-by.d.ts +10 -0
- package/dist/group-by.d.ts.map +1 -0
- package/dist/group-by.js +145 -0
- package/dist/group-having.d.ts +8 -0
- package/dist/group-having.d.ts.map +1 -0
- package/dist/group-having.js +97 -0
- package/dist/hooks.d.ts +41 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +149 -0
- package/dist/hosts.d.ts +3 -0
- package/dist/hosts.d.ts.map +1 -0
- package/dist/hosts.js +8 -0
- package/dist/identify.d.ts +15 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/identify.js +71 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/internal.d.ts +28 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +39 -0
- package/dist/lifecycle.d.ts +21 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +149 -0
- package/dist/list-update.d.ts +12 -0
- package/dist/list-update.d.ts.map +1 -0
- package/dist/list-update.js +52 -0
- package/dist/live.d.ts +37 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +139 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +60 -0
- package/dist/memory-aggregate.d.ts +7 -0
- package/dist/memory-aggregate.d.ts.map +1 -0
- package/dist/memory-aggregate.js +107 -0
- package/dist/memory-conditions.d.ts +8 -0
- package/dist/memory-conditions.d.ts.map +1 -0
- package/dist/memory-conditions.js +82 -0
- package/dist/memory-draft.d.ts +34 -0
- package/dist/memory-draft.d.ts.map +1 -0
- package/dist/memory-draft.js +255 -0
- package/dist/memory-json.d.ts +6 -0
- package/dist/memory-json.d.ts.map +1 -0
- package/dist/memory-json.js +86 -0
- package/dist/memory-read.d.ts +45 -0
- package/dist/memory-read.d.ts.map +1 -0
- package/dist/memory-read.js +244 -0
- package/dist/memory-search.d.ts +16 -0
- package/dist/memory-search.d.ts.map +1 -0
- package/dist/memory-search.js +110 -0
- package/dist/memory-values.d.ts +23 -0
- package/dist/memory-values.d.ts.map +1 -0
- package/dist/memory-values.js +82 -0
- package/dist/memory.d.ts +4 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +157 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +131 -0
- package/dist/model.d.ts +239 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1 -0
- package/dist/nested-write.d.ts +64 -0
- package/dist/nested-write.d.ts.map +1 -0
- package/dist/nested-write.js +274 -0
- package/dist/number-update.d.ts +18 -0
- package/dist/number-update.d.ts.map +1 -0
- package/dist/number-update.js +96 -0
- package/dist/plugin-app.d.ts +48 -0
- package/dist/plugin-app.d.ts.map +1 -0
- package/dist/plugin-app.js +230 -0
- package/dist/plugin-code.d.ts +17 -0
- package/dist/plugin-code.d.ts.map +1 -0
- package/dist/plugin-code.js +125 -0
- package/dist/plugin-use.d.ts +22 -0
- package/dist/plugin-use.d.ts.map +1 -0
- package/dist/plugin-use.js +161 -0
- package/dist/plugin.d.ts +357 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +17 -0
- package/dist/query-aggregate.d.ts +82 -0
- package/dist/query-aggregate.d.ts.map +1 -0
- package/dist/query-aggregate.js +1 -0
- package/dist/query-args.d.ts +41 -0
- package/dist/query-args.d.ts.map +1 -0
- package/dist/query-args.js +263 -0
- package/dist/query-bulk.d.ts +35 -0
- package/dist/query-bulk.d.ts.map +1 -0
- package/dist/query-bulk.js +1 -0
- package/dist/query-conditions.d.ts +9 -0
- package/dist/query-conditions.d.ts.map +1 -0
- package/dist/query-conditions.js +46 -0
- package/dist/query-context.d.ts +32 -0
- package/dist/query-context.d.ts.map +1 -0
- package/dist/query-context.js +55 -0
- package/dist/query-filter.d.ts +46 -0
- package/dist/query-filter.d.ts.map +1 -0
- package/dist/query-filter.js +1 -0
- package/dist/query-group.d.ts +82 -0
- package/dist/query-group.d.ts.map +1 -0
- package/dist/query-group.js +1 -0
- package/dist/query-json.d.ts +9 -0
- package/dist/query-json.d.ts.map +1 -0
- package/dist/query-json.js +66 -0
- package/dist/query-order.d.ts +15 -0
- package/dist/query-order.d.ts.map +1 -0
- package/dist/query-order.js +115 -0
- package/dist/query-project.d.ts +15 -0
- package/dist/query-project.d.ts.map +1 -0
- package/dist/query-project.js +45 -0
- package/dist/query-search.d.ts +24 -0
- package/dist/query-search.d.ts.map +1 -0
- package/dist/query-search.js +2 -0
- package/dist/query-values.d.ts +28 -0
- package/dist/query-values.d.ts.map +1 -0
- package/dist/query-values.js +91 -0
- package/dist/query-where.d.ts +46 -0
- package/dist/query-where.d.ts.map +1 -0
- package/dist/query-where.js +200 -0
- package/dist/query-write.d.ts +14 -0
- package/dist/query-write.d.ts.map +1 -0
- package/dist/query-write.js +260 -0
- package/dist/query.d.ts +254 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1 -0
- package/dist/record.d.ts +257 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1 -0
- package/dist/recorder.d.ts +32 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +152 -0
- package/dist/relation-write.d.ts +41 -0
- package/dist/relation-write.d.ts.map +1 -0
- package/dist/relation-write.js +112 -0
- package/dist/relations.d.ts +42 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +119 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +71 -0
- package/dist/running.d.ts +29 -0
- package/dist/running.d.ts.map +1 -0
- package/dist/running.js +39 -0
- package/dist/schema-indexes.d.ts +11 -0
- package/dist/schema-indexes.d.ts.map +1 -0
- package/dist/schema-indexes.js +59 -0
- package/dist/schema-virtuals.d.ts +19 -0
- package/dist/schema-virtuals.d.ts.map +1 -0
- package/dist/schema-virtuals.js +100 -0
- package/dist/schema.d.ts +35 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +267 -0
- package/dist/scope.d.ts +46 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +82 -0
- package/dist/scratch.d.ts +14 -0
- package/dist/scratch.d.ts.map +1 -0
- package/dist/scratch.js +10 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +4 -0
- package/dist/symbols.d.ts +10 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +1 -0
- package/dist/transaction.d.ts +28 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +156 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/update-many.d.ts +7 -0
- package/dist/update-many.d.ts.map +1 -0
- package/dist/update-many.js +182 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +48 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +72 -0
- package/dist/virtual-read.d.ts +9 -0
- package/dist/virtual-read.d.ts.map +1 -0
- package/dist/virtual-read.js +145 -0
- package/dist/virtual.d.ts +142 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1 -0
- package/dist/write-context.d.ts +46 -0
- package/dist/write-context.d.ts.map +1 -0
- package/dist/write-context.js +102 -0
- package/dist/write-data.d.ts +26 -0
- package/dist/write-data.d.ts.map +1 -0
- package/dist/write-data.js +144 -0
- package/dist/write-plan.d.ts +107 -0
- package/dist/write-plan.d.ts.map +1 -0
- package/dist/write-plan.js +170 -0
- package/dist/write-target.d.ts +17 -0
- package/dist/write-target.d.ts.map +1 -0
- package/dist/write-target.js +59 -0
- package/dist/writes.d.ts +23 -0
- package/dist/writes.d.ts.map +1 -0
- package/dist/writes.js +246 -0
- package/dist/writing.d.ts +10 -0
- package/dist/writing.d.ts.map +1 -0
- package/dist/writing.js +66 -0
- package/package.json +42 -3
- package/src/access.ts +329 -0
- package/src/adapter-errors.ts +89 -0
- package/src/adapter-json.ts +21 -0
- package/src/adapter-migrations.ts +98 -0
- package/src/adapter.ts +363 -0
- package/src/aggregate.ts +136 -0
- package/src/app-internals.ts +26 -0
- package/src/app-options.ts +24 -0
- package/src/cascade.ts +297 -0
- package/src/cli-config.ts +45 -0
- package/src/cli-deploy.ts +235 -0
- package/src/cli-main.ts +165 -0
- package/src/cli-migrate.ts +143 -0
- package/src/cli.ts +5 -0
- package/src/complete.ts +73 -0
- package/src/console-logger.ts +87 -0
- package/src/create-many.ts +126 -0
- package/src/decimal.ts +74 -0
- package/src/define-app.ts +108 -0
- package/src/define-enum.ts +26 -0
- package/src/define-function.ts +188 -0
- package/src/define-model.ts +220 -0
- package/src/definition.ts +352 -0
- package/src/delete-many.ts +76 -0
- package/src/engine.ts +281 -0
- package/src/errors.ts +97 -0
- package/src/exact.ts +49 -0
- package/src/feed.ts +39 -0
- package/src/field-codec.ts +72 -0
- package/src/field-rules.ts +77 -0
- package/src/field-type.ts +219 -0
- package/src/fields.ts +383 -0
- package/src/function.ts +143 -0
- package/src/functions.ts +154 -0
- package/src/group-by.ts +186 -0
- package/src/group-having.ts +143 -0
- package/src/hooks.ts +215 -0
- package/src/hosts.ts +11 -0
- package/src/identify.ts +95 -0
- package/src/index.ts +125 -0
- package/src/internal.ts +68 -0
- package/src/lifecycle.ts +214 -0
- package/src/list-update.ts +71 -0
- package/src/live.ts +199 -0
- package/src/manifest.ts +71 -0
- package/src/memory-aggregate.ts +162 -0
- package/src/memory-conditions.ts +88 -0
- package/src/memory-draft.ts +319 -0
- package/src/memory-json.ts +91 -0
- package/src/memory-read.ts +311 -0
- package/src/memory-search.ts +149 -0
- package/src/memory-values.ts +106 -0
- package/src/memory.ts +173 -0
- package/src/migrate.ts +152 -0
- package/src/model.ts +312 -0
- package/src/nested-write.ts +390 -0
- package/src/number-update.ts +142 -0
- package/src/plugin-app.ts +308 -0
- package/src/plugin-code.ts +158 -0
- package/src/plugin-use.ts +217 -0
- package/src/plugin.ts +400 -0
- package/src/query-aggregate.ts +96 -0
- package/src/query-args.ts +375 -0
- package/src/query-bulk.ts +53 -0
- package/src/query-conditions.ts +53 -0
- package/src/query-context.ts +104 -0
- package/src/query-filter.ts +54 -0
- package/src/query-group.ts +137 -0
- package/src/query-json.ts +90 -0
- package/src/query-order.ts +158 -0
- package/src/query-project.ts +73 -0
- package/src/query-search.ts +31 -0
- package/src/query-values.ts +131 -0
- package/src/query-where.ts +318 -0
- package/src/query-write.ts +360 -0
- package/src/query.ts +400 -0
- package/src/record.ts +379 -0
- package/src/recorder.ts +212 -0
- package/src/relation-write.ts +176 -0
- package/src/relations.ts +165 -0
- package/src/rules.ts +188 -0
- package/src/running.ts +62 -0
- package/src/schema-indexes.ts +88 -0
- package/src/schema-virtuals.ts +134 -0
- package/src/schema.ts +367 -0
- package/src/scope.ts +123 -0
- package/src/scratch.ts +28 -0
- package/src/serve.ts +5 -0
- package/src/symbols.ts +11 -0
- package/src/transaction.ts +211 -0
- package/src/types.ts +79 -0
- package/src/update-many.ts +268 -0
- package/src/uuid.ts +51 -0
- package/src/validation.ts +107 -0
- package/src/virtual-read.ts +198 -0
- package/src/virtual.ts +187 -0
- package/src/write-context.ts +170 -0
- package/src/write-data.ts +188 -0
- package/src/write-plan.ts +273 -0
- package/src/write-target.ts +92 -0
- package/src/writes.ts +361 -0
- 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
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
|