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,318 @@
|
|
|
1
|
+
// A caller's `where`, checked and normalized for the adapter (D-20, D-73):
|
|
2
|
+
// shorthands become `{ equals }`, `undefined` goes (D-84), and an unknown
|
|
3
|
+
// key fails with its path.
|
|
4
|
+
import type { Condition, Filter, ScalarType } from './adapter.ts'
|
|
5
|
+
import { storedValue } from './decimal.ts'
|
|
6
|
+
import { InvalidQuery } from './errors.ts'
|
|
7
|
+
import type { FieldInfo, ModelInfo } from './schema.ts'
|
|
8
|
+
import { listCondition } from './query-conditions.ts'
|
|
9
|
+
import { isPathFilter, jsonCondition } from './query-json.ts'
|
|
10
|
+
import { searchedFields } from './query-search.ts'
|
|
11
|
+
import {
|
|
12
|
+
clean,
|
|
13
|
+
cleanText,
|
|
14
|
+
entries,
|
|
15
|
+
isObject,
|
|
16
|
+
misfit,
|
|
17
|
+
storedDate,
|
|
18
|
+
unknownKey,
|
|
19
|
+
} from './query-values.ts'
|
|
20
|
+
|
|
21
|
+
export type { Scalar, ScalarCheck } from './query-values.ts'
|
|
22
|
+
export {
|
|
23
|
+
clean,
|
|
24
|
+
entries,
|
|
25
|
+
integerRange,
|
|
26
|
+
isInteger,
|
|
27
|
+
isJson,
|
|
28
|
+
isObject,
|
|
29
|
+
misfit,
|
|
30
|
+
storedDate,
|
|
31
|
+
unknownKey,
|
|
32
|
+
} from './query-values.ts'
|
|
33
|
+
|
|
34
|
+
export type Models = ReadonlyMap<string, ModelInfo>
|
|
35
|
+
|
|
36
|
+
/** The model, or the table, of `name`; another name is a kernel bug. */
|
|
37
|
+
export function modelOf<T>(models: ReadonlyMap<string, T>, name: string): T {
|
|
38
|
+
const model = models.get(name)
|
|
39
|
+
if (model === undefined) throw new Error(`Unknown model ${name}.`)
|
|
40
|
+
return model
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const byId = (id: string): Filter => ({ id: { equals: id } })
|
|
44
|
+
export const byIds = (ids: readonly string[]): Filter => ({ id: { in: ids } })
|
|
45
|
+
|
|
46
|
+
/** What a query does with a field: a read rule refuses each (D-44, D-143, D-147). */
|
|
47
|
+
export type Verb = 'filters' | 'sorts' | 'aggregates' | 'compares'
|
|
48
|
+
|
|
49
|
+
/** What a caller other than Master may filter on and see (D-86). */
|
|
50
|
+
export type Scope = {
|
|
51
|
+
/** Fails on a field with a read rule (D-44). */
|
|
52
|
+
readonly check: (
|
|
53
|
+
model: ModelInfo,
|
|
54
|
+
field: string,
|
|
55
|
+
path: string,
|
|
56
|
+
verb: Verb,
|
|
57
|
+
) => void
|
|
58
|
+
/** Whether the caller runs a field read rule on it (D-44). */
|
|
59
|
+
readonly ruled: (model: ModelInfo, field: string) => boolean
|
|
60
|
+
/** A model's read filter; `undefined` when every row passes. */
|
|
61
|
+
readonly readable: (model: string) => Filter | undefined
|
|
62
|
+
/**
|
|
63
|
+
* The rows a `many` relation reads: its filter (D-151) and its target's
|
|
64
|
+
* read filter; `undefined` when every row passes.
|
|
65
|
+
*/
|
|
66
|
+
readonly related: (
|
|
67
|
+
model: ModelInfo,
|
|
68
|
+
field: Extract<FieldInfo, { readonly kind: 'many' }>,
|
|
69
|
+
) => Filter | undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `id`, `createdAt` and `updatedAt`, which every model has (D-22). */
|
|
73
|
+
const system: ReadonlyMap<string, ScalarType | 'id'> = new Map([
|
|
74
|
+
['id', 'id'],
|
|
75
|
+
['createdAt', 'dateTime'],
|
|
76
|
+
['updatedAt', 'dateTime'],
|
|
77
|
+
])
|
|
78
|
+
export const systemFields: readonly string[] = [...system.keys()]
|
|
79
|
+
|
|
80
|
+
type Single = ScalarType | 'id'
|
|
81
|
+
|
|
82
|
+
function typeOf(model: ModelInfo, name: string): ScalarType | 'id' | undefined {
|
|
83
|
+
const field = model.fields.get(name)
|
|
84
|
+
return field?.kind === 'scalar' ? field.type : system.get(name)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Whether `value` fits a field, or a system field, which has no info (D-22). */
|
|
88
|
+
const fits = (type: Single, value: unknown, field: FieldInfo | undefined) =>
|
|
89
|
+
field?.kind === 'scalar'
|
|
90
|
+
? misfit(value, field) === undefined
|
|
91
|
+
: type === 'dateTime'
|
|
92
|
+
? storedDate(value)
|
|
93
|
+
: typeof value === 'string' && clean(value)
|
|
94
|
+
|
|
95
|
+
const base = new Set(['equals', 'not', 'in', 'notIn'])
|
|
96
|
+
const ordered = new Set(['lt', 'lte', 'gt', 'gte'])
|
|
97
|
+
const text = new Set(['contains', 'startsWith', 'endsWith'])
|
|
98
|
+
|
|
99
|
+
function operators(type: Single): ReadonlySet<string> {
|
|
100
|
+
if (
|
|
101
|
+
type === 'number' ||
|
|
102
|
+
type === 'decimal' ||
|
|
103
|
+
type === 'dateTime' ||
|
|
104
|
+
type === 'date'
|
|
105
|
+
)
|
|
106
|
+
return new Set([...base, ...ordered])
|
|
107
|
+
if (type === 'string') return new Set([...base, ...text, 'mode'])
|
|
108
|
+
if (type === 'id' || type === 'enum') return new Set([...base, ...text])
|
|
109
|
+
return base
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The operands of a value other than the field's, as an aggregate's in a
|
|
114
|
+
* `having` (D-149): their type, and their check, which stores each.
|
|
115
|
+
*/
|
|
116
|
+
export type Operand = {
|
|
117
|
+
readonly type: ScalarType
|
|
118
|
+
readonly check: (item: unknown, at: string) => unknown
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scalarCondition(
|
|
122
|
+
model: ModelInfo,
|
|
123
|
+
name: string,
|
|
124
|
+
type: Single,
|
|
125
|
+
value: unknown,
|
|
126
|
+
path: string,
|
|
127
|
+
other?: Operand['check'],
|
|
128
|
+
): Condition | undefined {
|
|
129
|
+
const field = model.fields.get(name)
|
|
130
|
+
const fit = (item: unknown, at: string): unknown => {
|
|
131
|
+
if (!fits(type, item, field)) {
|
|
132
|
+
throw new InvalidQuery(at, `${at} does not fit ${model.name}.${name}.`)
|
|
133
|
+
}
|
|
134
|
+
return field?.kind === 'scalar' ? storedValue(item, field) : item
|
|
135
|
+
}
|
|
136
|
+
const check = (item: unknown, at: string, nullable: boolean): unknown =>
|
|
137
|
+
item === null && nullable ? null : (other ?? fit)(item, at)
|
|
138
|
+
// A primitive, a `Date` or a list is `equals`; an object is a filter, on
|
|
139
|
+
// `json` too, which compares whole values with `equals` as in Prisma.
|
|
140
|
+
if (value === null || !isObject(value)) {
|
|
141
|
+
return { equals: check(value, path, true) }
|
|
142
|
+
}
|
|
143
|
+
const allowed = operators(type)
|
|
144
|
+
const condition: { [operator: string]: unknown } = {}
|
|
145
|
+
let insensitive = false
|
|
146
|
+
for (const [operator, operand] of entries(value, path)) {
|
|
147
|
+
const at = `${path}.${operator}`
|
|
148
|
+
if (!allowed.has(operator)) throw unknownKey(at, model.name)
|
|
149
|
+
if (operator === 'in' || operator === 'notIn') {
|
|
150
|
+
if (!Array.isArray(operand)) {
|
|
151
|
+
throw new InvalidQuery(at, `${at} must be a list.`)
|
|
152
|
+
}
|
|
153
|
+
condition[operator] = operand.map((item: unknown, index) =>
|
|
154
|
+
check(item, `${at}.${index}`, false),
|
|
155
|
+
)
|
|
156
|
+
} else if (operator === 'mode') {
|
|
157
|
+
if (operand !== 'insensitive') {
|
|
158
|
+
throw new InvalidQuery(at, `${at} must be 'insensitive'.`)
|
|
159
|
+
}
|
|
160
|
+
insensitive = true
|
|
161
|
+
} else if (text.has(operator)) {
|
|
162
|
+
condition[operator] = cleanText(operand, at)
|
|
163
|
+
} else {
|
|
164
|
+
condition[operator] = check(operand, at, base.has(operator))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// `mode` alone filters nothing, as in Prisma (D-140).
|
|
168
|
+
if (Object.keys(condition).length === 0) return undefined
|
|
169
|
+
return insensitive ? { ...condition, mode: 'insensitive' } : condition
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A scalar field's condition, or with `operand` another value's of it. */
|
|
173
|
+
export function fieldCondition(
|
|
174
|
+
model: ModelInfo,
|
|
175
|
+
name: string,
|
|
176
|
+
value: unknown,
|
|
177
|
+
path: string,
|
|
178
|
+
operand?: Operand,
|
|
179
|
+
): Condition | undefined {
|
|
180
|
+
const type = operand?.type ?? typeOf(model, name)
|
|
181
|
+
if (type === undefined) throw unknownKey(path, model.name)
|
|
182
|
+
return scalarCondition(model, name, type, value, path, operand?.check)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A relation operator that sees readable targets only (D-86): an unreadable
|
|
187
|
+
* row is absent, so `is: null` holds when the target is unreadable.
|
|
188
|
+
*/
|
|
189
|
+
function restricted(
|
|
190
|
+
operator: string,
|
|
191
|
+
inner: Filter | null,
|
|
192
|
+
readable: Filter,
|
|
193
|
+
): Condition {
|
|
194
|
+
if (inner === null) {
|
|
195
|
+
return operator === 'is' ? { isNot: readable } : { is: readable }
|
|
196
|
+
}
|
|
197
|
+
if (operator === 'every') {
|
|
198
|
+
return { every: { OR: [{ NOT: [readable] }, inner] } }
|
|
199
|
+
}
|
|
200
|
+
return { [operator]: { AND: [readable, inner] } }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function relationFilters(
|
|
204
|
+
models: Models,
|
|
205
|
+
model: ModelInfo,
|
|
206
|
+
field: Extract<FieldInfo, { readonly kind: 'one' | 'many' }>,
|
|
207
|
+
value: unknown,
|
|
208
|
+
path: string,
|
|
209
|
+
scope: Scope | undefined,
|
|
210
|
+
): Filter[] {
|
|
211
|
+
const target = modelOf(models, field.target)
|
|
212
|
+
const readable =
|
|
213
|
+
field.kind === 'one'
|
|
214
|
+
? scope?.readable(field.target)
|
|
215
|
+
: scope?.related(model, field)
|
|
216
|
+
const allowed =
|
|
217
|
+
field.kind === 'one' ? ['is', 'isNot'] : ['some', 'every', 'none']
|
|
218
|
+
const filters: Filter[] = []
|
|
219
|
+
for (const [operator, operand] of entries(value, path)) {
|
|
220
|
+
const at = `${path}.${operator}`
|
|
221
|
+
if (!allowed.includes(operator)) throw unknownKey(at, model.name)
|
|
222
|
+
const inner =
|
|
223
|
+
operand === null && field.kind === 'one'
|
|
224
|
+
? null
|
|
225
|
+
: normalizeWhere(models, target, operand, at, scope)
|
|
226
|
+
filters.push({
|
|
227
|
+
[field.name]:
|
|
228
|
+
readable === undefined
|
|
229
|
+
? { [operator]: inner }
|
|
230
|
+
: restricted(operator, inner, readable),
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
return filters
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function list(
|
|
237
|
+
models: Models,
|
|
238
|
+
model: ModelInfo,
|
|
239
|
+
value: unknown,
|
|
240
|
+
path: string,
|
|
241
|
+
single: boolean,
|
|
242
|
+
scope: Scope | undefined,
|
|
243
|
+
stored: boolean,
|
|
244
|
+
): Filter[] {
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
return value.map((item: unknown, index) =>
|
|
247
|
+
normalizeWhere(models, model, item, `${path}.${index}`, scope, stored),
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
if (single) return [normalizeWhere(models, model, value, path, scope, stored)]
|
|
251
|
+
throw new InvalidQuery(path, `${path} must be a list.`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A `where` of `model`, normalized; `path` names it in errors. With a
|
|
256
|
+
* `scope`, relation filters see readable rows only (D-86). Only a
|
|
257
|
+
* `beforeFind`'s, `stored`, filters a plugin field, in its stored form
|
|
258
|
+
* (D-122).
|
|
259
|
+
*/
|
|
260
|
+
export function normalizeWhere(
|
|
261
|
+
models: Models,
|
|
262
|
+
model: ModelInfo,
|
|
263
|
+
where: unknown,
|
|
264
|
+
path: string,
|
|
265
|
+
scope?: Scope,
|
|
266
|
+
stored = false,
|
|
267
|
+
): Filter {
|
|
268
|
+
const filter: { [key: string]: Condition | Filter[] } = {}
|
|
269
|
+
const relations: Filter[] = []
|
|
270
|
+
for (const [key, value] of entries(where, path)) {
|
|
271
|
+
const at = `${path}.${key}`
|
|
272
|
+
if (key === 'AND' || key === 'NOT' || key === 'OR') {
|
|
273
|
+
filter[key] = list(models, model, value, at, key !== 'OR', scope, stored)
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (key === '_search' && searchedFields(model.fields.values()).length > 0) {
|
|
277
|
+
if (value !== undefined) filter[key] = { search: cleanText(value, at) }
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
const field = model.fields.get(key)
|
|
281
|
+
const type = typeOf(model, key)
|
|
282
|
+
if (field === undefined && type === undefined) {
|
|
283
|
+
throw unknownKey(at, model.name)
|
|
284
|
+
}
|
|
285
|
+
const codec = field?.kind === 'scalar' ? field.codec : undefined
|
|
286
|
+
if (codec !== undefined && !stored) {
|
|
287
|
+
throw new InvalidQuery(
|
|
288
|
+
at,
|
|
289
|
+
`${at}: ${model.name}.${key} is a ${codec.plugin} field, which only a beforeFind filters.`,
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
scope?.check(model, key, at, 'filters')
|
|
293
|
+
if (field !== undefined && field.kind !== 'scalar') {
|
|
294
|
+
relations.push(...relationFilters(models, model, field, value, at, scope))
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
const condition =
|
|
298
|
+
type === undefined
|
|
299
|
+
? undefined
|
|
300
|
+
: field?.kind === 'scalar' && field.list
|
|
301
|
+
? listCondition(model, field, value, at)
|
|
302
|
+
: type === 'json' && isPathFilter(value)
|
|
303
|
+
? jsonCondition(model.name, value, at)
|
|
304
|
+
: scalarCondition(model, key, type, value, at)
|
|
305
|
+
if (condition !== undefined) filter[key] = condition
|
|
306
|
+
}
|
|
307
|
+
const [only, ...others] = relations
|
|
308
|
+
if (only === undefined) return filter
|
|
309
|
+
// One relation filter stays in place; more join `AND`.
|
|
310
|
+
if (others.length === 0 && filter['AND'] === undefined) {
|
|
311
|
+
return { ...filter, ...only }
|
|
312
|
+
}
|
|
313
|
+
const and = filter['AND']
|
|
314
|
+
return {
|
|
315
|
+
...filter,
|
|
316
|
+
AND: [...(Array.isArray(and) ? and : []), ...relations],
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// A write's data after its `before*` hooks, turned into the adapter's
|
|
2
|
+
// changes: values and relation writes by id, from either side (D-20, D-78),
|
|
3
|
+
// and the rows its relation writes create, update and delete (D-128, D-138).
|
|
4
|
+
// The caller's data was checked before the hooks (write-data.ts): what fails
|
|
5
|
+
// here is a hook's or a default's value (D-88).
|
|
6
|
+
import { recordOf } from './access.ts'
|
|
7
|
+
import type { Row } from './adapter.ts'
|
|
8
|
+
import { InvalidQuery } from './errors.ts'
|
|
9
|
+
import { runHook } from './hooks.ts'
|
|
10
|
+
import { resolveData, resolveMany, resolveOne } from './nested-write.ts'
|
|
11
|
+
import type { Operations } from './number-update.ts'
|
|
12
|
+
import type { FieldInfo, ModelInfo } from './schema.ts'
|
|
13
|
+
import type { QueryScope } from './scope.ts'
|
|
14
|
+
import { madeId, uuidv7 } from './uuid.ts'
|
|
15
|
+
import { byId, entries, modelOf, unknownKey } from './query-where.ts'
|
|
16
|
+
import type { Context, Relation } from './write-context.ts'
|
|
17
|
+
import {
|
|
18
|
+
fail,
|
|
19
|
+
holds,
|
|
20
|
+
linking,
|
|
21
|
+
pathOf,
|
|
22
|
+
resolving,
|
|
23
|
+
scalar,
|
|
24
|
+
trusted,
|
|
25
|
+
viaOf,
|
|
26
|
+
} from './write-context.ts'
|
|
27
|
+
import type { Draft } from './write-data.ts'
|
|
28
|
+
import { prepared, readData } from './write-data.ts'
|
|
29
|
+
import type { Reads, Step } from './write-plan.ts'
|
|
30
|
+
import { Plan } from './write-plan.ts'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A row a relation write creates, as `Target.create` creates it (D-128):
|
|
34
|
+
* its access rules unless a hook or a default wrote it, its defaults, its
|
|
35
|
+
* validation and `beforeCreate`, then its own relation writes.
|
|
36
|
+
*/
|
|
37
|
+
async function created(
|
|
38
|
+
context: Context,
|
|
39
|
+
field: Relation,
|
|
40
|
+
data: Draft,
|
|
41
|
+
at: readonly (string | number)[],
|
|
42
|
+
skips: boolean,
|
|
43
|
+
): Promise<{ readonly id: string; readonly steps: Step[] }> {
|
|
44
|
+
const { scope, plan } = context
|
|
45
|
+
const model = modelOf(context.scope.models, field.target)
|
|
46
|
+
const made = madeId(data)
|
|
47
|
+
// A hook's data may come back in another write: it gets a fresh id.
|
|
48
|
+
const id = skips || plan.made.has(made) ? uuidv7() : made
|
|
49
|
+
const path = ['data', ...at].join('.')
|
|
50
|
+
const draft = readData(scope.models, model, data, true, path, field.inverse)
|
|
51
|
+
const inverse = model.fields.get(field.inverse)
|
|
52
|
+
if (inverse?.kind === 'one' && inverse.stored) {
|
|
53
|
+
draft[field.inverse] = { connect: { id: context.id } }
|
|
54
|
+
}
|
|
55
|
+
const child = { ...context, model, id, set: {}, at }
|
|
56
|
+
await resolveData(resolving(child, skips, true), draft)
|
|
57
|
+
const sent: Readonly<Draft> = skips ? {} : { ...draft }
|
|
58
|
+
if (!skips) scope.access?.allow(model, 'create', { data: sent })
|
|
59
|
+
await prepared(model, draft, scope.caller, true, at)
|
|
60
|
+
await runHook(model, 'beforeCreate', { caller: scope.caller, data: draft })
|
|
61
|
+
plan.events.push({ model, operation: 'create', id })
|
|
62
|
+
const steps = await inserted({ ...child, sent, before: [], after: [] }, draft)
|
|
63
|
+
return { id, steps }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A linked row a relation write updates, as `Target.update` updates it
|
|
68
|
+
* (D-138): its rules unless a hook or a default wrote it, its validation,
|
|
69
|
+
* `beforeUpdate`, then its own relation writes.
|
|
70
|
+
*/
|
|
71
|
+
async function changed(
|
|
72
|
+
context: Context,
|
|
73
|
+
field: Relation,
|
|
74
|
+
update: { readonly data: Draft; readonly row: Row },
|
|
75
|
+
at: readonly (string | number)[],
|
|
76
|
+
skips: boolean,
|
|
77
|
+
): Promise<Step[]> {
|
|
78
|
+
const { scope, plan } = context
|
|
79
|
+
const { data, row } = update
|
|
80
|
+
const model = modelOf(context.scope.models, field.target)
|
|
81
|
+
const child = { ...context, model, id: row.id, set: {}, at }
|
|
82
|
+
await resolveData(resolving(child, skips, false), data)
|
|
83
|
+
const sent: Readonly<Draft> = skips ? {} : { ...data }
|
|
84
|
+
const via = viaOf(context, field)
|
|
85
|
+
if (!skips)
|
|
86
|
+
scope.access?.allow(model, 'update', { record: row, data: sent, via })
|
|
87
|
+
await prepared(model, data, scope.caller, false, at)
|
|
88
|
+
const record = recordOf(model, row)
|
|
89
|
+
await runHook(model, 'beforeUpdate', { caller: scope.caller, record, data })
|
|
90
|
+
plan.events.push({ model, operation: 'update', id: row.id, before: row })
|
|
91
|
+
return updateSteps({ ...child, sent, before: [], after: [] }, data)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A linked row a relation write deletes, as `Target.delete` (D-138). */
|
|
95
|
+
function dropped(
|
|
96
|
+
context: Context,
|
|
97
|
+
field: Relation,
|
|
98
|
+
row: Row,
|
|
99
|
+
path: string,
|
|
100
|
+
skips: boolean,
|
|
101
|
+
): Step {
|
|
102
|
+
const model = modelOf(context.scope.models, field.target)
|
|
103
|
+
const via = viaOf(context, field)
|
|
104
|
+
if (!skips) context.scope.access?.allow(model, 'delete', { record: row, via })
|
|
105
|
+
// The parent empties its own key to the row: the cascade leaves it.
|
|
106
|
+
const except =
|
|
107
|
+
field.kind === 'one' && field.stored
|
|
108
|
+
? { model: context.model.name, field: field.name, id: context.id }
|
|
109
|
+
: undefined
|
|
110
|
+
return {
|
|
111
|
+
kind: 'drop',
|
|
112
|
+
model: model.name,
|
|
113
|
+
rows: [row],
|
|
114
|
+
path,
|
|
115
|
+
...(except === undefined ? {} : { except }),
|
|
116
|
+
written: context.plan.written,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function one(
|
|
121
|
+
context: Context,
|
|
122
|
+
field: Extract<FieldInfo, { readonly kind: 'one' }>,
|
|
123
|
+
value: unknown,
|
|
124
|
+
creating: boolean,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const skips = trusted(context, field, value)
|
|
127
|
+
const write = await resolveOne(
|
|
128
|
+
resolving(context, skips, creating),
|
|
129
|
+
field,
|
|
130
|
+
value,
|
|
131
|
+
)
|
|
132
|
+
const at = [...context.at, field.name, write.kind]
|
|
133
|
+
if (write.kind === 'update') {
|
|
134
|
+
context.after.push(...(await changed(context, field, write, at, skips)))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (write.kind === 'delete') {
|
|
138
|
+
context.after.push(
|
|
139
|
+
dropped(
|
|
140
|
+
context,
|
|
141
|
+
field,
|
|
142
|
+
write.row,
|
|
143
|
+
`${pathOf(context, field.name)}.delete`,
|
|
144
|
+
skips,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
// A key the other side holds changes its row: it is updated (D-124).
|
|
149
|
+
const operation = field.stored ? 'read' : 'update'
|
|
150
|
+
let target = write.kind === 'connect' ? write.id : undefined
|
|
151
|
+
if (target !== undefined) {
|
|
152
|
+
context.plan.connect(field.target, [target], skips, operation)
|
|
153
|
+
}
|
|
154
|
+
if (write.kind === 'create') {
|
|
155
|
+
const made = await created(context, field, write.data, write.at, skips)
|
|
156
|
+
target = made.id
|
|
157
|
+
// The row that stores the key is written after the row it points to.
|
|
158
|
+
;(field.stored ? context.before : context.after).push(...made.steps)
|
|
159
|
+
}
|
|
160
|
+
if (field.stored) {
|
|
161
|
+
if (target === undefined && !field.optional) {
|
|
162
|
+
fail(context, [field.name], 'Required.')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
context.set[field.name] = target ?? null
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
// A deleted row holds no key; another one may not be left empty (D-37).
|
|
169
|
+
if (write.kind === 'delete') return
|
|
170
|
+
if (!creating && (await holds(context, field, target))) {
|
|
171
|
+
fail(
|
|
172
|
+
context,
|
|
173
|
+
[field.name],
|
|
174
|
+
`The ${field.target} it points to cannot be left without one.`,
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (write.kind === 'connect') {
|
|
179
|
+
context.after.push({
|
|
180
|
+
kind: 'move',
|
|
181
|
+
model: field.target,
|
|
182
|
+
field: field.inverse,
|
|
183
|
+
via: viaOf(context, field),
|
|
184
|
+
where: byId(write.id),
|
|
185
|
+
to: context.id,
|
|
186
|
+
trusted: skips,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Deletes, then creates, disconnects and connects, then updates (D-138). */
|
|
192
|
+
async function many(
|
|
193
|
+
context: Context,
|
|
194
|
+
field: Extract<FieldInfo, { readonly kind: 'many' }>,
|
|
195
|
+
value: unknown,
|
|
196
|
+
creating: boolean,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const skips = trusted(context, field, value)
|
|
199
|
+
const write = await resolveMany(
|
|
200
|
+
resolving(context, skips, creating),
|
|
201
|
+
field,
|
|
202
|
+
value,
|
|
203
|
+
)
|
|
204
|
+
const operation = field.join ? 'read' : 'update'
|
|
205
|
+
context.plan.connect(field.target, write.connect, skips, operation)
|
|
206
|
+
for (const { row, path } of write.delete) {
|
|
207
|
+
context.after.push(dropped(context, field, row, path, skips))
|
|
208
|
+
}
|
|
209
|
+
const made: string[] = []
|
|
210
|
+
for (const { data, at } of write.create) {
|
|
211
|
+
const child = await created(context, field, data, at, skips)
|
|
212
|
+
made.push(child.id)
|
|
213
|
+
context.after.push(...child.steps)
|
|
214
|
+
}
|
|
215
|
+
linking(context, field, write, made, skips)
|
|
216
|
+
for (const [index, update] of write.update.entries()) {
|
|
217
|
+
const at = [...context.at, field.name, 'update', index, 'data']
|
|
218
|
+
context.after.push(...(await changed(context, field, update, at, skips)))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A hook's value the plan refuses is the hook's error, not the caller's. */
|
|
223
|
+
function hookError(context: Context, error: unknown): unknown {
|
|
224
|
+
if (!(error instanceof InvalidQuery)) return error
|
|
225
|
+
return new TypeError(
|
|
226
|
+
`${context.model.name}: a before hook wrote invalid data. ${error.message}`,
|
|
227
|
+
{ cause: error },
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function fields(
|
|
232
|
+
context: Context,
|
|
233
|
+
data: Draft,
|
|
234
|
+
creating: boolean,
|
|
235
|
+
): Promise<Set<string>> {
|
|
236
|
+
const given = new Set<string>()
|
|
237
|
+
const at = ['data', ...context.at].join('.')
|
|
238
|
+
for (const [name, value] of entries(data, at)) {
|
|
239
|
+
const field = context.model.fields.get(name)
|
|
240
|
+
// The caller's data was read before the hooks: a hook wrote the key.
|
|
241
|
+
if (field === undefined) {
|
|
242
|
+
throw hookError(
|
|
243
|
+
context,
|
|
244
|
+
unknownKey(pathOf(context, name), context.model.name),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
given.add(name)
|
|
248
|
+
try {
|
|
249
|
+
if (field.kind === 'scalar') scalar(context, field, value)
|
|
250
|
+
else if (field.kind === 'one') await one(context, field, value, creating)
|
|
251
|
+
else await many(context, field, value, creating)
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// The caller's relation value reaches rows only now (D-138).
|
|
254
|
+
throw trusted(context, field, value) ? hookError(context, error) : error
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return given
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function contextOf(
|
|
261
|
+
scope: QueryScope,
|
|
262
|
+
model: ModelInfo,
|
|
263
|
+
id: string,
|
|
264
|
+
sent: Readonly<Draft>,
|
|
265
|
+
reads: Reads,
|
|
266
|
+
): Context {
|
|
267
|
+
const plan = new Plan()
|
|
268
|
+
const set = {}
|
|
269
|
+
return {
|
|
270
|
+
scope,
|
|
271
|
+
model,
|
|
272
|
+
id,
|
|
273
|
+
plan,
|
|
274
|
+
reads,
|
|
275
|
+
sent,
|
|
276
|
+
set,
|
|
277
|
+
before: [],
|
|
278
|
+
after: [],
|
|
279
|
+
at: [],
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** A created row's steps: the rows it points to, its insert, then the rest. */
|
|
284
|
+
async function inserted(context: Context, draft: Draft): Promise<Step[]> {
|
|
285
|
+
const { model, id, plan } = context
|
|
286
|
+
plan.made.add(id)
|
|
287
|
+
const given = await fields(context, draft, true)
|
|
288
|
+
for (const field of model.fields.values()) {
|
|
289
|
+
if (given.has(field.name) || field.kind === 'many') continue
|
|
290
|
+
if (field.kind === 'one' && !field.stored) continue
|
|
291
|
+
// A required field may be left to a hook: checked after it (D-88).
|
|
292
|
+
// A list left unwritten is empty (D-120).
|
|
293
|
+
if (field.kind === 'scalar' && field.list) {
|
|
294
|
+
context.set[field.name] = []
|
|
295
|
+
} else if (field.optional) context.set[field.name] = null
|
|
296
|
+
else fail(context, [field.name], 'Required.')
|
|
297
|
+
}
|
|
298
|
+
const now = Date.now()
|
|
299
|
+
const row: Row = {
|
|
300
|
+
...context.set,
|
|
301
|
+
id,
|
|
302
|
+
createdAt: new Date(now),
|
|
303
|
+
updatedAt: new Date(now),
|
|
304
|
+
}
|
|
305
|
+
const insert = { kind: 'insert', model: model.name, row } as const
|
|
306
|
+
return [...context.before, insert, ...context.after]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** An updated row's steps; `updatedAt` always moves. */
|
|
310
|
+
async function updateSteps(
|
|
311
|
+
context: Context,
|
|
312
|
+
draft: Draft,
|
|
313
|
+
operations: Operations = new Map(),
|
|
314
|
+
): Promise<Step[]> {
|
|
315
|
+
const { model, id, plan } = context
|
|
316
|
+
plan.written.add(`${model.name}:${id}`)
|
|
317
|
+
await fields(context, draft, false)
|
|
318
|
+
return [
|
|
319
|
+
...context.before,
|
|
320
|
+
{
|
|
321
|
+
kind: 'update',
|
|
322
|
+
model: model.name,
|
|
323
|
+
where: byId(id),
|
|
324
|
+
set: { ...context.set, updatedAt: new Date(Date.now()) },
|
|
325
|
+
...(operations.size === 0
|
|
326
|
+
? {}
|
|
327
|
+
: { compute: Object.fromEntries(operations) }),
|
|
328
|
+
},
|
|
329
|
+
...context.after,
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `create`'s changes: the row with its id and timestamps (D-22). */
|
|
334
|
+
export async function createPlan(
|
|
335
|
+
scope: QueryScope,
|
|
336
|
+
model: ModelInfo,
|
|
337
|
+
draft: Draft,
|
|
338
|
+
sent: Readonly<Draft>,
|
|
339
|
+
reads: Reads,
|
|
340
|
+
id: string = uuidv7(),
|
|
341
|
+
): Promise<{ readonly id: string; readonly plan: Plan }> {
|
|
342
|
+
const context = contextOf(scope, model, id, sent, reads)
|
|
343
|
+
context.plan.steps.push(...(await inserted(context, draft)))
|
|
344
|
+
return { id: context.id, plan: context.plan }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** `update`'s changes on the stored row. */
|
|
348
|
+
export async function updatePlan(
|
|
349
|
+
scope: QueryScope,
|
|
350
|
+
model: ModelInfo,
|
|
351
|
+
id: string,
|
|
352
|
+
draft: Draft,
|
|
353
|
+
sent: Readonly<Draft>,
|
|
354
|
+
reads: Reads,
|
|
355
|
+
operations: Operations = new Map(),
|
|
356
|
+
): Promise<Plan> {
|
|
357
|
+
const context = contextOf(scope, model, id, sent, reads)
|
|
358
|
+
context.plan.steps.push(...(await updateSteps(context, draft, operations)))
|
|
359
|
+
return context.plan
|
|
360
|
+
}
|