@clivly/core 0.9.6 → 0.10.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/README.md +30 -10
- package/dist/builder.cjs +599 -0
- package/dist/builder.d.cts +336 -0
- package/dist/builder.d.mts +336 -0
- package/dist/builder.mjs +589 -0
- package/dist/drizzle.cjs +9 -1
- package/dist/drizzle.d.cts +4 -2
- package/dist/drizzle.d.mts +4 -2
- package/dist/drizzle.mjs +9 -1
- package/dist/{entity-config-BAaZOIID.cjs → entity-config-C5jBmG2C.cjs} +115 -71
- package/dist/{entity-config-buOIVtMM.d.cts → entity-config-DCa785OE.d.cts} +27 -1
- package/dist/{entity-config-buOIVtMM.d.mts → entity-config-DCa785OE.d.mts} +27 -1
- package/dist/{entity-config-kusaRiDs.mjs → entity-config-DpHPMFIS.mjs} +104 -42
- package/dist/entity-config.cjs +1 -1
- package/dist/entity-config.d.cts +1 -1
- package/dist/entity-config.d.mts +1 -1
- package/dist/entity-config.mjs +1 -1
- package/dist/entity-heuristics.cjs +34 -1
- package/dist/entity-heuristics.d.cts +8 -2
- package/dist/entity-heuristics.d.mts +8 -2
- package/dist/entity-heuristics.mjs +34 -2
- package/dist/index.cjs +136 -199
- package/dist/index.d.cts +135 -89
- package/dist/index.d.mts +135 -89
- package/dist/index.mjs +124 -188
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +2 -16
- package/dist/mappings-config.d.cts +1 -1
- package/dist/mappings-config.d.mts +1 -1
- package/dist/mappings-config.mjs +1 -15
- package/dist/{projection-OJYCYmS_.cjs → projection-DFgCoyZl.cjs} +0 -6
- package/dist/{projection-BXw_iiw5.mjs → projection-SCNrLJMV.mjs} +1 -1
- package/dist/schema-meta-Ac7938mT.mjs +46 -0
- package/dist/schema-meta-Cgi0MnHh.cjs +69 -0
- package/dist/sync-engine.cjs +1 -1
- package/dist/sync-engine.d.cts +1 -1
- package/dist/sync-engine.d.mts +1 -1
- package/dist/sync-engine.mjs +1 -1
- package/dist/types.cjs +11 -0
- package/dist/types.d.cts +28 -2
- package/dist/types.d.mts +28 -2
- package/dist/types.mjs +9 -1
- package/dist/{view-compiler-u3GGRgnv.mjs → view-compiler-CLvZ0wJW.mjs} +1 -1
- package/dist/{view-compiler-D30HNRnA.cjs → view-compiler-DP4Fbj7-.cjs} +1 -1
- package/dist/view-compiler.cjs +1 -1
- package/dist/view-compiler.d.cts +1 -1
- package/dist/view-compiler.d.mts +1 -1
- package/dist/view-compiler.mjs +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { F as ProjectionMapping, m as DiscoveredSchemaTable } from "./entity-config-DCa785OE.cjs";
|
|
2
|
+
//#region src/relationship-graph.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A bidirectional relationship graph derived from discovery metadata.
|
|
5
|
+
*
|
|
6
|
+
* The retired builder's join suggester scanned only the *joined* table for a
|
|
7
|
+
* key pointing at the base table — the parent-to-child direction — so it
|
|
8
|
+
* auto-filled exactly the joins that fan out and could never fill the safe
|
|
9
|
+
* child-to-parent ones. (That helper was removed with the builder it served;
|
|
10
|
+
* this note records the mistake so it is not repeated.) This graph walks a
|
|
11
|
+
* table's own FKs (child -> parent) *and* every other table's FKs that point
|
|
12
|
+
* at it (parent -> child), so both directions are navigable.
|
|
13
|
+
*
|
|
14
|
+
* An edge is identified by its complete FK column pair, not by its table
|
|
15
|
+
* pair: `tickets.assignee_id -> users.id` and `tickets.reviewer_id ->
|
|
16
|
+
* users.id` are two distinct edges, not one collapsed relationship.
|
|
17
|
+
*
|
|
18
|
+
* Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
|
|
19
|
+
* A table that fails `hasRichMeta` cannot be proven to have any — or no —
|
|
20
|
+
* relationships, so it is marked `offerable: false` with a reason rather
|
|
21
|
+
* than silently rendered as a legitimately isolated node with zero edges.
|
|
22
|
+
*/
|
|
23
|
+
type RelationshipDirection = "child-to-parent" | "parent-to-child";
|
|
24
|
+
type RelationshipCardinality = "to-one" | "to-many";
|
|
25
|
+
interface RelationshipEdge {
|
|
26
|
+
cardinality: RelationshipCardinality;
|
|
27
|
+
direction: RelationshipDirection;
|
|
28
|
+
/** The FK column on the child side of the relationship. */
|
|
29
|
+
fromColumn: string;
|
|
30
|
+
/** The table this edge is being viewed from. */
|
|
31
|
+
fromTable: string;
|
|
32
|
+
/** The referenced column on the parent side of the relationship. */
|
|
33
|
+
toColumn: string;
|
|
34
|
+
/** The table this edge points to. */
|
|
35
|
+
toTable: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Why a table (or, later, a path) cannot be offered.
|
|
39
|
+
*
|
|
40
|
+
* A closed discriminated union, not free text. Every case has to be rendered
|
|
41
|
+
* as its own piece of copy for a non-technical user, and copy belongs to the
|
|
42
|
+
* UI, not here — so this carries the machine-readable case only. Closing the
|
|
43
|
+
* union is what makes the UI's switch exhaustive: a case added here without a
|
|
44
|
+
* matching message is a type error, rather than a new case reaching a user
|
|
45
|
+
* untranslated.
|
|
46
|
+
*
|
|
47
|
+
* Task 4 added the starting-record eligibility cases (`composite-pk`,
|
|
48
|
+
* `no-primary-key`). Add members; do not widen the type back to `string`.
|
|
49
|
+
*/
|
|
50
|
+
/** Discovery reported no `columnsMeta`/`uniqueConstraints` for the table. */
|
|
51
|
+
interface NoRichMetaReason {
|
|
52
|
+
code: "no-rich-meta";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The table has more than one primary-key column.
|
|
56
|
+
*
|
|
57
|
+
* This is a UI limitation, not a platform one: `projectionIdentitySchema`
|
|
58
|
+
* already supports `{ kind: "composite", parts }` and `checkIdentitySafety`
|
|
59
|
+
* already validates it — the builder simply does not emit that identity kind
|
|
60
|
+
* in v1. The main casualty is the junction table, which usually carries a
|
|
61
|
+
* composite PK and is exactly the shape someone reaches for when modelling a
|
|
62
|
+
* many-to-many, so the UI copy for this case must say "not yet" and must not
|
|
63
|
+
* imply the data cannot support it.
|
|
64
|
+
*/
|
|
65
|
+
interface CompositePrimaryKeyReason {
|
|
66
|
+
code: "composite-pk";
|
|
67
|
+
}
|
|
68
|
+
/** The table has rich metadata but no column is flagged as a primary key. */
|
|
69
|
+
interface NoPrimaryKeyReason {
|
|
70
|
+
code: "no-primary-key";
|
|
71
|
+
}
|
|
72
|
+
type IneligibilityReason = NoRichMetaReason | CompositePrimaryKeyReason | NoPrimaryKeyReason;
|
|
73
|
+
interface RelationshipNode {
|
|
74
|
+
/** False when the table's metadata is too thin to prove anything about it. */
|
|
75
|
+
offerable: boolean;
|
|
76
|
+
/** Present only when `offerable` is false; names the machine-readable case. */
|
|
77
|
+
reason?: IneligibilityReason;
|
|
78
|
+
table: string;
|
|
79
|
+
}
|
|
80
|
+
interface RelationshipGraph {
|
|
81
|
+
edges: RelationshipEdge[];
|
|
82
|
+
nodes: RelationshipNode[];
|
|
83
|
+
}
|
|
84
|
+
/** Every edge whose `fromTable` is `table`, in either direction. */
|
|
85
|
+
declare function edgesFrom(graph: RelationshipGraph, table: string): RelationshipEdge[];
|
|
86
|
+
/**
|
|
87
|
+
* Build the bidirectional relationship graph for a discovered schema.
|
|
88
|
+
*
|
|
89
|
+
* No table, column, or relationship is ever named in this function — the
|
|
90
|
+
* only input is the schema itself.
|
|
91
|
+
*/
|
|
92
|
+
declare function buildRelationshipGraph(schema: DiscoveredSchemaTable[]): RelationshipGraph;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/path-enumeration.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* Breadth-first enumeration of the paths reachable from a starting table.
|
|
97
|
+
*
|
|
98
|
+
* This is the "browse outward" engine behind the derived-object builder: the
|
|
99
|
+
* user picks a starting list and we show them what else is reachable, without
|
|
100
|
+
* ever naming a join, a base table, or an ON condition.
|
|
101
|
+
*
|
|
102
|
+
* Three properties matter enough to be spelled out:
|
|
103
|
+
*
|
|
104
|
+
* 1. **Cycles terminate.** A table already on a path is not revisited, so a
|
|
105
|
+
* self-referencing table yields its one-hop neighbours and stops. Parallel
|
|
106
|
+
* edges are still distinct: edge identity is the FK column pair, not the
|
|
107
|
+
* table pair, so two FKs into the same table stay two relationships.
|
|
108
|
+
*
|
|
109
|
+
* 2. **To-many is sticky.** Once a path crosses a to-many edge, everything at
|
|
110
|
+
* or beyond it is a *collection* — a list of rows, not a single value. Those
|
|
111
|
+
* paths are not pickable columns and must never be compiled into joins; the
|
|
112
|
+
* UI shows them with a "start a list of X" action instead.
|
|
113
|
+
*
|
|
114
|
+
* 3. **Truncation is reported, never silent.** Hitting a budget is part of the
|
|
115
|
+
* return value so the UI can say deeper paths are hidden and offer a
|
|
116
|
+
* narrower search, rather than quietly showing a partial answer as if it
|
|
117
|
+
* were the whole one.
|
|
118
|
+
*
|
|
119
|
+
* No table, column, or relationship is ever named here — the only inputs are
|
|
120
|
+
* the graph and the starting table.
|
|
121
|
+
*/
|
|
122
|
+
/** Spec-mandated: how many edges deep a path may go. */
|
|
123
|
+
declare const MAX_HOPS = 3;
|
|
124
|
+
/** Spec-mandated: how many candidate paths may be considered before stopping. */
|
|
125
|
+
declare const MAX_CANDIDATES = 200;
|
|
126
|
+
/** Spec-mandated: how many ranked results may be displayed. */
|
|
127
|
+
declare const MAX_RESULTS = 50;
|
|
128
|
+
/** Which budget stopped the walk. Absent when nothing was cut. */
|
|
129
|
+
type PathTruncationCause = "candidate-budget" | "result-limit";
|
|
130
|
+
interface EnumeratedPath {
|
|
131
|
+
/**
|
|
132
|
+
* The edges walked from the starting table, in order. Also the path's
|
|
133
|
+
* identity: two paths differ if any edge differs, including which FK
|
|
134
|
+
* column was followed.
|
|
135
|
+
*/
|
|
136
|
+
edges: RelationshipEdge[];
|
|
137
|
+
/**
|
|
138
|
+
* True once the path has crossed a to-many edge. Such a path resolves to
|
|
139
|
+
* many rows, so it is a collection to start a new list from, never a
|
|
140
|
+
* column to pick.
|
|
141
|
+
*/
|
|
142
|
+
isCollection: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* A stable identifier for the path, derived only from the edges walked.
|
|
145
|
+
* Deterministic across runs, which is what keeps Task 3's generated join
|
|
146
|
+
* keys stable.
|
|
147
|
+
*/
|
|
148
|
+
key: string;
|
|
149
|
+
/** The table the path ends at. */
|
|
150
|
+
toTable: string;
|
|
151
|
+
}
|
|
152
|
+
interface PathEnumeration {
|
|
153
|
+
/** How many candidate paths were considered, capped at `MAX_CANDIDATES`. */
|
|
154
|
+
candidatesConsidered: number;
|
|
155
|
+
/** Ranked, capped at `MAX_RESULTS`. */
|
|
156
|
+
paths: EnumeratedPath[];
|
|
157
|
+
/** True when a budget hid paths that would otherwise have been returned. */
|
|
158
|
+
truncated: boolean;
|
|
159
|
+
/** Which budget did the cutting. Absent when `truncated` is false. */
|
|
160
|
+
truncatedBy?: PathTruncationCause;
|
|
161
|
+
}
|
|
162
|
+
interface EnumeratePathsOptions {
|
|
163
|
+
/** Column names per table, used only to resolve `field`. */
|
|
164
|
+
columnsByTable?: Map<string, readonly string[]>;
|
|
165
|
+
/**
|
|
166
|
+
* A field the user is looking for. Paths ending at a table that has a
|
|
167
|
+
* column of exactly this name rank above their peers at the same depth.
|
|
168
|
+
* Purely a ranking hint — it never filters.
|
|
169
|
+
*/
|
|
170
|
+
field?: string;
|
|
171
|
+
maxCandidates?: number;
|
|
172
|
+
maxHops?: number;
|
|
173
|
+
maxResults?: number;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Enumerate every path reachable from `startTable`, ranked and budgeted.
|
|
177
|
+
*
|
|
178
|
+
* Breadth-first, so the candidate budget always cuts the deepest frontier
|
|
179
|
+
* rather than an arbitrary slice of a half-explored level.
|
|
180
|
+
*
|
|
181
|
+
* Note the order of operations: ranking happens *after* the candidate budget
|
|
182
|
+
* has already stopped the walk. A path that would have ranked highly but sits
|
|
183
|
+
* beyond the 200th candidate is therefore never seen, and no amount of
|
|
184
|
+
* ranking recovers it. That is inherent to bounding the search rather than a
|
|
185
|
+
* defect — and it is precisely why truncation is reported, so the UI can
|
|
186
|
+
* offer a narrower search instead of implying the list is complete.
|
|
187
|
+
*/
|
|
188
|
+
declare function enumeratePaths(graph: RelationshipGraph, startTable: string, options?: EnumeratePathsOptions): PathEnumeration;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/path-compiler.d.ts
|
|
191
|
+
/** A field the user picked, optionally reached through a path. */
|
|
192
|
+
interface PickedField {
|
|
193
|
+
/** The column on the path's destination table (or on the start table). */
|
|
194
|
+
column: string;
|
|
195
|
+
/** The renameable name this field is exposed as. Becomes a binding key. */
|
|
196
|
+
name: string;
|
|
197
|
+
/**
|
|
198
|
+
* How the field is reached. Absent (or a zero-edge path) means the field is
|
|
199
|
+
* a column of the starting table itself.
|
|
200
|
+
*/
|
|
201
|
+
path?: EnumeratedPath;
|
|
202
|
+
}
|
|
203
|
+
interface CompilePathsOptions {
|
|
204
|
+
entityKey: string;
|
|
205
|
+
pickedFields: PickedField[];
|
|
206
|
+
schema: DiscoveredSchemaTable[];
|
|
207
|
+
startTable: string;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Thrown when picks cannot compile to a valid projection.
|
|
211
|
+
*
|
|
212
|
+
* A distinct class so a builder UI can tell "this cannot be expressed" apart
|
|
213
|
+
* from a genuine bug, and so callers can catch it narrowly. Failing loudly is
|
|
214
|
+
* the point: the alternative — emitting a plausible-looking mapping — is the
|
|
215
|
+
* defect class this whole body of work exists to close.
|
|
216
|
+
*/
|
|
217
|
+
declare class PathCompileError extends Error {
|
|
218
|
+
constructor(message: string);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Compile picked fields into a projection mapping.
|
|
222
|
+
*
|
|
223
|
+
* Throws `PathCompileError` when the picks cannot be expressed — a collection
|
|
224
|
+
* path, a missing primary key, a reserved or duplicated field name. Every
|
|
225
|
+
* mapping it returns satisfies `projectionSchema` and, given a schema whose
|
|
226
|
+
* tables carry rich metadata, `validateProjection`.
|
|
227
|
+
*/
|
|
228
|
+
declare function compilePaths(options: CompilePathsOptions): ProjectionMapping;
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/starting-record-change.d.ts
|
|
231
|
+
/**
|
|
232
|
+
* What a proposed starting-record change costs, in picked fields.
|
|
233
|
+
*
|
|
234
|
+
* ## Why this lives in core and not in the builder UI
|
|
235
|
+
*
|
|
236
|
+
* "Which of my fields survive if I change what this is a list of?" reads like
|
|
237
|
+
* a set operation on what the user already picked, and it is not. Reachability
|
|
238
|
+
* is DIRECTIONAL: `unit.property_id -> property.id` makes a property one thing
|
|
239
|
+
* when seen from a unit, and makes units many things when seen from a
|
|
240
|
+
* property. So a field that is a single value — a pickable column — from one
|
|
241
|
+
* starting record can be a *collection* from another, and a collection is not
|
|
242
|
+
* pickable at all.
|
|
243
|
+
*
|
|
244
|
+
* That is why the obvious implementation is wrong. Intersecting the two
|
|
245
|
+
* starting records' path keys says "a route to `unit` exists from `property`,
|
|
246
|
+
* so `unit.label` survives", which is false: that route fans out. Worse, path
|
|
247
|
+
* keys are derived from the edges walked, so the key spaces of two different
|
|
248
|
+
* starting records barely overlap even where the same field genuinely does
|
|
249
|
+
* survive — the intersection is both too generous and too stingy, in different
|
|
250
|
+
* places.
|
|
251
|
+
*
|
|
252
|
+
* The only sound answer is to re-enumerate from the proposed starting record
|
|
253
|
+
* and ask, per pick, whether that same column is reachable there as a single
|
|
254
|
+
* value. That requires the relationship graph, so it belongs to the engine.
|
|
255
|
+
*
|
|
256
|
+
* ## Why picks come back repointed rather than merely flagged
|
|
257
|
+
*
|
|
258
|
+
* A surviving pick's old `path` describes a route from the OLD starting
|
|
259
|
+
* record. Handing it back unchanged would emit a join anchored to a table no
|
|
260
|
+
* longer in the query. A survivor is therefore returned with the route it has
|
|
261
|
+
* from the new starting record — which may be shorter, longer, or absent
|
|
262
|
+
* entirely when the field is now a column of the starting record itself.
|
|
263
|
+
*/
|
|
264
|
+
interface StartingRecordChangeImpact {
|
|
265
|
+
/**
|
|
266
|
+
* Picks that survive, each carrying the route it has from the NEW starting
|
|
267
|
+
* record. Order follows the input.
|
|
268
|
+
*/
|
|
269
|
+
keptFields: PickedField[];
|
|
270
|
+
/**
|
|
271
|
+
* How many distinct connections to other information fall away.
|
|
272
|
+
*
|
|
273
|
+
* Counted over routes, not fields: several removed fields can share one
|
|
274
|
+
* route, and a field that was a column of the old starting record was
|
|
275
|
+
* never reached by a connection at all. Neither fact is derivable from
|
|
276
|
+
* `removedFields.length`, which is why this is counted here rather than in
|
|
277
|
+
* the UI.
|
|
278
|
+
*/
|
|
279
|
+
removedConnectionCount: number;
|
|
280
|
+
/** Picks not reachable as a single value from the new starting record. */
|
|
281
|
+
removedFields: PickedField[];
|
|
282
|
+
}
|
|
283
|
+
interface ClassifyStartingRecordChangeOptions {
|
|
284
|
+
/** The starting record in force now. */
|
|
285
|
+
from: string;
|
|
286
|
+
/** The fields the user has picked, as `compilePaths` consumes them. */
|
|
287
|
+
picks: PickedField[];
|
|
288
|
+
schema: DiscoveredSchemaTable[];
|
|
289
|
+
/** The starting record being proposed. */
|
|
290
|
+
to: string;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Classify what changing the starting record does to a set of picked fields.
|
|
294
|
+
*
|
|
295
|
+
* Pure and total: an unknown `to` table simply has no reachable paths, so
|
|
296
|
+
* every pick that is not one of its own columns is reported removed. It never
|
|
297
|
+
* throws, because its caller is a confirmation dialog that must be able to
|
|
298
|
+
* render an answer for any proposed change, including a nonsensical one.
|
|
299
|
+
*/
|
|
300
|
+
declare function classifyStartingRecordChange(options: ClassifyStartingRecordChangeOptions): StartingRecordChangeImpact;
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/starting-record-eligibility.d.ts
|
|
303
|
+
/**
|
|
304
|
+
* Whether a table may be offered as a derived object's "starting record" —
|
|
305
|
+
* the grain choice where one output row represents one row of that table —
|
|
306
|
+
* and why not, when it may not.
|
|
307
|
+
*
|
|
308
|
+
* `path-compiler.ts`'s `basePrimaryKey` enforces the same single-column-PK
|
|
309
|
+
* requirement, but as a thrown `PathCompileError` deep inside compilation.
|
|
310
|
+
* This module answers the earlier question the builder UI actually needs:
|
|
311
|
+
* before the user has picked anything, which tables can even be offered as
|
|
312
|
+
* a starting point, and what do we tell them about the ones that can't.
|
|
313
|
+
*
|
|
314
|
+
* Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
|
|
315
|
+
* A table that fails `hasRichMeta` cannot be proven to have — or lack — a
|
|
316
|
+
* primary key, so it is reported unofferable with its own reason rather
|
|
317
|
+
* than folded into (or confused with) the no-primary-key case, which is a
|
|
318
|
+
* known, proven fact about a fully-described table.
|
|
319
|
+
*/
|
|
320
|
+
interface StartingRecordOption {
|
|
321
|
+
/** False when this table cannot be the grain of a derived object (yet). */
|
|
322
|
+
offerable: boolean;
|
|
323
|
+
/** Present only when `offerable` is false; names the machine-readable case. */
|
|
324
|
+
reason?: IneligibilityReason;
|
|
325
|
+
table: string;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* List every table in the discovered schema with its starting-record
|
|
329
|
+
* eligibility, in schema order.
|
|
330
|
+
*
|
|
331
|
+
* No table, column, or relationship is ever named in this function — the
|
|
332
|
+
* only input is the schema itself.
|
|
333
|
+
*/
|
|
334
|
+
declare function listStartingRecordOptions(schema: DiscoveredSchemaTable[]): StartingRecordOption[];
|
|
335
|
+
//#endregion
|
|
336
|
+
export { type ClassifyStartingRecordChangeOptions, type CompilePathsOptions, type CompositePrimaryKeyReason, type EnumeratePathsOptions, type EnumeratedPath, type IneligibilityReason, MAX_CANDIDATES, MAX_HOPS, MAX_RESULTS, type NoPrimaryKeyReason, type NoRichMetaReason, PathCompileError, type PathEnumeration, type PathTruncationCause, type PickedField, type RelationshipCardinality, type RelationshipDirection, type RelationshipEdge, type RelationshipGraph, type RelationshipNode, type StartingRecordChangeImpact, type StartingRecordOption, buildRelationshipGraph, classifyStartingRecordChange, compilePaths, edgesFrom, enumeratePaths, listStartingRecordOptions };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { F as ProjectionMapping, m as DiscoveredSchemaTable } from "./entity-config-DCa785OE.mjs";
|
|
2
|
+
//#region src/relationship-graph.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A bidirectional relationship graph derived from discovery metadata.
|
|
5
|
+
*
|
|
6
|
+
* The retired builder's join suggester scanned only the *joined* table for a
|
|
7
|
+
* key pointing at the base table — the parent-to-child direction — so it
|
|
8
|
+
* auto-filled exactly the joins that fan out and could never fill the safe
|
|
9
|
+
* child-to-parent ones. (That helper was removed with the builder it served;
|
|
10
|
+
* this note records the mistake so it is not repeated.) This graph walks a
|
|
11
|
+
* table's own FKs (child -> parent) *and* every other table's FKs that point
|
|
12
|
+
* at it (parent -> child), so both directions are navigable.
|
|
13
|
+
*
|
|
14
|
+
* An edge is identified by its complete FK column pair, not by its table
|
|
15
|
+
* pair: `tickets.assignee_id -> users.id` and `tickets.reviewer_id ->
|
|
16
|
+
* users.id` are two distinct edges, not one collapsed relationship.
|
|
17
|
+
*
|
|
18
|
+
* Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
|
|
19
|
+
* A table that fails `hasRichMeta` cannot be proven to have any — or no —
|
|
20
|
+
* relationships, so it is marked `offerable: false` with a reason rather
|
|
21
|
+
* than silently rendered as a legitimately isolated node with zero edges.
|
|
22
|
+
*/
|
|
23
|
+
type RelationshipDirection = "child-to-parent" | "parent-to-child";
|
|
24
|
+
type RelationshipCardinality = "to-one" | "to-many";
|
|
25
|
+
interface RelationshipEdge {
|
|
26
|
+
cardinality: RelationshipCardinality;
|
|
27
|
+
direction: RelationshipDirection;
|
|
28
|
+
/** The FK column on the child side of the relationship. */
|
|
29
|
+
fromColumn: string;
|
|
30
|
+
/** The table this edge is being viewed from. */
|
|
31
|
+
fromTable: string;
|
|
32
|
+
/** The referenced column on the parent side of the relationship. */
|
|
33
|
+
toColumn: string;
|
|
34
|
+
/** The table this edge points to. */
|
|
35
|
+
toTable: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Why a table (or, later, a path) cannot be offered.
|
|
39
|
+
*
|
|
40
|
+
* A closed discriminated union, not free text. Every case has to be rendered
|
|
41
|
+
* as its own piece of copy for a non-technical user, and copy belongs to the
|
|
42
|
+
* UI, not here — so this carries the machine-readable case only. Closing the
|
|
43
|
+
* union is what makes the UI's switch exhaustive: a case added here without a
|
|
44
|
+
* matching message is a type error, rather than a new case reaching a user
|
|
45
|
+
* untranslated.
|
|
46
|
+
*
|
|
47
|
+
* Task 4 added the starting-record eligibility cases (`composite-pk`,
|
|
48
|
+
* `no-primary-key`). Add members; do not widen the type back to `string`.
|
|
49
|
+
*/
|
|
50
|
+
/** Discovery reported no `columnsMeta`/`uniqueConstraints` for the table. */
|
|
51
|
+
interface NoRichMetaReason {
|
|
52
|
+
code: "no-rich-meta";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The table has more than one primary-key column.
|
|
56
|
+
*
|
|
57
|
+
* This is a UI limitation, not a platform one: `projectionIdentitySchema`
|
|
58
|
+
* already supports `{ kind: "composite", parts }` and `checkIdentitySafety`
|
|
59
|
+
* already validates it — the builder simply does not emit that identity kind
|
|
60
|
+
* in v1. The main casualty is the junction table, which usually carries a
|
|
61
|
+
* composite PK and is exactly the shape someone reaches for when modelling a
|
|
62
|
+
* many-to-many, so the UI copy for this case must say "not yet" and must not
|
|
63
|
+
* imply the data cannot support it.
|
|
64
|
+
*/
|
|
65
|
+
interface CompositePrimaryKeyReason {
|
|
66
|
+
code: "composite-pk";
|
|
67
|
+
}
|
|
68
|
+
/** The table has rich metadata but no column is flagged as a primary key. */
|
|
69
|
+
interface NoPrimaryKeyReason {
|
|
70
|
+
code: "no-primary-key";
|
|
71
|
+
}
|
|
72
|
+
type IneligibilityReason = NoRichMetaReason | CompositePrimaryKeyReason | NoPrimaryKeyReason;
|
|
73
|
+
interface RelationshipNode {
|
|
74
|
+
/** False when the table's metadata is too thin to prove anything about it. */
|
|
75
|
+
offerable: boolean;
|
|
76
|
+
/** Present only when `offerable` is false; names the machine-readable case. */
|
|
77
|
+
reason?: IneligibilityReason;
|
|
78
|
+
table: string;
|
|
79
|
+
}
|
|
80
|
+
interface RelationshipGraph {
|
|
81
|
+
edges: RelationshipEdge[];
|
|
82
|
+
nodes: RelationshipNode[];
|
|
83
|
+
}
|
|
84
|
+
/** Every edge whose `fromTable` is `table`, in either direction. */
|
|
85
|
+
declare function edgesFrom(graph: RelationshipGraph, table: string): RelationshipEdge[];
|
|
86
|
+
/**
|
|
87
|
+
* Build the bidirectional relationship graph for a discovered schema.
|
|
88
|
+
*
|
|
89
|
+
* No table, column, or relationship is ever named in this function — the
|
|
90
|
+
* only input is the schema itself.
|
|
91
|
+
*/
|
|
92
|
+
declare function buildRelationshipGraph(schema: DiscoveredSchemaTable[]): RelationshipGraph;
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/path-enumeration.d.ts
|
|
95
|
+
/**
|
|
96
|
+
* Breadth-first enumeration of the paths reachable from a starting table.
|
|
97
|
+
*
|
|
98
|
+
* This is the "browse outward" engine behind the derived-object builder: the
|
|
99
|
+
* user picks a starting list and we show them what else is reachable, without
|
|
100
|
+
* ever naming a join, a base table, or an ON condition.
|
|
101
|
+
*
|
|
102
|
+
* Three properties matter enough to be spelled out:
|
|
103
|
+
*
|
|
104
|
+
* 1. **Cycles terminate.** A table already on a path is not revisited, so a
|
|
105
|
+
* self-referencing table yields its one-hop neighbours and stops. Parallel
|
|
106
|
+
* edges are still distinct: edge identity is the FK column pair, not the
|
|
107
|
+
* table pair, so two FKs into the same table stay two relationships.
|
|
108
|
+
*
|
|
109
|
+
* 2. **To-many is sticky.** Once a path crosses a to-many edge, everything at
|
|
110
|
+
* or beyond it is a *collection* — a list of rows, not a single value. Those
|
|
111
|
+
* paths are not pickable columns and must never be compiled into joins; the
|
|
112
|
+
* UI shows them with a "start a list of X" action instead.
|
|
113
|
+
*
|
|
114
|
+
* 3. **Truncation is reported, never silent.** Hitting a budget is part of the
|
|
115
|
+
* return value so the UI can say deeper paths are hidden and offer a
|
|
116
|
+
* narrower search, rather than quietly showing a partial answer as if it
|
|
117
|
+
* were the whole one.
|
|
118
|
+
*
|
|
119
|
+
* No table, column, or relationship is ever named here — the only inputs are
|
|
120
|
+
* the graph and the starting table.
|
|
121
|
+
*/
|
|
122
|
+
/** Spec-mandated: how many edges deep a path may go. */
|
|
123
|
+
declare const MAX_HOPS = 3;
|
|
124
|
+
/** Spec-mandated: how many candidate paths may be considered before stopping. */
|
|
125
|
+
declare const MAX_CANDIDATES = 200;
|
|
126
|
+
/** Spec-mandated: how many ranked results may be displayed. */
|
|
127
|
+
declare const MAX_RESULTS = 50;
|
|
128
|
+
/** Which budget stopped the walk. Absent when nothing was cut. */
|
|
129
|
+
type PathTruncationCause = "candidate-budget" | "result-limit";
|
|
130
|
+
interface EnumeratedPath {
|
|
131
|
+
/**
|
|
132
|
+
* The edges walked from the starting table, in order. Also the path's
|
|
133
|
+
* identity: two paths differ if any edge differs, including which FK
|
|
134
|
+
* column was followed.
|
|
135
|
+
*/
|
|
136
|
+
edges: RelationshipEdge[];
|
|
137
|
+
/**
|
|
138
|
+
* True once the path has crossed a to-many edge. Such a path resolves to
|
|
139
|
+
* many rows, so it is a collection to start a new list from, never a
|
|
140
|
+
* column to pick.
|
|
141
|
+
*/
|
|
142
|
+
isCollection: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* A stable identifier for the path, derived only from the edges walked.
|
|
145
|
+
* Deterministic across runs, which is what keeps Task 3's generated join
|
|
146
|
+
* keys stable.
|
|
147
|
+
*/
|
|
148
|
+
key: string;
|
|
149
|
+
/** The table the path ends at. */
|
|
150
|
+
toTable: string;
|
|
151
|
+
}
|
|
152
|
+
interface PathEnumeration {
|
|
153
|
+
/** How many candidate paths were considered, capped at `MAX_CANDIDATES`. */
|
|
154
|
+
candidatesConsidered: number;
|
|
155
|
+
/** Ranked, capped at `MAX_RESULTS`. */
|
|
156
|
+
paths: EnumeratedPath[];
|
|
157
|
+
/** True when a budget hid paths that would otherwise have been returned. */
|
|
158
|
+
truncated: boolean;
|
|
159
|
+
/** Which budget did the cutting. Absent when `truncated` is false. */
|
|
160
|
+
truncatedBy?: PathTruncationCause;
|
|
161
|
+
}
|
|
162
|
+
interface EnumeratePathsOptions {
|
|
163
|
+
/** Column names per table, used only to resolve `field`. */
|
|
164
|
+
columnsByTable?: Map<string, readonly string[]>;
|
|
165
|
+
/**
|
|
166
|
+
* A field the user is looking for. Paths ending at a table that has a
|
|
167
|
+
* column of exactly this name rank above their peers at the same depth.
|
|
168
|
+
* Purely a ranking hint — it never filters.
|
|
169
|
+
*/
|
|
170
|
+
field?: string;
|
|
171
|
+
maxCandidates?: number;
|
|
172
|
+
maxHops?: number;
|
|
173
|
+
maxResults?: number;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Enumerate every path reachable from `startTable`, ranked and budgeted.
|
|
177
|
+
*
|
|
178
|
+
* Breadth-first, so the candidate budget always cuts the deepest frontier
|
|
179
|
+
* rather than an arbitrary slice of a half-explored level.
|
|
180
|
+
*
|
|
181
|
+
* Note the order of operations: ranking happens *after* the candidate budget
|
|
182
|
+
* has already stopped the walk. A path that would have ranked highly but sits
|
|
183
|
+
* beyond the 200th candidate is therefore never seen, and no amount of
|
|
184
|
+
* ranking recovers it. That is inherent to bounding the search rather than a
|
|
185
|
+
* defect — and it is precisely why truncation is reported, so the UI can
|
|
186
|
+
* offer a narrower search instead of implying the list is complete.
|
|
187
|
+
*/
|
|
188
|
+
declare function enumeratePaths(graph: RelationshipGraph, startTable: string, options?: EnumeratePathsOptions): PathEnumeration;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/path-compiler.d.ts
|
|
191
|
+
/** A field the user picked, optionally reached through a path. */
|
|
192
|
+
interface PickedField {
|
|
193
|
+
/** The column on the path's destination table (or on the start table). */
|
|
194
|
+
column: string;
|
|
195
|
+
/** The renameable name this field is exposed as. Becomes a binding key. */
|
|
196
|
+
name: string;
|
|
197
|
+
/**
|
|
198
|
+
* How the field is reached. Absent (or a zero-edge path) means the field is
|
|
199
|
+
* a column of the starting table itself.
|
|
200
|
+
*/
|
|
201
|
+
path?: EnumeratedPath;
|
|
202
|
+
}
|
|
203
|
+
interface CompilePathsOptions {
|
|
204
|
+
entityKey: string;
|
|
205
|
+
pickedFields: PickedField[];
|
|
206
|
+
schema: DiscoveredSchemaTable[];
|
|
207
|
+
startTable: string;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Thrown when picks cannot compile to a valid projection.
|
|
211
|
+
*
|
|
212
|
+
* A distinct class so a builder UI can tell "this cannot be expressed" apart
|
|
213
|
+
* from a genuine bug, and so callers can catch it narrowly. Failing loudly is
|
|
214
|
+
* the point: the alternative — emitting a plausible-looking mapping — is the
|
|
215
|
+
* defect class this whole body of work exists to close.
|
|
216
|
+
*/
|
|
217
|
+
declare class PathCompileError extends Error {
|
|
218
|
+
constructor(message: string);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Compile picked fields into a projection mapping.
|
|
222
|
+
*
|
|
223
|
+
* Throws `PathCompileError` when the picks cannot be expressed — a collection
|
|
224
|
+
* path, a missing primary key, a reserved or duplicated field name. Every
|
|
225
|
+
* mapping it returns satisfies `projectionSchema` and, given a schema whose
|
|
226
|
+
* tables carry rich metadata, `validateProjection`.
|
|
227
|
+
*/
|
|
228
|
+
declare function compilePaths(options: CompilePathsOptions): ProjectionMapping;
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/starting-record-change.d.ts
|
|
231
|
+
/**
|
|
232
|
+
* What a proposed starting-record change costs, in picked fields.
|
|
233
|
+
*
|
|
234
|
+
* ## Why this lives in core and not in the builder UI
|
|
235
|
+
*
|
|
236
|
+
* "Which of my fields survive if I change what this is a list of?" reads like
|
|
237
|
+
* a set operation on what the user already picked, and it is not. Reachability
|
|
238
|
+
* is DIRECTIONAL: `unit.property_id -> property.id` makes a property one thing
|
|
239
|
+
* when seen from a unit, and makes units many things when seen from a
|
|
240
|
+
* property. So a field that is a single value — a pickable column — from one
|
|
241
|
+
* starting record can be a *collection* from another, and a collection is not
|
|
242
|
+
* pickable at all.
|
|
243
|
+
*
|
|
244
|
+
* That is why the obvious implementation is wrong. Intersecting the two
|
|
245
|
+
* starting records' path keys says "a route to `unit` exists from `property`,
|
|
246
|
+
* so `unit.label` survives", which is false: that route fans out. Worse, path
|
|
247
|
+
* keys are derived from the edges walked, so the key spaces of two different
|
|
248
|
+
* starting records barely overlap even where the same field genuinely does
|
|
249
|
+
* survive — the intersection is both too generous and too stingy, in different
|
|
250
|
+
* places.
|
|
251
|
+
*
|
|
252
|
+
* The only sound answer is to re-enumerate from the proposed starting record
|
|
253
|
+
* and ask, per pick, whether that same column is reachable there as a single
|
|
254
|
+
* value. That requires the relationship graph, so it belongs to the engine.
|
|
255
|
+
*
|
|
256
|
+
* ## Why picks come back repointed rather than merely flagged
|
|
257
|
+
*
|
|
258
|
+
* A surviving pick's old `path` describes a route from the OLD starting
|
|
259
|
+
* record. Handing it back unchanged would emit a join anchored to a table no
|
|
260
|
+
* longer in the query. A survivor is therefore returned with the route it has
|
|
261
|
+
* from the new starting record — which may be shorter, longer, or absent
|
|
262
|
+
* entirely when the field is now a column of the starting record itself.
|
|
263
|
+
*/
|
|
264
|
+
interface StartingRecordChangeImpact {
|
|
265
|
+
/**
|
|
266
|
+
* Picks that survive, each carrying the route it has from the NEW starting
|
|
267
|
+
* record. Order follows the input.
|
|
268
|
+
*/
|
|
269
|
+
keptFields: PickedField[];
|
|
270
|
+
/**
|
|
271
|
+
* How many distinct connections to other information fall away.
|
|
272
|
+
*
|
|
273
|
+
* Counted over routes, not fields: several removed fields can share one
|
|
274
|
+
* route, and a field that was a column of the old starting record was
|
|
275
|
+
* never reached by a connection at all. Neither fact is derivable from
|
|
276
|
+
* `removedFields.length`, which is why this is counted here rather than in
|
|
277
|
+
* the UI.
|
|
278
|
+
*/
|
|
279
|
+
removedConnectionCount: number;
|
|
280
|
+
/** Picks not reachable as a single value from the new starting record. */
|
|
281
|
+
removedFields: PickedField[];
|
|
282
|
+
}
|
|
283
|
+
interface ClassifyStartingRecordChangeOptions {
|
|
284
|
+
/** The starting record in force now. */
|
|
285
|
+
from: string;
|
|
286
|
+
/** The fields the user has picked, as `compilePaths` consumes them. */
|
|
287
|
+
picks: PickedField[];
|
|
288
|
+
schema: DiscoveredSchemaTable[];
|
|
289
|
+
/** The starting record being proposed. */
|
|
290
|
+
to: string;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Classify what changing the starting record does to a set of picked fields.
|
|
294
|
+
*
|
|
295
|
+
* Pure and total: an unknown `to` table simply has no reachable paths, so
|
|
296
|
+
* every pick that is not one of its own columns is reported removed. It never
|
|
297
|
+
* throws, because its caller is a confirmation dialog that must be able to
|
|
298
|
+
* render an answer for any proposed change, including a nonsensical one.
|
|
299
|
+
*/
|
|
300
|
+
declare function classifyStartingRecordChange(options: ClassifyStartingRecordChangeOptions): StartingRecordChangeImpact;
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/starting-record-eligibility.d.ts
|
|
303
|
+
/**
|
|
304
|
+
* Whether a table may be offered as a derived object's "starting record" —
|
|
305
|
+
* the grain choice where one output row represents one row of that table —
|
|
306
|
+
* and why not, when it may not.
|
|
307
|
+
*
|
|
308
|
+
* `path-compiler.ts`'s `basePrimaryKey` enforces the same single-column-PK
|
|
309
|
+
* requirement, but as a thrown `PathCompileError` deep inside compilation.
|
|
310
|
+
* This module answers the earlier question the builder UI actually needs:
|
|
311
|
+
* before the user has picked anything, which tables can even be offered as
|
|
312
|
+
* a starting point, and what do we tell them about the ones that can't.
|
|
313
|
+
*
|
|
314
|
+
* Absent metadata is UNKNOWN, never a negative fact (see `schema-meta.ts`).
|
|
315
|
+
* A table that fails `hasRichMeta` cannot be proven to have — or lack — a
|
|
316
|
+
* primary key, so it is reported unofferable with its own reason rather
|
|
317
|
+
* than folded into (or confused with) the no-primary-key case, which is a
|
|
318
|
+
* known, proven fact about a fully-described table.
|
|
319
|
+
*/
|
|
320
|
+
interface StartingRecordOption {
|
|
321
|
+
/** False when this table cannot be the grain of a derived object (yet). */
|
|
322
|
+
offerable: boolean;
|
|
323
|
+
/** Present only when `offerable` is false; names the machine-readable case. */
|
|
324
|
+
reason?: IneligibilityReason;
|
|
325
|
+
table: string;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* List every table in the discovered schema with its starting-record
|
|
329
|
+
* eligibility, in schema order.
|
|
330
|
+
*
|
|
331
|
+
* No table, column, or relationship is ever named in this function — the
|
|
332
|
+
* only input is the schema itself.
|
|
333
|
+
*/
|
|
334
|
+
declare function listStartingRecordOptions(schema: DiscoveredSchemaTable[]): StartingRecordOption[];
|
|
335
|
+
//#endregion
|
|
336
|
+
export { type ClassifyStartingRecordChangeOptions, type CompilePathsOptions, type CompositePrimaryKeyReason, type EnumeratePathsOptions, type EnumeratedPath, type IneligibilityReason, MAX_CANDIDATES, MAX_HOPS, MAX_RESULTS, type NoPrimaryKeyReason, type NoRichMetaReason, PathCompileError, type PathEnumeration, type PathTruncationCause, type PickedField, type RelationshipCardinality, type RelationshipDirection, type RelationshipEdge, type RelationshipGraph, type RelationshipNode, type StartingRecordChangeImpact, type StartingRecordOption, buildRelationshipGraph, classifyStartingRecordChange, compilePaths, edgesFrom, enumeratePaths, listStartingRecordOptions };
|