@clivly/core 0.9.7 → 0.10.1

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 (48) hide show
  1. package/README.md +17 -10
  2. package/dist/builder.cjs +599 -0
  3. package/dist/builder.d.cts +336 -0
  4. package/dist/builder.d.mts +336 -0
  5. package/dist/builder.mjs +589 -0
  6. package/dist/drizzle.d.cts +1 -1
  7. package/dist/drizzle.d.mts +1 -1
  8. package/dist/{entity-config-25v_udeY.cjs → entity-config-C5jBmG2C.cjs} +83 -68
  9. package/dist/{entity-config-buOIVtMM.d.cts → entity-config-DCa785OE.d.cts} +27 -1
  10. package/dist/{entity-config-buOIVtMM.d.mts → entity-config-DCa785OE.d.mts} +27 -1
  11. package/dist/{entity-config-DwGXbUo_.mjs → entity-config-DpHPMFIS.mjs} +71 -38
  12. package/dist/entity-config.cjs +1 -1
  13. package/dist/entity-config.d.cts +1 -1
  14. package/dist/entity-config.d.mts +1 -1
  15. package/dist/entity-config.mjs +1 -1
  16. package/dist/entity-heuristics.cjs +34 -1
  17. package/dist/entity-heuristics.d.cts +8 -2
  18. package/dist/entity-heuristics.d.mts +8 -2
  19. package/dist/entity-heuristics.mjs +34 -2
  20. package/dist/index.cjs +136 -199
  21. package/dist/index.d.cts +135 -89
  22. package/dist/index.d.mts +135 -89
  23. package/dist/index.mjs +124 -188
  24. package/dist/mapping-form.d.cts +1 -1
  25. package/dist/mapping-form.d.mts +1 -1
  26. package/dist/mappings-config.cjs +2 -16
  27. package/dist/mappings-config.d.cts +1 -1
  28. package/dist/mappings-config.d.mts +1 -1
  29. package/dist/mappings-config.mjs +1 -15
  30. package/dist/{projection-OJYCYmS_.cjs → projection-DFgCoyZl.cjs} +0 -6
  31. package/dist/{projection-BXw_iiw5.mjs → projection-SCNrLJMV.mjs} +1 -1
  32. package/dist/schema-meta-Ac7938mT.mjs +46 -0
  33. package/dist/schema-meta-Cgi0MnHh.cjs +69 -0
  34. package/dist/sync-engine.cjs +1 -1
  35. package/dist/sync-engine.d.cts +1 -1
  36. package/dist/sync-engine.d.mts +1 -1
  37. package/dist/sync-engine.mjs +1 -1
  38. package/dist/types.cjs +11 -0
  39. package/dist/types.d.cts +28 -2
  40. package/dist/types.d.mts +28 -2
  41. package/dist/types.mjs +9 -1
  42. package/dist/{view-compiler-u3GGRgnv.mjs → view-compiler-CLvZ0wJW.mjs} +1 -1
  43. package/dist/{view-compiler-D30HNRnA.cjs → view-compiler-DP4Fbj7-.cjs} +1 -1
  44. package/dist/view-compiler.cjs +1 -1
  45. package/dist/view-compiler.d.cts +1 -1
  46. package/dist/view-compiler.d.mts +1 -1
  47. package/dist/view-compiler.mjs +1 -1
  48. package/package.json +11 -1
package/dist/index.mjs CHANGED
@@ -1,217 +1,153 @@
1
1
  import { authCustom } from "./auth-adapter.mjs";
