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,176 @@
|
|
|
1
|
+
// A relation write's operations (D-20, D-128, D-138), their shape checked:
|
|
2
|
+
// `connect`, `connectOrCreate` and `create`, and in an update `delete`,
|
|
3
|
+
// `disconnect`, `update` and, on a `many`, `set`.
|
|
4
|
+
import { InvalidQuery } from './errors.ts'
|
|
5
|
+
import { clean, entries, isObject, unknownKey } from './query-where.ts'
|
|
6
|
+
import type { Draft } from './write-data.ts'
|
|
7
|
+
|
|
8
|
+
/** `{ id }`, exactly. */
|
|
9
|
+
function ref(value: unknown, path: string): string {
|
|
10
|
+
const given = entries(value, path)
|
|
11
|
+
const [first] = given
|
|
12
|
+
if (
|
|
13
|
+
given.length !== 1 ||
|
|
14
|
+
first?.[0] !== 'id' ||
|
|
15
|
+
typeof first[1] !== 'string' ||
|
|
16
|
+
!clean(first[1])
|
|
17
|
+
) {
|
|
18
|
+
throw new InvalidQuery(path, `${path} must be { id }.`)
|
|
19
|
+
}
|
|
20
|
+
return first[1]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The operations a relation write takes (D-128, D-138). */
|
|
24
|
+
function allowed(kind: 'one' | 'many', creating: boolean): readonly string[] {
|
|
25
|
+
if (creating) return ['connect', 'connectOrCreate', 'create']
|
|
26
|
+
const changing = kind === 'one' ? ['update'] : ['set', 'update']
|
|
27
|
+
return [
|
|
28
|
+
'connect',
|
|
29
|
+
'connectOrCreate',
|
|
30
|
+
'create',
|
|
31
|
+
'delete',
|
|
32
|
+
'disconnect',
|
|
33
|
+
...changing,
|
|
34
|
+
].toSorted()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function operations(
|
|
38
|
+
value: unknown,
|
|
39
|
+
path: string,
|
|
40
|
+
model: string,
|
|
41
|
+
kind: 'one' | 'many',
|
|
42
|
+
creating: boolean,
|
|
43
|
+
): Map<string, unknown> {
|
|
44
|
+
if (value === null) {
|
|
45
|
+
throw new InvalidQuery(path, `${path} cannot be null; use disconnect.`)
|
|
46
|
+
}
|
|
47
|
+
const given = new Map(entries(value, path))
|
|
48
|
+
for (const key of given.keys()) {
|
|
49
|
+
if (!allowed(kind, creating).includes(key))
|
|
50
|
+
throw unknownKey(`${path}.${key}`, model)
|
|
51
|
+
}
|
|
52
|
+
return given
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Data a nested `create` or `update` writes (D-128, D-138). */
|
|
56
|
+
function made(value: unknown, path: string): Draft {
|
|
57
|
+
if (!isObject(value)) {
|
|
58
|
+
throw new InvalidQuery(path, `${path} must be an object.`)
|
|
59
|
+
}
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** An object of exactly `keys`. */
|
|
64
|
+
function exactly(
|
|
65
|
+
value: unknown,
|
|
66
|
+
path: string,
|
|
67
|
+
keys: readonly [string, string],
|
|
68
|
+
): Map<string, unknown> {
|
|
69
|
+
const given = new Map(entries(value, path))
|
|
70
|
+
if (given.size !== 2 || !keys.every((key) => given.has(key))) {
|
|
71
|
+
throw new InvalidQuery(path, `${path} must be { ${keys.join(', ')} }.`)
|
|
72
|
+
}
|
|
73
|
+
return given
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A `connectOrCreate`: the unique target it looks for, the row it creates. */
|
|
77
|
+
export type OrCreate = { readonly where: unknown; readonly create: Draft }
|
|
78
|
+
|
|
79
|
+
function orCreate(value: unknown, path: string): OrCreate {
|
|
80
|
+
const given = exactly(value, path, ['where', 'create'])
|
|
81
|
+
return {
|
|
82
|
+
where: given.get('where'),
|
|
83
|
+
create: made(given.get('create'), `${path}.create`),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A `many` relation's `update`: a unique target, and its data. */
|
|
88
|
+
export type Updating = { readonly where: unknown; readonly data: Draft }
|
|
89
|
+
|
|
90
|
+
function updating(value: unknown, path: string): Updating {
|
|
91
|
+
const given = exactly(value, path, ['where', 'data'])
|
|
92
|
+
return {
|
|
93
|
+
where: given.get('where'),
|
|
94
|
+
data: made(given.get('data'), `${path}.data`),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function list(value: unknown, path: string): readonly unknown[] {
|
|
99
|
+
if (!Array.isArray(value))
|
|
100
|
+
throw new InvalidQuery(path, `${path} must be a list.`)
|
|
101
|
+
return value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A `one` relation write. */
|
|
105
|
+
export type OneWrite =
|
|
106
|
+
| { readonly kind: 'connect'; readonly id: string }
|
|
107
|
+
| { readonly kind: 'create'; readonly data: Draft }
|
|
108
|
+
| ({ readonly kind: 'connectOrCreate' } & OrCreate)
|
|
109
|
+
| { readonly kind: 'update'; readonly data: Draft }
|
|
110
|
+
| { readonly kind: 'delete' }
|
|
111
|
+
| { readonly kind: 'disconnect' }
|
|
112
|
+
|
|
113
|
+
export function oneWrite(
|
|
114
|
+
value: unknown,
|
|
115
|
+
path: string,
|
|
116
|
+
model: string,
|
|
117
|
+
creating: boolean,
|
|
118
|
+
): OneWrite {
|
|
119
|
+
const given = operations(value, path, model, 'one', creating)
|
|
120
|
+
const [entry] = given
|
|
121
|
+
if (entry === undefined || given.size !== 1) {
|
|
122
|
+
throw new InvalidQuery(
|
|
123
|
+
path,
|
|
124
|
+
`${path} takes one of ${allowed('one', creating).join(', ')}.`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const [key, operand] = entry
|
|
128
|
+
const at = `${path}.${key}`
|
|
129
|
+
if (key === 'connect') return { kind: key, id: ref(operand, at) }
|
|
130
|
+
if (key === 'create' || key === 'update') {
|
|
131
|
+
return { kind: key, data: made(operand, at) }
|
|
132
|
+
}
|
|
133
|
+
if (key === 'connectOrCreate') return { kind: key, ...orCreate(operand, at) }
|
|
134
|
+
if (operand !== true) throw new InvalidQuery(at, `${at} must be true.`)
|
|
135
|
+
return { kind: key === 'delete' ? 'delete' : 'disconnect' }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** A `many` relation write, each operation's list. */
|
|
139
|
+
export type ManyWrite = {
|
|
140
|
+
readonly connect: string[]
|
|
141
|
+
readonly disconnect: string[]
|
|
142
|
+
readonly create: Draft[]
|
|
143
|
+
readonly set: string[] | undefined
|
|
144
|
+
readonly connectOrCreate: OrCreate[]
|
|
145
|
+
readonly update: Updating[]
|
|
146
|
+
readonly delete: unknown[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function manyWrite(
|
|
150
|
+
value: unknown,
|
|
151
|
+
path: string,
|
|
152
|
+
model: string,
|
|
153
|
+
creating: boolean,
|
|
154
|
+
): ManyWrite {
|
|
155
|
+
const given = operations(value, path, model, 'many', creating)
|
|
156
|
+
const each = <T>(key: string, read: (item: unknown, at: string) => T): T[] =>
|
|
157
|
+
list(given.get(key) ?? [], `${path}.${key}`).map((item, index) =>
|
|
158
|
+
read(item, `${path}.${key}.${index}`),
|
|
159
|
+
)
|
|
160
|
+
if (given.has('set') && (given.has('connect') || given.has('disconnect'))) {
|
|
161
|
+
throw new InvalidQuery(
|
|
162
|
+
`${path}.set`,
|
|
163
|
+
`${path}.set cannot be used with connect or disconnect.`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
// A row disconnected and connected again stays linked (D-101).
|
|
168
|
+
connect: each('connect', ref),
|
|
169
|
+
disconnect: each('disconnect', ref),
|
|
170
|
+
create: each('create', made),
|
|
171
|
+
set: given.has('set') ? each('set', ref) : undefined,
|
|
172
|
+
connectOrCreate: each('connectOrCreate', orCreate),
|
|
173
|
+
update: each('update', updating),
|
|
174
|
+
delete: each('delete', (item) => item),
|
|
175
|
+
}
|
|
176
|
+
}
|
package/src/relations.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The filter of a `many` relation, from its model's `.with({ relations })`
|
|
2
|
+
// (D-151): every read of the relation sees only the rows it keeps, Master's
|
|
3
|
+
// too, and a filtered relation takes no `set`.
|
|
4
|
+
import type { Filter } from './adapter.ts'
|
|
5
|
+
import type { FieldOptions } from './definition.ts'
|
|
6
|
+
import { InvalidQuery, Refused } from './errors.ts'
|
|
7
|
+
import type { Models, Scope } from './query-where.ts'
|
|
8
|
+
import { isObject, modelOf, normalizeWhere } from './query-where.ts'
|
|
9
|
+
import type { Caller } from './rules.ts'
|
|
10
|
+
import { filterOf, isReadFilter, readFilter } from './rules.ts'
|
|
11
|
+
import type { FieldInfo, ModelInfo } from './schema.ts'
|
|
12
|
+
|
|
13
|
+
type Many = Extract<FieldInfo, { readonly kind: 'many' }>
|
|
14
|
+
|
|
15
|
+
function relationsOf(model: ModelInfo): { readonly [field: string]: unknown } {
|
|
16
|
+
const { rules } = model.definition
|
|
17
|
+
const relations = isObject(rules) ? rules['relations'] : undefined
|
|
18
|
+
return isObject(relations) ? relations : {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stops startup on a filter that names no `many` of the model, or on a view
|
|
23
|
+
* without one (D-151).
|
|
24
|
+
*/
|
|
25
|
+
export function checkRelations(model: ModelInfo): void {
|
|
26
|
+
for (const [name, rule] of Object.entries(relationsOf(model))) {
|
|
27
|
+
const where = `${model.name}.with.relations.${name}`
|
|
28
|
+
if (model.fields.get(name)?.kind !== 'many') {
|
|
29
|
+
throw new Refused(`${where}: ${name} is not a many relation.`)
|
|
30
|
+
}
|
|
31
|
+
if (!isObject(rule) || typeof rule['where'] !== 'function') {
|
|
32
|
+
throw new Refused(`${where} takes a where function.`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const field of model.fields.values()) {
|
|
36
|
+
if (field.kind === 'many' && field.from !== undefined) {
|
|
37
|
+
if (!isFiltered(model, field.name)) {
|
|
38
|
+
throw new Refused(
|
|
39
|
+
`${model.name}.${field.name} is a view; filter it in .with({ relations }).`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A view (D-151): `source`, the `many` its `from` names, under its own name
|
|
48
|
+
* and options. Stops startup unless `from` names a `many` of the model to the
|
|
49
|
+
* same target that is no view.
|
|
50
|
+
*/
|
|
51
|
+
export function viewField(
|
|
52
|
+
view: {
|
|
53
|
+
readonly model: string
|
|
54
|
+
readonly name: string
|
|
55
|
+
readonly target: string
|
|
56
|
+
readonly options: FieldOptions
|
|
57
|
+
},
|
|
58
|
+
from: string,
|
|
59
|
+
source: FieldInfo | 'view' | undefined,
|
|
60
|
+
): FieldInfo {
|
|
61
|
+
const { model, name, target, options } = view
|
|
62
|
+
const at = `${model}.${name}`
|
|
63
|
+
if (options['inverse'] !== undefined) {
|
|
64
|
+
throw new Refused(`${at} is a view and takes no inverse.`)
|
|
65
|
+
}
|
|
66
|
+
if (source === 'view') {
|
|
67
|
+
throw new Refused(`${at}: from '${from}' is a view; name its relation.`)
|
|
68
|
+
}
|
|
69
|
+
if (source?.kind !== 'many') {
|
|
70
|
+
throw new Refused(`${at}: from '${from}' is not a many relation.`)
|
|
71
|
+
}
|
|
72
|
+
if (source.target !== target) {
|
|
73
|
+
throw new Refused(
|
|
74
|
+
`${at}: from '${from}' relates to ${source.target}, not ${target}.`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return { ...source, name, from, options }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether `.with({ relations })` filters `field` of `model`. */
|
|
81
|
+
export function isFiltered(model: ModelInfo, field: string): boolean {
|
|
82
|
+
return Object.hasOwn(relationsOf(model), field)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Fails on a `set` of a filtered relation, which would detach hidden rows. */
|
|
86
|
+
export function refuseSet(model: ModelInfo, field: string, path: string): void {
|
|
87
|
+
if (!isFiltered(model, field)) return
|
|
88
|
+
throw new InvalidQuery(
|
|
89
|
+
path,
|
|
90
|
+
`${model.name}.${field} is filtered; use connect and disconnect, not set.`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** No row passes: a filter that returns `false`. */
|
|
95
|
+
const closed: Filter = { OR: [] }
|
|
96
|
+
const pending = Symbol('pending')
|
|
97
|
+
|
|
98
|
+
/** The relation filters of one query, run once each for its caller. */
|
|
99
|
+
export class Relations {
|
|
100
|
+
private readonly filters = new Map<
|
|
101
|
+
string,
|
|
102
|
+
Filter | undefined | typeof pending
|
|
103
|
+
>()
|
|
104
|
+
private readonly models: Models
|
|
105
|
+
private readonly caller: Caller
|
|
106
|
+
|
|
107
|
+
constructor(models: Models, caller: Caller) {
|
|
108
|
+
this.models = models
|
|
109
|
+
this.caller = caller
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The rows of `model.field` its filter keeps; `undefined` for all. */
|
|
113
|
+
filter(model: ModelInfo, field: Many): Filter | undefined {
|
|
114
|
+
const key = `${model.name}.${field.name}`
|
|
115
|
+
const known = this.filters.get(key)
|
|
116
|
+
if (known === pending) {
|
|
117
|
+
throw new TypeError(`${key}: its filter reads the relation itself.`)
|
|
118
|
+
}
|
|
119
|
+
if (this.filters.has(key)) return known
|
|
120
|
+
const rule = relationsOf(model)[field.name]
|
|
121
|
+
this.filters.set(key, pending)
|
|
122
|
+
const filter = isObject(rule)
|
|
123
|
+
? this.run(model, field, rule['where'])
|
|
124
|
+
: undefined
|
|
125
|
+
this.filters.set(key, filter)
|
|
126
|
+
return filter
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private run(
|
|
130
|
+
model: ModelInfo,
|
|
131
|
+
field: Many,
|
|
132
|
+
rule: unknown,
|
|
133
|
+
): Filter | undefined {
|
|
134
|
+
const path = `${model.name}.with.relations.${field.name}.where`
|
|
135
|
+
if (typeof rule !== 'function')
|
|
136
|
+
throw new TypeError(`${path} is a function.`)
|
|
137
|
+
const result: unknown = Reflect.apply(rule, undefined, [
|
|
138
|
+
{ caller: this.caller, where: readFilter },
|
|
139
|
+
])
|
|
140
|
+
if (result === true) return undefined
|
|
141
|
+
if (result === false) return closed
|
|
142
|
+
if (!isReadFilter(result)) {
|
|
143
|
+
throw new TypeError(`${path} returns where({ ... }) or a boolean.`)
|
|
144
|
+
}
|
|
145
|
+
const target = modelOf(this.models, field.target)
|
|
146
|
+
// Trusted server code, as a read rule's own filter (D-86).
|
|
147
|
+
return normalizeWhere(
|
|
148
|
+
this.models,
|
|
149
|
+
target,
|
|
150
|
+
filterOf(result),
|
|
151
|
+
path,
|
|
152
|
+
trusted(this),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The scope of trusted filters: no access rule, relation filters only. */
|
|
158
|
+
export function trusted(relations: Relations): Scope {
|
|
159
|
+
return {
|
|
160
|
+
check: () => undefined,
|
|
161
|
+
ruled: () => false,
|
|
162
|
+
readable: () => undefined,
|
|
163
|
+
related: (model, field) => relations.filter(model, field),
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/rules.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// What every access rule receives, and the kernel's ready-made rules (D-10).
|
|
2
|
+
import type { RecordOf } from './record.ts'
|
|
3
|
+
import type { modelFields } from './symbols.ts'
|
|
4
|
+
|
|
5
|
+
export type Operation = 'read' | 'create' | 'update' | 'delete'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The application declares its user model here, once; without it,
|
|
9
|
+
* `caller.user` is `{ id }` (D-24, D-62).
|
|
10
|
+
*/
|
|
11
|
+
export interface Register {}
|
|
12
|
+
|
|
13
|
+
/** The user record a registry gives `caller.user`. */
|
|
14
|
+
export type UserOf<R> = R extends {
|
|
15
|
+
readonly user: { readonly [modelFields]: infer F }
|
|
16
|
+
}
|
|
17
|
+
? RecordOf<F>
|
|
18
|
+
: { readonly id: string }
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The keys plugins add to a user caller, each plugin declaring its own
|
|
22
|
+
* (D-121): `declare module 'ultimabase' { interface UserCallerKeys { ... } }`.
|
|
23
|
+
*/
|
|
24
|
+
export interface UserCallerKeys {}
|
|
25
|
+
|
|
26
|
+
type Master = {
|
|
27
|
+
readonly kind: 'master'
|
|
28
|
+
readonly master: { readonly source: 'code' | 'key' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Who code says sends an operation, as `as` and a plugin's `identify` give
|
|
33
|
+
* it: a user caller without the keys plugins add (D-121).
|
|
34
|
+
*/
|
|
35
|
+
export type Identity =
|
|
36
|
+
| { readonly kind: 'anonymous' }
|
|
37
|
+
| { readonly kind: 'user'; readonly user: UserOf<Register> }
|
|
38
|
+
| Master
|
|
39
|
+
|
|
40
|
+
/** Who sends an operation, as rules, hooks and functions see it (D-19). */
|
|
41
|
+
export type Caller =
|
|
42
|
+
| { readonly kind: 'anonymous' }
|
|
43
|
+
| ({
|
|
44
|
+
readonly kind: 'user'
|
|
45
|
+
readonly user: UserOf<Register>
|
|
46
|
+
} & UserCallerKeys)
|
|
47
|
+
| Master
|
|
48
|
+
|
|
49
|
+
const masters = new WeakSet<object>()
|
|
50
|
+
|
|
51
|
+
/** A Master caller; the kernel's code and transports only (D-19, D-49). */
|
|
52
|
+
export function createMaster(source: 'code' | 'key'): Master {
|
|
53
|
+
const caller: Master = Object.freeze({
|
|
54
|
+
kind: 'master',
|
|
55
|
+
master: Object.freeze({ source }),
|
|
56
|
+
})
|
|
57
|
+
masters.add(caller)
|
|
58
|
+
return caller
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether a caller is a Master the kernel created. */
|
|
62
|
+
export function isMaster(caller: object): caller is Master {
|
|
63
|
+
return masters.has(caller)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Server code acting with every right; only the kernel creates it. */
|
|
67
|
+
export const master: Master = createMaster('code')
|
|
68
|
+
|
|
69
|
+
/** A rule that reads only the caller and the operation fits any access key. */
|
|
70
|
+
export type AnyRule = (facts: {
|
|
71
|
+
readonly caller: Caller
|
|
72
|
+
readonly operation: Operation
|
|
73
|
+
}) => boolean
|
|
74
|
+
|
|
75
|
+
/** A rule that reads only the caller also fits a function's access (D-27). */
|
|
76
|
+
export type CallerRule = (facts: { readonly caller: Caller }) => boolean
|
|
77
|
+
|
|
78
|
+
type Allow = {
|
|
79
|
+
readonly public: CallerRule
|
|
80
|
+
readonly readOnly: AnyRule
|
|
81
|
+
readonly private: CallerRule
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Open to everyone, open to reads, closed to everyone but Master (D-10). */
|
|
85
|
+
export const allow: Allow = Object.freeze({
|
|
86
|
+
public: () => true,
|
|
87
|
+
readOnly: ({ operation }) => operation === 'read',
|
|
88
|
+
private: () => false,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const filters = new WeakMap<object, unknown>()
|
|
92
|
+
|
|
93
|
+
/** The read filter a read rule's `where` returns (D-10). */
|
|
94
|
+
export function readFilter(where: unknown): object {
|
|
95
|
+
const token = Object.freeze({})
|
|
96
|
+
filters.set(token, where)
|
|
97
|
+
return token
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether a rule's result is a read filter. */
|
|
101
|
+
export function isReadFilter(value: unknown): value is object {
|
|
102
|
+
return typeof value === 'object' && value !== null && filters.has(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The `where` a read filter holds. */
|
|
106
|
+
export function filterOf(token: object): unknown {
|
|
107
|
+
return filters.get(token)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type Results = { readonly decided: boolean; readonly filters: object[] }
|
|
111
|
+
|
|
112
|
+
// A rule returns a boolean, or a filter in a read rule (D-76, D-81).
|
|
113
|
+
function collect(
|
|
114
|
+
rules: readonly Rule[],
|
|
115
|
+
facts: unknown,
|
|
116
|
+
decisive: boolean,
|
|
117
|
+
): Results {
|
|
118
|
+
const found: object[] = []
|
|
119
|
+
for (const rule of rules) {
|
|
120
|
+
const result: unknown = Reflect.apply(rule, undefined, [facts])
|
|
121
|
+
if (result === decisive) return { decided: true, filters: [] }
|
|
122
|
+
if (isReadFilter(result)) found.push(result)
|
|
123
|
+
else if (typeof result !== 'boolean') {
|
|
124
|
+
throw new TypeError('A rule returns a boolean or a read filter.')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { decided: false, filters: found }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function combine(
|
|
131
|
+
rules: readonly Rule[],
|
|
132
|
+
decisive: boolean,
|
|
133
|
+
operator: 'AND' | 'OR',
|
|
134
|
+
): (facts: unknown) => unknown {
|
|
135
|
+
return (facts) => {
|
|
136
|
+
const { decided, filters: found } = collect(rules, facts, decisive)
|
|
137
|
+
if (decided) return decisive
|
|
138
|
+
const [first, ...others] = found
|
|
139
|
+
if (first === undefined) return !decisive
|
|
140
|
+
if (others.length === 0) return first
|
|
141
|
+
return readFilter({ [operator]: found.map(filterOf) })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
type Rule = (facts: never) => unknown
|
|
145
|
+
|
|
146
|
+
/** A rule already typed, such as a `const` holding `signedIn(...)`. */
|
|
147
|
+
type Named = (facts: never) => unknown
|
|
148
|
+
type Both<U> = (U extends unknown ? (u: U) => void : never) extends (
|
|
149
|
+
u: infer I,
|
|
150
|
+
) => void
|
|
151
|
+
? I
|
|
152
|
+
: never
|
|
153
|
+
/** The rule combining named rules: it needs every rule's facts. */
|
|
154
|
+
type Combined<Rules extends readonly Named[]> = (
|
|
155
|
+
facts: Both<Parameters<Rules[number]>[0]>,
|
|
156
|
+
) => ReturnType<Rules[number]>
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* True when one of the rules is (D-10). The facts and the result come from
|
|
160
|
+
* where it is used, never from the rules, so each rule is typed by its place.
|
|
161
|
+
* In a read rule, the filters combine with `OR` (D-81): `true` from any rule
|
|
162
|
+
* lets every row through, `false` adds nothing.
|
|
163
|
+
*/
|
|
164
|
+
export function anyOf<Facts, R = boolean>(
|
|
165
|
+
...rules: readonly NoInfer<(facts: Facts) => R>[]
|
|
166
|
+
): (facts: Facts) => R
|
|
167
|
+
/** Outside any place, the facts are those every rule needs. */
|
|
168
|
+
export function anyOf<const Rules extends readonly Named[]>(
|
|
169
|
+
...rules: Rules
|
|
170
|
+
): Combined<Rules>
|
|
171
|
+
export function anyOf(...rules: readonly Rule[]): (facts: never) => unknown {
|
|
172
|
+
return combine(rules, true, 'OR')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* True when every rule is (D-81), typed as `anyOf`. In a read rule, the
|
|
177
|
+
* filters combine with `AND`: `false` from any rule hides every row, `true`
|
|
178
|
+
* adds nothing.
|
|
179
|
+
*/
|
|
180
|
+
export function allOf<Facts, R = boolean>(
|
|
181
|
+
...rules: readonly NoInfer<(facts: Facts) => R>[]
|
|
182
|
+
): (facts: Facts) => R
|
|
183
|
+
export function allOf<const Rules extends readonly Named[]>(
|
|
184
|
+
...rules: Rules
|
|
185
|
+
): Combined<Rules>
|
|
186
|
+
export function allOf(...rules: readonly Rule[]): (facts: never) => unknown {
|
|
187
|
+
return combine(rules, false, 'AND')
|
|
188
|
+
}
|
package/src/running.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The running application (D-18): its resolved models and functions, its
|
|
2
|
+
// adapter, its logger, its plugins' APIs (D-89) and the operations `stop`
|
|
3
|
+
// waits for (D-91), one per realm.
|
|
4
|
+
import type { Adapter } from './adapter.ts'
|
|
5
|
+
import type { Logger } from './app-options.ts'
|
|
6
|
+
import type { Complete } from './complete.ts'
|
|
7
|
+
import type { FunctionDefinition } from './definition.ts'
|
|
8
|
+
import type { Resolved } from './schema.ts'
|
|
9
|
+
|
|
10
|
+
export type Running = {
|
|
11
|
+
readonly resolved: Resolved
|
|
12
|
+
/** Its functions, by their `defineFunction` value (D-75). */
|
|
13
|
+
readonly functions: ReadonlyMap<object, FunctionDefinition>
|
|
14
|
+
readonly adapter: Adapter
|
|
15
|
+
readonly logger: Logger
|
|
16
|
+
/** Each plugin's API, by its name (D-16). */
|
|
17
|
+
readonly apis: ReadonlyMap<string, unknown>
|
|
18
|
+
readonly work: Work
|
|
19
|
+
/** Completes a user caller with its plugins' keys (D-121). */
|
|
20
|
+
readonly complete: Complete
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The operations running, `*Commit` hooks included, that `stop` waits for. */
|
|
24
|
+
export class Work {
|
|
25
|
+
private count = 0
|
|
26
|
+
private readonly idle: (() => void)[] = []
|
|
27
|
+
|
|
28
|
+
async run<R>(run: () => Promise<R>): Promise<R> {
|
|
29
|
+
this.count += 1
|
|
30
|
+
try {
|
|
31
|
+
return await run()
|
|
32
|
+
} finally {
|
|
33
|
+
this.count -= 1
|
|
34
|
+
if (this.count === 0) for (const done of this.idle.splice(0)) done()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async settled(): Promise<void> {
|
|
39
|
+
if (this.count === 0) return
|
|
40
|
+
await new Promise<void>((done) => {
|
|
41
|
+
this.idle.push(done)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let running: Running | undefined
|
|
47
|
+
|
|
48
|
+
export function register(app: Running): void {
|
|
49
|
+
if (running !== undefined) {
|
|
50
|
+
throw new Error('An application is already running; stop it first.')
|
|
51
|
+
}
|
|
52
|
+
running = app
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function unregister(app: Running): void {
|
|
56
|
+
if (running === app) running = undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The running application, `undefined` before `start`. */
|
|
60
|
+
export function current(): Running | undefined {
|
|
61
|
+
return running
|
|
62
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The indexes of a model, checked against its fields (D-52, D-120, D-141),
|
|
2
|
+
// and its searched fields (D-148).
|
|
3
|
+
import type { SchemaField } from './adapter.ts'
|
|
4
|
+
import type { IndexDefinition } from './definition.ts'
|
|
5
|
+
import type { ModelInfo } from './schema.ts'
|
|
6
|
+
import { readRules } from './access.ts'
|
|
7
|
+
import { Refused } from './errors.ts'
|
|
8
|
+
|
|
9
|
+
/** An index on a list is a GIN index of that list alone (D-120). */
|
|
10
|
+
function listIndex(model: string, name: string, index: IndexDefinition): void {
|
|
11
|
+
if (index.fields.length > 1) {
|
|
12
|
+
throw new Refused(
|
|
13
|
+
`${model}: the list '${name}' is indexed alone, in an index of its own.`,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
if (index.unique === true) {
|
|
17
|
+
throw new Refused(`${model}: the list '${name}' cannot be unique.`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A `contains` index covers one string field, not a list (D-141). */
|
|
22
|
+
function containsIndex(
|
|
23
|
+
model: string,
|
|
24
|
+
fields: ReadonlyMap<string, SchemaField>,
|
|
25
|
+
index: IndexDefinition,
|
|
26
|
+
): void {
|
|
27
|
+
const [name] = index.fields
|
|
28
|
+
const field = name === undefined ? undefined : fields.get(name)
|
|
29
|
+
if (
|
|
30
|
+
index.fields.length !== 1 ||
|
|
31
|
+
index.unique === true ||
|
|
32
|
+
field?.kind !== 'scalar' ||
|
|
33
|
+
field.type !== 'string' ||
|
|
34
|
+
field.list
|
|
35
|
+
) {
|
|
36
|
+
throw new Refused(
|
|
37
|
+
`${model}: a contains index covers one string field that is not a list, and is not unique.`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Refuses an index over a field that is not one of the model's columns. */
|
|
43
|
+
export function checkIndexes(
|
|
44
|
+
model: string,
|
|
45
|
+
fields: ReadonlyMap<string, SchemaField>,
|
|
46
|
+
indexes: readonly IndexDefinition[],
|
|
47
|
+
): void {
|
|
48
|
+
for (const index of indexes) {
|
|
49
|
+
for (const name of index.fields) {
|
|
50
|
+
const field = fields.get(name)
|
|
51
|
+
if (
|
|
52
|
+
field === undefined ||
|
|
53
|
+
field.kind === 'many' ||
|
|
54
|
+
(field.kind === 'one' && !field.stored)
|
|
55
|
+
) {
|
|
56
|
+
throw new Refused(
|
|
57
|
+
`${model}: index field '${name}' is not a scalar or a stored one relation.`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
if (field.kind === 'scalar' && field.list) {
|
|
61
|
+
listIndex(model, name, index)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (index.contains === true) containsIndex(model, fields, index)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A searched field is the kernel's string, which every caller may read: a
|
|
70
|
+
* match would reveal a value its read rule hides (D-148).
|
|
71
|
+
*/
|
|
72
|
+
export function checkSearch(model: ModelInfo): void {
|
|
73
|
+
const rules = readRules(model)
|
|
74
|
+
for (const field of model.fields.values()) {
|
|
75
|
+
if (field.kind !== 'scalar' || field.search !== true) continue
|
|
76
|
+
const name = `${model.name}.${field.name}`
|
|
77
|
+
if (field.codec !== undefined) {
|
|
78
|
+
throw new Refused(
|
|
79
|
+
`${name}: a ${field.codec.plugin} field is not searched.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (rules[field.name] !== undefined) {
|
|
83
|
+
throw new Refused(
|
|
84
|
+
`${name} has a read rule, and a search would reveal its value: remove search or the rule.`,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|