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,137 @@
|
|
|
1
|
+
// `groupBy`'s arguments and result (D-146): `having` (D-149) and `orderBy`
|
|
2
|
+
// name the fields of `by` or an aggregate, and each group has the keys the
|
|
3
|
+
// arguments ask for.
|
|
4
|
+
import type {
|
|
5
|
+
Aggregated,
|
|
6
|
+
AggregateArgs,
|
|
7
|
+
Aggregates,
|
|
8
|
+
DecimalKeys,
|
|
9
|
+
IsOrdered,
|
|
10
|
+
KeysWith,
|
|
11
|
+
Summed,
|
|
12
|
+
ValueOf,
|
|
13
|
+
} from './query-aggregate.ts'
|
|
14
|
+
import type { Exact } from './exact.ts'
|
|
15
|
+
import type { ScalarFilter } from './query-filter.ts'
|
|
16
|
+
import type { RecordOf, StoredRecord } from './record.ts'
|
|
17
|
+
|
|
18
|
+
type Direction = 'asc' | 'desc'
|
|
19
|
+
type Sorted<K extends PropertyKey> = {
|
|
20
|
+
readonly [P in K]?: Direction | undefined
|
|
21
|
+
}
|
|
22
|
+
/** A value's sort: an optional one puts its `null`s first or last (D-149). */
|
|
23
|
+
export type SortOrder<V> = null extends V
|
|
24
|
+
? | Direction
|
|
25
|
+
| {
|
|
26
|
+
readonly sort: Direction
|
|
27
|
+
readonly nulls?: 'first' | 'last' | undefined
|
|
28
|
+
}
|
|
29
|
+
: Direction
|
|
30
|
+
/** The `one` relations the row stores (D-37). */
|
|
31
|
+
type StoredOnes<F, N> = Exclude<
|
|
32
|
+
keyof StoredRecord<F, N & string>,
|
|
33
|
+
keyof RecordOf<F>
|
|
34
|
+
>
|
|
35
|
+
/** What a group takes: a scalar an aggregate takes, or a stored relation. */
|
|
36
|
+
export type GroupField<F, N> = Aggregated<F> | StoredOnes<F, N>
|
|
37
|
+
|
|
38
|
+
type GroupOrderBy<F, N, B extends PropertyKey> = {
|
|
39
|
+
readonly [K in B]?: SortOrder<GroupValue<F, N, K>> | undefined
|
|
40
|
+
} & {
|
|
41
|
+
readonly _count?:
|
|
42
|
+
| Direction
|
|
43
|
+
| ({ readonly _all?: Direction | undefined } & Sorted<Aggregated<F>>)
|
|
44
|
+
| undefined
|
|
45
|
+
readonly _sum?: Sorted<Summed<F>> | undefined
|
|
46
|
+
readonly _avg?: Sorted<Summed<F>> | undefined
|
|
47
|
+
readonly _min?: Sorted<KeysWith<F, number | string | Date>> | undefined
|
|
48
|
+
readonly _max?: Sorted<KeysWith<F, number | string | Date>> | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A value's filter: an id's has no `mode`, a decimal's or a day's compares. */
|
|
52
|
+
type ValueFilter<F, K> = K extends 'id'
|
|
53
|
+
? Omit<ScalarFilter<string>, 'mode'>
|
|
54
|
+
: ScalarFilter<ValueOf<F, K>, K extends keyof F ? IsOrdered<F[K]> : false>
|
|
55
|
+
type Filtered<V, Ordered = false> = V | ScalarFilter<V, Ordered> | undefined
|
|
56
|
+
|
|
57
|
+
/** Field `K`'s aggregates, each filtered as the value it gives (D-149). */
|
|
58
|
+
type AggregateFilters<F, K> = {
|
|
59
|
+
readonly _count?: Filtered<number>
|
|
60
|
+
} & (K extends Summed<F>
|
|
61
|
+
? {
|
|
62
|
+
readonly _sum?: K extends DecimalKeys<F>
|
|
63
|
+
? Filtered<string, true>
|
|
64
|
+
: Filtered<number>
|
|
65
|
+
readonly _avg?: Filtered<number>
|
|
66
|
+
}
|
|
67
|
+
: unknown) &
|
|
68
|
+
(K extends KeysWith<F, number | string | Date>
|
|
69
|
+
? {
|
|
70
|
+
readonly _min?: ValueOf<F, K> | ValueFilter<F, K> | undefined
|
|
71
|
+
readonly _max?: ValueOf<F, K> | ValueFilter<F, K> | undefined
|
|
72
|
+
}
|
|
73
|
+
: unknown)
|
|
74
|
+
|
|
75
|
+
/** The groups kept: a field of `by` by its value, any field by its aggregates. */
|
|
76
|
+
type Having<F, N, B extends PropertyKey> = {
|
|
77
|
+
readonly [K in Aggregated<F>]?:
|
|
78
|
+
| (K extends B
|
|
79
|
+
? GroupValue<F, N, K> | (ValueFilter<F, K> & AggregateFilters<F, K>)
|
|
80
|
+
: AggregateFilters<F, K>)
|
|
81
|
+
| undefined
|
|
82
|
+
} & {
|
|
83
|
+
readonly AND?: Having<F, N, B> | readonly Having<F, N, B>[] | undefined
|
|
84
|
+
readonly OR?: readonly Having<F, N, B>[] | undefined
|
|
85
|
+
readonly NOT?: Having<F, N, B> | readonly Having<F, N, B>[] | undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type GroupByArgs<
|
|
89
|
+
F,
|
|
90
|
+
N,
|
|
91
|
+
B extends GroupField<F, N> = GroupField<F, N>,
|
|
92
|
+
> = AggregateArgs<F> & {
|
|
93
|
+
readonly by: readonly [B, ...B[]]
|
|
94
|
+
readonly having?: Having<F, N, B> | undefined
|
|
95
|
+
readonly orderBy?:
|
|
96
|
+
GroupOrderBy<F, N, B> | readonly GroupOrderBy<F, N, B>[] | undefined
|
|
97
|
+
readonly skip?: number | undefined
|
|
98
|
+
readonly take?: number | undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Arguments `A`, checked against the fields of their `by`: no unknown key,
|
|
103
|
+
* and a `having` that fits them, which `Exact` does not check as it skips
|
|
104
|
+
* values: a value for a field outside `by` would pass.
|
|
105
|
+
*/
|
|
106
|
+
export type GroupChecked<
|
|
107
|
+
F,
|
|
108
|
+
N,
|
|
109
|
+
A extends { readonly by: readonly unknown[] },
|
|
110
|
+
> = Exact<A, GroupByArgs<F, N, A['by'][number] & GroupField<F, N>>> &
|
|
111
|
+
(A extends { readonly having: infer H }
|
|
112
|
+
? [H] extends [Having<F, N, A['by'][number] & PropertyKey> | undefined]
|
|
113
|
+
? unknown
|
|
114
|
+
: {
|
|
115
|
+
readonly having: {
|
|
116
|
+
readonly error: 'a field outside by takes aggregates only'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
: unknown)
|
|
120
|
+
|
|
121
|
+
/** A relation's target id, `null` when unset or unreadable (D-86). */
|
|
122
|
+
type GroupValue<F, N, K> =
|
|
123
|
+
K extends StoredOnes<F, N>
|
|
124
|
+
? { readonly id: string } | null
|
|
125
|
+
: K extends keyof StoredRecord<F, N & string>
|
|
126
|
+
? StoredRecord<F, N & string>[K]
|
|
127
|
+
: never
|
|
128
|
+
|
|
129
|
+
/** What `groupBy` returns for the arguments `A`. */
|
|
130
|
+
export type Groups<F, N, A> = A extends {
|
|
131
|
+
readonly by: readonly (infer B)[]
|
|
132
|
+
}
|
|
133
|
+
? ({ readonly [K in B & PropertyKey]: GroupValue<F, N, K> } & Aggregates<
|
|
134
|
+
F,
|
|
135
|
+
A
|
|
136
|
+
>)[]
|
|
137
|
+
: never
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A json field's path filter (D-149), Prisma's PostgreSQL JSON filter in
|
|
2
|
+
// camelCase: `{ meta: { path: ['author', 'name'], equals: 'Ada' } }`.
|
|
3
|
+
import type { Condition } from './adapter.ts'
|
|
4
|
+
import { InvalidQuery } from './errors.ts'
|
|
5
|
+
import {
|
|
6
|
+
clean,
|
|
7
|
+
cleanText,
|
|
8
|
+
entries,
|
|
9
|
+
isJson,
|
|
10
|
+
isObject,
|
|
11
|
+
unknownKey,
|
|
12
|
+
} from './query-values.ts'
|
|
13
|
+
|
|
14
|
+
const json = (operand: unknown, at: string): unknown => {
|
|
15
|
+
if (isJson(operand)) return operand
|
|
16
|
+
throw new InvalidQuery(at, `${at} must be a JSON value.`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const operands: {
|
|
20
|
+
readonly [operator: string]: (operand: unknown, at: string) => unknown
|
|
21
|
+
} = {
|
|
22
|
+
equals: json,
|
|
23
|
+
not: json,
|
|
24
|
+
...Object.fromEntries(
|
|
25
|
+
['lt', 'lte', 'gt', 'gte'].map((operator) => [
|
|
26
|
+
operator,
|
|
27
|
+
(operand: unknown, at: string) => {
|
|
28
|
+
if (typeof operand === 'string' && clean(operand)) return operand
|
|
29
|
+
if (typeof operand === 'number' && Number.isFinite(operand)) {
|
|
30
|
+
return operand
|
|
31
|
+
}
|
|
32
|
+
throw new InvalidQuery(at, `${at} must be a number or a string.`)
|
|
33
|
+
},
|
|
34
|
+
]),
|
|
35
|
+
),
|
|
36
|
+
...Object.fromEntries(
|
|
37
|
+
['stringContains', 'stringStartsWith', 'stringEndsWith'].map((operator) => [
|
|
38
|
+
operator,
|
|
39
|
+
cleanText,
|
|
40
|
+
]),
|
|
41
|
+
),
|
|
42
|
+
mode: (operand, at) => {
|
|
43
|
+
if (operand === 'insensitive') return operand
|
|
44
|
+
throw new InvalidQuery(at, `${at} must be 'insensitive'.`)
|
|
45
|
+
},
|
|
46
|
+
// A value is a list of one, as in Prisma.
|
|
47
|
+
arrayContains: (operand, at) => {
|
|
48
|
+
const value = json(operand, at)
|
|
49
|
+
if (!Array.isArray(value)) return [value]
|
|
50
|
+
const items: readonly unknown[] = value
|
|
51
|
+
return items
|
|
52
|
+
},
|
|
53
|
+
arrayStartsWith: json,
|
|
54
|
+
arrayEndsWith: json,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether a json field's filter is a path filter, which names `path`. */
|
|
58
|
+
export const isPathFilter = (value: unknown): boolean =>
|
|
59
|
+
isObject(value) && value['path'] !== undefined
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A path filter of `model`'s json field, `undefined` when it has no operator,
|
|
63
|
+
* as `mode` alone (D-140).
|
|
64
|
+
*/
|
|
65
|
+
export function jsonCondition(
|
|
66
|
+
model: string,
|
|
67
|
+
value: unknown,
|
|
68
|
+
path: string,
|
|
69
|
+
): Condition | undefined {
|
|
70
|
+
const condition: { [operator: string]: unknown } = {}
|
|
71
|
+
for (const [operator, operand] of entries(value, path)) {
|
|
72
|
+
const at = `${path}.${operator}`
|
|
73
|
+
if (operator === 'path') {
|
|
74
|
+
if (!Array.isArray(operand)) {
|
|
75
|
+
throw new InvalidQuery(at, `${at} must be a list of keys.`)
|
|
76
|
+
}
|
|
77
|
+
condition['path'] = operand.map((key: unknown, index) =>
|
|
78
|
+
cleanText(key, `${at}.${index}`),
|
|
79
|
+
)
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const check = operands[operator]
|
|
83
|
+
if (check === undefined) throw unknownKey(at, model)
|
|
84
|
+
condition[operator] = check(operand, at)
|
|
85
|
+
}
|
|
86
|
+
const tested = Object.keys(condition).filter(
|
|
87
|
+
(operator) => operator !== 'path' && operator !== 'mode',
|
|
88
|
+
)
|
|
89
|
+
return tested.length === 0 ? undefined : condition
|
|
90
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// A read's `orderBy` (D-20): the row's fields, a field reached through
|
|
2
|
+
// `one` relations, or the number of a `many` relation's rows (D-134), or
|
|
3
|
+
// how well the row matches the read's `_search` (D-148).
|
|
4
|
+
import type { Order, Sort, Step } from './adapter.ts'
|
|
5
|
+
import { InvalidQuery } from './errors.ts'
|
|
6
|
+
import type { ModelInfo } from './schema.ts'
|
|
7
|
+
import { searchedFields } from './query-search.ts'
|
|
8
|
+
import type { Models, Scope } from './query-where.ts'
|
|
9
|
+
import {
|
|
10
|
+
entries,
|
|
11
|
+
isObject,
|
|
12
|
+
modelOf,
|
|
13
|
+
systemFields,
|
|
14
|
+
unknownKey,
|
|
15
|
+
} from './query-where.ts'
|
|
16
|
+
|
|
17
|
+
export function direction(value: unknown, path: string): 'asc' | 'desc' {
|
|
18
|
+
if (value !== 'asc' && value !== 'desc') {
|
|
19
|
+
throw new InvalidQuery(path, `${path} must be 'asc' or 'desc'.`)
|
|
20
|
+
}
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A sort key's value: a direction, or on an optional field `{ sort, nulls }`
|
|
26
|
+
* (D-149).
|
|
27
|
+
*/
|
|
28
|
+
export function sortOf(value: unknown, path: string, optional: boolean): Sort {
|
|
29
|
+
if (!isObject(value)) return { direction: direction(value, path) }
|
|
30
|
+
if (!optional) {
|
|
31
|
+
throw new InvalidQuery(
|
|
32
|
+
path,
|
|
33
|
+
`${path} is required: it sorts by 'asc' or 'desc'.`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
const { sort, nulls, ...rest } = value
|
|
37
|
+
const [extra] = Object.keys(rest)
|
|
38
|
+
if (extra !== undefined) {
|
|
39
|
+
throw new InvalidQuery(
|
|
40
|
+
`${path}.${extra}`,
|
|
41
|
+
`${path} takes sort and nulls only.`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
const sorted = direction(sort, `${path}.sort`)
|
|
45
|
+
if (nulls === undefined) return { direction: sorted }
|
|
46
|
+
if (nulls !== 'first' && nulls !== 'last') {
|
|
47
|
+
const at = `${path}.nulls`
|
|
48
|
+
throw new InvalidQuery(at, `${at} must be 'first' or 'last'.`)
|
|
49
|
+
}
|
|
50
|
+
return { direction: sorted, nulls }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A scalar field that sorts: no json, no list, no plugin field (D-94). */
|
|
54
|
+
function sortable(model: ModelInfo, field: string, path: string): void {
|
|
55
|
+
const known = model.fields.get(field)
|
|
56
|
+
if (known === undefined) {
|
|
57
|
+
if (!systemFields.includes(field)) throw unknownKey(path, model.name)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (known.kind !== 'scalar') return
|
|
61
|
+
if (known.type !== 'json' && !known.list && known.codec === undefined) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const kind =
|
|
65
|
+
known.codec !== undefined
|
|
66
|
+
? `a ${known.codec.plugin} field`
|
|
67
|
+
: known.type === 'json'
|
|
68
|
+
? 'a json field'
|
|
69
|
+
: 'a list'
|
|
70
|
+
throw new InvalidQuery(path, `${path} is ${kind}, which cannot be sorted.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The sort keys of one object of `orderBy`, in its keys' order. */
|
|
74
|
+
function keys(
|
|
75
|
+
models: Models,
|
|
76
|
+
model: ModelInfo,
|
|
77
|
+
part: unknown,
|
|
78
|
+
at: string,
|
|
79
|
+
scope: Scope | undefined,
|
|
80
|
+
through: readonly Step[],
|
|
81
|
+
search: string | undefined,
|
|
82
|
+
): Order[] {
|
|
83
|
+
const order: Order[] = []
|
|
84
|
+
const path = through.length === 0 ? {} : { through }
|
|
85
|
+
const searched = searchedFields(model.fields.values()).length > 0
|
|
86
|
+
for (const [field, value] of entries(part, at)) {
|
|
87
|
+
const where = `${at}.${field}`
|
|
88
|
+
if (field === '_relevance' && through.length === 0 && searched) {
|
|
89
|
+
if (search === undefined) {
|
|
90
|
+
throw new InvalidQuery(
|
|
91
|
+
where,
|
|
92
|
+
`${where} sorts by the _search at the top of the read's where, which has none.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
order.push({
|
|
96
|
+
field,
|
|
97
|
+
direction: direction(value, where),
|
|
98
|
+
relevance: search,
|
|
99
|
+
})
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
sortable(model, field, where)
|
|
103
|
+
scope?.check(model, field, where, 'sorts')
|
|
104
|
+
const known = model.fields.get(field)
|
|
105
|
+
if (known === undefined || known.kind === 'scalar') {
|
|
106
|
+
const optional = known?.optional === true
|
|
107
|
+
order.push({ field, ...sortOf(value, where, optional), ...path })
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const target = modelOf(models, known.target)
|
|
111
|
+
if (known.kind === 'one') {
|
|
112
|
+
const readable = scope?.readable(target.name)
|
|
113
|
+
const step = { relation: field, ...(readable && { where: readable }) }
|
|
114
|
+
const next = [...through, step]
|
|
115
|
+
order.push(...keys(models, target, value, where, scope, next, search))
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
const [only, ...rest] = isObject(value) ? Object.entries(value) : []
|
|
119
|
+
if (only?.[0] !== '_count' || rest.length > 0) {
|
|
120
|
+
throw new InvalidQuery(
|
|
121
|
+
where,
|
|
122
|
+
`${where} is a many relation, sorted by { _count: 'asc' | 'desc' }.`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
const sorted = direction(only[1], `${where}._count`)
|
|
126
|
+
const count = scope?.related(model, known) ?? {}
|
|
127
|
+
order.push({ field, direction: sorted, ...path, count })
|
|
128
|
+
}
|
|
129
|
+
return order
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A read's sort keys, ending with `id` so the order is total; `search` is
|
|
134
|
+
* the text of its where's `_search`.
|
|
135
|
+
*/
|
|
136
|
+
export function orderBy(
|
|
137
|
+
models: Models,
|
|
138
|
+
model: ModelInfo,
|
|
139
|
+
value: unknown,
|
|
140
|
+
path: string,
|
|
141
|
+
scope: Scope | undefined,
|
|
142
|
+
search?: string,
|
|
143
|
+
): Order[] {
|
|
144
|
+
const parts = Array.isArray(value) ? value : [value]
|
|
145
|
+
const order = parts.flatMap((part: unknown, index) => {
|
|
146
|
+
const at = Array.isArray(value) ? `${path}.${index}` : path
|
|
147
|
+
return keys(models, model, part, at, scope, [], search)
|
|
148
|
+
})
|
|
149
|
+
// The order is total, so a cursor and a page are exact.
|
|
150
|
+
const total = order.some(
|
|
151
|
+
(each) =>
|
|
152
|
+
each.field === 'id' &&
|
|
153
|
+
each.through === undefined &&
|
|
154
|
+
each.count === undefined,
|
|
155
|
+
)
|
|
156
|
+
if (!total) order.push({ field: 'id', direction: 'asc' })
|
|
157
|
+
return order
|
|
158
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A row as a query returns it (D-149): the fields of its shape, its
|
|
2
|
+
// relations below, its virtuals as the read computed them (D-151), without
|
|
3
|
+
// the fields the caller's read rules hide (D-44).
|
|
4
|
+
import type { Shape } from './query-args.ts'
|
|
5
|
+
import type { ModelInfo } from './schema.ts'
|
|
6
|
+
import { isObject } from './query-where.ts'
|
|
7
|
+
|
|
8
|
+
/** The fields of a row its field read rules hide (D-44). */
|
|
9
|
+
export type Hidden = (
|
|
10
|
+
model: ModelInfo,
|
|
11
|
+
row: { readonly [field: string]: unknown },
|
|
12
|
+
fields: Iterable<string>,
|
|
13
|
+
) => ReadonlySet<string>
|
|
14
|
+
|
|
15
|
+
/** The values of each row's virtuals, as a read computed them (D-151). */
|
|
16
|
+
export type Computed = WeakMap<object, Map<string, unknown>>
|
|
17
|
+
|
|
18
|
+
/** The virtuals of `shape` a field read rule hides, its own or a need's. */
|
|
19
|
+
export function unseenVirtuals(
|
|
20
|
+
shape: Shape,
|
|
21
|
+
row: { readonly [field: string]: unknown },
|
|
22
|
+
hidden: Hidden | undefined,
|
|
23
|
+
): ReadonlySet<string> {
|
|
24
|
+
const reads = [...shape.virtuals.values()].map(({ info }) => info)
|
|
25
|
+
const names = reads.flatMap((info) => [info.name, ...info.needs])
|
|
26
|
+
const unseen = hidden?.(shape.model, row, names)
|
|
27
|
+
return new Set(
|
|
28
|
+
reads
|
|
29
|
+
.filter((info) => [info.name, ...info.needs].some((n) => unseen?.has(n)))
|
|
30
|
+
.map((info) => info.name),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A row as the query returns it, through the shape, without hidden fields. */
|
|
35
|
+
export function project(
|
|
36
|
+
row: unknown,
|
|
37
|
+
shape: Shape,
|
|
38
|
+
hidden?: Hidden,
|
|
39
|
+
computed?: Computed,
|
|
40
|
+
): unknown {
|
|
41
|
+
if (Array.isArray(row)) {
|
|
42
|
+
return row.map((item: unknown) => project(item, shape, hidden, computed))
|
|
43
|
+
}
|
|
44
|
+
if (!isObject(row)) return null
|
|
45
|
+
const names = [...shape.fields, ...shape.relations.keys(), ...shape.counts]
|
|
46
|
+
const unseen = hidden?.(shape.model, row, names)
|
|
47
|
+
const result: { [field: string]: unknown } = {}
|
|
48
|
+
for (const field of shape.fields) {
|
|
49
|
+
if (!unseen?.has(field)) result[field] = row[field] ?? null
|
|
50
|
+
}
|
|
51
|
+
for (const [name, nested] of shape.relations) {
|
|
52
|
+
if (!unseen?.has(name)) {
|
|
53
|
+
result[name] = project(row[name], nested, hidden, computed)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const values = computed?.get(row)
|
|
57
|
+
const unseenVirtual =
|
|
58
|
+
shape.virtuals.size > 0 ? unseenVirtuals(shape, row, hidden) : new Set()
|
|
59
|
+
for (const name of shape.virtuals.keys()) {
|
|
60
|
+
if (!unseenVirtual.has(name)) result[name] = values?.get(name) ?? null
|
|
61
|
+
}
|
|
62
|
+
if (shape.counts.length > 0) {
|
|
63
|
+
const counts: { readonly [name: string]: unknown } = isObject(row['_count'])
|
|
64
|
+
? row['_count']
|
|
65
|
+
: {}
|
|
66
|
+
result['_count'] = Object.fromEntries(
|
|
67
|
+
shape.counts
|
|
68
|
+
.filter((name) => !unseen?.has(name))
|
|
69
|
+
.map((name) => [name, counts[name] ?? 0]),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Full-text search (D-148): `_search` keeps the rows whose searched fields
|
|
2
|
+
// match a text, `_relevance` sorts them by how well.
|
|
3
|
+
import type { SchemaField } from './adapter.ts'
|
|
4
|
+
import type { fieldType } from './symbols.ts'
|
|
5
|
+
|
|
6
|
+
/** The fields of `F` built with `search: true`. */
|
|
7
|
+
type Searched<F> = {
|
|
8
|
+
[K in keyof F]: F[K] extends {
|
|
9
|
+
readonly [fieldType]: { readonly options: { readonly search: true } }
|
|
10
|
+
}
|
|
11
|
+
? K
|
|
12
|
+
: never
|
|
13
|
+
}[keyof F]
|
|
14
|
+
|
|
15
|
+
/** A model with a searched field takes `_search` in its `where`. */
|
|
16
|
+
export type SearchWhere<F> = [Searched<F>] extends [never]
|
|
17
|
+
? unknown
|
|
18
|
+
: { readonly _search?: string | undefined }
|
|
19
|
+
|
|
20
|
+
/** And `_relevance` at the top of its `orderBy`. */
|
|
21
|
+
export type Relevance<F> = [Searched<F>] extends [never]
|
|
22
|
+
? unknown
|
|
23
|
+
: { readonly _relevance?: 'asc' | 'desc' | undefined }
|
|
24
|
+
|
|
25
|
+
/** The names of a model's searched fields, in declaration order. */
|
|
26
|
+
export const searchedFields = (
|
|
27
|
+
fields: Iterable<SchemaField>,
|
|
28
|
+
): readonly string[] =>
|
|
29
|
+
[...fields].flatMap((field) =>
|
|
30
|
+
field.kind === 'scalar' && field.search === true ? [field.name] : [],
|
|
31
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The values a query or a write may hold, checked against a field (D-94,
|
|
2
|
+
// D-139): what PostgreSQL stores as written, in each scalar type.
|
|
3
|
+
import type { ScalarType } from './adapter.ts'
|
|
4
|
+
import { decimalIssue } from './decimal.ts'
|
|
5
|
+
import { InvalidQuery } from './errors.ts'
|
|
6
|
+
import type { FieldInfo } from './schema.ts'
|
|
7
|
+
|
|
8
|
+
export type Scalar = Extract<FieldInfo, { readonly kind: 'scalar' }>
|
|
9
|
+
|
|
10
|
+
/** What a scalar value is checked against (D-139). */
|
|
11
|
+
export type ScalarCheck = Pick<
|
|
12
|
+
Scalar,
|
|
13
|
+
'type' | 'list' | 'integer' | 'values' | 'precision' | 'scale'
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
/** An integer field's values (D-132). */
|
|
17
|
+
export const isInteger = (value: unknown): boolean =>
|
|
18
|
+
Number.isInteger(value) &&
|
|
19
|
+
Number(value) >= -2_147_483_648 &&
|
|
20
|
+
Number(value) <= 2_147_483_647
|
|
21
|
+
|
|
22
|
+
export function isObject(
|
|
23
|
+
value: unknown,
|
|
24
|
+
): value is { readonly [key: string]: unknown } {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === 'object' &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
!Array.isArray(value) &&
|
|
29
|
+
!(value instanceof Date)
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The entries of an argument object, without `undefined` (D-84). */
|
|
34
|
+
export function entries(value: unknown, path: string): [string, unknown][] {
|
|
35
|
+
if (!isObject(value))
|
|
36
|
+
throw new InvalidQuery(path, `${path} must be an object.`)
|
|
37
|
+
return Object.entries(value).filter(([, item]) => item !== undefined)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A string PostgreSQL stores as written: no NUL, no lone surrogate (D-94). */
|
|
41
|
+
export const clean = (value: string): boolean =>
|
|
42
|
+
value.isWellFormed() && !value.includes('\0')
|
|
43
|
+
|
|
44
|
+
export const cleanText = (value: unknown, at: string): string => {
|
|
45
|
+
if (typeof value === 'string' && clean(value)) return value
|
|
46
|
+
throw new InvalidQuery(at, `${at} must be a clean string.`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A date of years 1 to 9999 in UTC, which PostgreSQL reads back (D-94). */
|
|
50
|
+
export const storedDate = (value: unknown): value is Date => {
|
|
51
|
+
const year = value instanceof Date ? value.getUTCFullYear() : Number.NaN
|
|
52
|
+
return year >= 1 && year <= 9999
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A real day of years 1 to 9999, written `'2026-07-31'` (D-150). */
|
|
56
|
+
export function isDay(value: unknown): boolean {
|
|
57
|
+
const match =
|
|
58
|
+
typeof value === 'string' && /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
59
|
+
if (!match) return false
|
|
60
|
+
const [year = 0, month = 0, day = 0] = match.slice(1).map(Number)
|
|
61
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
|
|
62
|
+
const days =
|
|
63
|
+
month === 2 ? (leap ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31
|
|
64
|
+
return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** JSON data whose strings and keys are clean (D-94). */
|
|
68
|
+
export function isJson(value: unknown): boolean {
|
|
69
|
+
if (value === null || typeof value === 'boolean') return true
|
|
70
|
+
if (typeof value === 'string') return clean(value)
|
|
71
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
72
|
+
if (Array.isArray(value)) return value.every(isJson)
|
|
73
|
+
if (!isObject(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
return Object.entries(value).every(
|
|
77
|
+
([key, item]) => clean(key) && isJson(item),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function unknownKey(path: string, model: string): InvalidQuery {
|
|
82
|
+
return new InvalidQuery(path, `Unknown key '${path}' on ${model}.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const integerRange = 'from -2,147,483,648 to 2,147,483,647'
|
|
86
|
+
|
|
87
|
+
/** Each scalar type's check, and its message when a value does not fit. */
|
|
88
|
+
const misfits: {
|
|
89
|
+
readonly [T in ScalarType]: (
|
|
90
|
+
value: unknown,
|
|
91
|
+
field: ScalarCheck,
|
|
92
|
+
) => string | undefined
|
|
93
|
+
} = {
|
|
94
|
+
string: (value) => {
|
|
95
|
+
if (typeof value !== 'string') return 'Expected a string.'
|
|
96
|
+
return clean(value)
|
|
97
|
+
? undefined
|
|
98
|
+
: 'Expected a string without a NUL or a lone surrogate.'
|
|
99
|
+
},
|
|
100
|
+
number: (value, field) => {
|
|
101
|
+
if (field.integer !== true) {
|
|
102
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
103
|
+
? undefined
|
|
104
|
+
: 'Expected a finite number.'
|
|
105
|
+
}
|
|
106
|
+
return isInteger(value)
|
|
107
|
+
? undefined
|
|
108
|
+
: `Expected a whole number ${integerRange}.`
|
|
109
|
+
},
|
|
110
|
+
decimal: decimalIssue,
|
|
111
|
+
boolean: (value) =>
|
|
112
|
+
typeof value === 'boolean' ? undefined : 'Expected a boolean.',
|
|
113
|
+
dateTime: (value) =>
|
|
114
|
+
storedDate(value) ? undefined : 'Expected a Date of years 1 to 9999.',
|
|
115
|
+
date: (value) =>
|
|
116
|
+
isDay(value) ? undefined : "Expected a day written '2026-07-31'.",
|
|
117
|
+
json: (value) =>
|
|
118
|
+
isJson(value)
|
|
119
|
+
? undefined
|
|
120
|
+
: 'Expected JSON data, its strings without a NUL or a lone surrogate.',
|
|
121
|
+
enum: (value, field) =>
|
|
122
|
+
typeof value === 'string' && (field.values ?? []).includes(value)
|
|
123
|
+
? undefined
|
|
124
|
+
: `Expected one of ${(field.values ?? []).join(', ')}.`,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The message of a value that does not fit `field`, if it does not. */
|
|
128
|
+
export const misfit = (
|
|
129
|
+
value: unknown,
|
|
130
|
+
field: ScalarCheck,
|
|
131
|
+
): string | undefined => misfits[field.type](value, field)
|