2
- import { a as projectionSchema, i as identityRefs, n as PROJECTION_KEY_MESSAGE, r as PROJECTION_KEY_PATTERN, t as BASE_REF } from "./projection-BXw_iiw5.mjs";
3
- import { _ as columnMetaFor, a as MATERIALIZING_CONCEPTS, b as primaryKeyColumns, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, g as joinFansOut, h as fanOutJoins, i as ClivlyConfigError, l as filterSchema, m as validateProjection, n as CRM_CONCEPTS, p as validateEntitiesConfig, s as defineClivlyConfig, t as CANONICAL_FIELDS, v as coversUniqueKey, y as hasRichMeta } from "./entity-config-DwGXbUo_.mjs";
2
+ import { i as projectionSchema, n as PROJECTION_KEY_PATTERN, r as identityRefs, t as BASE_REF } from "./projection-SCNrLJMV.mjs";
3
+ import { i as primaryKeyColumns, n as coversUniqueKey, r as hasRichMeta, t as columnMetaFor } from "./schema-meta-Ac7938mT.mjs";
4
+ import { _ as joinFansOut, a as MATERIALIZING_CONCEPTS, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, g as fanOutJoins, h as validateProjectionFilter, i as ClivlyConfigError, l as filterSchema, m as validateProjection, n as CRM_CONCEPTS, p as validateEntitiesConfig, s as defineClivlyConfig, t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-DpHPMFIS.mjs";
4
5
  import { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
5
6
  import { mappingsToEntitiesConfig } from "./mappings-config.mjs";
6
- import { c as compileProjectionIdentity, d as compileProjectionSelect, f as quoteIdent, i as explainEntitiesConfig, l as compileProjectionJoins, n as compileEntityView, o as PREVIEW_MAX_ROWS, r as compileEntityViews, s as compileProjectionFrom, t as DEFAULT_VIEW_PREFIX, u as compileProjectionPreview } from "./view-compiler-u3GGRgnv.mjs";
7
+ import { c as compileProjectionIdentity, d as compileProjectionSelect, f as quoteIdent, i as explainEntitiesConfig, l as compileProjectionJoins, n as compileEntityView, o as PREVIEW_MAX_ROWS, r as compileEntityViews, s as compileProjectionFrom, t as DEFAULT_VIEW_PREFIX, u as compileProjectionPreview } from "./view-compiler-CLvZ0wJW.mjs";
7
8
  import { reconcile, runSync } from "./sync-engine.mjs";
8
- //#region src/projection-draft.ts
9
- function emptyProjectionDraft(baseTable) {
10
- return {
11
- baseTable,
12
- entityKey: "",
13
- fieldBindings: {},
14
- identity: { kind: "base_pk" },
15
- joins: []
16
- };
17
- }
9
+ import { OPEN_STAGE_TYPE, isClosedStageType } from "./types.mjs";
10
+ //#region src/slug.ts
18
11
  /**
19
- * The ON conditions implied by a foreign key between the two tables, so the
20
- * builder can pre-fill a join instead of making the user hand-pick columns.
21
- * Returns [] when no FK relates them — the user then picks manually.
12
+ * The one `slugify`.
13
+ *
14
+ * Three byte-identical copies existed before this module — in `@clivly/api`
15
+ * (the custom object-type slug), in `apps/web`'s onboarding route (the
16
+ * organisation slug), and in `@clivly/sdk`. The first two now import this; the
17
+ * SDK's copy stays where it is on purpose, because `@clivly/sdk` cannot depend
18
+ * on this package and its own comment records that its pattern is deliberately
19
+ * kept in step with the server's by hand.
20
+ *
21
+ * Note that the two callers this replaces slug *different things* — an
22
+ * object type and an organisation. They share a transform, not a namespace.
23
+ * Sharing the function stops the transform drifting; it does not make those two
24
+ * identifiers the same identifier.
22
25
  */
23
- function suggestJoinConditions(baseTable, joinTable, schema, joinKey) {
24
- const join = schema.find((t) => t.name === joinTable);
25
- const conditions = [];
26
- for (const column of join?.columnsMeta ?? []) if (column.references?.table === baseTable) conditions.push({
27
- left: {
28
- table: joinKey,
29
- column: column.name
30
- },
31
- op: "eq",
32
- right: {
33
- table: BASE_REF,
34
- column: column.references.column
35
- }
36
- });
37
- return conditions;
38
- }
26
+ const NON_ALNUM = /[^a-z0-9]+/g;
27
+ const EDGE_HYPHENS = /(^-|-$)/g;
39
28
  /**
40
- * Add a join on `joinTable`, keyed and pre-filled.
29
+ * Lowercase, trim, collapse every run of non-alphanumerics to one hyphen, and
30
+ * drop leading/trailing hyphens. `"Participant Info"` becomes
31
+ * `"participant-info"`.
41
32
  *
42
- * The key is the table's own name where that is free, because it is what the
43
- * user already calls the table; a collision falls back to `_2`, `_3` and so on.
44
- * Uniqueness is not cosmetic — the key is the SQL alias and the name every ref
45
- * addresses the table by, so two joins sharing one would make every ref
46
- * ambiguous. `base` is reserved for the base table and can never be taken.
33
+ * The result is NOT guaranteed to be an acceptable slug. Both
34
+ * `OBJECT_TYPE_SLUG_PATTERN` (server) and `PROJECTION_KEY_PATTERN` (this
35
+ * package) require a leading letter, and this function happily returns
36
+ * `"2024-invoices"` for `"2024 Invoices"`. Callers that need an
37
+ * acceptance-guaranteed identifier want `toObjectTypeSlug`.
47
38
  */
48
- function addDraftJoin(draft, joinTable, schema) {
49
- const taken = /* @__PURE__ */ new Set([BASE_REF, ...draft.joins.map((j) => j.key)]);
50
- let key = joinTable;
51
- let suffix = 2;
52
- while (taken.has(key)) {
53
- key = `${joinTable}_${suffix}`;
54
- suffix += 1;
55
- }
56
- return {
57
- ...draft,
58
- joins: [...draft.joins, {
59
- key,
60
- table: joinTable,
61
- type: "left",
62
- on: suggestJoinConditions(draft.baseTable, joinTable, schema, key)
63
- }]
64
- };
39
+ function slugify(value) {
40
+ return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
65
41
  }
66
42
  /**
67
- * Remove a join, and everything that addressed it.
43
+ * Prefixed onto a slug that would otherwise not start with a letter.
68
44
  *
69
- * Refs to a removed join are refs to a table the projection no longer declares.
70
- * Left in place they fail validation as "unknown ref table", which blames the
71
- * field — or, worse, the identity — for the join's removal. Identity falls back
72
- * to the base primary key, the same default a new draft starts from.
45
+ * Chosen over trimming the offending characters because dropping them loses
46
+ * information the user typed: `"2024 Invoices"` would become `"invoices"`,
47
+ * which collides with a genuinely different object of that name.
73
48
  */
74
- function removeDraftJoin(draft, joinKey) {
75
- const fieldBindings = Object.fromEntries(Object.entries(draft.fieldBindings).filter(([, binding]) => binding.from.table !== joinKey));
76
- const identityDependsOnJoin = identityRefs(draft.identity, "").some((ref) => ref.table === joinKey);
77
- return {
78
- ...draft,
79
- joins: draft.joins.filter((join) => join.key !== joinKey),
80
- fieldBindings,
81
- identity: identityDependsOnJoin ? { kind: "base_pk" } : draft.identity
82
- };
83
- }
49
+ const LEADING_DIGIT_PREFIX = "obj-";
50
+ /** Mirrors `PROJECTION_KEY_PATTERN` / the server's `OBJECT_TYPE_SLUG_PATTERN`. */
51
+ const MUST_START_WITH_LETTER = /^[a-z]/;
84
52
  /**
85
- * Every table a field or identity may currently read from, in ref order: the
86
- * base table first, then each join.
53
+ * A slug the server will accept, derived from a display name.
87
54
  *
88
- * Returns [] when the base table is not in the discovered schema — there is
89
- * nothing truthful to offer, and inventing columns would let someone build a
90
- * projection that cannot compile.
55
+ * `slugify` alone is not enough: the server's `OBJECT_TYPE_SLUG_PATTERN`
56
+ * (`apps/server/src/index.ts`) demands a leading letter, so a name beginning
57
+ * with a digit slugs to something the platform rejects at push time — long
58
+ * after the object was authored. `@clivly/sdk`'s `custom-slug.ts` already knows
59
+ * this case and deliberately withholds `"2024-invoices"` as a suggestion
60
+ * because it would be rejected too.
61
+ *
62
+ * The prefix is applied ONLY when the derived slug does not already start with
63
+ * a letter, so the overwhelmingly common name is untouched.
64
+ *
65
+ * This value must be shown to whoever authored the name. It is the identifier
66
+ * the host app declares as its `objectType`, so a slug derived silently is a
67
+ * slug the developer cannot copy into their config.
68
+ *
69
+ * Returns an empty string for a name with no alphanumerics at all — the caller
70
+ * decides whether that is "keep typing" or an error, since only it knows
71
+ * whether the field is still being edited.
91
72
  */
92
- function draftRefOptions(draft, schema) {
93
- const byName = new Map(schema.map((table) => [table.name, table]));
94
- const base = byName.get(draft.baseTable);
95
- if (!base) return [];
96
- const options = [{
97
- ref: BASE_REF,
98
- label: `${base.name} (${BASE_REF})`,
99
- columns: base.columns
100
- }];
101
- for (const join of draft.joins) {
102
- const table = byName.get(join.table);
103
- if (table) options.push({
104
- ref: join.key,
105
- label: `${table.name} (${join.key})`,
106
- columns: table.columns
107
- });
108
- }
109
- return options;
73
+ function toObjectTypeSlug(displayName) {
74
+ const slug = slugify(displayName);
75
+ if (slug === "") return "";
76
+ return MUST_START_WITH_LETTER.test(slug) ? slug : `${LEADING_DIGIT_PREFIX}${slug}`;
77
+ }
78
+ //#endregion
79
+ //#region src/stage-rules.ts
80
+ const CONDITION_OPS = /* @__PURE__ */ new Set([
81
+ "is_empty",
82
+ "is_not_empty",
83
+ "equals",
84
+ "not_equals",
85
+ "greater_than",
86
+ "less_than",
87
+ "before",
88
+ "after"
89
+ ]);
90
+ function isCondition(value) {
91
+ if (!value || typeof value !== "object") return false;
92
+ const condition = value;
93
+ return typeof condition.field === "string" && condition.field.length > 0 && typeof condition.op === "string" && CONDITION_OPS.has(condition.op) && (condition.value === void 0 || typeof condition.value === "string");
110
94
  }
111
- /** A draft becomes a projection once it has a valid key and ≥1 binding. */
112
- function draftToProjection(draft) {
113
- if (!PROJECTION_KEY_PATTERN.test(draft.entityKey)) return null;
114
- if (Object.keys(draft.fieldBindings).length === 0) return null;
115
- return {
116
- version: 1,
117
- kind: "projection",
118
- entityKey: draft.entityKey,
119
- baseTable: draft.baseTable,
120
- joins: draft.joins,
121
- fieldBindings: draft.fieldBindings,
122
- identity: draft.identity
123
- };
95
+ /** Host fields arrive as strings; null and "" are both absent. */
96
+ function isEmpty(value) {
97
+ return value === null || value === void 0 || value === "";
124
98
  }
125
- const PROBE_KEY = "draft";
126
- /**
127
- * A draft rendered as a projection so the semantic checks can run against it
128
- * while it is being written, not only once it is finished.
129
- *
130
- * `entityKey` is stood in for rather than taken from the draft: it is the one
131
- * field `validateProjection` never looks at, and a half-typed key would
132
- * otherwise have to be valid before any join or identity could be checked. The
133
- * missing or malformed key is reported separately, by `draftErrors`.
134
- */
135
- function probeProjection(draft) {
136
- return {
137
- version: 1,
138
- kind: "projection",
139
- entityKey: PROBE_KEY,
140
- baseTable: draft.baseTable,
141
- joins: draft.joins,
142
- fieldBindings: draft.fieldBindings,
143
- identity: draft.identity
144
- };
99
+ function compareNumbers(raw, operand, op) {
100
+ const left = Number(raw);
101
+ const right = Number(operand);
102
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
103
+ return op === "greater_than" ? left > right : left < right;
145
104
  }
146
- /**
147
- * `validateProjection` appends its own `projection.` segment to the base path it
148
- * is given, because in a config the base is the entity that owns the projection
149
- * (`entities.accounts` → `entities.accounts.projection.identity`). A draft has
150
- * no owning entity, so passing "projection" as the base produced
151
- * `projection.projection.identity` — a path no caller could match against, and
152
- * the reason `draftErrors` was only ever consumed as a flat list of messages.
153
- *
154
- * Passing an empty base and dropping the leading separator leaves one
155
- * convention: every path here starts `projection.` exactly once, whether it came
156
- * from this module or from the validator.
157
- */
158
- function semanticErrors(draft, schema) {
159
- return validateProjection(probeProjection(draft), schema, "").map((error) => ({
160
- ...error,
161
- path: error.path.startsWith(".") ? error.path.slice(1) : error.path
162
- }));
105
+ function compareDates(raw, operand, op) {
106
+ const left = new Date(raw).getTime();
107
+ const right = new Date(operand ?? "").getTime();
108
+ if (Number.isNaN(left) || Number.isNaN(right)) return false;
109
+ return op === "before" ? left < right : left > right;
163
110
  }
164
111
  /**
165
- * Every reason this draft cannot be saved, one per cause and each anchored to
166
- * the section that owns it.
112
+ * Whether one condition holds for a host row.
167
113
  *
168
- * One lumped "Name the object and map a field." was all this used to return for
169
- * an incomplete draft, at the bare path "projection". Two consequences, both
170
- * bad: a builder had nowhere to attach the message, and — because it returned
171
- * before validating — a fanning join under a `base_pk` identity stayed silent
172
- * until the draft was otherwise complete, which is well after the point where
173
- * the user chose the join.
114
+ * `mappedFields` is the set of columns the organization actually exposed. A
115
+ * condition naming anything outside it never matches, whatever the operator: a
116
+ * rule referencing a dropped column must fail safe, because treating the field
117
+ * as null would make `is_empty` match every deal.
174
118
  */
175
- function draftErrors(draft, schema) {
176
- const errors = [];
177
- if (draft.entityKey === "") errors.push({
178
- path: "projection.entityKey",
179
- message: "Key is required."
180
- });
181
- else if (!PROJECTION_KEY_PATTERN.test(draft.entityKey)) errors.push({
182
- path: "projection.entityKey",
183
- message: `Key ${PROJECTION_KEY_MESSAGE}.`
184
- });
185
- if (Object.keys(draft.fieldBindings).length === 0) errors.push({
186
- path: "projection.fieldBindings",
187
- message: "Map at least one field."
188
- });
189
- if (draft.baseTable === "") {
190
- errors.push({
191
- path: "projection.baseTable",
192
- message: "Pick a base table."
193
- });
194
- return errors;
195
- }
196
- errors.push(...semanticErrors(draft, schema));
197
- return errors;
119
+ function matchesCondition(condition, fields, mappedFields) {
120
+ if (!mappedFields.has(condition.field)) return false;
121
+ const raw = fields[condition.field] ?? null;
122
+ if (condition.op === "is_empty") return isEmpty(raw);
123
+ if (condition.op === "is_not_empty") return !isEmpty(raw);
124
+ if (isEmpty(raw)) return false;
125
+ const value = raw;
126
+ if (condition.op === "equals") return value === condition.value;
127
+ if (condition.op === "not_equals") return value !== condition.value;
128
+ if (condition.op === "greater_than" || condition.op === "less_than") return compareNumbers(value, condition.value, condition.op);
129
+ return compareDates(value, condition.value, condition.op);
198
130
  }
199
131
  /**
200
- * Per-join fan-out, for the builder's inline cardinality hints.
132
+ * The id of the first rule whose conditions all hold, or null when none do.
133
+ *
134
+ * Returns a rule id rather than a stage key because the caller needs the
135
+ * matched rule's `allowClose` before it may move a deal into a closed stage.
201
136
  *
202
- * Answered from the joins alone. Joins are chosen before the key and the fields
203
- * are filled in, and a fan-out warning is precisely what should steer the
204
- * identity choice while it is being made — requiring a complete draft put the
205
- * hint after the decision it exists to inform.
137
+ * `rules` must arrive already ordered — stage position first, then rule
138
+ * position within a stage. Sorting needs stage rows this module deliberately
139
+ * does not see.
140
+ *
141
+ * A rule with no conditions never matches: an empty list would otherwise
142
+ * vacuously AND to true and capture every deal, which is the opposite of what
143
+ * a half-written rule should do.
206
144
  */
207
- function describeCardinality(draft, schema) {
208
- const projection = probeProjection(draft);
209
- const tablesByName = new Map(schema.map((t) => [t.name, t]));
210
- const fanning = new Set(fanOutJoins(projection, tablesByName).map((j) => j.key));
211
- return projection.joins.map((join) => ({
212
- joinKey: join.key,
213
- fansOut: fanning.has(join.key)
214
- }));
145
+ function evaluateStage(rules, fields, mappedFields) {
146
+ for (const rule of rules) {
147
+ if (!Array.isArray(rule.conditions) || rule.conditions.length === 0 || !rule.conditions.every(isCondition)) continue;
148
+ if (rule.conditions.every((condition) => matchesCondition(condition, fields, mappedFields))) return rule.id;
149
+ }
150
+ return null;
215
151
  }
216
152
  //#endregion
217
- export { BASE_REF, CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, DEFAULT_VIEW_PREFIX, MATERIALIZING_CONCEPTS, PREVIEW_MAX_ROWS, addDraftJoin, authCustom, buildFilterFromDiscriminator, buildRelationships, columnMetaFor, compileEntityView, compileEntityViews, compileProjectionFrom, compileProjectionIdentity, compileProjectionJoins, compileProjectionPreview, compileProjectionSelect, coversUniqueKey, defineClivlyConfig, describeCardinality, draftErrors, draftRefOptions, draftToProjection, emptyProjectionDraft, entitiesConfigSchema, explainEntitiesConfig, fanOutJoins, filterSchema, hasRichMeta, identityRefs, joinFansOut, mappingsToEntitiesConfig, primaryKeyColumns, projectionSchema, quoteIdent, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, removeDraftJoin, runSync, suggestJoinConditions, validateEntitiesConfig, validateProjection };
153
+ export { BASE_REF, CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, DEFAULT_VIEW_PREFIX, MATERIALIZING_CONCEPTS, OPEN_STAGE_TYPE, PREVIEW_MAX_ROWS, PROJECTION_KEY_PATTERN, authCustom, buildFilterFromDiscriminator, buildRelationships, columnMetaFor, compileEntityView, compileEntityViews, compileProjectionFrom, compileProjectionIdentity, compileProjectionJoins, compileProjectionPreview, compileProjectionSelect, coversUniqueKey, defineClivlyConfig, entitiesConfigSchema, evaluateStage, explainEntitiesConfig, fanOutJoins, filterSchema, hasRichMeta, identityRefs, isClosedStageType, joinFansOut, mappingsToEntitiesConfig, matchesCondition, primaryKeyColumns, projectionFields, projectionSchema, quoteIdent, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, slugify, toObjectTypeSlug, validateEntitiesConfig, validateProjection, validateProjectionFilter };
@@ -1,4 +1,4 @@
1
- import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.cjs";
1
+ import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-DCa785OE.cjs";
2
2
  //#region src/mapping-form.d.ts
3
3
  /**
4
4
  * A relationship the mapping UI can offer or has configured, in the shape the
@@ -1,4 +1,4 @@
1
- import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.mjs";
1
+ import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-DCa785OE.mjs";
2
2
  //#region src/mapping-form.d.ts
3
3
  /**
4
4
  * A relationship the mapping UI can offer or has configured, in the shape the
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-25v_udeY.cjs");
2
+ const require_entity_config = require("./entity-config-C5jBmG2C.cjs");
3
3
  //#region src/mappings-config.ts
4
4
  const RESERVED_FIELD = "id";
5
5
  /**
@@ -48,26 +48,12 @@ function uniquify(base, used) {
48
48
  used.add(key);
49
49
  return key;
50
50
  }
51
- /**
52
- * A projection's `fields` map, derived mechanically from its bindings.
53
- *
54
- * `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
55
- * projection entity must populate it. The values are ref *tokens*
56
- * (`"base.full_name"`), never host column names — the compiler reads
57
- * `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
58
- * column check for projection entities precisely because of this.
59
- */
60
- function projectionFields(projection) {
61
- const fields = {};
62
- for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
63
- return fields;
64
- }
65
51
  function buildEntity(row, refIndex) {
66
52
  const entity = {
67
53
  concept: row.targetConcept,
68
54
  source: row.projection?.baseTable ?? row.sourceTable,
69
55
  role: row.role ?? DEFAULT_ROLE,
70
- fields: row.projection ? projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
56
+ fields: row.projection ? require_entity_config.projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
71
57
  };
72
58
  if (row.projection) entity.projection = row.projection;
73
59
  if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
@@ -1,4 +1,4 @@
1
- import { P as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.cjs";
1
+ import { F as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-DCa785OE.cjs";
2
2
  //#region src/mappings-config.d.ts
3
3
  /**
4
4
  * Mappings → Entity Config bridge.
@@ -1,4 +1,4 @@
1
- import { P as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.mjs";
1
+ import { F as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-DCa785OE.mjs";
2
2
  //#region src/mappings-config.d.ts
3
3
  /**
4
4
  * Mappings → Entity Config bridge.
@@ -1,4 +1,4 @@
1
- import { t as CANONICAL_FIELDS } from "./entity-config-DwGXbUo_.mjs";
1
+ import { t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-DpHPMFIS.mjs";
2
2
  //#region src/mappings-config.ts
3
3
  const RESERVED_FIELD = "id";
4
4
  /**
@@ -47,20 +47,6 @@ function uniquify(base, used) {
47
47
  used.add(key);
48
48
  return key;
49
49
  }
50
- /**
51
- * A projection's `fields` map, derived mechanically from its bindings.
52
- *
53
- * `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
54
- * projection entity must populate it. The values are ref *tokens*
55
- * (`"base.full_name"`), never host column names — the compiler reads
56
- * `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
57
- * column check for projection entities precisely because of this.
58
- */
59
- function projectionFields(projection) {
60
- const fields = {};
61
- for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
62
- return fields;
63
- }
64
50
  function buildEntity(row, refIndex) {
65
51
  const entity = {
66
52
  concept: row.targetConcept,
@@ -99,12 +99,6 @@ Object.defineProperty(exports, "BASE_REF", {
99
99
  return BASE_REF;
100
100
  }
101
101
  });
102
- Object.defineProperty(exports, "PROJECTION_KEY_MESSAGE", {
103
- enumerable: true,
104
- get: function() {
105
- return PROJECTION_KEY_MESSAGE;
106
- }
107
- });
108
102
  Object.defineProperty(exports, "PROJECTION_KEY_PATTERN", {
109
103
  enumerable: true,
110
104
  get: function() {
@@ -93,4 +93,4 @@ function identityRefs(identity, basePkColumn) {
93
93
  return identity.parts;
94
94
  }
95
95
  //#endregion
96
- export { projectionSchema as a, identityRefs as i, PROJECTION_KEY_MESSAGE as n, PROJECTION_KEY_PATTERN as r, BASE_REF as t };
96
+ export { projectionSchema as i, PROJECTION_KEY_PATTERN as n, identityRefs as r, BASE_REF as t };
@@ -0,0 +1,46 @@
1
+ //#region src/schema-meta.ts
2
+ /**
3
+ * Accessors over a discovered table's schema-v2 metadata.
4
+ *
5
+ * Every helper treats *absent* metadata as **unknown**, never as a negative
6
+ * fact. `coversUniqueKey` returning false on a v1 payload is what makes
7
+ * projection validation fail closed: we cannot prove a join is many-to-one, so
8
+ * we refuse to compile it rather than guess.
9
+ */
10
+ function columnMetaFor(table, column) {
11
+ return table.columnsMeta?.find((c) => c.name === column);
12
+ }
13
+ /**
14
+ * Every primary-key column. More than one entry means a composite PK, which is
15
+ * why `base_pk` identity is rejected for such tables.
16
+ */
17
+ function primaryKeyColumns(table) {
18
+ return (table.columnsMeta ?? []).filter((c) => c.isPrimaryKey).map((c) => c.name);
19
+ }
20
+ /**
21
+ * Do `columns` contain every column of at least one unique key on `table`?
22
+ *
23
+ * This is the fan-out test. A join whose ON columns cover a unique key of the
24
+ * joined table matches at most one row per base row; one that does not can
25
+ * match many.
26
+ */
27
+ function coversUniqueKey(table, columns) {
28
+ const present = new Set(columns);
29
+ return (table.uniqueConstraints ?? []).some((key) => key.length > 0 && key.every((column) => present.has(column)));
30
+ }
31
+ /**
32
+ * Is this table's payload rich enough to validate a projection against?
33
+ *
34
+ * A type predicate rather than a plain boolean so the narrowing is visible to
35
+ * the compiler: callers that guard on this can iterate `columnsMeta` directly
36
+ * instead of casting, and a cast that outlives its guard becomes a type error
37
+ * rather than a silent walk over `undefined`.
38
+ *
39
+ * The invariant is unchanged: this reports whether the metadata is *known*,
40
+ * never whether the table has keys. Absent metadata stays UNKNOWN.
41
+ */
42
+ function hasRichMeta(table) {
43
+ return Boolean(table.columnsMeta && table.uniqueConstraints);
44
+ }
45
+ //#endregion
46
+ export { primaryKeyColumns as i, coversUniqueKey as n, hasRichMeta as r, columnMetaFor as t };
@@ -0,0 +1,69 @@
1
+ //#region src/schema-meta.ts
2
+ /**
3
+ * Accessors over a discovered table's schema-v2 metadata.
4
+ *
5
+ * Every helper treats *absent* metadata as **unknown**, never as a negative
6
+ * fact. `coversUniqueKey` returning false on a v1 payload is what makes
7
+ * projection validation fail closed: we cannot prove a join is many-to-one, so
8
+ * we refuse to compile it rather than guess.
9
+ */
10
+ function columnMetaFor(table, column) {
11
+ return table.columnsMeta?.find((c) => c.name === column);
12
+ }
13
+ /**
14
+ * Every primary-key column. More than one entry means a composite PK, which is
15
+ * why `base_pk` identity is rejected for such tables.
16
+ */
17
+ function primaryKeyColumns(table) {
18
+ return (table.columnsMeta ?? []).filter((c) => c.isPrimaryKey).map((c) => c.name);
19
+ }
20
+ /**
21
+ * Do `columns` contain every column of at least one unique key on `table`?
22
+ *
23
+ * This is the fan-out test. A join whose ON columns cover a unique key of the
24
+ * joined table matches at most one row per base row; one that does not can
25
+ * match many.
26
+ */
27
+ function coversUniqueKey(table, columns) {
28
+ const present = new Set(columns);
29
+ return (table.uniqueConstraints ?? []).some((key) => key.length > 0 && key.every((column) => present.has(column)));
30
+ }
31
+ /**
32
+ * Is this table's payload rich enough to validate a projection against?
33
+ *
34
+ * A type predicate rather than a plain boolean so the narrowing is visible to
35
+ * the compiler: callers that guard on this can iterate `columnsMeta` directly
36
+ * instead of casting, and a cast that outlives its guard becomes a type error
37
+ * rather than a silent walk over `undefined`.
38
+ *
39
+ * The invariant is unchanged: this reports whether the metadata is *known*,
40
+ * never whether the table has keys. Absent metadata stays UNKNOWN.
41
+ */
42
+ function hasRichMeta(table) {
43
+ return Boolean(table.columnsMeta && table.uniqueConstraints);
44
+ }
45
+ //#endregion
46
+ Object.defineProperty(exports, "columnMetaFor", {
47
+ enumerable: true,
48
+ get: function() {
49
+ return columnMetaFor;
50
+ }
51
+ });
52
+ Object.defineProperty(exports, "coversUniqueKey", {
53
+ enumerable: true,
54
+ get: function() {
55
+ return coversUniqueKey;
56
+ }
57
+ });
58
+ Object.defineProperty(exports, "hasRichMeta", {
59
+ enumerable: true,
60
+ get: function() {
61
+ return hasRichMeta;
62
+ }
63
+ });
64
+ Object.defineProperty(exports, "primaryKeyColumns", {
65
+ enumerable: true,
66
+ get: function() {
67
+ return primaryKeyColumns;
68
+ }
69
+ });
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_view_compiler = require("./view-compiler-D30HNRnA.cjs");
2
+ const require_view_compiler = require("./view-compiler-DP4Fbj7-.cjs");
3
3
  //#region src/sync-engine.ts
4
4
  /**
5
5
  * Diff a view's rows against the current mirror population for one entity.
@@ -1,4 +1,4 @@
1
- import { a as ClivlyEntitiesConfig } from "./entity-config-buOIVtMM.cjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-DCa785OE.cjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.cjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ClivlyEntitiesConfig } from "./entity-config-buOIVtMM.mjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-DCa785OE.mjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.mjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { r as compileEntityViews } from "./view-compiler-u3GGRgnv.mjs";
1
+ import { r as compileEntityViews } from "./view-compiler-CLvZ0wJW.mjs";
2
2
  //#region src/sync-engine.ts
3
3
  /**
4
4
  * Diff a view's rows against the current mirror population for one entity.
package/dist/types.cjs CHANGED
@@ -0,0 +1,11 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/types.ts
3
+ /** Named once so SQL predicates and UI fallbacks cannot drift apart. */
4
+ const OPEN_STAGE_TYPE = "open";
5
+ /** A closed deal is a historical fact: automation never reopens one. */
6
+ function isClosedStageType(type) {
7
+ return type === "won" || type === "lost";
8
+ }
9
+ //#endregion
10
+ exports.OPEN_STAGE_TYPE = OPEN_STAGE_TYPE;
11
+ exports.isClosedStageType = isClosedStageType;