@thomasfosterau/effect-drizzle-ext 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/LICENSE +21 -0
- package/README.md +508 -2
- package/dist/columns.d.ts +113 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/columns.js +146 -0
- package/dist/columns.js.map +1 -0
- package/dist/constraints.d.ts +117 -0
- package/dist/constraints.d.ts.map +1 -0
- package/dist/constraints.js +93 -0
- package/dist/constraints.js.map +1 -0
- package/dist/database.d.ts +189 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +51 -0
- package/dist/database.js.map +1 -0
- package/dist/derive.d.ts +97 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +142 -0
- package/dist/derive.js.map +1 -0
- package/dist/errors.d.ts +157 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +287 -0
- package/dist/errors.js.map +1 -0
- package/dist/index-CFhgcF4N.d.ts +418 -0
- package/dist/index-CFhgcF4N.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/jsonapi/derive.d.ts +28 -0
- package/dist/jsonapi/derive.d.ts.map +1 -0
- package/dist/jsonapi/derive.js +28 -0
- package/dist/jsonapi/derive.js.map +1 -0
- package/dist/jsonapi/index.d.ts +189 -0
- package/dist/jsonapi/index.d.ts.map +1 -0
- package/dist/jsonapi/index.js +164 -0
- package/dist/jsonapi/index.js.map +1 -0
- package/dist/query.d.ts +111 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +142 -0
- package/dist/query.js.map +1 -0
- package/dist/references.d.ts +38 -0
- package/dist/references.d.ts.map +1 -0
- package/dist/references.js +60 -0
- package/dist/references.js.map +1 -0
- package/dist/sort.d.ts +129 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +225 -0
- package/dist/sort.js.map +1 -0
- package/dist/src-CBeuhy5b.js +478 -0
- package/dist/src-CBeuhy5b.js.map +1 -0
- package/dist/update.d.ts +61 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +64 -0
- package/dist/update.js.map +1 -0
- package/dist/versioned-chain.d.ts +524 -0
- package/dist/versioned-chain.d.ts.map +1 -0
- package/dist/versioned-chain.js +298 -0
- package/dist/versioned-chain.js.map +1 -0
- package/package.json +109 -3
- package/src/columns.ts +186 -0
- package/src/constraints.ts +225 -0
- package/src/database.ts +273 -0
- package/src/derivation-error.ts +27 -0
- package/src/derive.ts +320 -0
- package/src/errors.ts +466 -0
- package/src/index.ts +653 -0
- package/src/jsonapi/derive.ts +74 -0
- package/src/jsonapi/index.ts +355 -0
- package/src/query.ts +183 -0
- package/src/references.ts +103 -0
- package/src/sort.ts +368 -0
- package/src/tree.ts +367 -0
- package/src/update.ts +88 -0
- package/src/versioned-chain.ts +1027 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
4
|
+
import * as R from "effect/Record";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
8
|
+
// src/derivation-error.ts
|
|
9
|
+
/**
|
|
10
|
+
* The construction-time failure every module-load derivation throws. Kept in a
|
|
11
|
+
* module of its own that imports nothing from Drizzle, so the Drizzle-free
|
|
12
|
+
* entry (`.`, which `defineTree` lives in) and `./derive` share one class: a
|
|
13
|
+
* consumer matching on it structurally, or with `instanceof`, sees the same
|
|
14
|
+
* error from either. Not a published subpath.
|
|
15
|
+
*
|
|
16
|
+
* @module derivation-error
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A construction-time derivation failure: a table or a tree could not be built
|
|
20
|
+
* from its declarations (no builder registered for a column kind, a
|
|
21
|
+
* `references` annotation with no resolver, numeric bounds on a column the
|
|
22
|
+
* table has no column for, an unresolvable FK target, a tree level whose
|
|
23
|
+
* parent is missing or forms a cycle).
|
|
24
|
+
*
|
|
25
|
+
* **Thrown, never failed.** Derivation runs at module load, before an Effect
|
|
26
|
+
* runtime exists, so the only way to stop a misdeclared table or tree from
|
|
27
|
+
* reaching production is to make its `import` fail. It is a tagged error all
|
|
28
|
+
* the same, so a consumer that does catch one (a registry loader, a codegen
|
|
29
|
+
* script) can match it structurally.
|
|
30
|
+
*/
|
|
31
|
+
var DerivationError = class extends Schema.TaggedError()("DerivationError", { message: Schema.String }) {};
|
|
32
|
+
// src/tree.ts
|
|
33
|
+
/**
|
|
34
|
+
* The interpreter behind `defineTree` (`.`) and `defineResourceTree`
|
|
35
|
+
* (`./jsonapi`): one Drizzle-free declaration of an ontology's level tree, from
|
|
36
|
+
* which every structural fact the database layer needs is derived at module
|
|
37
|
+
* load. Not a published subpath: the two entries re-export its types and wrap
|
|
38
|
+
* {@link buildTree} with their own leaf reader.
|
|
39
|
+
*
|
|
40
|
+
* **Shape.** A tree is an ordered record of levels. Each level names its
|
|
41
|
+
* `parent` (another level; absent means a child of the root, i.e. a branch
|
|
42
|
+
* root), may carry level `columns` (`dbColumn`-annotated fields contributed to
|
|
43
|
+
* every descendant's view, landing on that level's version table) and a level
|
|
44
|
+
* `kind`, and is either a **marker** (no `leaf`) or a **leaf** (a `leaf`
|
|
45
|
+
* payload: a struct's fields in `.`, a `Resource.make` definition in
|
|
46
|
+
* `./jsonapi`). A leaf's name is its discriminator value.
|
|
47
|
+
*
|
|
48
|
+
* **Order.** Declaration order is preserved wherever it becomes SQL order: the
|
|
49
|
+
* level list, every descendant and subtree-leaf list (the discriminator CHECK
|
|
50
|
+
* value sets), and each level's columns.
|
|
51
|
+
*
|
|
52
|
+
* **Failures.** Every check throws {@link DerivationError} at module load:
|
|
53
|
+
* there is no runtime yet, and a misdeclared tree must stop its `import`.
|
|
54
|
+
*
|
|
55
|
+
* @module tree
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Names no level column or leaf column may take: the chain's own columns, which
|
|
59
|
+
* the derived tables and views project from the root and from every level.
|
|
60
|
+
*/
|
|
61
|
+
const RESERVED_COLUMN_NAMES = [
|
|
62
|
+
"id",
|
|
63
|
+
"version",
|
|
64
|
+
"action",
|
|
65
|
+
"type"
|
|
66
|
+
];
|
|
67
|
+
/** Throw the tree's {@link DerivationError}. */
|
|
68
|
+
const treeError = (message) => {
|
|
69
|
+
throw new DerivationError({ message: `defineTree: ${message}` });
|
|
70
|
+
};
|
|
71
|
+
/** Throw for a level whose `parent` names no level of the tree. */
|
|
72
|
+
const unknownParent = (name, parent) => treeError(`level "${name}" names parent "${parent}", which is not a level of this tree`);
|
|
73
|
+
/** A canonical array index, which a JS object would reorder ahead of declaration order. */
|
|
74
|
+
const isArrayIndex = (name) => /^(?:0|[1-9]\d*)$/.test(name);
|
|
75
|
+
/** The first column on a path whose key or column name an earlier one took. */
|
|
76
|
+
const firstClash = (path) => Arr.findFirst(Arr.map(path, (later, i) => Option.map(Arr.findFirst(Arr.take(path, i), (earlier) => earlier.key === later.key || earlier.column === later.column), (earlier) => [earlier, later])), Option.isSome).pipe(Option.flatten);
|
|
77
|
+
/**
|
|
78
|
+
* Check a tree declaration and derive every level's facts. Throws
|
|
79
|
+
* {@link DerivationError} at module load for an empty tree, a level name a JS
|
|
80
|
+
* object would reorder (an array index) or that is empty, a parent naming no
|
|
81
|
+
* level, a parent cycle, a leaf with children, a marker with no leaf below it,
|
|
82
|
+
* a level column without a `dbColumn` annotation, a column taking a reserved
|
|
83
|
+
* name (`id`, `version`, `action`, `type`), or two columns on one root-to-leaf
|
|
84
|
+
* path sharing a field key or a column name. The entries' own payload checks
|
|
85
|
+
* come from `reader`.
|
|
86
|
+
*/
|
|
87
|
+
const buildTree = (spec, reader) => {
|
|
88
|
+
const names = R.keys(spec);
|
|
89
|
+
if (!Arr.isReadonlyArrayNonEmpty(names)) treeError("a tree needs at least one level");
|
|
90
|
+
Arr.forEach(names, (name) => {
|
|
91
|
+
if (name === "" || isArrayIndex(name)) treeError(`level name "${name}" is not allowed: an empty name or an array index would not keep declaration order`);
|
|
92
|
+
});
|
|
93
|
+
const has = (name) => R.has(spec, name);
|
|
94
|
+
const entryOf = (name) => spec[name];
|
|
95
|
+
const parentOf = (name) => Option.map(Option.fromUndefinedOr(entryOf(name).parent), (parent) => has(parent) ? parent : unknownParent(name, parent));
|
|
96
|
+
const leafOf = (name) => Option.fromUndefinedOr(entryOf(name).leaf);
|
|
97
|
+
const isLeafName = (name) => Option.isSome(leafOf(name));
|
|
98
|
+
const ancestorsOf = (name, trail) => Option.match(parentOf(name), {
|
|
99
|
+
onNone: () => [],
|
|
100
|
+
onSome: (parent) => Arr.contains(trail, parent) ? treeError(`the parents form a cycle: ${[...Arr.dropWhile(trail, (step) => step !== parent), parent].join(" → ")}`) : [...ancestorsOf(parent, [...trail, parent]), parent]
|
|
101
|
+
});
|
|
102
|
+
const ancestorsFor = (name) => ancestorsOf(name, [name]);
|
|
103
|
+
const leaves = Arr.filter(names, isLeafName);
|
|
104
|
+
const childrenOf = (name) => Arr.filter(names, (candidate) => Option.exists(parentOf(candidate), (parent) => parent === name));
|
|
105
|
+
const descendantsOf = (name) => Arr.filter(names, (candidate) => Arr.contains(ancestorsFor(candidate), name));
|
|
106
|
+
const subtreeLeavesOf = (name) => Arr.filter(leaves, (leaf) => leaf === name || Arr.contains(ancestorsFor(leaf), name));
|
|
107
|
+
Arr.forEach(names, (name) => {
|
|
108
|
+
if (isLeafName(name) && Arr.isReadonlyArrayNonEmpty(childrenOf(name))) treeError(`leaf "${name}" has children (${childrenOf(name).join(", ")}); only a marker level can be a parent`);
|
|
109
|
+
if (!isLeafName(name) && !Arr.isReadonlyArrayNonEmpty(subtreeLeavesOf(name))) treeError(`marker level "${name}" has no leaf below it, so its discriminator CHECK would admit nothing`);
|
|
110
|
+
});
|
|
111
|
+
const levelColumnsOf = (name) => {
|
|
112
|
+
const fields = entryOf(name).columns ?? {};
|
|
113
|
+
const columns = reader.levelColumns(fields);
|
|
114
|
+
const bare = Arr.filter(R.keys(fields), (key) => !R.has(columns, key));
|
|
115
|
+
return Arr.isReadonlyArrayNonEmpty(bare) ? treeError(`level "${name}" declares level columns with no dbColumn annotation (${bare.join(", ")}); a level column must back a database column`) : columns;
|
|
116
|
+
};
|
|
117
|
+
const leafColumnsOf = (name) => Option.match(leafOf(name), {
|
|
118
|
+
onNone: () => ({}),
|
|
119
|
+
onSome: (leaf) => reader.leafColumns(leaf, name)
|
|
120
|
+
});
|
|
121
|
+
const ownColumnsFor = (name) => {
|
|
122
|
+
const levelColumns = levelColumnsOf(name);
|
|
123
|
+
const leafColumns = leafColumnsOf(name);
|
|
124
|
+
const path = Arr.map([...R.toEntries(levelColumns), ...R.toEntries(leafColumns)], ([key, meta]) => ({
|
|
125
|
+
level: name,
|
|
126
|
+
key,
|
|
127
|
+
column: meta.column
|
|
128
|
+
}));
|
|
129
|
+
Arr.forEach(path, ({ key, column }) => {
|
|
130
|
+
if (Arr.contains(RESERVED_COLUMN_NAMES, key) || Arr.contains(RESERVED_COLUMN_NAMES, column)) treeError(`level "${name}" declares "${key}" (column "${column}"), but ${RESERVED_COLUMN_NAMES.join(", ")} are reserved for the chain`);
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
levelColumns,
|
|
134
|
+
leafColumns,
|
|
135
|
+
path
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const ownColumns = Arr.map(names, (name) => ({
|
|
139
|
+
name,
|
|
140
|
+
...ownColumnsFor(name)
|
|
141
|
+
}));
|
|
142
|
+
const ownOf = (name) => Option.getOrElse(Arr.findFirst(ownColumns, (own) => own.name === name), () => ({
|
|
143
|
+
name,
|
|
144
|
+
levelColumns: {},
|
|
145
|
+
leafColumns: {},
|
|
146
|
+
path: []
|
|
147
|
+
}));
|
|
148
|
+
Arr.forEach(leaves, (leaf) => {
|
|
149
|
+
const path = Arr.flatMap([...ancestorsFor(leaf), leaf], (name) => ownOf(name).path);
|
|
150
|
+
Option.map(firstClash(path), ([earlier, later]) => treeError(`"${later.key}" (column "${later.column}") on level "${later.level}" collides with "${earlier.key}" (column "${earlier.column}") on level "${earlier.level}"; both reach leaf "${leaf}"`));
|
|
151
|
+
});
|
|
152
|
+
const levels = Arr.map(names, (name) => ({
|
|
153
|
+
name,
|
|
154
|
+
entry: entryOf(name),
|
|
155
|
+
parent: parentOf(name),
|
|
156
|
+
kind: entryOf(name).kind ?? "plain",
|
|
157
|
+
isLeaf: isLeafName(name),
|
|
158
|
+
ancestors: ancestorsFor(name),
|
|
159
|
+
children: childrenOf(name),
|
|
160
|
+
descendants: descendantsOf(name),
|
|
161
|
+
subtreeLeaves: subtreeLeavesOf(name),
|
|
162
|
+
branch: Option.getOrElse(Arr.head(ancestorsFor(name)), () => name),
|
|
163
|
+
levelColumns: ownOf(name).levelColumns,
|
|
164
|
+
leafColumns: ownOf(name).leafColumns
|
|
165
|
+
}));
|
|
166
|
+
return {
|
|
167
|
+
spec,
|
|
168
|
+
names,
|
|
169
|
+
levels,
|
|
170
|
+
leaves,
|
|
171
|
+
branchRoots: Arr.filter(names, (name) => Option.isNone(parentOf(name))),
|
|
172
|
+
level: (name) => Option.getOrElse(Arr.findFirst(levels, (level) => level.name === name), () => treeError(`"${name}" is not a level of this tree`))
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
// src/index.ts
|
|
176
|
+
/**
|
|
177
|
+
* @copyright Thomas Foster
|
|
178
|
+
*
|
|
179
|
+
* **Effect Schema → Drizzle derivation: the annotation entry (no Drizzle).**
|
|
180
|
+
*
|
|
181
|
+
* A model attribute's base schema carries a {@link dbColumn} annotation
|
|
182
|
+
* describing the database column it backs; a downstream derivation (the
|
|
183
|
+
* {@link ./derive.ts} entry) reads that annotation and emits the matching
|
|
184
|
+
* Drizzle column builder. One declaration — the annotated field — is the single
|
|
185
|
+
* source for both the model projection and the column.
|
|
186
|
+
*
|
|
187
|
+
* This entry point has **no Drizzle dependency**: it is the half a client- /
|
|
188
|
+
* model-layer package can import without pulling the ORM into its bundle. The
|
|
189
|
+
* Drizzle-emitting half lives behind the {@link ./derive.ts} and
|
|
190
|
+
* {@link ./versioned-chain.ts} subpaths.
|
|
191
|
+
*
|
|
192
|
+
* **Why a phantom rides alongside the annotation.** Effect Schema annotations
|
|
193
|
+
* are erased from the static type (`schema.annotate(...)` returns the *same*
|
|
194
|
+
* type, and `resolveAnnotations` widens every read to the augmented
|
|
195
|
+
* `Annotations` interface). So a purely-annotation-driven derivation cannot stay
|
|
196
|
+
* type-safe — it can neither tell *which* fields are columns nor recover their
|
|
197
|
+
* precise column type at the type level. {@link dbColumn} therefore does two
|
|
198
|
+
* things: it attaches a real runtime annotation (the source of truth the runtime
|
|
199
|
+
* reads) **and** carries the metadata literally in a type-level phantom (so the
|
|
200
|
+
* column map stays precisely typed, preserving `$inferSelect`). Annotations
|
|
201
|
+
* alone are runtime-only; type safety needs the phantom.
|
|
202
|
+
*
|
|
203
|
+
* **Extensible column kinds.** The `pg` kind is an open `string` at this layer —
|
|
204
|
+
* the package bakes in no fixed set of kinds. A consumer pins its own kind union
|
|
205
|
+
* (e.g. `text | integer | boolean | timestamp | …custom`) via {@link dbColumnFor}
|
|
206
|
+
* and supplies the matching Drizzle builders to the derivation. So a repo can
|
|
207
|
+
* inject bespoke column kinds (a block-document store, a JSON codec, …) without
|
|
208
|
+
* the package knowing about them.
|
|
209
|
+
*
|
|
210
|
+
* **Why the readers take `Schema.Constraint`, not `Schema.Top`.** Every reader
|
|
211
|
+
* here only *reads* a schema (`resolveAnnotations`, `.ast`, the structural
|
|
212
|
+
* wrapper members); none of them calls `annotate` / `check` / `rebuild`. That is
|
|
213
|
+
* exactly effect's `Schema.Constraint` surface, and it is also the element type
|
|
214
|
+
* of `Schema.Struct.Fields` — so the field readers below can hand a struct's own
|
|
215
|
+
* field straight to a reader with no cast. `Schema.Top` is strictly narrower, so
|
|
216
|
+
* this widening accepts everything a `Schema.Top` parameter would.
|
|
217
|
+
*
|
|
218
|
+
* @module index
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* The annotation key under which {@link dbColumn} stores its metadata.
|
|
222
|
+
* {@link dbColumn} writes it; {@link readDbColumn} and {@link fieldColumns}
|
|
223
|
+
* read it. Namespaced to the package so a consumer's own annotations never
|
|
224
|
+
* collide.
|
|
225
|
+
*/
|
|
226
|
+
const DB_COLUMN_KEY = "effect-drizzle-ext/dbColumn";
|
|
227
|
+
/**
|
|
228
|
+
* Annotate a base schema with the database column it backs. Attaches a genuine
|
|
229
|
+
* Effect annotation (the runtime reads it via {@link readDbColumn}) and carries
|
|
230
|
+
* the metadata literally in the type (so the column derivation stays precisely
|
|
231
|
+
* typed). Wrap the result in a model helper as usual — the helper's projection
|
|
232
|
+
* carries the annotation through to the table factory.
|
|
233
|
+
*
|
|
234
|
+
* Accepts any `pg` string; pin the kind union with {@link dbColumnFor} to catch
|
|
235
|
+
* unknown-kind typos at the annotation site.
|
|
236
|
+
*/
|
|
237
|
+
const dbColumn = (schema, meta) => schema.annotate({ [DB_COLUMN_KEY]: meta });
|
|
238
|
+
/**
|
|
239
|
+
* A {@link dbColumn} constrained to a specific column-kind union `Kind`. A
|
|
240
|
+
* consumer pins its own kinds once — `const dbColumn = dbColumnFor<PgKind>()` —
|
|
241
|
+
* so an annotation naming a kind the derivation can't build is a type error at
|
|
242
|
+
* the annotation site rather than a runtime surprise.
|
|
243
|
+
*/
|
|
244
|
+
const dbColumnFor = () => (schema, meta) => schema.annotate({ [DB_COLUMN_KEY]: meta });
|
|
245
|
+
/**
|
|
246
|
+
* Read the db-column annotation off a schema at runtime. `Option.none()` when
|
|
247
|
+
* the schema declares none.
|
|
248
|
+
*
|
|
249
|
+
* Reads the schema's **own** annotations only: `Schema.resolveAnnotations` on a
|
|
250
|
+
* `Schema.optional(...)` / `Schema.NullOr(...)` wrapper does not see the inner
|
|
251
|
+
* schema's annotation. Use {@link fieldColumns} for a struct's fields, which
|
|
252
|
+
* peels those wrappers structurally.
|
|
253
|
+
*/
|
|
254
|
+
const readDbColumn = (schema) => Option.fromNullishOr(Schema.resolveAnnotations(schema)?.[DB_COLUMN_KEY]);
|
|
255
|
+
/**
|
|
256
|
+
* The runtime key under which {@link dbTable} attaches its metadata. A symbol
|
|
257
|
+
* **property on the target object**, not an Effect Schema annotation — and that
|
|
258
|
+
* is the reason {@link dbTable} is generic over any `T extends object` rather
|
|
259
|
+
* than over `Schema.Top`.
|
|
260
|
+
*
|
|
261
|
+
* `annotate` does not annotate in place: it **rebuilds** the schema. For a
|
|
262
|
+
* target assembled as `Object.assign(struct, { … })` — the shape a resource /
|
|
263
|
+
* model definition in the consuming applications takes — a rebuild drops every
|
|
264
|
+
* assigned property, and it breaks the referential identity that family
|
|
265
|
+
* membership and registry lockstep checks compare by. A non-enumerable own
|
|
266
|
+
* symbol property annotates in place, preserving both, and works on a plain
|
|
267
|
+
* object just as well as on a schema.
|
|
268
|
+
*/
|
|
269
|
+
const DB_TABLE_KEY = Symbol.for("effect-drizzle-ext/dbTable");
|
|
270
|
+
/**
|
|
271
|
+
* Annotate any object with the database table it backs. Attaches the metadata
|
|
272
|
+
* as a non-enumerable symbol property (see {@link DB_TABLE_KEY} for why not
|
|
273
|
+
* `annotate`) and carries it literally in the type, so a level-bound table
|
|
274
|
+
* factory can constrain its input to leaves anchored at its level. Returns the
|
|
275
|
+
* **same** object.
|
|
276
|
+
*
|
|
277
|
+
* Accepts any {@link TableMeta}; pin a level union with {@link dbTableFor}.
|
|
278
|
+
*/
|
|
279
|
+
const dbTable = (target, meta) => {
|
|
280
|
+
Object.defineProperty(target, DB_TABLE_KEY, {
|
|
281
|
+
value: meta,
|
|
282
|
+
enumerable: false
|
|
283
|
+
});
|
|
284
|
+
return target;
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* A {@link dbTable} constrained to a specific level union `Level`, requiring the
|
|
288
|
+
* full {@link DbTable} metadata. A consumer pins its own levels once — `const
|
|
289
|
+
* dbTable = dbTableFor<LeafLevel>()` — so an annotation naming a level the
|
|
290
|
+
* derivation has no parent binding for is a type error at the annotation site
|
|
291
|
+
* rather than a runtime surprise.
|
|
292
|
+
*/
|
|
293
|
+
const dbTableFor = () => (target, meta) => dbTable(target, meta);
|
|
294
|
+
/**
|
|
295
|
+
* Read the db-table metadata off a target at runtime. `Option.none()` when the
|
|
296
|
+
* target carries none.
|
|
297
|
+
*
|
|
298
|
+
* Typed to the {@link TableMeta} floor: a consumer that annotated with a
|
|
299
|
+
* {@link DbTable} recovers its `level` through the {@link DbTableOf} phantom,
|
|
300
|
+
* which is where the level is statically known.
|
|
301
|
+
*/
|
|
302
|
+
const readDbTable = (target) => Option.fromNullishOr(target[DB_TABLE_KEY]);
|
|
303
|
+
const BOUND_KEYS = [
|
|
304
|
+
"minimum",
|
|
305
|
+
"maximum",
|
|
306
|
+
"exclusiveMinimum",
|
|
307
|
+
"exclusiveMaximum"
|
|
308
|
+
];
|
|
309
|
+
/**
|
|
310
|
+
* The neutral `CheckInput` the walker hands a check's `toJsonSchema`: no outer
|
|
311
|
+
* type, no member schemas, which the bound filters ignore.
|
|
312
|
+
*/
|
|
313
|
+
const NEUTRAL_CHECK_INPUT = {
|
|
314
|
+
type: void 0,
|
|
315
|
+
schemas: []
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Whether a `toJsonSchema` output is a plain fragment. The approximate
|
|
319
|
+
* `[fragment, true]` form is an array, whose bound keys read as absent, so the
|
|
320
|
+
* walker reads no bounds from it.
|
|
321
|
+
*/
|
|
322
|
+
const isFragment = (output) => Predicate.isObject(output);
|
|
323
|
+
/** The JSON-Schema fragment a check's `toJsonSchema` annotation exposes, if any. */
|
|
324
|
+
const checkFragment = (check) => Option.flatMap(Option.fromNullishOr(check.annotations?.toJsonSchema), (toJsonSchema) => Option.filter(Option.flatMap(Result.getSuccess(Result.try(() => toJsonSchema(NEUTRAL_CHECK_INPUT))), Option.fromNullishOr), isFragment));
|
|
325
|
+
/** Merge a fragment's numeric bound keys over `bounds` (the later check wins a key). */
|
|
326
|
+
const mergeBounds = (fragment, bounds) => Arr.reduce(BOUND_KEYS, bounds, (acc, key) => {
|
|
327
|
+
const value = fragment[key];
|
|
328
|
+
return Predicate.isNumber(value) ? {
|
|
329
|
+
...acc,
|
|
330
|
+
[key]: value
|
|
331
|
+
} : acc;
|
|
332
|
+
});
|
|
333
|
+
/**
|
|
334
|
+
* Merge the numeric-bound fragments of a check list over `bounds`.
|
|
335
|
+
*
|
|
336
|
+
* `checks` is an `Option` because effect's own AST leaves the field absent; the
|
|
337
|
+
* callers convert that foreign `undefined` at the boundary with
|
|
338
|
+
* `Option.fromNullishOr`.
|
|
339
|
+
*/
|
|
340
|
+
const collectCheckBounds = (checks, bounds) => Arr.reduce(Option.getOrElse(checks, () => []), bounds, (acc, check) => {
|
|
341
|
+
const nested = Predicate.isTagged(check, "FilterGroup") ? collectCheckBounds(Option.some(check.checks), acc) : acc;
|
|
342
|
+
return Option.match(checkFragment(check), {
|
|
343
|
+
onNone: () => nested,
|
|
344
|
+
onSome: (fragment) => mergeBounds(fragment, nested)
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Recover the {@link NumericBounds} a single attribute schema declares, or
|
|
349
|
+
* `Option.none()` when it declares none. Reads the schema's own checks plus the
|
|
350
|
+
* checks of each union member — the `Schema.NullOr(Schema.Finite.check(...))`
|
|
351
|
+
* shape a nullable numeric attribute takes. Deliberately shallow: bounds
|
|
352
|
+
* buried deeper than one union level are a modelling smell, not a supported
|
|
353
|
+
* shape.
|
|
354
|
+
*
|
|
355
|
+
* Exported because the `./jsonapi` entry's resource-level bounds reader is the
|
|
356
|
+
* same walk over a different field source.
|
|
357
|
+
*/
|
|
358
|
+
const numericBounds = (schema) => {
|
|
359
|
+
const { ast } = schema;
|
|
360
|
+
const bounds = Arr.reduce(SchemaAST.isUnion(ast) ? ast.types : [], collectCheckBounds(Option.fromNullishOr(ast.checks), {}), (acc, member) => collectCheckBounds(Option.fromNullishOr(member.checks), acc));
|
|
361
|
+
return Arr.some(BOUND_KEYS, (key) => bounds[key] !== void 0) ? Option.some(bounds) : Option.none();
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* One structural peel of a field wrapper: the schema `schema` wraps, or
|
|
365
|
+
* `Option.none()` when it wraps nothing.
|
|
366
|
+
*
|
|
367
|
+
* Structural, not annotation-driven, because `Schema.resolveAnnotations` on a
|
|
368
|
+
* wrapper does not see the inner schema's annotations — the wrapper is a
|
|
369
|
+
* different schema with its own (empty) annotation record. An
|
|
370
|
+
* `optional` / `optionalKey` wrapper exposes its inner schema as `.schema`; a
|
|
371
|
+
* `NullOr` / `UndefinedOr` union exposes its members as `.members`, of which the
|
|
372
|
+
* `null` / `undefined` arms are the ones to skip.
|
|
373
|
+
*/
|
|
374
|
+
const underlyingSchema = (schema) => {
|
|
375
|
+
const wrapper = schema;
|
|
376
|
+
if (wrapper.schema !== void 0) return Option.some(wrapper.schema);
|
|
377
|
+
return Arr.findFirst(wrapper.members ?? [], (member) => {
|
|
378
|
+
const tag = member.ast._tag;
|
|
379
|
+
return tag !== "Null" && tag !== "Undefined";
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* How many wrappers deep the peel walks before giving up. The deepest shape the
|
|
384
|
+
* projection supports is `optional(NullOr(annotated))`, which is three peels
|
|
385
|
+
* (`optionalKey` → `UndefinedOr` → `NullOr`); the cap is slack above that, and
|
|
386
|
+
* it is what keeps a self-referential schema from recursing forever.
|
|
387
|
+
*/
|
|
388
|
+
const MAX_WRAPPER_DEPTH = 8;
|
|
389
|
+
/**
|
|
390
|
+
* The schema that actually carries the {@link dbColumn} annotation for a struct
|
|
391
|
+
* field: the field itself when it is annotated, otherwise the first annotated
|
|
392
|
+
* schema found by peeling its wrappers. `Option.none()` for a field that backs
|
|
393
|
+
* no column.
|
|
394
|
+
*
|
|
395
|
+
* Returning the *schema* rather than the metadata is what lets
|
|
396
|
+
* {@link fieldColumnBounds} read the bounds off the same node the column was
|
|
397
|
+
* declared on — bounds on `dbColumn(NullOr(Number.check(…)), …)` sit one union
|
|
398
|
+
* level below the annotated node, which is exactly what {@link numericBounds}
|
|
399
|
+
* walks.
|
|
400
|
+
*/
|
|
401
|
+
const columnSchema = (field, depth = 0) => {
|
|
402
|
+
if (depth > MAX_WRAPPER_DEPTH) return Option.none();
|
|
403
|
+
if (Option.isSome(readDbColumn(field))) return Option.some(field);
|
|
404
|
+
return Option.flatMap(underlyingSchema(field), (inner) => columnSchema(inner, depth + 1));
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Project a `Schema.Struct`'s fields to the per-field {@link DbColumn} metadata
|
|
408
|
+
* the derivation consumes, dropping fields that declare no column. Peels the
|
|
409
|
+
* `optional` / `NullOr` wrappers a field may apply. The type-level counterpart
|
|
410
|
+
* is {@link FieldColumnsMetaOf}.
|
|
411
|
+
*/
|
|
412
|
+
const fieldColumns = (fields) => R.getSomes(R.map(fields, (field) => Option.flatMap(columnSchema(field), readDbColumn)));
|
|
413
|
+
/**
|
|
414
|
+
* Project a `Schema.Struct`'s per-field numeric bounds into plain data a
|
|
415
|
+
* `CHECK`-constraint derivation consumes: for each {@link dbColumn}-annotated
|
|
416
|
+
* field whose schema carries numeric-bound checks, the bounds paired with the
|
|
417
|
+
* column metadata. Fields without a column, or without bounds, drop. The
|
|
418
|
+
* Drizzle-emitting counterpart is the `./derive` entry's
|
|
419
|
+
* `deriveFieldCheckConstraints`.
|
|
420
|
+
*/
|
|
421
|
+
const fieldColumnBounds = (fields) => R.getSomes(R.map(fields, (field) => Option.flatMap(columnSchema(field), (schema) => Option.zipWith(readDbColumn(schema), numericBounds(schema), (column, bounds) => ({
|
|
422
|
+
column,
|
|
423
|
+
...bounds
|
|
424
|
+
})))));
|
|
425
|
+
/**
|
|
426
|
+
* A {@link defineTree} whose every entry also carries the consumer's
|
|
427
|
+
* extension record `Ext` (pulpit's `owned` / `pageable` flags, say); the
|
|
428
|
+
* library reads none of its members and hands each entry back on
|
|
429
|
+
* `tree.level(name).entry`. Pin it once — `const defineTree =
|
|
430
|
+
* defineTreeFor<RegistryFlags>()` — so an entry missing a flag is a type
|
|
431
|
+
* error. Throws {@link DerivationError} at module load as `defineTree` does.
|
|
432
|
+
*/
|
|
433
|
+
const defineTreeFor = () => (spec) => buildTree(spec, {
|
|
434
|
+
levelColumns: fieldColumns,
|
|
435
|
+
leafColumns: fieldColumns
|
|
436
|
+
});
|
|
437
|
+
/**
|
|
438
|
+
* Declare an ontology's level tree once, Drizzle-free, so a models package can
|
|
439
|
+
* state it and the database layer derive every table, constraint and view from
|
|
440
|
+
* it. Each entry is a level: a **marker** (no `leaf`) or a **leaf**, whose
|
|
441
|
+
* `leaf` is the struct fields of its domain columns (`dbColumn`-annotated) and
|
|
442
|
+
* whose name is its discriminator value. An entry names its `parent` (absent:
|
|
443
|
+
* a branch root under the tree's root), and may carry level `columns`
|
|
444
|
+
* contributed to every descendant's view and a level `kind`.
|
|
445
|
+
*
|
|
446
|
+
* Returns the checked {@link Tree}: per level, its ancestors, children,
|
|
447
|
+
* descendants, subtree leaves (the discriminator CHECK value set) and branch,
|
|
448
|
+
* all in declaration order. A parent naming no level of the tree, or the level
|
|
449
|
+
* itself, is a type error. Throws {@link DerivationError} at module load for
|
|
450
|
+
* every structural fault the types can't catch: a missing parent or a cycle, a
|
|
451
|
+
* leaf with children, a marker with no leaf below it, an unannotated level
|
|
452
|
+
* column, a reserved column name (`id`, `version`, `action`, `type`), or two
|
|
453
|
+
* columns on one root-to-leaf path sharing a field key or a column name.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* ```ts
|
|
457
|
+
* import { dbColumn, defineTree } from "@thomasfosterau/effect-drizzle-ext";
|
|
458
|
+
* import * as Schema from "effect/Schema";
|
|
459
|
+
*
|
|
460
|
+
* const tree = defineTree({
|
|
461
|
+
* work: {
|
|
462
|
+
* columns: { title: dbColumn(Schema.String, { column: "title", pg: "text", notNull: true }) },
|
|
463
|
+
* },
|
|
464
|
+
* book: {
|
|
465
|
+
* parent: "work",
|
|
466
|
+
* leaf: { pages: dbColumn(Schema.Finite, { column: "pages", pg: "integer", notNull: false }) },
|
|
467
|
+
* },
|
|
468
|
+
* film: { parent: "work", leaf: {} },
|
|
469
|
+
* });
|
|
470
|
+
*
|
|
471
|
+
* console.log(tree.level("work").subtreeLeaves); // ["book", "film"]
|
|
472
|
+
* console.log(tree.level("book").ancestors); // ["work"]
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
const defineTree = defineTreeFor();
|
|
476
|
+
export { dbTableFor as a, fieldColumnBounds as c, readDbColumn as d, readDbTable as f, DerivationError as h, dbTable as i, fieldColumns as l, treeError as m, dbColumn as n, defineTree as o, buildTree as p, dbColumnFor as r, defineTreeFor as s, DB_COLUMN_KEY as t, numericBounds as u };
|
|
477
|
+
|
|
478
|
+
//# sourceMappingURL=src-CBeuhy5b.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src-CBeuhy5b.js","names":[],"sources":["../src/derivation-error.ts","../src/tree.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Thomas Foster
|
|
3
|
+
*
|
|
4
|
+
* **The update tri-state at a single seam.** {@link updateSetMap} builds an
|
|
5
|
+
* `UPDATE … SET` map from a decoded update input's **own keys**:
|
|
6
|
+
*
|
|
7
|
+
* | input | meaning | in the `SET` map |
|
|
8
|
+
* | -------------------- | -------------- | ----------------------- |
|
|
9
|
+
* | `{ col: value }` | set | `col: value` |
|
|
10
|
+
* | `{ col: null }` | clear | `col: null` |
|
|
11
|
+
* | key absent / `undefined` | carry forward | not present at all |
|
|
12
|
+
*
|
|
13
|
+
* An update input schema encodes the three states across key presence and
|
|
14
|
+
* the value — `Schema.optionalKey(Schema.NullOr(X))` for a clearable field,
|
|
15
|
+
* `Schema.optionalKey(X)` for one that may not be cleared — so decoding has
|
|
16
|
+
* already refused `null` for a column that must not be cleared. What this
|
|
17
|
+
* module must never do is fold the first two rows together: `col: input.col
|
|
18
|
+
* ?? null` turns a carry-forward into a clear, which is silent data loss
|
|
19
|
+
* (website #1877).
|
|
20
|
+
*
|
|
21
|
+
* **Nothing here is `Option`.** The `null` / `undefined` split *is* the
|
|
22
|
+
* tri-state; collapsing either side to one `Option` erases the difference
|
|
23
|
+
* between "leave alone" and "clear". The `SET` map is also Drizzle-facing,
|
|
24
|
+
* where `undefined` means "not supplied" and an `Option` would be written as
|
|
25
|
+
* a value.
|
|
26
|
+
*
|
|
27
|
+
* The preferenceflow application's shape (`apps/api/src/operations/
|
|
28
|
+
* update-fields.ts`); the website application's carry-forward merge
|
|
29
|
+
* (`mergeUpdateFields(current, input)`) is the same rule applied to a
|
|
30
|
+
* pre-read row, and needs no helper once the write is a `SET` map.
|
|
31
|
+
*
|
|
32
|
+
* @module update
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The `SET` (or `VALUES`) map {@link updateSetMap} produces from input `I`
|
|
36
|
+
* and writable column keys `K`: every writable key of `I`, optional (it is
|
|
37
|
+
* present only when the input supplied a value), with `undefined` removed
|
|
38
|
+
* from its value type (a present `null` stays).
|
|
39
|
+
*/
|
|
40
|
+
export type UpdateSet<I, K extends string> = { readonly [P in Extract<keyof I, K>]?: Exclude<I[P], undefined>; };
|
|
41
|
+
/**
|
|
42
|
+
* The `SET` map for an update: every key of `input` that names a writable
|
|
43
|
+
* column and carries a **present** value (`null` included), in the input's
|
|
44
|
+
* own key order. Keys outside `writable` — the routing `id`, a
|
|
45
|
+
* server-owned column, a relationship the table does not carry, an unknown
|
|
46
|
+
* key — are dropped; an `undefined`-valued key (an explicit carry-forward)
|
|
47
|
+
* is dropped too, since the tri-state's first two rows must stay apart.
|
|
48
|
+
*
|
|
49
|
+
* `writable` is any iterable of column keys; pass a module-scope `HashSet`
|
|
50
|
+
* to build it once.
|
|
51
|
+
*/
|
|
52
|
+
export declare const updateSetMap: <I extends Readonly<Record<string, unknown>>, K extends string>(input: I, writable: Iterable<K>) => UpdateSet<I, K>;
|
|
53
|
+
/**
|
|
54
|
+
* The `VALUES` map for a create: every key of `input` naming a writable
|
|
55
|
+
* column with a present value. An absent optional is left to the column
|
|
56
|
+
* default rather than written as `undefined`, and a key the table does not
|
|
57
|
+
* carry is dropped — the same projection as {@link updateSetMap}, without
|
|
58
|
+
* the tri-state reading (a create has nothing to carry forward).
|
|
59
|
+
*/
|
|
60
|
+
export declare const insertValues: <I extends Readonly<Record<string, unknown>>, K extends string>(input: I, writable: Iterable<K>) => UpdateSet<I, K>;
|
|
61
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","names":[],"sources":["../src/update.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YA4CY,UAAU,GAAG,gCACb,KAAK,cAAc,GAAG,MAAM,QAAQ,EAAE;;;;;;;;;;;;qBAiBrC,eAAgB,UAAU,SAAS,0BAA0B,kBAAgB,OACjF,GAAC,UACE,SAAS,OAClB,UAAU,GAAG;;;;;;;;qBAmBH,eAAgB,UAAU,SAAS,0BAA0B,kBAAgB,OACjF,GAAC,UACE,SAAS,OAClB,UAAU,GAAG"}
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as Array from "effect/Array";
|
|
2
|
+
import * as Record from "effect/Record";
|
|
3
|
+
import * as HashSet from "effect/HashSet";
|
|
4
|
+
// src/update.ts
|
|
5
|
+
/**
|
|
6
|
+
* @copyright Thomas Foster
|
|
7
|
+
*
|
|
8
|
+
* **The update tri-state at a single seam.** {@link updateSetMap} builds an
|
|
9
|
+
* `UPDATE … SET` map from a decoded update input's **own keys**:
|
|
10
|
+
*
|
|
11
|
+
* | input | meaning | in the `SET` map |
|
|
12
|
+
* | -------------------- | -------------- | ----------------------- |
|
|
13
|
+
* | `{ col: value }` | set | `col: value` |
|
|
14
|
+
* | `{ col: null }` | clear | `col: null` |
|
|
15
|
+
* | key absent / `undefined` | carry forward | not present at all |
|
|
16
|
+
*
|
|
17
|
+
* An update input schema encodes the three states across key presence and
|
|
18
|
+
* the value — `Schema.optionalKey(Schema.NullOr(X))` for a clearable field,
|
|
19
|
+
* `Schema.optionalKey(X)` for one that may not be cleared — so decoding has
|
|
20
|
+
* already refused `null` for a column that must not be cleared. What this
|
|
21
|
+
* module must never do is fold the first two rows together: `col: input.col
|
|
22
|
+
* ?? null` turns a carry-forward into a clear, which is silent data loss
|
|
23
|
+
* (website #1877).
|
|
24
|
+
*
|
|
25
|
+
* **Nothing here is `Option`.** The `null` / `undefined` split *is* the
|
|
26
|
+
* tri-state; collapsing either side to one `Option` erases the difference
|
|
27
|
+
* between "leave alone" and "clear". The `SET` map is also Drizzle-facing,
|
|
28
|
+
* where `undefined` means "not supplied" and an `Option` would be written as
|
|
29
|
+
* a value.
|
|
30
|
+
*
|
|
31
|
+
* The preferenceflow application's shape (`apps/api/src/operations/
|
|
32
|
+
* update-fields.ts`); the website application's carry-forward merge
|
|
33
|
+
* (`mergeUpdateFields(current, input)`) is the same rule applied to a
|
|
34
|
+
* pre-read row, and needs no helper once the write is a `SET` map.
|
|
35
|
+
*
|
|
36
|
+
* @module update
|
|
37
|
+
*/
|
|
38
|
+
const toHashSet = (writable) => HashSet.isHashSet(writable) ? writable : HashSet.fromIterable(writable);
|
|
39
|
+
/**
|
|
40
|
+
* The `SET` map for an update: every key of `input` that names a writable
|
|
41
|
+
* column and carries a **present** value (`null` included), in the input's
|
|
42
|
+
* own key order. Keys outside `writable` — the routing `id`, a
|
|
43
|
+
* server-owned column, a relationship the table does not carry, an unknown
|
|
44
|
+
* key — are dropped; an `undefined`-valued key (an explicit carry-forward)
|
|
45
|
+
* is dropped too, since the tri-state's first two rows must stay apart.
|
|
46
|
+
*
|
|
47
|
+
* `writable` is any iterable of column keys; pass a module-scope `HashSet`
|
|
48
|
+
* to build it once.
|
|
49
|
+
*/
|
|
50
|
+
const updateSetMap = (input, writable) => {
|
|
51
|
+
const columns = toHashSet(writable);
|
|
52
|
+
return Record.fromEntries(Array.filter(Record.toEntries(input), ([key, value]) => value !== void 0 && HashSet.has(columns, key)));
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The `VALUES` map for a create: every key of `input` naming a writable
|
|
56
|
+
* column with a present value. An absent optional is left to the column
|
|
57
|
+
* default rather than written as `undefined`, and a key the table does not
|
|
58
|
+
* carry is dropped — the same projection as {@link updateSetMap}, without
|
|
59
|
+
* the tri-state reading (a create has nothing to carry forward).
|
|
60
|
+
*/
|
|
61
|
+
const insertValues = (input, writable) => updateSetMap(input, writable);
|
|
62
|
+
export { insertValues, updateSetMap };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.js","names":[],"sources":["../src/update.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAM,aAA+B,aACnC,QAAQ,UAAU,QAAQ,IAAI,WAAW,QAAQ,aAAa,QAAQ;;;;;;;;;;;;AAaxE,MAAa,gBACX,OACA,aACoB;CACpB,MAAM,UAAmC,UAAU,QAAQ;CAG3D,OAAO,OAAO,YACZ,MAAM,OACJ,OAAO,UAAU,KAAK,IACrB,CAAC,KAAK,WAAW,UAAU,KAAA,KAAa,QAAQ,IAAI,SAAS,GAAG,CACnE,CACF;AACF;;;;;;;;AASA,MAAa,gBACX,OACA,aACoB,aAAa,OAAO,QAAQ"}
|