@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.
Files changed (50) hide show
  1. package/README.md +30 -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.cjs +9 -1
  7. package/dist/drizzle.d.cts +4 -2
  8. package/dist/drizzle.d.mts +4 -2
  9. package/dist/drizzle.mjs +9 -1
  10. package/dist/{entity-config-BAaZOIID.cjs → entity-config-C5jBmG2C.cjs} +115 -71
  11. package/dist/{entity-config-buOIVtMM.d.cts → entity-config-DCa785OE.d.cts} +27 -1
  12. package/dist/{entity-config-buOIVtMM.d.mts → entity-config-DCa785OE.d.mts} +27 -1
  13. package/dist/{entity-config-kusaRiDs.mjs → entity-config-DpHPMFIS.mjs} +104 -42
  14. package/dist/entity-config.cjs +1 -1
  15. package/dist/entity-config.d.cts +1 -1
  16. package/dist/entity-config.d.mts +1 -1
  17. package/dist/entity-config.mjs +1 -1
  18. package/dist/entity-heuristics.cjs +34 -1
  19. package/dist/entity-heuristics.d.cts +8 -2
  20. package/dist/entity-heuristics.d.mts +8 -2
  21. package/dist/entity-heuristics.mjs +34 -2
  22. package/dist/index.cjs +136 -199
  23. package/dist/index.d.cts +135 -89
  24. package/dist/index.d.mts +135 -89
  25. package/dist/index.mjs +124 -188
  26. package/dist/mapping-form.d.cts +1 -1
  27. package/dist/mapping-form.d.mts +1 -1
  28. package/dist/mappings-config.cjs +2 -16
  29. package/dist/mappings-config.d.cts +1 -1
  30. package/dist/mappings-config.d.mts +1 -1
  31. package/dist/mappings-config.mjs +1 -15
  32. package/dist/{projection-OJYCYmS_.cjs → projection-DFgCoyZl.cjs} +0 -6
  33. package/dist/{projection-BXw_iiw5.mjs → projection-SCNrLJMV.mjs} +1 -1
  34. package/dist/schema-meta-Ac7938mT.mjs +46 -0
  35. package/dist/schema-meta-Cgi0MnHh.cjs +69 -0
  36. package/dist/sync-engine.cjs +1 -1
  37. package/dist/sync-engine.d.cts +1 -1
  38. package/dist/sync-engine.d.mts +1 -1
  39. package/dist/sync-engine.mjs +1 -1
  40. package/dist/types.cjs +11 -0
  41. package/dist/types.d.cts +28 -2
  42. package/dist/types.d.mts +28 -2
  43. package/dist/types.mjs +9 -1
  44. package/dist/{view-compiler-u3GGRgnv.mjs → view-compiler-CLvZ0wJW.mjs} +1 -1
  45. package/dist/{view-compiler-D30HNRnA.cjs → view-compiler-DP4Fbj7-.cjs} +1 -1
  46. package/dist/view-compiler.cjs +1 -1
  47. package/dist/view-compiler.d.cts +1 -1
  48. package/dist/view-compiler.d.mts +1 -1
  49. package/dist/view-compiler.mjs +1 -1
  50. package/package.json +11 -1
@@ -1,40 +1,6 @@
1
- const require_projection = require("./projection-OJYCYmS_.cjs");
1
+ const require_projection = require("./projection-DFgCoyZl.cjs");
2
+ const require_schema_meta = require("./schema-meta-Cgi0MnHh.cjs");
2
3
  let zod = require("zod");
3
- //#region src/schema-meta.ts
4
- /**
5
- * Accessors over a discovered table's schema-v2 metadata.
6
- *
7
- * Every helper treats *absent* metadata as **unknown**, never as a negative
8
- * fact. `coversUniqueKey` returning false on a v1 payload is what makes
9
- * projection validation fail closed: we cannot prove a join is many-to-one, so
10
- * we refuse to compile it rather than guess.
11
- */
12
- function columnMetaFor(table, column) {
13
- return table.columnsMeta?.find((c) => c.name === column);
14
- }
15
- /**
16
- * Every primary-key column. More than one entry means a composite PK, which is
17
- * why `base_pk` identity is rejected for such tables.
18
- */
19
- function primaryKeyColumns(table) {
20
- return (table.columnsMeta ?? []).filter((c) => c.isPrimaryKey).map((c) => c.name);
21
- }
22
- /**
23
- * Do `columns` contain every column of at least one unique key on `table`?
24
- *
25
- * This is the fan-out test. A join whose ON columns cover a unique key of the
26
- * joined table matches at most one row per base row; one that does not can
27
- * match many.
28
- */
29
- function coversUniqueKey(table, columns) {
30
- const present = new Set(columns);
31
- return (table.uniqueConstraints ?? []).some((key) => key.length > 0 && key.every((column) => present.has(column)));
32
- }
33
- /** Is this table's payload rich enough to validate a projection against? */
34
- function hasRichMeta(table) {
35
- return Boolean(table.columnsMeta && table.uniqueConstraints);
36
- }
37
- //#endregion
38
4
  //#region src/projection-cardinality.ts
