@routier/core 0.2.1 → 0.4.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/LICENSE +21 -0
- package/{readme.md → README.md} +3 -3
- package/dist/assertions/index.cjs +191 -0
- package/dist/assertions/index.cjs.map +1 -0
- package/dist/assertions/index.js +43 -63
- package/dist/assertions/index.js.map +1 -1
- package/dist/capabilities/index.cjs +820 -0
- package/dist/capabilities/index.cjs.map +1 -0
- package/dist/capabilities/index.js +584 -558
- package/dist/capabilities/index.js.map +1 -1
- package/dist/codegen/handlers/CloneHandlerBuilder.d.ts +10 -1
- package/dist/codegen/handlers/clone/CloneArrayHandler.d.ts +3 -0
- package/dist/codegen/handlers/clone/CloneDateHandler.d.ts +13 -0
- package/dist/codegen/handlers/clone/CloneValueHandler.d.ts +16 -0
- package/dist/codegen/handlers/compare/CompareComputedHandler.d.ts +10 -0
- package/dist/codegen/handlers/compare/CompareFunctionHandler.d.ts +10 -0
- package/dist/codegen/handlers/deserialize/DeserializeArrayHandler.d.ts +14 -0
- package/dist/codegen/handlers/enableChangeTracking/EnableChangeTrackingArrayHandler.d.ts +11 -0
- package/dist/codegen/handlers/enrichment/EnrichmentArrayHandler.d.ts +11 -0
- package/dist/codegen/handlers/freeze/FreezeArrayHandler.d.ts +6 -0
- package/dist/codegen/handlers/hash/HashArrayHandler.d.ts +10 -0
- package/dist/codegen/handlers/hash/HashFileHandler.d.ts +24 -0
- package/dist/codegen/handlers/hash/HashObjectHandler.d.ts +10 -0
- package/dist/codegen/handlers/hash/HashTransformHandler.d.ts +23 -0
- package/dist/codegen/handlers/merge/MergeArrayHandler.d.ts +20 -0
- package/dist/codegen/handlers/merge/MergeObjectHandler.d.ts +10 -0
- package/dist/codegen/handlers/serialize/SerializeArrayHandler.d.ts +19 -0
- package/dist/codegen/handlers/strip/StripComputedHandler.d.ts +10 -0
- package/dist/codegen/handlers/strip/StripFunctionHandler.d.ts +9 -0
- package/dist/codegen/handlers/types.d.ts +55 -1
- package/dist/codegen/index.cjs +618 -0
- package/dist/codegen/index.cjs.map +1 -0
- package/dist/codegen/index.js +99 -127
- package/dist/codegen/index.js.map +1 -1
- package/dist/collections/MemoryDataCollection.d.ts +15 -1
- package/dist/collections/TagCollection.d.ts +0 -1
- package/dist/collections/index.cjs +702 -0
- package/dist/collections/index.cjs.map +1 -0
- package/dist/collections/index.js +282 -276
- package/dist/collections/index.js.map +1 -1
- package/dist/errors/OptimisticConcurrencyError.d.ts +18 -0
- package/dist/errors/PluginDestroyedError.d.ts +10 -0
- package/dist/errors/index.cjs +132 -0
- package/dist/errors/index.cjs.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +61 -19
- package/dist/errors/index.js.map +1 -1
- package/dist/expressions/evaluate.d.ts +53 -0
- package/dist/expressions/index.cjs +2087 -0
- package/dist/expressions/index.cjs.map +1 -0
- package/dist/expressions/index.d.ts +1 -1
- package/dist/expressions/index.js +1727 -1082
- package/dist/expressions/index.js.map +1 -1
- package/dist/expressions/parser.d.ts +19 -0
- package/dist/expressions/types.d.ts +87 -2
- package/dist/index.cjs +14377 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +12374 -7931
- package/dist/index.js.map +1 -1
- package/dist/performance/index.cjs +217 -0
- package/dist/performance/index.cjs.map +1 -0
- package/dist/performance/index.js +120 -55
- package/dist/performance/index.js.map +1 -1
- package/dist/pipeline/index.cjs +576 -0
- package/dist/pipeline/index.cjs.map +1 -0
- package/dist/pipeline/index.js +223 -161
- package/dist/pipeline/index.js.map +1 -1
- package/dist/plugins/BatchingDbPlugin.d.ts +139 -0
- package/dist/plugins/CacheDbPlugin.d.ts +70 -0
- package/dist/plugins/ConcurrencyDbPlugin.d.ts +78 -0
- package/dist/plugins/EphemeralDataPlugin.d.ts +57 -1
- package/dist/plugins/RetryDbPlugin.d.ts +77 -0
- package/dist/plugins/index.cjs +6551 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +6271 -1170
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/query/QueryOptionsCollection.d.ts +34 -0
- package/dist/plugins/query/explain.d.ts +82 -0
- package/dist/plugins/query/formatExplanation.d.ts +9 -0
- package/dist/plugins/query/index.d.ts +4 -0
- package/dist/plugins/query/join.d.ts +210 -0
- package/dist/plugins/query/similarity.d.ts +38 -0
- package/dist/plugins/query/types.d.ts +75 -1
- package/dist/plugins/translators/DataTranslator.d.ts +28 -0
- package/dist/plugins/translators/JsonTranslator.d.ts +27 -0
- package/dist/plugins/translators/SqlTranslator.d.ts +45 -0
- package/dist/plugins/translators/TupleTranslator.d.ts +45 -0
- package/dist/plugins/translators/index.d.ts +1 -0
- package/dist/plugins/types.d.ts +125 -3
- package/dist/plugins/wire/handler.d.ts +122 -0
- package/dist/plugins/wire/index.d.ts +4 -0
- package/dist/plugins/wire/persist.d.ts +43 -0
- package/dist/plugins/wire/query.d.ts +49 -0
- package/dist/plugins/wire/types.d.ts +139 -0
- package/dist/results/index.cjs +174 -0
- package/dist/results/index.cjs.map +1 -0
- package/dist/results/index.js +29 -44
- package/dist/results/index.js.map +1 -1
- package/dist/schema/PropertyInfo.d.ts +31 -2
- package/dist/schema/SchemaDefinition.d.ts +23 -1
- package/dist/schema/builder.d.ts +35 -1
- package/dist/schema/communication/broadcast.d.ts +17 -1
- package/dist/schema/index.cjs +6911 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +5330 -4541
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/property/base/SchemaBase.d.ts +48 -2
- package/dist/schema/property/modifiers/SchemaNullable.d.ts +10 -0
- package/dist/schema/property/modifiers/SchemaOptional.d.ts +19 -0
- package/dist/schema/property/modifiers/SchemaSearchable.d.ts +34 -0
- package/dist/schema/property/modifiers/index.d.ts +1 -0
- package/dist/schema/property/types/SchemaFile.d.ts +60 -0
- package/dist/schema/property/types/SchemaString.d.ts +21 -1
- package/dist/schema/property/types/SchemaVector.d.ts +61 -0
- package/dist/schema/property/types/index.d.ts +2 -0
- package/dist/schema/table/SchemaTransform.d.ts +40 -0
- package/dist/schema/table/index.d.ts +1 -0
- package/dist/schema/types.d.ts +166 -8
- package/dist/schema/utils/propertyKind.d.ts +4 -0
- package/dist/types/index.cjs +23 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.js +0 -14
- package/dist/types/index.js.map +1 -1
- package/dist/utilities/index.cjs +1013 -0
- package/dist/utilities/index.cjs.map +1 -0
- package/dist/utilities/index.js +541 -379
- package/dist/utilities/index.js.map +1 -1
- package/dist/utilities/logger.d.ts +48 -0
- package/package.json +156 -98
- package/dist/codegen/SlotPath.test.d.ts +0 -1
- package/dist/codegen/blocks.test.d.ts +0 -1
- package/dist/codegen/utils.test.d.ts +0 -1
- package/dist/collections/Changes.test.d.ts +0 -1
- package/dist/collections/IdSet.test.d.ts +0 -1
- package/dist/collections/MemoryDataCollection.test.d.ts +0 -1
- package/dist/collections/TagCollection.contract.test.d.ts +0 -1
- package/dist/collections/TagCollection.test.d.ts +0 -1
- package/dist/expressions/parser.test.d.ts +0 -1
- package/dist/expressions/sql.d.ts +0 -28
- package/dist/expressions/sql.test.d.ts +0 -1
- package/dist/expressions/utils.test.d.ts +0 -1
- package/dist/pipeline/SyncronousQueue.test.d.ts +0 -1
- package/dist/plugins/EphemeralDataPlugin.test.d.ts +0 -1
- package/dist/plugins/query/Query.test.d.ts +0 -1
- package/dist/plugins/query/QueryOptionsCollection.test.d.ts +0 -1
- package/dist/plugins/translators/JsonTranslator.test.d.ts +0 -1
- package/dist/plugins/translators/SqlTranslator.test.d.ts +0 -1
- package/dist/plugins/translators/TranslatedValues.test.d.ts +0 -1
- package/dist/results/Result.test.d.ts +0 -1
- package/dist/results/utils.test.d.ts +0 -1
- package/dist/schema/PropertyInfo.test.d.ts +0 -1
- package/dist/schema/communication/broadcast.test.d.ts +0 -1
- package/dist/schema/schemaGeneration.test.d.ts +0 -1
- package/dist/schema/utils/standardJsonSchema.test.d.ts +0 -1
- package/dist/utilities/arrays.test.d.ts +0 -1
- package/dist/utilities/dates.test.d.ts +0 -1
- package/dist/utilities/runtime.test.d.ts +0 -1
- package/dist/utilities/strings.test.d.ts +0 -1
- package/dist/utilities/uuid.test.d.ts +0 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DataTranslator } from "./DataTranslator";
|
|
2
|
+
import { QueryOption } from "../query/types";
|
|
3
|
+
/**
|
|
4
|
+
* The memory half's pass over JOIN TUPLES.
|
|
5
|
+
*
|
|
6
|
+
* `JsonTranslator` cannot do this job, and the reason is one line of it: `map` and `group` walk
|
|
7
|
+
* `option.value.fields` and deserialize each field through its `PropertyInfo`. A tuple has no
|
|
8
|
+
* schema and no fields — `getFields` over `([p, m]) => ({ ... })` extracts nothing meaningful —
|
|
9
|
+
* so that loop would either no-op or write properties onto a two-element array.
|
|
10
|
+
*
|
|
11
|
+
* Everything a join query is allowed to do after the join is expressible as a plain closure
|
|
12
|
+
* over the tuple, and that is all this does. The lambdas the caller wrote (`([p, m]) => ...`)
|
|
13
|
+
* are applied as-is, which is also why the result is identical whichever backend produced the
|
|
14
|
+
* pairs.
|
|
15
|
+
*
|
|
16
|
+
* Both halves are already in entity shape when the tuples arrive here (the wire contract), so
|
|
17
|
+
* there is nothing left to deserialize — the reason the missing field loop costs nothing.
|
|
18
|
+
*
|
|
19
|
+
* The absent operations are absent by design, not by omission: `sum`/`min`/`max`/`distinct` and
|
|
20
|
+
* `group` are not declared on the tuple queryable's type, so reaching them means something
|
|
21
|
+
* built an option this API cannot express, and a throw naming it is the honest answer.
|
|
22
|
+
*/
|
|
23
|
+
export declare class TupleTranslator<TRoot extends {}, TShape> extends DataTranslator<TRoot, TShape> {
|
|
24
|
+
filter<TResult>(data: unknown, option: QueryOption<TShape, "filter">): TResult;
|
|
25
|
+
map<TResult>(data: unknown, option: QueryOption<TShape, "map">): TResult;
|
|
26
|
+
sort<TResult>(data: unknown, option: QueryOption<TShape, "sort">): TResult;
|
|
27
|
+
skip<TResult>(data: unknown, option: QueryOption<TShape, "skip">): TResult;
|
|
28
|
+
take<TResult>(data: unknown, option: QueryOption<TShape, "take">): TResult;
|
|
29
|
+
count<TResult extends number>(data: unknown, _: QueryOption<TShape, "count">): TResult;
|
|
30
|
+
/**
|
|
31
|
+
* Already joined by the time anything reaches here.
|
|
32
|
+
*
|
|
33
|
+
* The pairs were produced either by the plugin (its translator's `join`) or by the datastore
|
|
34
|
+
* before this pass ran, so the option is a record of what happened rather than work to do.
|
|
35
|
+
*/
|
|
36
|
+
join(data: unknown, _: QueryOption<TShape, "join">): TShape;
|
|
37
|
+
group<TResult>(_: unknown, __: QueryOption<TShape, "group">): TResult;
|
|
38
|
+
sum<TResult extends number>(_: unknown, __: QueryOption<TShape, "sum">): TResult;
|
|
39
|
+
min<TResult extends string | number | Date>(_: unknown, __: QueryOption<TShape, "min">): TResult;
|
|
40
|
+
max<TResult extends string | number | Date>(_: unknown, __: QueryOption<TShape, "max">): TResult;
|
|
41
|
+
distinct<TResult>(_: unknown, __: QueryOption<TShape, "distinct">): TResult;
|
|
42
|
+
nearest(_: unknown, __: QueryOption<TShape, "nearest">): TShape;
|
|
43
|
+
private notSupported;
|
|
44
|
+
private notAnArray;
|
|
45
|
+
}
|
package/dist/plugins/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PluginEventCallbackPartialResult, PluginEventCallbackResult } from "../results";
|
|
2
|
+
import { ExecutedQuery } from "./query/explain";
|
|
2
3
|
import { QueryOptionsCollection } from "./query/QueryOptionsCollection";
|
|
3
4
|
import { CompiledSchema, InferType } from '../schema';
|
|
4
5
|
import { BulkPersistChanges, BulkPersistResult, SchemaCollection } from "../collections";
|
|
@@ -7,6 +8,28 @@ import { ITranslatedValue } from "./translators";
|
|
|
7
8
|
* Interface for a database plugin, which provides query, destroy, and bulk operations.
|
|
8
9
|
*/
|
|
9
10
|
export interface IDbPlugin {
|
|
11
|
+
/**
|
|
12
|
+
* Uniquely identifies the database this plugin talks to, INCLUDING host or path where a
|
|
13
|
+
* bare name would collide — `orders.db` in two directories is two databases, and `mydb`
|
|
14
|
+
* on two hosts is two databases. Two instances over the same database must return the
|
|
15
|
+
* same string, in this process and in any other; two over different databases must not.
|
|
16
|
+
*
|
|
17
|
+
* Used to scope schema subscription channels, so instances of one database (another tab,
|
|
18
|
+
* a worker) see each other's change notifications and unrelated databases holding the
|
|
19
|
+
* same schema do not.
|
|
20
|
+
*
|
|
21
|
+
* Required rather than optional on purpose. An absent value used to fall back to scoping
|
|
22
|
+
* by schema alone, which shares one channel across every database holding that schema —
|
|
23
|
+
* the exact cross-talk this prevents, arrived at by omission. Requiring it also makes a
|
|
24
|
+
* wrapper that forgets to forward it a compile error rather than a silent regression.
|
|
25
|
+
*
|
|
26
|
+
* Derive it, never generate it: a random value is unique per PROCESS, not per database,
|
|
27
|
+
* so another tab would never match one and cross-context notifications would stop.
|
|
28
|
+
*
|
|
29
|
+
* Must not contain credentials — it becomes part of a channel key, so build it from
|
|
30
|
+
* host/port/database rather than returning a connection string.
|
|
31
|
+
*/
|
|
32
|
+
readonly databaseName: string;
|
|
10
33
|
/**
|
|
11
34
|
* Executes a query operation on the database.
|
|
12
35
|
* @param event The query event containing schema, parent, and query operation.
|
|
@@ -28,7 +51,48 @@ export interface IDbPlugin {
|
|
|
28
51
|
/**
|
|
29
52
|
* Event for a query operation, including schema, parent, and the query operation.
|
|
30
53
|
*/
|
|
31
|
-
export type DbPluginQueryEvent<TRoot extends {}, TShape> = DbPluginOperationEvent<IQuery<TRoot, TShape
|
|
54
|
+
export type DbPluginQueryEvent<TRoot extends {}, TShape> = DbPluginOperationEvent<IQuery<TRoot, TShape>> & {
|
|
55
|
+
/**
|
|
56
|
+
* Whether the caller asked for an explanation. Required, never optional: a query is
|
|
57
|
+
* either explained or it is not, and "unset" is not a third state.
|
|
58
|
+
*
|
|
59
|
+
* A plugin is free to ignore it. One that reports unconditionally is correct; one that
|
|
60
|
+
* checks the flag to skip building report strings is also correct. What a plugin must
|
|
61
|
+
* NOT do is treat `true` as an instruction it has to obey — a plugin that cannot report
|
|
62
|
+
* simply doesn't, and the datastore marks the step as not reported.
|
|
63
|
+
*/
|
|
64
|
+
explain: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Where a plugin reports what it executed. Pushing to it is how a plugin supports
|
|
67
|
+
* `.explain()` — a plugin that never pushes still answers queries, and its explanations
|
|
68
|
+
* mark the database step as not reported (`executedQueriesUnsupported`) instead of
|
|
69
|
+
* showing statements.
|
|
70
|
+
*
|
|
71
|
+
* The datastore decides whether anyone sees it: with `explain` on it reads the array and
|
|
72
|
+
* an empty one means "not supported"; with `explain` off it takes no action either way.
|
|
73
|
+
*
|
|
74
|
+
* An array the DATASTORE creates and the plugin pushes into, rather than a value the plugin
|
|
75
|
+
* returns. The result envelope is rebuilt in at least six places between a plugin and the
|
|
76
|
+
* caller — the memory half re-translates, joins build fresh tuple values, the cache
|
|
77
|
+
* reconstructs from stored entries — so anything carried on it is discarded before arrival.
|
|
78
|
+
* The event is not rebuilt, and an array survives the shallow spread in `ConcurrencyDbPlugin`
|
|
79
|
+
* because both sides then hold the same array. Assigning a new one would not.
|
|
80
|
+
*
|
|
81
|
+
* Push once per query actually executed, in execution order, so a join reports both reads.
|
|
82
|
+
* `text` is whatever the backend runs — SQL for a SQL engine, a description of the access
|
|
83
|
+
* path for a store that has no statement. A plugin that answered without touching its
|
|
84
|
+
* backend pushes a description of that instead — `CacheDbPlugin` pushes "cache hit" —
|
|
85
|
+
* because pushing nothing reads as "this plugin does not report".
|
|
86
|
+
*
|
|
87
|
+
* Push AFTER the query runs, not before. `RetryDbPlugin` re-invokes with the same event, so
|
|
88
|
+
* a plugin that pushes first reports an entry per failed attempt.
|
|
89
|
+
*
|
|
90
|
+
* The array accumulates for as long as the event lives. That is why `.explain()` is not
|
|
91
|
+
* offered on a subscribed queryable: `subscribeQuery` builds its event once and re-issues it
|
|
92
|
+
* on every change notification, which would grow this without bound.
|
|
93
|
+
*/
|
|
94
|
+
executedQueries: ExecutedQuery[];
|
|
95
|
+
};
|
|
32
96
|
/**
|
|
33
97
|
* Event for bulk operations, including schema, parent, and the entity changes.
|
|
34
98
|
*/
|
|
@@ -71,12 +135,69 @@ export type ReplicationPluginOptions = {
|
|
|
71
135
|
*/
|
|
72
136
|
read?: IDbPlugin;
|
|
73
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* A value inside a delta. Arrays and Dates are values, not sub-structures to descend into.
|
|
140
|
+
*
|
|
141
|
+
* Descending into them would be both wrong and useless: an element-wise array delta cannot
|
|
142
|
+
* express "the last element was removed", and a partial Date is meaningless.
|
|
143
|
+
*/
|
|
144
|
+
type DeltaValue<V> = V extends readonly unknown[] ? V : V extends Date ? V : V extends object ? DeltaProperties<V> : V;
|
|
145
|
+
type DeltaProperties<T> = {
|
|
146
|
+
[K in keyof T]?: DeltaValue<T[K]>;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* What changed about an entity, expressed as a **partial entity**.
|
|
150
|
+
*
|
|
151
|
+
* A change two levels deep appears where it actually lives —
|
|
152
|
+
* `{ nested: { inner: { value } } }` — not as a flattened key.
|
|
153
|
+
*
|
|
154
|
+
* This deliberately carries no storage vocabulary. It used to be typed
|
|
155
|
+
* `{ [key: string]: string | number | Date }`, which was wrong twice over: it excluded
|
|
156
|
+
* booleans, nulls, arrays and objects that the schema happily allows, and its flat
|
|
157
|
+
* scalar shape was really a SQL `SET column = ?` list — one storage family's concern
|
|
158
|
+
* leaking into the contract every plugin sees.
|
|
159
|
+
*
|
|
160
|
+
* Translating this into storage terms belongs to the plugin. A document store can merge it
|
|
161
|
+
* as-is; a SQL plugin decides which columns it touches and how a nested value is encoded
|
|
162
|
+
* (see `toColumnAssignments` in `@routier/sql-plugin-core`, which stores nested objects and
|
|
163
|
+
* arrays as JSON). Core does not need to know, and must not.
|
|
164
|
+
*/
|
|
165
|
+
export type EntityDelta<T extends {}> = DeltaProperties<InferType<T>>;
|
|
74
166
|
export type EntityUpdateInfo<T extends {}> = {
|
|
75
167
|
entity: InferType<T>;
|
|
76
168
|
changeType: EntityChangeType;
|
|
77
|
-
delta:
|
|
78
|
-
|
|
169
|
+
delta: EntityDelta<T>;
|
|
170
|
+
/**
|
|
171
|
+
* Present when the schema declares a `.concurrency()` token: the update must be
|
|
172
|
+
* applied ONLY IF the stored row's `column` still equals `expected` (the value the
|
|
173
|
+
* writer read). The entity/delta already carry the bumped value to store on success.
|
|
174
|
+
* A plugin that finds a mismatch must fail the whole save with an
|
|
175
|
+
* OptimisticConcurrencyError naming the conflicted rows — never apply partially.
|
|
176
|
+
*/
|
|
177
|
+
concurrency?: {
|
|
178
|
+
column: string;
|
|
179
|
+
expected: number;
|
|
79
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* The values these properties held BEFORE this update — keyed like `delta`, which holds
|
|
183
|
+
* the values they hold after.
|
|
184
|
+
*
|
|
185
|
+
* DATASTORE-INTERNAL. The datastore strips it before the plugin is called
|
|
186
|
+
* (`DataStore.onSavePreparedChanges`), so no plugin ever receives it and nothing goes over
|
|
187
|
+
* a wire. It exists for save-pipeline participants that must undo work keyed by an old
|
|
188
|
+
* value — a search index has to delete the rows for terms that just left a field, and
|
|
189
|
+
* `delta` only says what the field says now.
|
|
190
|
+
*
|
|
191
|
+
* Always populated for an update. It is part of what an update IS, not something a
|
|
192
|
+
* declaration switches on — a consumer can rely on it without knowing what else the store
|
|
193
|
+
* declared, and there is one code path to reason about rather than two.
|
|
194
|
+
*
|
|
195
|
+
* Which properties appear depends on what the change-tracking mode can know. Proxy and
|
|
196
|
+
* immutable name exactly the properties that changed. Diff detects change by comparing a
|
|
197
|
+
* content hash, so it cannot say WHICH property moved and reports every root property —
|
|
198
|
+
* the same "assume everything" convention its empty `delta` already uses.
|
|
199
|
+
*/
|
|
200
|
+
previous?: EntityDelta<T>;
|
|
80
201
|
};
|
|
81
202
|
export type TaggedEntity<T> = {
|
|
82
203
|
entity: T;
|
|
@@ -96,3 +217,4 @@ export type IQuery<TRoot extends {}, TShape> = {
|
|
|
96
217
|
get changeTracking(): boolean;
|
|
97
218
|
};
|
|
98
219
|
export type EntityChangeType = "propertiesChanged" | "markedDirty" | "notModified";
|
|
220
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ReadonlySchemaCollection } from "../../collections/ReadonlySchemaCollection";
|
|
2
|
+
import { Filter, ParamsFilter } from "../../expressions";
|
|
3
|
+
import { CompiledSchema } from "../../schema";
|
|
4
|
+
import { IDbPlugin } from "../types";
|
|
5
|
+
import { ScopeProvider } from "./query";
|
|
6
|
+
import { SerializedRequest, SerializedResponse } from "./types";
|
|
7
|
+
/**
|
|
8
|
+
* The receiving half: takes a serialized request, executes it, returns a serialized response.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately not a server. It is one async function from JSON to JSON, with no notion of HTTP, so
|
|
11
|
+
* the same handler sits behind Express, a Cloudflare Worker, a Lambda, a WebSocket message, or a
|
|
12
|
+
* worker `postMessage`. Transport is the caller's business; this is the part that would otherwise be
|
|
13
|
+
* rewritten per framework.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const handle = createRequestHandler({ plugin, schemas });
|
|
17
|
+
*
|
|
18
|
+
* app.post("/routier", async (req, res) => res.json(await handle(req.body)));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## Security is YOURS, and this gives you the two places to put it
|
|
22
|
+
*
|
|
23
|
+
* There is no built-in notion of a user, a tenant, a role or a token, and there should not be: this
|
|
24
|
+
* library cannot know what your identities are or what they may see. What it can do is make sure
|
|
25
|
+
* there is nowhere for a decision to be forgotten. Two hooks, and both receive a `context` you built
|
|
26
|
+
* from the request:
|
|
27
|
+
*
|
|
28
|
+
* - **`authorize`** — may this caller do this at all? Called once per request, before anything is
|
|
29
|
+
* deserialized or executed, with the action and every collection the request touches.
|
|
30
|
+
* - **`scope`** — which ROWS may this caller see? A filter the receiver ANDs into every read of a
|
|
31
|
+
* collection, and checks every written row against.
|
|
32
|
+
*
|
|
33
|
+
* With neither supplied, the endpoint answers anything for anyone. That is the correct default for a
|
|
34
|
+
* function with no idea who is calling it — and the moment you name a context type, passing one
|
|
35
|
+
* becomes required, so a policy cannot be half-wired.
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const handle = createRequestHandler<{ tenantId: string }>({
|
|
39
|
+
* plugin,
|
|
40
|
+
* schemas,
|
|
41
|
+
* authorize: ({ action, context }) => context.tenantId != null || "not signed in",
|
|
42
|
+
* scope: ({ context }) => ({ filter: ([row, p]) => row.tenantId === p.tenantId, params: context }),
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* app.post("/routier", async (req, res) => {
|
|
46
|
+
* const context = { tenantId: req.user?.tenantId }; // from the REQUEST, never from the body
|
|
47
|
+
* res.json(await handle(req.body, context));
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* ## Errors are values
|
|
52
|
+
*
|
|
53
|
+
* A failure comes back as `{ ok: false, error }` rather than as a rejected promise, so a transport
|
|
54
|
+
* cannot accidentally turn a query error into a 500 with no body. The caller decides the status.
|
|
55
|
+
*/
|
|
56
|
+
/** What a hook is told about the request it is judging. */
|
|
57
|
+
export type RequestInfo<TContext> = {
|
|
58
|
+
action: "query" | "persist" | "destroy";
|
|
59
|
+
/** Every collection this request touches, including the inner side of any join. */
|
|
60
|
+
collectionNames: string[];
|
|
61
|
+
/** Whatever the transport built from the request — a user, a tenant, a token. Never the body. */
|
|
62
|
+
context: TContext;
|
|
63
|
+
/** The raw request, for a policy that needs to look closer. Treat it as caller-controlled input. */
|
|
64
|
+
request: SerializedRequest;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* May this request proceed?
|
|
68
|
+
*
|
|
69
|
+
* `true` to allow. `false` or a string to refuse — a string becomes the error message, which is the
|
|
70
|
+
* cheapest way to say WHY without inventing an error type. Throwing also refuses.
|
|
71
|
+
*
|
|
72
|
+
* Called once, before deserialization, so a refused request never reaches a schema or a plugin.
|
|
73
|
+
*/
|
|
74
|
+
export type AuthorizeHook<TContext> = (info: RequestInfo<TContext>) => boolean | string | Promise<boolean | string>;
|
|
75
|
+
/** What a scope hook is asked. One collection at a time, since each may be scoped differently. */
|
|
76
|
+
export type ScopeInfo<TContext> = {
|
|
77
|
+
collectionName: string;
|
|
78
|
+
schema: CompiledSchema<any>;
|
|
79
|
+
context: TContext;
|
|
80
|
+
action: "query" | "persist";
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The rows of one collection this caller may touch, as a filter.
|
|
84
|
+
*
|
|
85
|
+
* Written exactly like a collection's own `.scope()` — a filter, optionally with params — so the same
|
|
86
|
+
* expression is pushed into the database on reads and checked against each row on writes. Return
|
|
87
|
+
* `null` for a collection this caller may see in full.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* scope: ({ collectionName, context }) =>
|
|
91
|
+
* collectionName === "orders"
|
|
92
|
+
* ? { filter: ([row, p]) => row.tenantId === p.tenantId, params: { tenantId: context.tenantId } }
|
|
93
|
+
* : null
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export type ScopeHook<TContext> = (info: ScopeInfo<TContext>) => {
|
|
97
|
+
filter: Filter<any> | ParamsFilter<any, any>;
|
|
98
|
+
params?: {};
|
|
99
|
+
} | null;
|
|
100
|
+
export type RequestHandlerOptions<TContext> = {
|
|
101
|
+
/** The plugin that actually holds the data. Anything implementing `IDbPlugin`. */
|
|
102
|
+
plugin: IDbPlugin;
|
|
103
|
+
/** Every collection this endpoint will answer for. A name absent from here is refused. */
|
|
104
|
+
schemas: ReadonlySchemaCollection;
|
|
105
|
+
/** May this caller do this? See `AuthorizeHook`. Absent means yes, to everyone. */
|
|
106
|
+
authorize?: AuthorizeHook<TContext>;
|
|
107
|
+
/** Which rows may this caller touch? See `ScopeHook`. Absent means all of them. */
|
|
108
|
+
scope?: ScopeHook<TContext>;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a `destroy` request may drop the database. **Defaults to false.**
|
|
111
|
+
*
|
|
112
|
+
* `HttpTransportDbPlugin` never sends one, but an endpoint answers whatever arrives — and a
|
|
113
|
+
* hand-written `{"kind":"destroy"}` would otherwise wipe the store for anyone who could reach
|
|
114
|
+
* the route. Destroying a database is not something a remote caller should be able to ask for by
|
|
115
|
+
* default, so it is opt-in and still passes through `authorize`.
|
|
116
|
+
*/
|
|
117
|
+
allowDestroy?: boolean;
|
|
118
|
+
};
|
|
119
|
+
export type RequestHandler<TContext> = (request: SerializedRequest, context: TContext) => Promise<SerializedResponse>;
|
|
120
|
+
export declare const createRequestHandler: <TContext = void>(options: RequestHandlerOptions<TContext>) => RequestHandler<TContext>;
|
|
121
|
+
/** Re-exported so a caller can type its own scope provider without reaching for the query module. */
|
|
122
|
+
export type { ScopeProvider };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BulkPersistChanges, BulkPersistResult } from "../../collections";
|
|
2
|
+
import { CompiledSchema } from "../../schema";
|
|
3
|
+
import { ReadonlySchemaCollection } from "../../collections/ReadonlySchemaCollection";
|
|
4
|
+
import { SchemaResolver } from "./query";
|
|
5
|
+
import { SerializedPersistRequest, SerializedResponse } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Saves, in the form that survives a wire.
|
|
8
|
+
*
|
|
9
|
+
* Simpler than a query, because a change set holds no functions: adds, updates and removes are
|
|
10
|
+
* entities, and an entity reaching a plugin has already been through `preprocess` — so it is in
|
|
11
|
+
* STORAGE shape, where a Date is already an ISO string and a nested object is already whatever the
|
|
12
|
+
* schema said to store. It is JSON by the time it gets here.
|
|
13
|
+
*
|
|
14
|
+
* Two things are deliberately left behind:
|
|
15
|
+
*
|
|
16
|
+
* - **Tags.** `SchemaPersistChanges.tags` is caller-side metadata for correlating a save with its
|
|
17
|
+
* echo locally. The receiver has no use for it and no business seeing it.
|
|
18
|
+
* - **Schema ids.** Collections are NAMED. An id is a hash of the schema's own shape, so it would
|
|
19
|
+
* couple both sides to identical schema definitions; a name lets the receiver resolve its own.
|
|
20
|
+
*/
|
|
21
|
+
export declare const serializeBulkPersist: (changes: BulkPersistChanges, schemas: ReadonlySchemaCollection) => SerializedPersistRequest;
|
|
22
|
+
/**
|
|
23
|
+
* Rebuilds a change set from its wire form, keyed by the RECEIVER's schema ids.
|
|
24
|
+
*
|
|
25
|
+
* @throws when a named collection is not one this store declares. A save aimed at data this side
|
|
26
|
+
* does not have must not be silently dropped — the caller would be told it succeeded.
|
|
27
|
+
*/
|
|
28
|
+
export declare const deserializeBulkPersist: (request: SerializedPersistRequest, resolveSchema: SchemaResolver) => {
|
|
29
|
+
changes: BulkPersistChanges;
|
|
30
|
+
schemas: CompiledSchema<any>[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Serializes the ECHO of a save — the part the change tracker cannot do without.
|
|
34
|
+
*
|
|
35
|
+
* A save's result is not a receipt. It carries the rows as the database wrote them, including any
|
|
36
|
+
* identity the database assigned, and the change tracker matches each one back to the addition that
|
|
37
|
+
* produced it. Returning a count instead would leave every inserted entity without its key.
|
|
38
|
+
*/
|
|
39
|
+
export declare const serializePersistResult: (result: BulkPersistResult, schemas: ReadonlySchemaCollection) => SerializedResponse;
|
|
40
|
+
/** Rebuilds a save's echo against the SENDER's schema ids, which is what its change tracker holds. */
|
|
41
|
+
export declare const deserializePersistResult: (response: Extract<SerializedResponse, {
|
|
42
|
+
kind: "persist";
|
|
43
|
+
}>, resolveSchema: SchemaResolver) => BulkPersistResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Expression } from "../../expressions";
|
|
2
|
+
import { CompiledSchema } from "../../schema";
|
|
3
|
+
import { QueryOptionsCollection } from "../query/QueryOptionsCollection";
|
|
4
|
+
import { SerializedQueryOption } from "./types";
|
|
5
|
+
/**
|
|
6
|
+
* Splits options into the PREFIX that can be sent and the remainder that cannot.
|
|
7
|
+
*
|
|
8
|
+
* A prefix, not a filtered subset, and that is the whole correctness argument. Options are ordered,
|
|
9
|
+
* and most are not idempotent: sending `count` while keeping `map` local would count unmapped rows,
|
|
10
|
+
* and applying `take` on both sides would window twice. So the split stops at the first option that
|
|
11
|
+
* cannot travel, and everything from there on runs where the closures are.
|
|
12
|
+
*
|
|
13
|
+
* It is the same shape as the database/memory split this composes with — one more cut of the same
|
|
14
|
+
* ordered list, for one more reason.
|
|
15
|
+
*/
|
|
16
|
+
export declare const splitSendableOptions: <T>(options: QueryOptionsCollection<T>) => {
|
|
17
|
+
sendable: QueryOptionsCollection<T>;
|
|
18
|
+
local: QueryOptionsCollection<T>;
|
|
19
|
+
};
|
|
20
|
+
export declare const serializeQueryOptions: <T>(options: QueryOptionsCollection<T>) => SerializedQueryOption[];
|
|
21
|
+
/** How the receiver finds a collection it was sent the NAME of. */
|
|
22
|
+
export type SchemaResolver = (collectionName: string) => CompiledSchema<any> | null;
|
|
23
|
+
/**
|
|
24
|
+
* A filter the RECEIVER adds to every read of a collection, whatever the sender asked for.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` for a collection with nothing to add. See `createRequestHandler` for the policy
|
|
27
|
+
* side; this is only how it reaches the options.
|
|
28
|
+
*/
|
|
29
|
+
export type ScopeProvider = (schema: CompiledSchema<any>) => Expression | null;
|
|
30
|
+
/**
|
|
31
|
+
* Rebuilds query options from their wire form, against the receiver's own schemas.
|
|
32
|
+
*
|
|
33
|
+
* The closures that were dropped are reconstructed here rather than sent:
|
|
34
|
+
*
|
|
35
|
+
* - a **sort selector** from its property, which is all `JsonTranslator.sort` reads;
|
|
36
|
+
* - a **filter predicate** from its expression tree, via `toStrictPredicate` — which THROWS rather
|
|
37
|
+
* than keeping a row it cannot judge, because on a receiver a filter that quietly stops filtering
|
|
38
|
+
* returns rows the requester excluded.
|
|
39
|
+
*
|
|
40
|
+
* @throws when a named property or collection is not declared by the receiver's schemas. A payload
|
|
41
|
+
* describing data this side does not have is a disagreement, and it has to be loud.
|
|
42
|
+
*/
|
|
43
|
+
export declare const deserializeQueryOptions: (serialized: SerializedQueryOption[], schema: CompiledSchema<any>, resolveSchema: SchemaResolver,
|
|
44
|
+
/**
|
|
45
|
+
* A receiver-side filter per collection, applied to this collection AND to every collection a
|
|
46
|
+
* join reaches. Prepended, so it is ANDed with whatever the sender sent and there is no order of
|
|
47
|
+
* options that removes it.
|
|
48
|
+
*/
|
|
49
|
+
scopeFor?: ScopeProvider) => QueryOptionsCollection<any>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { SerializedExpression } from "../../expressions";
|
|
2
|
+
import { JoinKind } from "../query/join";
|
|
3
|
+
import { ExecutedQuery } from "../query/explain";
|
|
4
|
+
import { QueryOrdering } from "../query/types";
|
|
5
|
+
/**
|
|
6
|
+
* The wire format for a whole Routier operation.
|
|
7
|
+
*
|
|
8
|
+
* This is what makes a plugin that owns no database possible: the query itself travels, and a
|
|
9
|
+
* receiver executes it against whatever plugin IT has. Nothing here is HTTP-specific — it is plain
|
|
10
|
+
* JSON, so the same payload works over fetch, a WebSocket, a worker `postMessage`, or a queue.
|
|
11
|
+
*
|
|
12
|
+
* ## Two rules shape every type below
|
|
13
|
+
*
|
|
14
|
+
* 1. **No functions cross.** A query option carries live closures — a sort selector, a filter
|
|
15
|
+
* predicate, a map projection — and none of them survive `JSON.stringify`. Where a closure can
|
|
16
|
+
* be REBUILT from data it is dropped and reconstructed on arrival (a sort selector from its
|
|
17
|
+
* property, a filter predicate from its expression tree). Where it cannot, the option does not
|
|
18
|
+
* travel at all and the sender runs it locally; see `map` and `group`.
|
|
19
|
+
* 2. **The receiver's schema is the authority.** Collections are named, never described. A payload
|
|
20
|
+
* says "teams", and the receiver resolves its own compiled schema for that name. Sending a
|
|
21
|
+
* schema would let the sender decide what its properties are, which is backwards for anything
|
|
22
|
+
* crossing a trust boundary — and it is the same reason `expressionFromJson` takes the schema
|
|
23
|
+
* rather than reading an id out of the payload.
|
|
24
|
+
*/
|
|
25
|
+
/** A query option, in the form that survives a wire. */
|
|
26
|
+
export type SerializedQueryOption = {
|
|
27
|
+
name: "skip";
|
|
28
|
+
value: number;
|
|
29
|
+
} | {
|
|
30
|
+
name: "take";
|
|
31
|
+
value: number;
|
|
32
|
+
}
|
|
33
|
+
/** The selector is dropped and rebuilt from the property on arrival. */
|
|
34
|
+
| {
|
|
35
|
+
name: "sort";
|
|
36
|
+
value: {
|
|
37
|
+
propertyName: string;
|
|
38
|
+
direction: QueryOrdering;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The expression only — no closure and no params bag.
|
|
43
|
+
*
|
|
44
|
+
* A filter reaching a query option is already BOUND: `ParamReferenceExpression` never escapes
|
|
45
|
+
* the parser, so every param value is already a literal in the tree. The receiver rebuilds a
|
|
46
|
+
* runnable predicate from the tree with `toStrictPredicate`.
|
|
47
|
+
*/
|
|
48
|
+
| {
|
|
49
|
+
name: "filter";
|
|
50
|
+
value: {
|
|
51
|
+
expression: SerializedExpression;
|
|
52
|
+
};
|
|
53
|
+
} | {
|
|
54
|
+
name: "nearest";
|
|
55
|
+
value: {
|
|
56
|
+
propertyName: string;
|
|
57
|
+
vector: number[];
|
|
58
|
+
count: number;
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
name: "join";
|
|
62
|
+
value: {
|
|
63
|
+
kind: JoinKind;
|
|
64
|
+
/** Named, not described — the receiver resolves its own schema for it. */
|
|
65
|
+
innerCollectionName: string;
|
|
66
|
+
outerKeyPath: string;
|
|
67
|
+
innerKeyPath: string;
|
|
68
|
+
innerOptions: SerializedQueryOption[];
|
|
69
|
+
semiJoinKeyThreshold: number;
|
|
70
|
+
};
|
|
71
|
+
} | {
|
|
72
|
+
name: "count" | "min" | "max" | "sum" | "distinct";
|
|
73
|
+
value: true;
|
|
74
|
+
};
|
|
75
|
+
export type SerializedQueryRequest = {
|
|
76
|
+
kind: "query";
|
|
77
|
+
collectionName: string;
|
|
78
|
+
options: SerializedQueryOption[];
|
|
79
|
+
/**
|
|
80
|
+
* Whether the caller wants the response to say what the server ran. Required — a query is
|
|
81
|
+
* either explained or it is not. A server whose plugin does not report answers `true` the
|
|
82
|
+
* same as `false`, and the caller's explanation marks the remote step as not reported.
|
|
83
|
+
*/
|
|
84
|
+
explain: boolean;
|
|
85
|
+
};
|
|
86
|
+
/** One entity update, as `EntityUpdateInfo` minus nothing — every field of it is already JSON. */
|
|
87
|
+
export type SerializedUpdate = {
|
|
88
|
+
entity: unknown;
|
|
89
|
+
changeType: "propertiesChanged" | "markedDirty" | "notModified";
|
|
90
|
+
delta: unknown;
|
|
91
|
+
concurrency?: {
|
|
92
|
+
column: string;
|
|
93
|
+
expected: number;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export type SerializedSchemaChanges = {
|
|
97
|
+
collectionName: string;
|
|
98
|
+
adds: unknown[];
|
|
99
|
+
updates: SerializedUpdate[];
|
|
100
|
+
removes: unknown[];
|
|
101
|
+
};
|
|
102
|
+
export type SerializedPersistRequest = {
|
|
103
|
+
kind: "persist";
|
|
104
|
+
changes: SerializedSchemaChanges[];
|
|
105
|
+
};
|
|
106
|
+
export type SerializedDestroyRequest = {
|
|
107
|
+
kind: "destroy";
|
|
108
|
+
};
|
|
109
|
+
export type SerializedRequest = SerializedQueryRequest | SerializedPersistRequest | SerializedDestroyRequest;
|
|
110
|
+
/** What a receiver sends back. Errors are a value, not a transport status. */
|
|
111
|
+
export type SerializedResponse =
|
|
112
|
+
/**
|
|
113
|
+
* `executedQueries` carries what the SERVER's plugin ran, so `.explain()` on a client sees
|
|
114
|
+
* through the wire rather than reporting a blank. Optional on the response, unlike on a
|
|
115
|
+
* local event: a plugin that does not report has nothing to send, and the client's
|
|
116
|
+
* explanation then marks the remote step as not reported. There is no flag on either end —
|
|
117
|
+
* the wire forwards whatever the plugin pushed, or nothing.
|
|
118
|
+
*/
|
|
119
|
+
{
|
|
120
|
+
ok: true;
|
|
121
|
+
kind: "query";
|
|
122
|
+
value: unknown;
|
|
123
|
+
executedQueries?: ExecutedQuery[];
|
|
124
|
+
} | {
|
|
125
|
+
ok: true;
|
|
126
|
+
kind: "persist";
|
|
127
|
+
changes: Array<{
|
|
128
|
+
collectionName: string;
|
|
129
|
+
adds: unknown[];
|
|
130
|
+
updates: unknown[];
|
|
131
|
+
removes: unknown[];
|
|
132
|
+
}>;
|
|
133
|
+
} | {
|
|
134
|
+
ok: true;
|
|
135
|
+
kind: "destroy";
|
|
136
|
+
} | {
|
|
137
|
+
ok: false;
|
|
138
|
+
error: string;
|
|
139
|
+
};
|