@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +508 -2
  3. package/dist/columns.d.ts +113 -0
  4. package/dist/columns.d.ts.map +1 -0
  5. package/dist/columns.js +146 -0
  6. package/dist/columns.js.map +1 -0
  7. package/dist/constraints.d.ts +117 -0
  8. package/dist/constraints.d.ts.map +1 -0
  9. package/dist/constraints.js +93 -0
  10. package/dist/constraints.js.map +1 -0
  11. package/dist/database.d.ts +189 -0
  12. package/dist/database.d.ts.map +1 -0
  13. package/dist/database.js +51 -0
  14. package/dist/database.js.map +1 -0
  15. package/dist/derive.d.ts +97 -0
  16. package/dist/derive.d.ts.map +1 -0
  17. package/dist/derive.js +142 -0
  18. package/dist/derive.js.map +1 -0
  19. package/dist/errors.d.ts +157 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +287 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index-CFhgcF4N.d.ts +418 -0
  24. package/dist/index-CFhgcF4N.d.ts.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +1 -0
  27. package/dist/jsonapi/derive.d.ts +28 -0
  28. package/dist/jsonapi/derive.d.ts.map +1 -0
  29. package/dist/jsonapi/derive.js +28 -0
  30. package/dist/jsonapi/derive.js.map +1 -0
  31. package/dist/jsonapi/index.d.ts +189 -0
  32. package/dist/jsonapi/index.d.ts.map +1 -0
  33. package/dist/jsonapi/index.js +164 -0
  34. package/dist/jsonapi/index.js.map +1 -0
  35. package/dist/query.d.ts +111 -0
  36. package/dist/query.d.ts.map +1 -0
  37. package/dist/query.js +142 -0
  38. package/dist/query.js.map +1 -0
  39. package/dist/references.d.ts +38 -0
  40. package/dist/references.d.ts.map +1 -0
  41. package/dist/references.js +60 -0
  42. package/dist/references.js.map +1 -0
  43. package/dist/sort.d.ts +129 -0
  44. package/dist/sort.d.ts.map +1 -0
  45. package/dist/sort.js +225 -0
  46. package/dist/sort.js.map +1 -0
  47. package/dist/src-CBeuhy5b.js +478 -0
  48. package/dist/src-CBeuhy5b.js.map +1 -0
  49. package/dist/update.d.ts +61 -0
  50. package/dist/update.d.ts.map +1 -0
  51. package/dist/update.js +64 -0
  52. package/dist/update.js.map +1 -0
  53. package/dist/versioned-chain.d.ts +524 -0
  54. package/dist/versioned-chain.d.ts.map +1 -0
  55. package/dist/versioned-chain.js +298 -0
  56. package/dist/versioned-chain.js.map +1 -0
  57. package/package.json +109 -3
  58. package/src/columns.ts +186 -0
  59. package/src/constraints.ts +225 -0
  60. package/src/database.ts +273 -0
  61. package/src/derivation-error.ts +27 -0
  62. package/src/derive.ts +320 -0
  63. package/src/errors.ts +466 -0
  64. package/src/index.ts +653 -0
  65. package/src/jsonapi/derive.ts +74 -0
  66. package/src/jsonapi/index.ts +355 -0
  67. package/src/query.ts +183 -0
  68. package/src/references.ts +103 -0
  69. package/src/sort.ts +368 -0
  70. package/src/tree.ts +367 -0
  71. package/src/update.ts +88 -0
  72. 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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"}
@@ -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"}