39
5
  /**
40
6
  * Cardinality analysis for projection joins.
@@ -55,7 +21,7 @@ function joinSideColumns(join) {
55
21
  return columns;
56
22
  }
57
23
  function joinFansOut(join, joinTable) {
58
- return !coversUniqueKey(joinTable, joinSideColumns(join));
24
+ return !require_schema_meta.coversUniqueKey(joinTable, joinSideColumns(join));
59
25
  }
60
26
  /** Every join in the projection that can multiply rows. */
61
27
  function fanOutJoins(projection, tablesByName) {
@@ -135,10 +101,10 @@ function checkTablesExist(errors, projection, resolver, base) {
135
101
  ok = false;
136
102
  continue;
137
103
  }
138
- if (!hasRichMeta(table)) {
104
+ if (!require_schema_meta.hasRichMeta(table)) {
139
105
  errors.push({
140
106
  path,
141
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
107
+ message: `table "${name}" was discovered without column types or unique keys, which a projection needs to prove its id is unique. Redeploy or restart the app so it re-reports its schema (or run \`clivly push-schema\`); if the table is declared by hand as \`{ name, columns }\`, use \`discoverFromDrizzle\` so keys and types come with it.`
142
108
  });
143
109
  ok = false;
144
110
  }
@@ -183,10 +149,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
183
149
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
184
150
  for (const ref of require_projection.identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
185
151
  }
152
+ /**
153
+ * Prove that a single-column identity (`column`, or `base_pk` with an
154
+ * explicit column) is actually unique.
155
+ *
156
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
157
+ * check in this file: a column not provably covered by a unique key is
158
+ * rejected, never assumed safe.
159
+ */
160
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
161
+ const table = resolver.tableFor(ref.table);
162
+ if (!table) return;
163
+ if (!require_schema_meta.coversUniqueKey(table, [ref.column])) errors.push({
164
+ path,
165
+ message: `identity column "${ref.table}.${ref.column}" is not covered by a unique key of "${table.name}" — pick a column with a unique constraint, or use a composite identity that is`
166
+ });
167
+ }
186
168
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
187
169
  const path = `${base}.projection.identity`;
188
- const pk = primaryKeyColumns(baseTable);
189
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
170
+ const pk = require_schema_meta.primaryKeyColumns(baseTable);
171
+ const { identity } = projection;
172
+ if (identity.kind === "base_pk" && !identity.column) {
190
173
  if (pk.length === 0) {
191
174
  errors.push({
192
175
  path,
@@ -201,11 +184,23 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
201
184
  });
202
185
  return;
203
186
  }
187
+ return;
188
+ }
189
+ if (identity.kind === "base_pk" && identity.column) {
190
+ checkSingleColumnIdentity(errors, resolver, path, {
191
+ table: require_projection.BASE_REF,
192
+ column: identity.column
193
+ });
194
+ return;
204
195
  }
205
- if (projection.identity.kind !== "composite") return;
206
- for (const part of projection.identity.parts) {
196
+ if (identity.kind === "column") {
197
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
198
+ return;
199
+ }
200
+ if (identity.kind !== "composite") return;
201
+ for (const part of identity.parts) {
207
202
  const table = resolver.tableFor(part.table);
208
- const meta = table && columnMetaFor(table, part.column);
203
+ const meta = table && require_schema_meta.columnMetaFor(table, part.column);
209
204
  if (!meta) continue;
210
205
  if (meta.nullable !== false) errors.push({
211
206
  path,
@@ -239,12 +234,70 @@ function checkFanOut(errors, projection, resolver, base, basePk) {
239
234
  });
240
235
  return;
241
236
  }
242
- if (!coversUniqueKey(joinTable, require_projection.identityRefs(projection.identity, basePk).filter((ref) => ref.table === join.key).map((ref) => ref.column))) errors.push({
237
+ if (!require_schema_meta.coversUniqueKey(joinTable, require_projection.identityRefs(projection.identity, basePk).filter((ref) => ref.table === join.key).map((ref) => ref.column))) errors.push({
243
238
  path: identityPath,
244
239
  message: `identity does not uniquely identify a row of "${joinTable.name}", which fans out — include a unique key of that table`
245
240
  });
246
241
  }
247
242
  /**
243
+ * One error naming a filter column that is not on the projection's base table,
244
+ * and the base table it would have to be on.
245
+ */
246
+ function columnOnBase(column, baseTable, path) {
247
+ if (baseTable?.columns.includes(column)) return [];
248
+ return [{
249
+ path,
250
+ message: `filter column "${column}" does not exist on "${baseTable?.name ?? "the base table"}" — a derived object is filtered by its base table's columns only, so name one of those, or move the condition into a join's \`on\``
251
+ }];
252
+ }
253
+ /**
254
+ * A derived object's filter is evaluated in the cloud against pushed rows, so
255
+ * it can only name columns of the base table.
256
+ *
257
+ * Every rejected shape here fails *silently* today, which is the whole reason
258
+ * the check exists — a filter that cannot be honoured must error, never quietly
259
+ * return the wrong rows. The three shapes fail in different directions:
260
+ *
261
+ * - `$raw` is opaque SQL meant for the embedded model. The cloud path cannot
262
+ * evaluate it and passes every row, so the filter is ignored and the object
263
+ * syncs UNFILTERED.
264
+ * - A joined-table column resolves to nothing on a pushed row, so under a
265
+ * positive operator (`eq`, `in`) no row matches and the object syncs EMPTY.
266
+ * - The same unresolvable column under a NEGATIVE operator (`ne`, `isNull`,
267
+ * `notIn`) is the dangerous one: the missing cell reads as null, so the
268
+ * predicate is true for every row and a filter meant to narrow the sync
269
+ * ADMITS EVERYTHING. Nobody notices extra rows arriving.
270
+ *
271
+ * Rejecting an unresolvable column closes the last two together.
272
+ */
273
+ function validateProjectionFilter(filter, projection, schema, base) {
274
+ if (!filter) return [];
275
+ const path = `${base}.filter`;
276
+ if ("$raw" in filter) return [{
277
+ path,
278
+ message: "a $raw filter cannot be evaluated against a derived object's rows — it is opaque SQL, so the cloud passes every row through unfiltered. Express it as column conditions on the base table instead"
279
+ }];
280
+ const baseTable = schema.find((t) => t.name === projection.baseTable);
281
+ const errors = [];
282
+ for (const column of Object.keys(filter)) {
283
+ if (column.includes(".")) {
284
+ const [ref, ...rest] = column.split(".");
285
+ const bare = rest.join(".");
286
+ if (ref !== "base") {
287
+ errors.push({
288
+ path,
289
+ message: `filter column "${column}" names the join "${ref}", but a derived object is filtered by its base table ("${projection.baseTable}") only — filter on a base column, or move the condition into that join's \`on\``
290
+ });
291
+ continue;
292
+ }
293
+ errors.push(...columnOnBase(bare, baseTable, path));
294
+ continue;
295
+ }
296
+ errors.push(...columnOnBase(column, baseTable, path));
297
+ }
298
+ return errors;
299
+ }
300
+ /**
248
301
  * Validate a projection against the discovered schema. Returns every error
249
302
  * found (not just the first) so a builder UI can show them all at once.
250
303
  */
@@ -255,7 +308,7 @@ function validateProjection(projection, schema, base) {
255
308
  if (!checkTablesExist(errors, projection, resolver, base)) return errors;
256
309
  const baseTable = resolver.tablesByName.get(projection.baseTable);
257
310
  if (!baseTable) return errors;
258
- const basePk = primaryKeyColumns(baseTable)[0] ?? "id";
311
+ const basePk = require_schema_meta.primaryKeyColumns(baseTable)[0] ?? "id";
259
312
  checkRefs(errors, projection, resolver, base, basePk);
260
313
  checkIdentitySafety(errors, projection, resolver, base, baseTable);
261
314
  checkFanOut(errors, projection, resolver, base, basePk);
@@ -517,6 +570,15 @@ function customObjectDeclarations(config) {
517
570
  * Qualified rather than bare: two bindings can pull the same column name from
518
571
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
519
572
  * name cannot tell apart.
573
+ *
574
+ * This is the single derivation of that map, and both consumers depend on it
575
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
576
+ * entity's required `fields`; the cloud ingest path needs the same map because
577
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
578
+ * so `rowToSourceRow` derives the effective field map from the projection
579
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
580
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
581
+ * Deriving this a second time anywhere would let the copies drift.
520
582
  */
521
583
  function projectionFields(projection) {
522
584
  const fields = {};
@@ -655,7 +717,7 @@ function validateEntity(v, config, key, entity, schema) {
655
717
  if (entity.source !== entity.projection.baseTable) v.report(`${base}.source`, `"source" ("${entity.source}") must match "projection.baseTable" ("${entity.projection.baseTable}") — the view compiler reads baseTable and the sync engine reads source, so a mismatch makes them disagree about which table this entity is`);
656
718
  for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
657
719
  validateReadOnly(v, base, entity);
658
- validateFilter(v, base, entity);
720
+ for (const error of validateProjectionFilter(entity.filter, entity.projection, schema, base)) v.report(error.path, error.message);
659
721
  return;
660
722
  }
661
723
  validateFields(v, base, entity);
@@ -711,18 +773,6 @@ Object.defineProperty(exports, "MATERIALIZING_CONCEPTS", {
711
773
  return MATERIALIZING_CONCEPTS;
712
774
  }
713
775
  });
714
- Object.defineProperty(exports, "columnMetaFor", {
715
- enumerable: true,
716
- get: function() {
717
- return columnMetaFor;
718
- }
719
- });
720
- Object.defineProperty(exports, "coversUniqueKey", {
721
- enumerable: true,
722
- get: function() {
723
- return coversUniqueKey;
724
- }
725
- });
726
776
  Object.defineProperty(exports, "customObjectDeclarations", {
727
777
  enumerable: true,
728
778
  get: function() {
@@ -753,24 +803,12 @@ Object.defineProperty(exports, "filterSchema", {
753
803
  return filterSchema;
754
804
  }
755
805
  });
756
- Object.defineProperty(exports, "hasRichMeta", {
757
- enumerable: true,
758
- get: function() {
759
- return hasRichMeta;
760
- }
761
- });
762
806
  Object.defineProperty(exports, "joinFansOut", {
763
807
  enumerable: true,
764
808
  get: function() {
765
809
  return joinFansOut;
766
810
  }
767
811
  });
768
- Object.defineProperty(exports, "primaryKeyColumns", {
769
- enumerable: true,
770
- get: function() {
771
- return primaryKeyColumns;
772
- }
773
- });
774
812
  Object.defineProperty(exports, "projectionFields", {
775
813
  enumerable: true,
776
814
  get: function() {
@@ -801,3 +839,9 @@ Object.defineProperty(exports, "validateProjection", {
801
839
  return validateProjection;
802
840
  }
803
841
  });
842
+ Object.defineProperty(exports, "validateProjectionFilter", {
843
+ enumerable: true,
844
+ get: function() {
845
+ return validateProjectionFilter;
846
+ }
847
+ });
@@ -16,6 +16,23 @@ import { z } from "zod";
16
16
  */
17
17
  /** The reserved ref name for the projection's base table. */
18
18
  declare const BASE_REF = "base";
19
+ /**
20
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
21
+ *
22
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
23
+ * on the server. Those two were widened apart: the server learned hyphens
24
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
25
+ * `clivly status` reports that form as canonical — but this pattern was missed,
26
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
27
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
28
+ * opposite half of the product.
29
+ *
30
+ * Widening is the safe direction: every key that was valid before still is, so
31
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
32
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
33
+ * and so is a join's `key` when it is emitted as an alias.
34
+ */
35
+ declare const PROJECTION_KEY_PATTERN: RegExp;
19
36
  declare const projectionRefSchema: z.ZodObject<{
20
37
  table: z.ZodString;
21
38
  column: z.ZodString;
@@ -517,6 +534,15 @@ declare function customObjectDeclarations(config: ClivlyEntitiesConfig): CustomO
517
534
  * Qualified rather than bare: two bindings can pull the same column name from
518
535
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
519
536
  * name cannot tell apart.
537
+ *
538
+ * This is the single derivation of that map, and both consumers depend on it
539
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
540
+ * entity's required `fields`; the cloud ingest path needs the same map because
541
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
542
+ * so `rowToSourceRow` derives the effective field map from the projection
543
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
544
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
545
+ * Deriving this a second time anywhere would let the copies drift.
520
546
  */
521
547
  declare function projectionFields(projection: ProjectionMapping): Record<string, string>;
522
548
  declare function defineClivlyConfig(config: ClivlyEntitiesConfigInput, options?: {
@@ -577,4 +603,4 @@ interface ConfigValidationResult {
577
603
  */
578
604
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
579
605
  //#endregion
580
- export { BASE_REF as A, defineClivlyConfig as C, relationshipSchema as D, projectionFields as E, ProjectionRef as F, identityRefs as I, projectionSchema as L, ProjectionIdentity as M, ProjectionJoin as N, relationshipsSchema as O, ProjectionMapping as P, customObjectDeclarations as S, filterSchema as T, MATERIALIZING_CONCEPTS as _, ClivlyEntitiesConfig as a, RelationshipVia as b, ClivlyEntityConfigInput as c, CrmConcept as d, CustomObjectDeclaration as f, FilterValue as g, FilterExpr as h, ClivlyConfigError as i, ProjectionFieldBinding as j, validateEntitiesConfig as k, ConfigValidationError as l, DiscoveredSchemaTable as m, CRM_CONCEPTS as n, ClivlyEntitiesConfigInput as o, DiscoveredColumnMeta as p, CUSTOM_CONCEPT as r, ClivlyEntityConfig as s, CANONICAL_FIELDS as t, ConfigValidationResult as u, MaterializingConcept as v, entitiesConfigSchema as w, RelationshipsMap as x, RelationshipSpec as y };
606
+ export { BASE_REF as A, defineClivlyConfig as C, relationshipSchema as D, projectionFields as E, ProjectionMapping as F, ProjectionRef as I, identityRefs as L, ProjectionFieldBinding as M, ProjectionIdentity as N, relationshipsSchema as O, ProjectionJoin as P, projectionSchema as R, customObjectDeclarations as S, filterSchema as T, MATERIALIZING_CONCEPTS as _, ClivlyEntitiesConfig as a, RelationshipVia as b, ClivlyEntityConfigInput as c, CrmConcept as d, CustomObjectDeclaration as f, FilterValue as g, FilterExpr as h, ClivlyConfigError as i, PROJECTION_KEY_PATTERN as j, validateEntitiesConfig as k, ConfigValidationError as l, DiscoveredSchemaTable as m, CRM_CONCEPTS as n, ClivlyEntitiesConfigInput as o, DiscoveredColumnMeta as p, CUSTOM_CONCEPT as r, ClivlyEntityConfig as s, CANONICAL_FIELDS as t, ConfigValidationResult as u, MaterializingConcept as v, entitiesConfigSchema as w, RelationshipsMap as x, RelationshipSpec as y };
@@ -16,6 +16,23 @@ import { z } from "zod";
16
16
  */
17
17
  /** The reserved ref name for the projection's base table. */
18
18
  declare const BASE_REF = "base";
19
+ /**
20
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
21
+ *
22
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
23
+ * on the server. Those two were widened apart: the server learned hyphens
24
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
25
+ * `clivly status` reports that form as canonical — but this pattern was missed,
26
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
27
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
28
+ * opposite half of the product.
29
+ *
30
+ * Widening is the safe direction: every key that was valid before still is, so
31
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
32
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
33
+ * and so is a join's `key` when it is emitted as an alias.
34
+ */
35
+ declare const PROJECTION_KEY_PATTERN: RegExp;
19
36
  declare const projectionRefSchema: z.ZodObject<{
20
37
  table: z.ZodString;
21
38
  column: z.ZodString;
@@ -517,6 +534,15 @@ declare function customObjectDeclarations(config: ClivlyEntitiesConfig): CustomO
517
534
  * Qualified rather than bare: two bindings can pull the same column name from
518
535
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
519
536
  * name cannot tell apart.
537
+ *
538
+ * This is the single derivation of that map, and both consumers depend on it
539
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
540
+ * entity's required `fields`; the cloud ingest path needs the same map because
541
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
542
+ * so `rowToSourceRow` derives the effective field map from the projection
543
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
544
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
545
+ * Deriving this a second time anywhere would let the copies drift.
520
546
  */
521
547
  declare function projectionFields(projection: ProjectionMapping): Record<string, string>;
522
548
  declare function defineClivlyConfig(config: ClivlyEntitiesConfigInput, options?: {
@@ -577,4 +603,4 @@ interface ConfigValidationResult {
577
603
  */
578
604
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
579
605
  //#endregion
580
- export { BASE_REF as A, defineClivlyConfig as C, relationshipSchema as D, projectionFields as E, ProjectionRef as F, identityRefs as I, projectionSchema as L, ProjectionIdentity as M, ProjectionJoin as N, relationshipsSchema as O, ProjectionMapping as P, customObjectDeclarations as S, filterSchema as T, MATERIALIZING_CONCEPTS as _, ClivlyEntitiesConfig as a, RelationshipVia as b, ClivlyEntityConfigInput as c, CrmConcept as d, CustomObjectDeclaration as f, FilterValue as g, FilterExpr as h, ClivlyConfigError as i, ProjectionFieldBinding as j, validateEntitiesConfig as k, ConfigValidationError as l, DiscoveredSchemaTable as m, CRM_CONCEPTS as n, ClivlyEntitiesConfigInput as o, DiscoveredColumnMeta as p, CUSTOM_CONCEPT as r, ClivlyEntityConfig as s, CANONICAL_FIELDS as t, ConfigValidationResult as u, MaterializingConcept as v, entitiesConfigSchema as w, RelationshipsMap as x, RelationshipSpec as y };
606
+ export { BASE_REF as A, defineClivlyConfig as C, relationshipSchema as D, projectionFields as E, ProjectionMapping as F, ProjectionRef as I, identityRefs as L, ProjectionFieldBinding as M, ProjectionIdentity as N, relationshipsSchema as O, ProjectionJoin as P, projectionSchema as R, customObjectDeclarations as S, filterSchema as T, MATERIALIZING_CONCEPTS as _, ClivlyEntitiesConfig as a, RelationshipVia as b, ClivlyEntityConfigInput as c, CrmConcept as d, CustomObjectDeclaration as f, FilterValue as g, FilterExpr as h, ClivlyConfigError as i, PROJECTION_KEY_PATTERN as j, validateEntitiesConfig as k, ConfigValidationError as l, DiscoveredSchemaTable as m, CRM_CONCEPTS as n, ClivlyEntitiesConfigInput as o, DiscoveredColumnMeta as p, CUSTOM_CONCEPT as r, ClivlyEntityConfig as s, CANONICAL_FIELDS as t, ConfigValidationResult as u, MaterializingConcept as v, entitiesConfigSchema as w, RelationshipsMap as x, RelationshipSpec as y };
@@ -1,40 +1,6 @@
1
- import { a as projectionSchema, i as identityRefs, t as BASE_REF } from "./projection-BXw_iiw5.mjs";
1
+ import { i as projectionSchema, r as identityRefs, t as BASE_REF } from "./projection-SCNrLJMV.mjs";
2
+ import { i as primaryKeyColumns, n as coversUniqueKey, r as hasRichMeta, t as columnMetaFor } from "./schema-meta-Ac7938mT.mjs";
2
3
  import { z } from "zod";
3
- //#region src/schema-meta.ts
4
- /**
5
- * Accessors over a discovered table's schema-v2 metadata.
6
- *
7
- * Every helper treats *absent* metadata as **unknown**, never as a negative
8
- * fact. `coversUniqueKey` returning false on a v1 payload is what makes
9
- * projection validation fail closed: we cannot prove a join is many-to-one, so
10
- * we refuse to compile it rather than guess.
11
- */
12
- function columnMetaFor(table, column) {
13
- return table.columnsMeta?.find((c) => c.name === column);
14
- }
15
- /**
16
- * Every primary-key column. More than one entry means a composite PK, which is
17
- * why `base_pk` identity is rejected for such tables.
18
- */
19
- function primaryKeyColumns(table) {
20
- return (table.columnsMeta ?? []).filter((c) => c.isPrimaryKey).map((c) => c.name);
21
- }
22
- /**
23
- * Do `columns` contain every column of at least one unique key on `table`?
24
- *
25
- * This is the fan-out test. A join whose ON columns cover a unique key of the
26
- * joined table matches at most one row per base row; one that does not can
27
- * match many.
28
- */
29
- function coversUniqueKey(table, columns) {
30
- const present = new Set(columns);
31
- return (table.uniqueConstraints ?? []).some((key) => key.length > 0 && key.every((column) => present.has(column)));
32
- }
33
- /** Is this table's payload rich enough to validate a projection against? */
34
- function hasRichMeta(table) {
35
- return Boolean(table.columnsMeta && table.uniqueConstraints);
36
- }
37
- //#endregion
38
4
  //#region src/projection-cardinality.ts
39
5
  /**
40
6
  * Cardinality analysis for projection joins.
@@ -138,7 +104,7 @@ function checkTablesExist(errors, projection, resolver, base) {
138
104
  if (!hasRichMeta(table)) {
139
105
  errors.push({
140
106
  path,
141
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
107
+ message: `table "${name}" was discovered without column types or unique keys, which a projection needs to prove its id is unique. Redeploy or restart the app so it re-reports its schema (or run \`clivly push-schema\`); if the table is declared by hand as \`{ name, columns }\`, use \`discoverFromDrizzle\` so keys and types come with it.`
142
108
  });
143
109
  ok = false;
144
110
  }
@@ -183,10 +149,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
183
149
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
184
150
  for (const ref of identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
185
151
  }
152
+ /**
153
+ * Prove that a single-column identity (`column`, or `base_pk` with an
154
+ * explicit column) is actually unique.
155
+ *
156
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
157
+ * check in this file: a column not provably covered by a unique key is
158
+ * rejected, never assumed safe.
159
+ */
160
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
161
+ const table = resolver.tableFor(ref.table);
162
+ if (!table) return;
163
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
164
+ path,
165
+ message: `identity column "${ref.table}.${ref.column}" is not covered by a unique key of "${table.name}" — pick a column with a unique constraint, or use a composite identity that is`
166
+ });
167
+ }
186
168
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
187
169
  const path = `${base}.projection.identity`;
188
170
  const pk = primaryKeyColumns(baseTable);
189
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
171
+ const { identity } = projection;
172
+ if (identity.kind === "base_pk" && !identity.column) {
190
173
  if (pk.length === 0) {
191
174
  errors.push({
192
175
  path,
@@ -201,9 +184,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
201
184
  });
202
185
  return;
203
186
  }
187
+ return;
204
188
  }
205
- if (projection.identity.kind !== "composite") return;
206
- for (const part of projection.identity.parts) {
189
+ if (identity.kind === "base_pk" && identity.column) {
190
+ checkSingleColumnIdentity(errors, resolver, path, {
191
+ table: BASE_REF,
192
+ column: identity.column
193
+ });
194
+ return;
195
+ }
196
+ if (identity.kind === "column") {
197
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
198
+ return;
199
+ }
200
+ if (identity.kind !== "composite") return;
201
+ for (const part of identity.parts) {
207
202
  const table = resolver.tableFor(part.table);
208
203
  const meta = table && columnMetaFor(table, part.column);
209
204
  if (!meta) continue;
@@ -245,6 +240,64 @@ function checkFanOut(errors, projection, resolver, base, basePk) {
245
240
  });
246
241
  }
247
242
  /**
243
+ * One error naming a filter column that is not on the projection's base table,
244
+ * and the base table it would have to be on.
245
+ */
246
+ function columnOnBase(column, baseTable, path) {
247
+ if (baseTable?.columns.includes(column)) return [];
248
+ return [{
249
+ path,
250
+ message: `filter column "${column}" does not exist on "${baseTable?.name ?? "the base table"}" — a derived object is filtered by its base table's columns only, so name one of those, or move the condition into a join's \`on\``
251
+ }];
252
+ }
253
+ /**
254
+ * A derived object's filter is evaluated in the cloud against pushed rows, so
255
+ * it can only name columns of the base table.
256
+ *
257
+ * Every rejected shape here fails *silently* today, which is the whole reason
258
+ * the check exists — a filter that cannot be honoured must error, never quietly
259
+ * return the wrong rows. The three shapes fail in different directions:
260
+ *
261
+ * - `$raw` is opaque SQL meant for the embedded model. The cloud path cannot
262
+ * evaluate it and passes every row, so the filter is ignored and the object
263
+ * syncs UNFILTERED.
264
+ * - A joined-table column resolves to nothing on a pushed row, so under a
265
+ * positive operator (`eq`, `in`) no row matches and the object syncs EMPTY.
266
+ * - The same unresolvable column under a NEGATIVE operator (`ne`, `isNull`,
267
+ * `notIn`) is the dangerous one: the missing cell reads as null, so the
268
+ * predicate is true for every row and a filter meant to narrow the sync
269
+ * ADMITS EVERYTHING. Nobody notices extra rows arriving.
270
+ *
271
+ * Rejecting an unresolvable column closes the last two together.
272
+ */
273
+ function validateProjectionFilter(filter, projection, schema, base) {
274
+ if (!filter) return [];
275
+ const path = `${base}.filter`;
276
+ if ("$raw" in filter) return [{
277
+ path,
278
+ message: "a $raw filter cannot be evaluated against a derived object's rows — it is opaque SQL, so the cloud passes every row through unfiltered. Express it as column conditions on the base table instead"
279
+ }];
280
+ const baseTable = schema.find((t) => t.name === projection.baseTable);
281
+ const errors = [];
282
+ for (const column of Object.keys(filter)) {
283
+ if (column.includes(".")) {
284
+ const [ref, ...rest] = column.split(".");
285
+ const bare = rest.join(".");
286
+ if (ref !== "base") {
287
+ errors.push({
288
+ path,
289
+ message: `filter column "${column}" names the join "${ref}", but a derived object is filtered by its base table ("${projection.baseTable}") only — filter on a base column, or move the condition into that join's \`on\``
290
+ });
291
+ continue;
292
+ }
293
+ errors.push(...columnOnBase(bare, baseTable, path));
294
+ continue;
295
+ }
296
+ errors.push(...columnOnBase(column, baseTable, path));
297
+ }
298
+ return errors;
299
+ }
300
+ /**
248
301
  * Validate a projection against the discovered schema. Returns every error
249
302
  * found (not just the first) so a builder UI can show them all at once.
250
303
  */
@@ -517,6 +570,15 @@ function customObjectDeclarations(config) {
517
570
  * Qualified rather than bare: two bindings can pull the same column name from
518
571
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
519
572
  * name cannot tell apart.
573
+ *
574
+ * This is the single derivation of that map, and both consumers depend on it
575
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
576
+ * entity's required `fields`; the cloud ingest path needs the same map because
577
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
578
+ * so `rowToSourceRow` derives the effective field map from the projection
579
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
580
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
581
+ * Deriving this a second time anywhere would let the copies drift.
520
582
  */
521
583
  function projectionFields(projection) {
522
584
  const fields = {};
@@ -655,7 +717,7 @@ function validateEntity(v, config, key, entity, schema) {
655
717
  if (entity.source !== entity.projection.baseTable) v.report(`${base}.source`, `"source" ("${entity.source}") must match "projection.baseTable" ("${entity.projection.baseTable}") — the view compiler reads baseTable and the sync engine reads source, so a mismatch makes them disagree about which table this entity is`);
656
718
  for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
657
719
  validateReadOnly(v, base, entity);
658
- validateFilter(v, base, entity);
720
+ for (const error of validateProjectionFilter(entity.filter, entity.projection, schema, base)) v.report(error.path, error.message);
659
721
  return;
660
722
  }
661
723
  validateFields(v, base, entity);
@@ -681,4 +743,4 @@ function validateEntitiesConfig(config, schema) {
681
743
  };
682
744
  }
683
745
  //#endregion
684
- export { columnMetaFor as _, MATERIALIZING_CONCEPTS as a, primaryKeyColumns as b, entitiesConfigSchema as c, relationshipSchema as d, relationshipsSchema as f, joinFansOut as g, fanOutJoins as h, ClivlyConfigError as i, filterSchema as l, validateProjection as m, CRM_CONCEPTS as n, customObjectDeclarations as o, validateEntitiesConfig as p, CUSTOM_CONCEPT as r, defineClivlyConfig as s, CANONICAL_FIELDS as t, projectionFields as u, coversUniqueKey as v, hasRichMeta as y };
746
+ export { joinFansOut as _, MATERIALIZING_CONCEPTS as a, entitiesConfigSchema as c, relationshipSchema as d, relationshipsSchema as f, fanOutJoins as g, validateProjectionFilter as h, ClivlyConfigError as i, filterSchema as l, validateProjection as m, CRM_CONCEPTS as n, customObjectDeclarations as o, validateEntitiesConfig as p, CUSTOM_CONCEPT as r, defineClivlyConfig as s, CANONICAL_FIELDS as t, projectionFields as u };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
2
+ const require_entity_config = require("./entity-config-C5jBmG2C.cjs");
3
3
  exports.CANONICAL_FIELDS = require_entity_config.CANONICAL_FIELDS;
4
4
  exports.CRM_CONCEPTS = require_entity_config.CRM_CONCEPTS;
5
5
  exports.CUSTOM_CONCEPT = require_entity_config.CUSTOM_CONCEPT;
@@ -1,2 +1,2 @@
1
- import { C as defineClivlyConfig, D as relationshipSchema, E as projectionFields, O as relationshipsSchema, S as customObjectDeclarations, T as filterSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipVia, c as ClivlyEntityConfigInput, d as CrmConcept, f as CustomObjectDeclaration, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, o as ClivlyEntitiesConfigInput, p as DiscoveredColumnMeta, r as CUSTOM_CONCEPT, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as entitiesConfigSchema, x as RelationshipsMap, y as RelationshipSpec } from "./entity-config-buOIVtMM.cjs";
1
+ import { C as defineClivlyConfig, D as relationshipSchema, E as projectionFields, O as relationshipsSchema, S as customObjectDeclarations, T as filterSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipVia, c as ClivlyEntityConfigInput, d as CrmConcept, f as CustomObjectDeclaration, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, o as ClivlyEntitiesConfigInput, p as DiscoveredColumnMeta, r as CUSTOM_CONCEPT, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as entitiesConfigSchema, x as RelationshipsMap, y as RelationshipSpec } from "./entity-config-DCa785OE.cjs";
2
2
  export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntitiesConfigInput, ClivlyEntityConfig, ClivlyEntityConfigInput, ConfigValidationError, ConfigValidationResult, CrmConcept, CustomObjectDeclaration, DiscoveredColumnMeta, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, customObjectDeclarations, defineClivlyConfig, entitiesConfigSchema, filterSchema, projectionFields, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -1,2 +1,2 @@
1
- import { C as defineClivlyConfig, D as relationshipSchema, E as projectionFields, O as relationshipsSchema, S as customObjectDeclarations, T as filterSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipVia, c as ClivlyEntityConfigInput, d as CrmConcept, f as CustomObjectDeclaration, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, o as ClivlyEntitiesConfigInput, p as DiscoveredColumnMeta, r as CUSTOM_CONCEPT, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as entitiesConfigSchema, x as RelationshipsMap, y as RelationshipSpec } from "./entity-config-buOIVtMM.mjs";
1
+ import { C as defineClivlyConfig, D as relationshipSchema, E as projectionFields, O as relationshipsSchema, S as customObjectDeclarations, T as filterSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipVia, c as ClivlyEntityConfigInput, d as CrmConcept, f as CustomObjectDeclaration, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, o as ClivlyEntitiesConfigInput, p as DiscoveredColumnMeta, r as CUSTOM_CONCEPT, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as entitiesConfigSchema, x as RelationshipsMap, y as RelationshipSpec } from "./entity-config-DCa785OE.mjs";
2
2
  export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntitiesConfigInput, ClivlyEntityConfig, ClivlyEntityConfigInput, ConfigValidationError, ConfigValidationResult, CrmConcept, CustomObjectDeclaration, DiscoveredColumnMeta, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, customObjectDeclarations, defineClivlyConfig, entitiesConfigSchema, filterSchema, projectionFields, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -1,2 +1,2 @@
1
- import { a as MATERIALIZING_CONCEPTS, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, i as ClivlyConfigError, l as filterSchema, n as CRM_CONCEPTS, o as customObjectDeclarations, p as validateEntitiesConfig, r as CUSTOM_CONCEPT, s as defineClivlyConfig, t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-kusaRiDs.mjs";
1
+ import { a as MATERIALIZING_CONCEPTS, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, i as ClivlyConfigError, l as filterSchema, n as CRM_CONCEPTS, o as customObjectDeclarations, p as validateEntitiesConfig, r as CUSTOM_CONCEPT, s as defineClivlyConfig, t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-DpHPMFIS.mjs";
2
2
  export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, MATERIALIZING_CONCEPTS, customObjectDeclarations, defineClivlyConfig, entitiesConfigSchema, filterSchema, projectionFields, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
2
+ const require_entity_config = require("./entity-config-C5jBmG2C.cjs");
3
3
  const require_mapping_score = require("./mapping-score.cjs");
4
4
  //#region src/entity-heuristics.ts
5
5
  const TARGET_CONCEPTS = [
@@ -251,6 +251,38 @@ function matchesConcept(tableName, concept) {
251
251
  if (entry.names.some((name) => candidates.some((candidate) => candidate.includes(name)))) return { exact: false };
252
252
  return null;
253
253
  }
254
+ const SENSITIVE_COLUMNS = /* @__PURE__ */ new Set([
255
+ "password",
256
+ "password_hash",
257
+ "passwordhash",
258
+ "access_token",
259
+ "accesstoken",
260
+ "api_key",
261
+ "apikey",
262
+ "client_secret",
263
+ "clientsecret",
264
+ "encrypted_password",
265
+ "encryptedpassword",
266
+ "refresh_token",
267
+ "refreshtoken",
268
+ "id_token",
269
+ "idtoken",
270
+ "password_digest",
271
+ "passworddigest",
272
+ "private_key",
273
+ "privatekey",
274
+ "secret",
275
+ "session_token",
276
+ "sessiontoken"
277
+ ]);
278
+ /**
279
+ * Does this table carry credentials? Such tables are withheld from suggestions
280
+ * and from bulk apply, so one click can never map an auth table into the CRM.
281
+ * They stay manually selectable — an explicit, informed choice is legitimate.
282
+ */
283
+ function hasSensitiveColumns(columns) {
284
+ return columns.some((column) => SENSITIVE_COLUMNS.has(column.toLowerCase().replaceAll("-", "_")));
285
+ }
254
286
  function metaFor(table, column) {
255
287
  return table.columnsMeta?.find((c) => c.name === column);
256
288
  }
@@ -463,6 +495,7 @@ function toFieldMap(scored) {
463
495
  }
464
496
  //#endregion
465
497
  exports.TARGET_CONCEPTS = TARGET_CONCEPTS;
498
+ exports.hasSensitiveColumns = hasSensitiveColumns;
466
499
  exports.matchesConcept = matchesConcept;
467
500
  exports.requiredFieldsFor = requiredFieldsFor;
468
501
  exports.scoreConcepts = scoreConcepts;