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
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// A plugin's manifest (D-57, D-95): checked by `definePlugin`, its ranges
|
|
2
|
+
// matched at `start` against the running kernel and the installed plugins.
|
|
3
|
+
import { satisfies, valid, validRange } from 'semver'
|
|
4
|
+
import kernel from '../package.json' with { type: 'json' }
|
|
5
|
+
import type { Logger } from './app-options.ts'
|
|
6
|
+
import type { PluginRecord } from './definition.ts'
|
|
7
|
+
import type { Manifest } from './plugin.ts'
|
|
8
|
+
import { isObject } from './query-where.ts'
|
|
9
|
+
import { Refused } from './errors.ts'
|
|
10
|
+
|
|
11
|
+
/** Checks a plugin's `manifest`: a name, a version and valid ranges. */
|
|
12
|
+
export function manifestOf(value: unknown, plugin: string): Manifest {
|
|
13
|
+
const path = `${plugin}.manifest`
|
|
14
|
+
if (!isObject(value)) throw new TypeError(`${path} is an object.`)
|
|
15
|
+
const { name, version, peerDependencies } = value
|
|
16
|
+
if (typeof name !== 'string' || name === '') {
|
|
17
|
+
throw new TypeError(`${path}.name is a package name.`)
|
|
18
|
+
}
|
|
19
|
+
if (typeof version !== 'string' || valid(version) === null) {
|
|
20
|
+
throw new TypeError(`${path}.version is a version, not ${String(version)}.`)
|
|
21
|
+
}
|
|
22
|
+
if (peerDependencies === undefined) return Object.freeze({ name, version })
|
|
23
|
+
if (!isObject(peerDependencies)) {
|
|
24
|
+
throw new TypeError(`${path}.peerDependencies is an object.`)
|
|
25
|
+
}
|
|
26
|
+
const ranges: { [name: string]: string } = {}
|
|
27
|
+
for (const [peer, range] of Object.entries(peerDependencies)) {
|
|
28
|
+
if (typeof range !== 'string' || validRange(range) === null) {
|
|
29
|
+
throw new TypeError(
|
|
30
|
+
`${path}.peerDependencies.${peer} is a version range, not ${String(range)}.`,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
ranges[peer] = range
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
name,
|
|
37
|
+
version,
|
|
38
|
+
peerDependencies: Object.freeze(ranges),
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A prerelease of the kernel runs the plugins written for its version range.
|
|
43
|
+
const accepts = (version: string, range: string): boolean =>
|
|
44
|
+
satisfies(version, range, { includePrerelease: true })
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Refuses a plugin whose ranges exclude the running kernel or an installed
|
|
48
|
+
* plugin, naming both versions; warns of one that gives the kernel no range.
|
|
49
|
+
*/
|
|
50
|
+
export function checkVersions(
|
|
51
|
+
plugins: readonly PluginRecord[],
|
|
52
|
+
logger: Logger,
|
|
53
|
+
): void {
|
|
54
|
+
const installed = new Map<string, string>([[kernel.name, kernel.version]])
|
|
55
|
+
for (const { code } of plugins) {
|
|
56
|
+
installed.set(code.manifest.name, code.manifest.version)
|
|
57
|
+
}
|
|
58
|
+
for (const { code } of plugins) {
|
|
59
|
+
const { name, version, peerDependencies = {} } = code.manifest
|
|
60
|
+
if (peerDependencies[kernel.name] === undefined) {
|
|
61
|
+
logger.warn(`${name} declares no ${kernel.name} range.`)
|
|
62
|
+
}
|
|
63
|
+
for (const [peer, range] of Object.entries(peerDependencies)) {
|
|
64
|
+
const found = installed.get(peer)
|
|
65
|
+
if (found === undefined || accepts(found, range)) continue
|
|
66
|
+
throw new Refused(
|
|
67
|
+
`${name} ${version} needs ${peer} ${range}; this is ${peer} ${found}.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// How `memory()` aggregates (D-143), as PostgreSQL: a `null` is skipped, and
|
|
2
|
+
// a sum, an average, a minimum or a maximum of no value is `null`.
|
|
3
|
+
// A decimal's sum is exact, at its field's scale, its average that sum's
|
|
4
|
+
// number divided (D-145). Groups are kept by `having` as rows by a `where`
|
|
5
|
+
// (D-149), and sorted as a read's rows (D-146).
|
|
6
|
+
import type {
|
|
7
|
+
Aggregate,
|
|
8
|
+
AggregateRead,
|
|
9
|
+
Filter,
|
|
10
|
+
GroupKey,
|
|
11
|
+
Read,
|
|
12
|
+
SchemaField,
|
|
13
|
+
} from './adapter.ts'
|
|
14
|
+
import { sumDecimals } from './decimal.ts'
|
|
15
|
+
import type { Truth } from './memory-conditions.ts'
|
|
16
|
+
import { and, isFilters, not, or, scalar } from './memory-conditions.ts'
|
|
17
|
+
import type { Reader, StoredRow } from './memory-read.ts'
|
|
18
|
+
import { compareKeys, compareValues, copy, sortable } from './memory-values.ts'
|
|
19
|
+
|
|
20
|
+
function of(
|
|
21
|
+
values: readonly unknown[],
|
|
22
|
+
operation: Aggregate['operation'],
|
|
23
|
+
field: SchemaField | undefined,
|
|
24
|
+
): unknown {
|
|
25
|
+
if (operation === 'count') return values.length
|
|
26
|
+
const [head, ...rest] = values
|
|
27
|
+
if (head === undefined) return null
|
|
28
|
+
if (operation === 'min' || operation === 'max') {
|
|
29
|
+
const sign = operation === 'min' ? 1 : -1
|
|
30
|
+
const order = (a: unknown, b: unknown) =>
|
|
31
|
+
compareValues(sortable(a, field), sortable(b, field))
|
|
32
|
+
const found = rest.reduce(
|
|
33
|
+
(best, item) => (sign * order(item, best) < 0 ? item : best),
|
|
34
|
+
head,
|
|
35
|
+
)
|
|
36
|
+
return copy(found)
|
|
37
|
+
}
|
|
38
|
+
const sum =
|
|
39
|
+
field?.kind === 'scalar' && field.type === 'decimal'
|
|
40
|
+
? sumDecimals(values.map(String), field.scale ?? 0)
|
|
41
|
+
: values.reduce((total: number, item) => total + Number(item), 0)
|
|
42
|
+
return operation === 'sum' ? sum : Number(sum) / values.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One value per aggregate of `rows`, in order. */
|
|
46
|
+
function aggregated(
|
|
47
|
+
rows: readonly StoredRow[],
|
|
48
|
+
aggregates: readonly Aggregate[],
|
|
49
|
+
fieldOf: (name: string) => SchemaField | undefined,
|
|
50
|
+
): unknown[] {
|
|
51
|
+
return aggregates.map(({ operation, field }) => {
|
|
52
|
+
if (field === undefined) return rows.length
|
|
53
|
+
const values = rows.map((row) => row[field] ?? null)
|
|
54
|
+
return of(
|
|
55
|
+
values.filter((item) => item !== null),
|
|
56
|
+
operation,
|
|
57
|
+
fieldOf(field),
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The values of a row's keys, a key through a relation its target's. */
|
|
63
|
+
function keysOf(
|
|
64
|
+
reader: Reader,
|
|
65
|
+
model: string,
|
|
66
|
+
row: StoredRow,
|
|
67
|
+
by: readonly GroupKey[],
|
|
68
|
+
): unknown[] {
|
|
69
|
+
return by.map(({ field, through }) => {
|
|
70
|
+
const reached = reader.reach(model, row, through)
|
|
71
|
+
return copy(reached?.row[field] ?? null)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Of the rows with the same keys, the first in `read`'s order (D-147). */
|
|
76
|
+
export function distinct(
|
|
77
|
+
reader: Reader,
|
|
78
|
+
read: Read,
|
|
79
|
+
rows: StoredRow[],
|
|
80
|
+
): StoredRow[] {
|
|
81
|
+
const { where, orderBy } = read
|
|
82
|
+
const sorted = reader.select(read.model, rows, {
|
|
83
|
+
...(where && { where }),
|
|
84
|
+
...(orderBy && { orderBy }),
|
|
85
|
+
})
|
|
86
|
+
const seen = new Set<string>()
|
|
87
|
+
return sorted.filter((row) => {
|
|
88
|
+
const id = JSON.stringify(
|
|
89
|
+
keysOf(reader, read.model, row, read.distinct ?? []),
|
|
90
|
+
)
|
|
91
|
+
if (seen.has(id)) return false
|
|
92
|
+
seen.add(id)
|
|
93
|
+
return true
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether a group's values pass `having`, whose keys are positions (D-149). */
|
|
98
|
+
function held(
|
|
99
|
+
filter: Filter,
|
|
100
|
+
values: readonly unknown[],
|
|
101
|
+
fields: readonly (SchemaField | undefined)[],
|
|
102
|
+
): Truth {
|
|
103
|
+
const truths: Truth[] = []
|
|
104
|
+
for (const [key, part] of Object.entries(filter)) {
|
|
105
|
+
if (part === undefined) continue
|
|
106
|
+
if (!isFilters(part)) {
|
|
107
|
+
const at = Number(key)
|
|
108
|
+
const sorted = (value: unknown) => sortable(value, fields[at])
|
|
109
|
+
truths.push(scalar(values[at] ?? null, part, sorted))
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
const inner = part.map((each) => held(each, values, fields))
|
|
113
|
+
if (key === 'AND') truths.push(and(inner))
|
|
114
|
+
else if (key === 'OR') truths.push(or(inner))
|
|
115
|
+
else truths.push(and(inner.map((truth) => not(truth))))
|
|
116
|
+
}
|
|
117
|
+
return and(truths)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Each group of `rows`: its keys' values, then its aggregates', in order. */
|
|
121
|
+
export function grouped(
|
|
122
|
+
reader: Reader,
|
|
123
|
+
read: AggregateRead,
|
|
124
|
+
rows: readonly StoredRow[],
|
|
125
|
+
): unknown[][] {
|
|
126
|
+
const by = read.by ?? []
|
|
127
|
+
const fieldOf = (name: string) => reader.field(read.model, name)
|
|
128
|
+
const groups = new Map<string, { keys: unknown[]; rows: StoredRow[] }>()
|
|
129
|
+
if (by.length === 0) groups.set('[]', { keys: [], rows: [] })
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
const keys = keysOf(reader, read.model, row, by)
|
|
132
|
+
const id = JSON.stringify(keys)
|
|
133
|
+
const group = groups.get(id) ?? { keys, rows: [] }
|
|
134
|
+
group.rows.push(row)
|
|
135
|
+
groups.set(id, group)
|
|
136
|
+
}
|
|
137
|
+
const lists = [...groups.values()].map((group) => [
|
|
138
|
+
...group.keys,
|
|
139
|
+
...aggregated(group.rows, read.aggregates, fieldOf),
|
|
140
|
+
])
|
|
141
|
+
// A key through a relation is an id; a decimal sorts by value (D-145).
|
|
142
|
+
const fields = [
|
|
143
|
+
...by.map(({ field, through }) => (through ? undefined : field)),
|
|
144
|
+
...read.aggregates.map(({ operation, field }) =>
|
|
145
|
+
operation === 'count' ? undefined : field,
|
|
146
|
+
),
|
|
147
|
+
].map((name) => (name === undefined ? undefined : fieldOf(name)))
|
|
148
|
+
const { having } = read
|
|
149
|
+
const kept =
|
|
150
|
+
having === undefined
|
|
151
|
+
? lists
|
|
152
|
+
: lists.filter((list) => held(having, list, fields) === true)
|
|
153
|
+
const order = read.orderBy ?? []
|
|
154
|
+
const sorted = (list: readonly unknown[]) =>
|
|
155
|
+
order.map(({ at }) => sortable(list[at], fields[at]))
|
|
156
|
+
kept.sort((a, b) => compareKeys(sorted(a), sorted(b), order))
|
|
157
|
+
const skip = read.skip ?? 0
|
|
158
|
+
return kept.slice(
|
|
159
|
+
skip,
|
|
160
|
+
read.take === undefined ? undefined : skip + read.take,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// How `memory()` tests a condition, with SQL's three-valued logic: `null` is
|
|
2
|
+
// unknown, as PostgreSQL compares it (D-94).
|
|
3
|
+
import type { Condition, Filter } from './adapter.ts'
|
|
4
|
+
import { jsonPath } from './memory-json.ts'
|
|
5
|
+
import { compareValues, lower, same, text } from './memory-values.ts'
|
|
6
|
+
|
|
7
|
+
export type Truth = boolean | null
|
|
8
|
+
|
|
9
|
+
export function and(truths: readonly Truth[]): Truth {
|
|
10
|
+
if (truths.includes(false)) return false
|
|
11
|
+
return truths.includes(null) ? null : true
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function or(truths: readonly Truth[]): Truth {
|
|
15
|
+
if (truths.includes(true)) return true
|
|
16
|
+
return truths.includes(null) ? null : false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function not(truth: Truth): Truth {
|
|
20
|
+
return truth === null ? null : !truth
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isFilters(
|
|
24
|
+
value: Condition | readonly Filter[],
|
|
25
|
+
): value is readonly Filter[] {
|
|
26
|
+
return Array.isArray(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function scalar(
|
|
30
|
+
stored: unknown,
|
|
31
|
+
condition: Condition,
|
|
32
|
+
sorted: (value: unknown) => unknown,
|
|
33
|
+
): Truth {
|
|
34
|
+
if (condition.path !== undefined) return jsonPath(stored, condition)
|
|
35
|
+
const truths: Truth[] = []
|
|
36
|
+
const known = stored !== null
|
|
37
|
+
const fold =
|
|
38
|
+
condition.mode === 'insensitive' ? lower : (item: unknown) => item
|
|
39
|
+
const value = fold(stored)
|
|
40
|
+
for (const [operator, raw] of Object.entries(condition)) {
|
|
41
|
+
if (raw === undefined || operator === 'mode') continue
|
|
42
|
+
const operand: unknown = Array.isArray(raw)
|
|
43
|
+
? raw.map((item: unknown) => fold(item))
|
|
44
|
+
: fold(raw)
|
|
45
|
+
if (operator === 'equals' || operator === 'not') {
|
|
46
|
+
const equal =
|
|
47
|
+
operand === null ? !known : known ? same(value, operand) : null
|
|
48
|
+
truths.push(operator === 'equals' ? equal : not(equal))
|
|
49
|
+
} else if (operator === 'in' || operator === 'notIn') {
|
|
50
|
+
const list: readonly unknown[] = Array.isArray(operand) ? operand : []
|
|
51
|
+
const found =
|
|
52
|
+
list.length === 0
|
|
53
|
+
? false
|
|
54
|
+
: known
|
|
55
|
+
? list.some((item) => same(value, item))
|
|
56
|
+
: null
|
|
57
|
+
truths.push(operator === 'in' ? found : not(found))
|
|
58
|
+
} else if (!known) {
|
|
59
|
+
truths.push(null)
|
|
60
|
+
} else if (operator.startsWith('has') || operator === 'isEmpty') {
|
|
61
|
+
const items: readonly unknown[] = Array.isArray(value) ? value : []
|
|
62
|
+
const wanted: readonly unknown[] = Array.isArray(operand)
|
|
63
|
+
? operand
|
|
64
|
+
: [operand]
|
|
65
|
+
const holds = (item: unknown): boolean => items.includes(item)
|
|
66
|
+
truths.push(
|
|
67
|
+
operator === 'isEmpty'
|
|
68
|
+
? (items.length === 0) === operand
|
|
69
|
+
: operator === 'hasEvery'
|
|
70
|
+
? wanted.every(holds)
|
|
71
|
+
: wanted.some(holds),
|
|
72
|
+
)
|
|
73
|
+
} else if (operator === 'contains') {
|
|
74
|
+
truths.push(text(value, operand, (a, b) => a.includes(b)))
|
|
75
|
+
} else if (operator === 'startsWith') {
|
|
76
|
+
truths.push(text(value, operand, (a, b) => a.startsWith(b)))
|
|
77
|
+
} else if (operator === 'endsWith') {
|
|
78
|
+
truths.push(text(value, operand, (a, b) => a.endsWith(b)))
|
|
79
|
+
} else {
|
|
80
|
+
const order = compareValues(sorted(value), sorted(operand))
|
|
81
|
+
if (operator === 'lt') truths.push(order < 0)
|
|
82
|
+
else if (operator === 'lte') truths.push(order <= 0)
|
|
83
|
+
else if (operator === 'gt') truths.push(order > 0)
|
|
84
|
+
else truths.push(order >= 0)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return and(truths)
|
|
88
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// The draft a memory() write changes: a copy of the store, and what the
|
|
2
|
+
// write changed for the feed (D-154).
|
|
3
|
+
import type {
|
|
4
|
+
AggregateRead,
|
|
5
|
+
Arithmetic,
|
|
6
|
+
Change,
|
|
7
|
+
Changed,
|
|
8
|
+
CountRead,
|
|
9
|
+
ListUpdate,
|
|
10
|
+
Read,
|
|
11
|
+
Row,
|
|
12
|
+
ScalarType,
|
|
13
|
+
Schema,
|
|
14
|
+
SchemaField,
|
|
15
|
+
Write,
|
|
16
|
+
} from './adapter.ts'
|
|
17
|
+
import { CheckFailed, OutOfRange, UniqueViolation } from './adapter.ts'
|
|
18
|
+
import type { Fields, Store, StoredRow } from './memory-read.ts'
|
|
19
|
+
import { Reader } from './memory-read.ts'
|
|
20
|
+
import { distinct, grouped } from './memory-aggregate.ts'
|
|
21
|
+
import { copy } from './memory-values.ts'
|
|
22
|
+
import { listed } from './list-update.ts'
|
|
23
|
+
import { computed } from './number-update.ts'
|
|
24
|
+
import { isInteger } from './query-where.ts'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A value as PostgreSQL gives it back: `-0` is `0`, which `pg` sends for a
|
|
28
|
+
* number, and JSON text for json (D-94).
|
|
29
|
+
*/
|
|
30
|
+
function storedAs(type: ScalarType | undefined, value: unknown): unknown {
|
|
31
|
+
if (type === 'json') {
|
|
32
|
+
const parsed: unknown = JSON.parse(JSON.stringify(value))
|
|
33
|
+
return parsed
|
|
34
|
+
}
|
|
35
|
+
if (type === 'number' && value === 0) return 0
|
|
36
|
+
return copy(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function keyOf(values: readonly unknown[]): string {
|
|
40
|
+
return JSON.stringify(
|
|
41
|
+
values.map((value) =>
|
|
42
|
+
value instanceof Date ? `date:${value.getTime()}` : value,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Draft {
|
|
48
|
+
readonly reader: Reader
|
|
49
|
+
readonly schema: Schema
|
|
50
|
+
/** What the write changed, in order, for the feed (D-154). */
|
|
51
|
+
readonly changed: Changed[] = []
|
|
52
|
+
|
|
53
|
+
constructor(store: Store, fields: Fields, schema: Schema) {
|
|
54
|
+
this.reader = new Reader(store, fields)
|
|
55
|
+
this.schema = schema
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
find(read: Read): Row[] {
|
|
59
|
+
const stored = [...this.reader.table(read.model).values()]
|
|
60
|
+
const rows = read.distinct ? distinct(this.reader, read, stored) : stored
|
|
61
|
+
return this.reader
|
|
62
|
+
.select(read.model, rows, read)
|
|
63
|
+
.map((row) => this.reader.output(read.model, row, read))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
count(read: CountRead): number {
|
|
67
|
+
return this.selected(read).length
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
aggregate(read: AggregateRead): unknown[][] {
|
|
71
|
+
return grouped(this.reader, read, this.selected(read))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private selected(read: CountRead): StoredRow[] {
|
|
75
|
+
const rows = [...this.reader.table(read.model).values()]
|
|
76
|
+
return rows.filter(
|
|
77
|
+
(row) => this.reader.matches(read.model, row, read.where) === true,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
write(write: Write): Row[] {
|
|
82
|
+
for (const [index, check] of (write.checks ?? []).entries()) {
|
|
83
|
+
if (this.count(check) !== check.count) throw new CheckFailed(index)
|
|
84
|
+
}
|
|
85
|
+
const read = write.read
|
|
86
|
+
if (read !== undefined && write.before === true) {
|
|
87
|
+
const { model } = read
|
|
88
|
+
const deletes = write.changes.some(
|
|
89
|
+
(change) => change.kind === 'delete' && change.model === model,
|
|
90
|
+
)
|
|
91
|
+
const update = write.changes.find(
|
|
92
|
+
(change) => change.kind === 'update' && change.model === model,
|
|
93
|
+
)
|
|
94
|
+
if (!deletes) {
|
|
95
|
+
// The rows its update changed, as it left them (D-133).
|
|
96
|
+
const changed = update && this.matching(model, update)
|
|
97
|
+
const ids = new Set(changed?.map((row) => row.id))
|
|
98
|
+
this.change(write.changes)
|
|
99
|
+
return this.find(read).filter((row) => ids.has(row.id))
|
|
100
|
+
}
|
|
101
|
+
const rows = this.find(read)
|
|
102
|
+
this.change(write.changes)
|
|
103
|
+
// The rows the write's delete removed, as they were (D-104).
|
|
104
|
+
const table = this.reader.table(read.model)
|
|
105
|
+
return rows.filter((row) => !table.has(row.id))
|
|
106
|
+
}
|
|
107
|
+
this.change(write.changes)
|
|
108
|
+
return read === undefined ? [] : this.find(read)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private change(changes: readonly Change[]): void {
|
|
112
|
+
const touched = new Set<string>()
|
|
113
|
+
this.reader.removed.clear()
|
|
114
|
+
for (const [index, change] of changes.entries()) {
|
|
115
|
+
touched.add(change.model)
|
|
116
|
+
if (change.kind === 'delete') {
|
|
117
|
+
const removed = this.remove(change)
|
|
118
|
+
this.reader.removed.set(index, removed)
|
|
119
|
+
this.note({ kind: 'delete', model: change.model, ids: [...removed] })
|
|
120
|
+
} else this.apply(change)
|
|
121
|
+
}
|
|
122
|
+
for (const model of touched) this.checkUnique(model)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private note(change: Changed): void {
|
|
126
|
+
const rows = 'field' in change ? change.pairs : change.ids
|
|
127
|
+
if (rows?.length !== 0) this.changed.push(change)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private matching(model: string, change: Change): StoredRow[] {
|
|
131
|
+
if (change.kind !== 'update' && change.kind !== 'delete') return []
|
|
132
|
+
return [...this.reader.table(model).values()].filter(
|
|
133
|
+
(row) => this.reader.matches(model, row, change.where) === true,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private stored(
|
|
138
|
+
model: string,
|
|
139
|
+
value: { readonly [field: string]: unknown },
|
|
140
|
+
): { [field: string]: unknown } {
|
|
141
|
+
return Object.fromEntries(
|
|
142
|
+
Object.entries(value).map(([key, item]) => {
|
|
143
|
+
const field = this.reader.field(model, key)
|
|
144
|
+
const type = field?.kind === 'scalar' ? field.type : undefined
|
|
145
|
+
return [key, storedAs(type, item)]
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The numbers and lists a change computes on `row` (D-132, D-149). */
|
|
151
|
+
private computed(
|
|
152
|
+
model: string,
|
|
153
|
+
row: StoredRow,
|
|
154
|
+
compute: { readonly [field: string]: Arithmetic | ListUpdate } = {},
|
|
155
|
+
): { [field: string]: unknown } {
|
|
156
|
+
return Object.fromEntries(
|
|
157
|
+
Object.entries(compute).map(([key, operation]): [string, unknown] => {
|
|
158
|
+
if ('elements' in operation) return [key, listed(row[key], operation)]
|
|
159
|
+
const field = this.reader.field(model, key)
|
|
160
|
+
const integer = field?.kind === 'scalar' && field.integer === true
|
|
161
|
+
const result = computed(row[key], operation, integer)
|
|
162
|
+
if (integer ? !isInteger(result) : !Number.isFinite(result)) {
|
|
163
|
+
throw new OutOfRange(model, key)
|
|
164
|
+
}
|
|
165
|
+
return [key, result]
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Applies `delete`; returns the ids it removed. */
|
|
171
|
+
private remove(change: Extract<Change, { kind: 'delete' }>): Set<string> {
|
|
172
|
+
const table = this.reader.table(change.model)
|
|
173
|
+
const removed = new Set<string>()
|
|
174
|
+
for (const row of this.matching(change.model, change)) {
|
|
175
|
+
removed.add(row.id)
|
|
176
|
+
table.delete(row.id)
|
|
177
|
+
for (const field of this.joinFields(change.model)) {
|
|
178
|
+
this.unlink(change.model, field, row.id, [
|
|
179
|
+
...(this.side(change.model, field.name).get(row.id) ?? []),
|
|
180
|
+
])
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return removed
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private apply(change: Exclude<Change, { kind: 'delete' }>): void {
|
|
187
|
+
const table = this.reader.table(change.model)
|
|
188
|
+
switch (change.kind) {
|
|
189
|
+
case 'insert': {
|
|
190
|
+
if (table.has(change.row.id)) {
|
|
191
|
+
throw new UniqueViolation(change.model, ['id'])
|
|
192
|
+
}
|
|
193
|
+
table.set(change.row.id, {
|
|
194
|
+
...this.stored(change.model, change.row),
|
|
195
|
+
id: change.row.id,
|
|
196
|
+
})
|
|
197
|
+
this.note({ kind: 'insert', model: change.model, ids: [change.row.id] })
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
case 'update': {
|
|
201
|
+
const rows = this.matching(change.model, change)
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
const set = {
|
|
204
|
+
...change.set,
|
|
205
|
+
...this.computed(change.model, row, change.compute),
|
|
206
|
+
}
|
|
207
|
+
table.set(row.id, {
|
|
208
|
+
...row,
|
|
209
|
+
...this.stored(change.model, set),
|
|
210
|
+
id: row.id,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
const fields = [
|
|
214
|
+
...Object.keys(change.set),
|
|
215
|
+
...Object.keys(change.compute ?? {}),
|
|
216
|
+
]
|
|
217
|
+
const ids = rows.map((row) => row.id)
|
|
218
|
+
this.note({ kind: 'update', model: change.model, ids, fields })
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
case 'link':
|
|
222
|
+
case 'unlink': {
|
|
223
|
+
const field = this.reader.field(change.model, change.field)
|
|
224
|
+
if (field?.kind !== 'many' || !field.join) {
|
|
225
|
+
throw new Error(`${change.model}.${change.field} has no join.`)
|
|
226
|
+
}
|
|
227
|
+
const { model, id } = change
|
|
228
|
+
const targets =
|
|
229
|
+
change.kind === 'unlink'
|
|
230
|
+
? this.unlink(model, field, id, change.targets)
|
|
231
|
+
: change.targets.filter((target) =>
|
|
232
|
+
this.link(model, field, id, target),
|
|
233
|
+
)
|
|
234
|
+
const pairs = targets.map((target): [string, string] => [id, target])
|
|
235
|
+
this.note({ kind: change.kind, model, field: field.name, pairs })
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private joinFields(model: string): SchemaField[] {
|
|
242
|
+
return [...(this.reader.fields.get(model)?.values() ?? [])].filter(
|
|
243
|
+
(field) =>
|
|
244
|
+
field.kind === 'many' && field.join && field.from === undefined,
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private side(model: string, field: string): Map<string, Set<string>> {
|
|
249
|
+
const name = `${model}.${field}`
|
|
250
|
+
let side = this.reader.store.joins.get(name)
|
|
251
|
+
if (side === undefined) {
|
|
252
|
+
side = new Map()
|
|
253
|
+
this.reader.store.joins.set(name, side)
|
|
254
|
+
}
|
|
255
|
+
return side
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private ids(model: string, field: string, id: string): Set<string> {
|
|
259
|
+
const side = this.side(model, field)
|
|
260
|
+
let ids = side.get(id)
|
|
261
|
+
if (ids === undefined) {
|
|
262
|
+
ids = new Set()
|
|
263
|
+
side.set(id, ids)
|
|
264
|
+
}
|
|
265
|
+
return ids
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Links `id` to `target` unless they were; tells whether it did. */
|
|
269
|
+
private link(
|
|
270
|
+
model: string,
|
|
271
|
+
field: Extract<SchemaField, { kind: 'many' }>,
|
|
272
|
+
id: string,
|
|
273
|
+
target: string,
|
|
274
|
+
): boolean {
|
|
275
|
+
const ids = this.ids(model, field.name, id)
|
|
276
|
+
if (ids.has(target)) return false
|
|
277
|
+
ids.add(target)
|
|
278
|
+
this.ids(field.target, field.inverse, target).add(id)
|
|
279
|
+
return true
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Unlinks `id` from `targets`; returns the targets it was linked to. */
|
|
283
|
+
private unlink(
|
|
284
|
+
model: string,
|
|
285
|
+
field: SchemaField,
|
|
286
|
+
id: string,
|
|
287
|
+
targets: readonly string[],
|
|
288
|
+
): string[] {
|
|
289
|
+
if (field.kind !== 'many') return []
|
|
290
|
+
return targets.filter((target) => {
|
|
291
|
+
const removed = this.side(model, field.name).get(id)?.delete(target)
|
|
292
|
+
this.side(field.target, field.inverse).get(target)?.delete(id)
|
|
293
|
+
return removed === true
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Unique fields, unique indexes and one-to-one keys; `null` never clashes. */
|
|
298
|
+
private checkUnique(model: string): void {
|
|
299
|
+
const definition = this.schema.models.find((each) => each.name === model)
|
|
300
|
+
if (definition === undefined) return
|
|
301
|
+
const sets: (readonly string[])[] = definition.indexes
|
|
302
|
+
.filter((index) => index.unique)
|
|
303
|
+
.map((index) => index.fields)
|
|
304
|
+
for (const field of definition.fields) {
|
|
305
|
+
if (field.kind !== 'many' && field.unique) sets.push([field.name])
|
|
306
|
+
}
|
|
307
|
+
const rows = [...this.reader.table(model).values()]
|
|
308
|
+
for (const fields of sets) {
|
|
309
|
+
const seen = new Set<string>()
|
|
310
|
+
for (const row of rows) {
|
|
311
|
+
const values = fields.map((field) => row[field] ?? null)
|
|
312
|
+
if (values.includes(null)) continue
|
|
313
|
+
const found = keyOf(values)
|
|
314
|
+
if (seen.has(found)) throw new UniqueViolation(model, fields)
|
|
315
|
+
seen.add(found)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|