clivly 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 (146) hide show
  1. package/README.md +24 -12
  2. package/{adapter-C2eRbBfo.d.cts → adapter-1TN0zohk.d.cts} +1 -1
  3. package/{adapter-D5LRa-NA.d.mts → adapter-uxirB88i.d.mts} +1 -1
  4. package/{auth-adapter-Bjgo8LhN.d.mts → auth-adapter-CXTiW3Mb.d.cts} +1 -1
  5. package/{auth-adapter-DWzgl5Yq.d.cts → auth-adapter-DSuYwv1c.d.mts} +1 -1
  6. package/auth-better-auth.d.cts +1 -1
  7. package/auth-better-auth.d.mts +1 -1
  8. package/auth-clerk.d.cts +1 -1
  9. package/auth-clerk.d.mts +1 -1
  10. package/auth-workos.d.cts +1 -1
  11. package/auth-workos.d.mts +1 -1
  12. package/core-adapter.d.cts +1 -1
  13. package/core-adapter.d.mts +1 -1
  14. package/core-auth-adapter.cjs +1 -1
  15. package/core-auth-adapter.d.cts +1 -1
  16. package/core-auth-adapter.d.mts +1 -1
  17. package/core-auth-adapter.mjs +1 -1
  18. package/core-drizzle.cjs +1 -1
  19. package/core-drizzle.d.cts +1 -1
  20. package/core-drizzle.d.mts +1 -1
  21. package/core-drizzle.mjs +1 -1
  22. package/core-entity-config.cjs +1 -1
  23. package/core-entity-config.d.cts +1 -1
  24. package/core-entity-config.d.mts +1 -1
  25. package/core-entity-config.mjs +1 -1
  26. package/core-entity-heuristics.cjs +1 -0
  27. package/core-entity-heuristics.d.cts +3 -3
  28. package/core-entity-heuristics.d.mts +3 -3
  29. package/core-entity-heuristics.mjs +2 -2
  30. package/core-mapping-form.cjs +1 -1
  31. package/core-mapping-form.d.cts +1 -1
  32. package/core-mapping-form.d.mts +1 -1
  33. package/core-mapping-form.mjs +1 -1
  34. package/core-mappings-config.cjs +1 -1
  35. package/core-mappings-config.d.cts +1 -1
  36. package/core-mappings-config.d.mts +1 -1
  37. package/core-mappings-config.mjs +1 -1
  38. package/core-sync-engine.cjs +1 -1
  39. package/core-sync-engine.d.cts +1 -1
  40. package/core-sync-engine.d.mts +1 -1
  41. package/core-sync-engine.mjs +1 -1
  42. package/core-types.cjs +4 -0
  43. package/core-types.d.cts +2 -2
  44. package/core-types.d.mts +2 -2
  45. package/core-types.mjs +2 -1
  46. package/core-view-compiler.cjs +1 -1
  47. package/core-view-compiler.d.cts +1 -1
  48. package/core-view-compiler.d.mts +1 -1
  49. package/core-view-compiler.mjs +1 -1
  50. package/core.cjs +18 -17
  51. package/core.d.cts +10 -10
  52. package/core.d.mts +10 -10
  53. package/core.mjs +10 -9
  54. package/{drizzle-DEtRrqtc.d.cts → drizzle-DTOuGXEn.d.cts} +4 -2
  55. package/{drizzle-bfjVzYR6.cjs → drizzle-DcB7OfBp.cjs} +9 -1
  56. package/{drizzle-CiK8MMEZ.d.mts → drizzle-SGapoly2.d.mts} +4 -2
  57. package/{drizzle-3kvhkwO7.mjs → drizzle-wbcuG-cy.mjs} +9 -1
  58. package/drizzle.cjs +58 -24
  59. package/drizzle.d.cts +10 -4
  60. package/drizzle.d.mts +10 -4
  61. package/drizzle.mjs +58 -24
  62. package/{entity-config-CusbTOJ1.mjs → entity-config-C4d3K9CP.mjs} +114 -8
  63. package/{entity-config-B2RA53nw.d.cts → entity-config-DNQAMbMS.d.cts} +27 -1
  64. package/{entity-config-B2RA53nw.d.mts → entity-config-DNQAMbMS.d.mts} +27 -1
  65. package/{entity-config-DE30_RpU.cjs → entity-config-DgAe0_YD.cjs} +119 -7
  66. package/index.cjs +56 -15
  67. package/index.mjs +56 -15
  68. package/internal-BAOG5eE4.d.mts +226 -0
  69. package/internal-BHJ0X3fC.d.cts +226 -0
  70. package/internal-Bgiimp5D.mjs +144 -0
  71. package/internal-Cz59xMj8.cjs +167 -0
  72. package/internal-adapter.d.cts +1 -1
  73. package/internal-adapter.d.mts +1 -1
  74. package/internal-auth-adapter.d.cts +1 -1
  75. package/internal-auth-adapter.d.mts +1 -1
  76. package/internal-drizzle.cjs +1 -1
  77. package/internal-drizzle.d.cts +1 -1
  78. package/internal-drizzle.d.mts +1 -1
  79. package/internal-drizzle.mjs +1 -1
  80. package/internal-entity-config.cjs +1 -1
  81. package/internal-entity-config.d.cts +1 -1
  82. package/internal-entity-config.d.mts +1 -1
  83. package/internal-entity-config.mjs +1 -1
  84. package/internal-entity-heuristics.cjs +34 -1
  85. package/internal-entity-heuristics.d.cts +8 -2
  86. package/internal-entity-heuristics.d.mts +8 -2
  87. package/internal-entity-heuristics.mjs +34 -2
  88. package/internal-mapping-form.d.cts +1 -1
  89. package/internal-mapping-form.d.mts +1 -1
  90. package/internal-mappings-config.cjs +1 -1
  91. package/internal-mappings-config.d.cts +1 -1
  92. package/internal-mappings-config.d.mts +1 -1
  93. package/internal-mappings-config.mjs +1 -1
  94. package/internal-sync-engine.cjs +1 -1
  95. package/internal-sync-engine.d.cts +1 -1
  96. package/internal-sync-engine.d.mts +1 -1
  97. package/internal-sync-engine.mjs +1 -1
  98. package/internal-types.cjs +4 -0
  99. package/internal-types.d.cts +2 -2
  100. package/internal-types.d.mts +2 -2
  101. package/internal-types.mjs +2 -1
  102. package/internal-view-compiler.cjs +1 -1
  103. package/internal-view-compiler.d.cts +1 -1
  104. package/internal-view-compiler.d.mts +1 -1
  105. package/internal-view-compiler.mjs +1 -1
  106. package/internal.cjs +17 -15
  107. package/internal.d.cts +10 -10
  108. package/internal.d.mts +10 -10
  109. package/internal.mjs +9 -8
  110. package/{mapping-form-DH77BXLv.d.mts → mapping-form-Cc7Ewv8c.d.mts} +1 -1
  111. package/{mapping-form-J0aU6O7l.d.cts → mapping-form-DwdbYAfR.d.cts} +1 -1
  112. package/{mappings-config-D9dsonoi.d.mts → mappings-config-Be9D43Np.d.mts} +1 -1
  113. package/{mappings-config-RQLGx8ub.d.cts → mappings-config-DUt4-9iO.d.cts} +1 -1
  114. package/{mappings-config-CjCmT-aU.cjs → mappings-config-Dx9ldgdf.cjs} +2 -16
  115. package/{mappings-config-7qQ47p9K.mjs → mappings-config-MoXBeI8p.mjs} +1 -15
  116. package/{namespace-probe-Ckn9yJ6F.d.mts → namespace-probe-9ScMDR0t.d.mts} +42 -2
  117. package/{namespace-probe-DaTJINS-.d.cts → namespace-probe-DC4tbmLj.d.cts} +42 -2
  118. package/package.json +1 -1
  119. package/{projection-hnPSgH6G.mjs → projection-CSGJbZlZ.mjs} +1 -1
  120. package/{projection-DEwL-D8R.cjs → projection-Dz6_i1-w.cjs} +0 -6
  121. package/{projection-sql-iHy-Cx6V.cjs → projection-sql-CbXCKt3s.cjs} +1 -1
  122. package/{projection-sql-9zzTVGvf.mjs → projection-sql-iHoiYr1R.mjs} +1 -1
  123. package/{sdk-w8Kv_XnW.cjs → sdk-B1vX4kUJ.cjs} +347 -17
  124. package/{sdk-Dkq6ELkf.mjs → sdk-D6gX0TXS.mjs} +347 -17
  125. package/sdk.cjs +2 -2
  126. package/sdk.d.cts +3 -3
  127. package/sdk.d.mts +3 -3
  128. package/sdk.mjs +2 -2
  129. package/{sync-engine-BLgwLpR6.cjs → sync-engine-B0FT1ax6.cjs} +1 -1
  130. package/{sync-engine-B_IzoUSx.mjs → sync-engine-CTSIcLlZ.mjs} +1 -1
  131. package/{sync-engine-BG2O1an2.d.cts → sync-engine-Df0kHAoJ.d.cts} +2 -2
  132. package/{sync-engine-DgQbrJON.d.mts → sync-engine-ugFacJ6G.d.mts} +2 -2
  133. package/{types-B1oRXdNP.d.cts → types-Cq1uHUKp.d.cts} +28 -2
  134. package/{types-B1oRXdNP.d.mts → types-Cq1uHUKp.d.mts} +28 -2
  135. package/types-Cq5gHCTq.cjs +20 -0
  136. package/types-YJTkXKzU.mjs +9 -0
  137. package/{view-compiler-D_6gfXFr.mjs → view-compiler-BzKk8nY3.mjs} +2 -2
  138. package/{view-compiler-CmEdAMwj.d.cts → view-compiler-D3zp9S3K.d.cts} +1 -1
  139. package/{view-compiler-Bo7pqXW3.d.mts → view-compiler-TpzDTu7z.d.mts} +1 -1
  140. package/{view-compiler-BHI5E4QI.cjs → view-compiler-ZKZbd3SK.cjs} +2 -2
  141. package/vite.d.cts +1 -1
  142. package/vite.d.mts +1 -1
  143. package/internal-BQ47iN6f.cjs +0 -259
  144. package/internal-ChufvXE6.mjs +0 -212
  145. package/internal-CvlFk3KC.d.mts +0 -180
  146. package/internal-DGUOLQAq.d.cts +0 -180
@@ -2137,6 +2137,23 @@ declare const ZodReadonly: $constructor<ZodReadonly>;
2137
2137
  */
2138
2138
  /** The reserved ref name for the projection's base table. */
2139
2139
  declare const BASE_REF = "base";
2140
+ /**
2141
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
2142
+ *
2143
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
2144
+ * on the server. Those two were widened apart: the server learned hyphens
2145
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
2146
+ * `clivly status` reports that form as canonical — but this pattern was missed,
2147
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
2148
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
2149
+ * opposite half of the product.
2150
+ *
2151
+ * Widening is the safe direction: every key that was valid before still is, so
2152
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
2153
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
2154
+ * and so is a join's `key` when it is emitted as an alias.
2155
+ */
2156
+ declare const PROJECTION_KEY_PATTERN: RegExp;
2140
2157
  declare const projectionRefSchema: ZodObject<{
2141
2158
  table: ZodString;
2142
2159
  column: ZodString;
@@ -2638,6 +2655,15 @@ declare function customObjectDeclarations(config: ClivlyEntitiesConfig): CustomO
2638
2655
  * Qualified rather than bare: two bindings can pull the same column name from
2639
2656
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
2640
2657
  * name cannot tell apart.
2658
+ *
2659
+ * This is the single derivation of that map, and both consumers depend on it
2660
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
2661
+ * entity's required `fields`; the cloud ingest path needs the same map because
2662
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
2663
+ * so `rowToSourceRow` derives the effective field map from the projection
2664
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
2665
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
2666
+ * Deriving this a second time anywhere would let the copies drift.
2641
2667
  */
2642
2668
  declare function projectionFields(projection: ProjectionMapping): Record<string, string>;
2643
2669
  declare function defineClivlyConfig(config: ClivlyEntitiesConfigInput, options?: {
@@ -2698,4 +2724,4 @@ interface ConfigValidationResult {
2698
2724
  */
2699
2725
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
2700
2726
  //#endregion
2701
- 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 };
2727
+ 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 };
@@ -2137,6 +2137,23 @@ declare const ZodReadonly: $constructor<ZodReadonly>;
2137
2137
  */
2138
2138
  /** The reserved ref name for the projection's base table. */
2139
2139
  declare const BASE_REF = "base";
2140
+ /**
2141
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
2142
+ *
2143
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
2144
+ * on the server. Those two were widened apart: the server learned hyphens
2145
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
2146
+ * `clivly status` reports that form as canonical — but this pattern was missed,
2147
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
2148
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
2149
+ * opposite half of the product.
2150
+ *
2151
+ * Widening is the safe direction: every key that was valid before still is, so
2152
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
2153
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
2154
+ * and so is a join's `key` when it is emitted as an alias.
2155
+ */
2156
+ declare const PROJECTION_KEY_PATTERN: RegExp;
2140
2157
  declare const projectionRefSchema: ZodObject<{
2141
2158
  table: ZodString;
2142
2159
  column: ZodString;
@@ -2638,6 +2655,15 @@ declare function customObjectDeclarations(config: ClivlyEntitiesConfig): CustomO
2638
2655
  * Qualified rather than bare: two bindings can pull the same column name from
2639
2656
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
2640
2657
  * name cannot tell apart.
2658
+ *
2659
+ * This is the single derivation of that map, and both consumers depend on it
2660
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
2661
+ * entity's required `fields`; the cloud ingest path needs the same map because
2662
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
2663
+ * so `rowToSourceRow` derives the effective field map from the projection
2664
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
2665
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
2666
+ * Deriving this a second time anywhere would let the copies drift.
2641
2667
  */
2642
2668
  declare function projectionFields(projection: ProjectionMapping): Record<string, string>;
2643
2669
  declare function defineClivlyConfig(config: ClivlyEntitiesConfigInput, options?: {
@@ -2698,4 +2724,4 @@ interface ConfigValidationResult {
2698
2724
  */
2699
2725
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
2700
2726
  //#endregion
2701
- 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 };
2727
+ 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,4 +1,4 @@
1
- const require_projection = require("./projection-DEwL-D8R.cjs");
1
+ const require_projection = require("./projection-Dz6_i1-w.cjs");
2
2
  //#region ../core/src/schema-meta.ts
3
3
  /**
4
4
  * Accessors over a discovered table's schema-v2 metadata.
@@ -29,7 +29,17 @@ function coversUniqueKey(table, columns) {
29
29
  const present = new Set(columns);
30
30
  return (table.uniqueConstraints ?? []).some((key) => key.length > 0 && key.every((column) => present.has(column)));
31
31
  }
32
- /** Is this table's payload rich enough to validate a projection against? */
32
+ /**
33
+ * Is this table's payload rich enough to validate a projection against?
34
+ *
35
+ * A type predicate rather than a plain boolean so the narrowing is visible to
36
+ * the compiler: callers that guard on this can iterate `columnsMeta` directly
37
+ * instead of casting, and a cast that outlives its guard becomes a type error
38
+ * rather than a silent walk over `undefined`.
39
+ *
40
+ * The invariant is unchanged: this reports whether the metadata is *known*,
41
+ * never whether the table has keys. Absent metadata stays UNKNOWN.
42
+ */
33
43
  function hasRichMeta(table) {
34
44
  return Boolean(table.columnsMeta && table.uniqueConstraints);
35
45
  }
@@ -137,7 +147,7 @@ function checkTablesExist(errors, projection, resolver, base) {
137
147
  if (!hasRichMeta(table)) {
138
148
  errors.push({
139
149
  path,
140
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
150
+ 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.`
141
151
  });
142
152
  ok = false;
143
153
  }
@@ -182,10 +192,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
182
192
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
183
193
  for (const ref of require_projection.identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
184
194
  }
195
+ /**
196
+ * Prove that a single-column identity (`column`, or `base_pk` with an
197
+ * explicit column) is actually unique.
198
+ *
199
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
200
+ * check in this file: a column not provably covered by a unique key is
201
+ * rejected, never assumed safe.
202
+ */
203
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
204
+ const table = resolver.tableFor(ref.table);
205
+ if (!table) return;
206
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
207
+ path,
208
+ 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`
209
+ });
210
+ }
185
211
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
186
212
  const path = `${base}.projection.identity`;
187
213
  const pk = primaryKeyColumns(baseTable);
188
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
214
+ const { identity } = projection;
215
+ if (identity.kind === "base_pk" && !identity.column) {
189
216
  if (pk.length === 0) {
190
217
  errors.push({
191
218
  path,
@@ -200,9 +227,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
200
227
  });
201
228
  return;
202
229
  }
230
+ return;
231
+ }
232
+ if (identity.kind === "base_pk" && identity.column) {
233
+ checkSingleColumnIdentity(errors, resolver, path, {
234
+ table: require_projection.BASE_REF,
235
+ column: identity.column
236
+ });
237
+ return;
203
238
  }
204
- if (projection.identity.kind !== "composite") return;
205
- for (const part of projection.identity.parts) {
239
+ if (identity.kind === "column") {
240
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
241
+ return;
242
+ }
243
+ if (identity.kind !== "composite") return;
244
+ for (const part of identity.parts) {
206
245
  const table = resolver.tableFor(part.table);
207
246
  const meta = table && columnMetaFor(table, part.column);
208
247
  if (!meta) continue;
@@ -244,6 +283,64 @@ function checkFanOut(errors, projection, resolver, base, basePk) {
244
283
  });
245
284
  }
246
285
  /**
286
+ * One error naming a filter column that is not on the projection's base table,
287
+ * and the base table it would have to be on.
288
+ */
289
+ function columnOnBase(column, baseTable, path) {
290
+ if (baseTable?.columns.includes(column)) return [];
291
+ return [{
292
+ path,
293
+ 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\``
294
+ }];
295
+ }
296
+ /**
297
+ * A derived object's filter is evaluated in the cloud against pushed rows, so
298
+ * it can only name columns of the base table.
299
+ *
300
+ * Every rejected shape here fails *silently* today, which is the whole reason
301
+ * the check exists — a filter that cannot be honoured must error, never quietly
302
+ * return the wrong rows. The three shapes fail in different directions:
303
+ *
304
+ * - `$raw` is opaque SQL meant for the embedded model. The cloud path cannot
305
+ * evaluate it and passes every row, so the filter is ignored and the object
306
+ * syncs UNFILTERED.
307
+ * - A joined-table column resolves to nothing on a pushed row, so under a
308
+ * positive operator (`eq`, `in`) no row matches and the object syncs EMPTY.
309
+ * - The same unresolvable column under a NEGATIVE operator (`ne`, `isNull`,
310
+ * `notIn`) is the dangerous one: the missing cell reads as null, so the
311
+ * predicate is true for every row and a filter meant to narrow the sync
312
+ * ADMITS EVERYTHING. Nobody notices extra rows arriving.
313
+ *
314
+ * Rejecting an unresolvable column closes the last two together.
315
+ */
316
+ function validateProjectionFilter(filter, projection, schema, base) {
317
+ if (!filter) return [];
318
+ const path = `${base}.filter`;
319
+ if ("$raw" in filter) return [{
320
+ path,
321
+ 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"
322
+ }];
323
+ const baseTable = schema.find((t) => t.name === projection.baseTable);
324
+ const errors = [];
325
+ for (const column of Object.keys(filter)) {
326
+ if (column.includes(".")) {
327
+ const [ref, ...rest] = column.split(".");
328
+ const bare = rest.join(".");
329
+ if (ref !== "base") {
330
+ errors.push({
331
+ path,
332
+ 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\``
333
+ });
334
+ continue;
335
+ }
336
+ errors.push(...columnOnBase(bare, baseTable, path));
337
+ continue;
338
+ }
339
+ errors.push(...columnOnBase(column, baseTable, path));
340
+ }
341
+ return errors;
342
+ }
343
+ /**
247
344
  * Validate a projection against the discovered schema. Returns every error
248
345
  * found (not just the first) so a builder UI can show them all at once.
249
346
  */
@@ -516,6 +613,15 @@ function customObjectDeclarations(config) {
516
613
  * Qualified rather than bare: two bindings can pull the same column name from
517
614
  * different tables (`cl.full_name` and `adv.full_name`), which a bare column
518
615
  * name cannot tell apart.
616
+ *
617
+ * This is the single derivation of that map, and both consumers depend on it
618
+ * emitting ref tokens. `mappingsToEntitiesConfig` populates a projection
619
+ * entity's required `fields`; the cloud ingest path needs the same map because
620
+ * a projection's persisted `crm_entity_mappings.field_map` is empty by design,
621
+ * so `rowToSourceRow` derives the effective field map from the projection
622
+ * instead. The tokens emitted here are exactly how `fromDrizzleProjection` keys
623
+ * the rows it pushes (`drizzle-projection.ts` `refToken`), so the two line up.
624
+ * Deriving this a second time anywhere would let the copies drift.
519
625
  */
520
626
  function projectionFields(projection) {
521
627
  const fields = {};
@@ -654,7 +760,7 @@ function validateEntity(v, config, key, entity, schema) {
654
760
  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`);
655
761
  for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
656
762
  validateReadOnly(v, base, entity);
657
- validateFilter(v, base, entity);
763
+ for (const error of validateProjectionFilter(entity.filter, entity.projection, schema, base)) v.report(error.path, error.message);
658
764
  return;
659
765
  }
660
766
  validateFields(v, base, entity);
@@ -800,3 +906,9 @@ Object.defineProperty(exports, "validateProjection", {
800
906
  return validateProjection;
801
907
  }
802
908
  });
909
+ Object.defineProperty(exports, "validateProjectionFilter", {
910
+ enumerable: true,
911
+ get: function() {
912
+ return validateProjectionFilter;
913
+ }
914
+ });
package/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const require_entity_config = require("./entity-config-DE30_RpU.cjs");
3
+ const require_entity_config = require("./entity-config-DgAe0_YD.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-w8Kv_XnW.cjs");
6
+ const require_sdk = require("./sdk-B1vX4kUJ.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -306,16 +306,27 @@ function addCommand(pm, packages) {
306
306
  */
307
307
  const CLIVLY_TICK_PATH = "/api/clivly/tick";
308
308
  /**
309
- * What `clivly login` writes into `.env` for `CLIVLY_SYNC_TRIGGER_URL`.
309
+ * The example shape `clivly init`/`clivly login` show for `CLIVLY_SYNC_TRIGGER_URL`
310
+ * — commented out in `.env`, not written as a live value.
310
311
  *
311
- * A visible template beats an empty line: the developer sees the shape they
312
- * eventually need — a public origin plus the tick path — instead of an unhelpful
313
- * `CLIVLY_SYNC_TRIGGER_URL=` they have to go and look up.
312
+ * It used to be scaffolded uncommented, on the theory that a visible template
313
+ * beats an empty line. The SDK's `isPlaceholderUrl` guard
314
+ * (`@clivly/sdk/trigger-url`) already keeps a bracketed placeholder like this
315
+ * one out of `syncTrigger.url` on every heartbeat — `resolveTriggerUrl` skips
316
+ * it in favour of the dev tunnel / platform-derived origin — and
317
+ * `assertSafeTriggerUrl` on the API side rejects it outright if it were ever
318
+ * sent, since `<yourDomain.com>` doesn't even parse as a URL. So it was never
319
+ * actually fireable (F-12 is not an SSRF/overwrite bug).
314
320
  *
315
- * It is safe to ship a truthy non-address here only because the SDK refuses to
316
- * treat a bracketed value as configured (`isPlaceholderUrl` in
317
- * `@clivly/sdk/trigger-url`). Change the brackets here and that guard stops
318
- * matching, so the two must stay in step.
321
+ * What it DID break: readiness reporting only checked *presence*, not
322
+ * validity, so an unedited placeholder made the dashboard's setup ladder and
323
+ * `triggerConfigured` report "done" while `clivly status` correctly flagged
324
+ * it — two surfaces disagreeing about a workspace that could never run "Run
325
+ * sync". See `hasWorkingTrigger` (`@clivly.com/api/sync-trigger`) for the
326
+ * fix. Commenting the example out here removes the confusing truthy-but-fake
327
+ * value at the source, on top of that fix, while keeping the shape visible
328
+ * for the (uncommon) case a developer needs it — most platforms are
329
+ * auto-detected, and `clivly dev` handles local development.
319
330
  */
320
331
  const TRIGGER_URL_PLACEHOLDER = `https://<yourDomain.com>${CLIVLY_TICK_PATH}`;
321
332
  //#endregion
@@ -1440,8 +1451,8 @@ function credentialEntries(credentials) {
1440
1451
  },
1441
1452
  {
1442
1453
  key: "CLIVLY_SYNC_TRIGGER_URL",
1443
- value: TRIGGER_URL_PLACEHOLDER,
1444
- comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1454
+ value: "",
1455
+ comment: `Public URL of your tick route. Only needed when the platform's own URL can't be auto-detected (see \`clivly status\`). Uncomment and set your deployed origin if so:\n# CLIVLY_SYNC_TRIGGER_URL=${TRIGGER_URL_PLACEHOLDER}`
1445
1456
  }
1446
1457
  ];
1447
1458
  }
@@ -1810,8 +1821,7 @@ const CLIVLY_ENV_KEYS = [
1810
1821
  },
1811
1822
  {
1812
1823
  key: "CLIVLY_SYNC_TRIGGER_URL",
1813
- value: TRIGGER_URL_PLACEHOLDER,
1814
- comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1824
+ comment: `Public URL of your tick route. Only needed when the platform's own URL can't be auto-detected (see \`clivly status\`). Uncomment and set your deployed origin if so:\n# CLIVLY_SYNC_TRIGGER_URL=${TRIGGER_URL_PLACEHOLDER}`
1815
1825
  },
1816
1826
  {
1817
1827
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
@@ -1871,6 +1881,7 @@ function renderImports(plan) {
1871
1881
  "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
1872
1882
  "import { fromDrizzle } from \"clivly/drizzle\";",
1873
1883
  "import { discoverFromDrizzle } from \"clivly/drizzle\";",
1884
+ "import { drizzleIntrospector } from \"clivly/drizzle\";",
1874
1885
  `import * as schema from "${plan.importPath}";`
1875
1886
  ];
1876
1887
  if (plan.db === null) {
@@ -1903,6 +1914,9 @@ export default createClivlySDK({
1903
1914
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
1904
1915
  // all when you add a mapping — not just the ones wired up as sources below.
1905
1916
  schema: discoverFromDrizzle(schema),
1917
+ // Lets derived objects authored in the dashboard run against this database.
1918
+ // Without it they are fetched, validated and then skipped.
1919
+ introspector: drizzleIntrospector(${dbExpr}),
1906
1920
  source: {
1907
1921
  // Keyed by CRM concept — contacts/companies/deals/custom.
1908
1922
  // NOT by your entity key. The entity's own \`source:\` above is a table name.
@@ -3201,6 +3215,32 @@ const STATUS_STAGES = [
3201
3215
  }
3202
3216
  },
3203
3217
  sdkChecksStage,
3218
+ {
3219
+ id: "derived-objects",
3220
+ label: "Dashboard-authored objects",
3221
+ needs: [configStage.id],
3222
+ run(ctx) {
3223
+ const remote = ctx.sdk?.describeRemoteProjections?.();
3224
+ if (!remote) return Promise.resolve({
3225
+ state: "skip",
3226
+ detail: "This app's installed clivly predates dashboard-authored objects."
3227
+ });
3228
+ if (!remote.capable) return Promise.resolve({
3229
+ state: "warn",
3230
+ detail: "Objects authored in the dashboard cannot run against this app.",
3231
+ hint: "Add `introspector: drizzleIntrospector(db)` to clivly.config.ts."
3232
+ });
3233
+ if (!remote.ran) return Promise.resolve({
3234
+ state: "pass",
3235
+ detail: "Ready; no sync has run in this process yet."
3236
+ });
3237
+ const broken = remote.invalid + remote.failed;
3238
+ return Promise.resolve({
3239
+ state: broken > 0 ? "warn" : "pass",
3240
+ detail: broken > 0 ? `${remote.ok} running, ${broken} not — open the object in Clivly for the reason.` : `${remote.ok} running.`
3241
+ });
3242
+ }
3243
+ },
3204
3244
  triggerUrlStage
3205
3245
  ];
3206
3246
  function defaultLoadSdk(path) {
@@ -3254,7 +3294,8 @@ const ALIAS_STAGE_IDS = {
3254
3294
  "config",
3255
3295
  "runtime",
3256
3296
  "boot-hook",
3257
- "setup-checks"
3297
+ "setup-checks",
3298
+ "derived-objects"
3258
3299
  ],
3259
3300
  connect: [
3260
3301
  "api-key",
package/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { t as CANONICAL_FIELDS } from "./entity-config-CusbTOJ1.mjs";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-C4d3K9CP.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
4
  import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-Dkq6ELkf.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-D6gX0TXS.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
@@ -305,16 +305,27 @@ function addCommand(pm, packages) {
305
305
  */
306
306
  const CLIVLY_TICK_PATH = "/api/clivly/tick";
307
307
  /**
308
- * What `clivly login` writes into `.env` for `CLIVLY_SYNC_TRIGGER_URL`.
308
+ * The example shape `clivly init`/`clivly login` show for `CLIVLY_SYNC_TRIGGER_URL`
309
+ * — commented out in `.env`, not written as a live value.
309
310
  *
310
- * A visible template beats an empty line: the developer sees the shape they
311
- * eventually need — a public origin plus the tick path — instead of an unhelpful
312
- * `CLIVLY_SYNC_TRIGGER_URL=` they have to go and look up.
311
+ * It used to be scaffolded uncommented, on the theory that a visible template
312
+ * beats an empty line. The SDK's `isPlaceholderUrl` guard
313
+ * (`@clivly/sdk/trigger-url`) already keeps a bracketed placeholder like this
314
+ * one out of `syncTrigger.url` on every heartbeat — `resolveTriggerUrl` skips
315
+ * it in favour of the dev tunnel / platform-derived origin — and
316
+ * `assertSafeTriggerUrl` on the API side rejects it outright if it were ever
317
+ * sent, since `<yourDomain.com>` doesn't even parse as a URL. So it was never
318
+ * actually fireable (F-12 is not an SSRF/overwrite bug).
313
319
  *
314
- * It is safe to ship a truthy non-address here only because the SDK refuses to
315
- * treat a bracketed value as configured (`isPlaceholderUrl` in
316
- * `@clivly/sdk/trigger-url`). Change the brackets here and that guard stops
317
- * matching, so the two must stay in step.
320
+ * What it DID break: readiness reporting only checked *presence*, not
321
+ * validity, so an unedited placeholder made the dashboard's setup ladder and
322
+ * `triggerConfigured` report "done" while `clivly status` correctly flagged
323
+ * it — two surfaces disagreeing about a workspace that could never run "Run
324
+ * sync". See `hasWorkingTrigger` (`@clivly.com/api/sync-trigger`) for the
325
+ * fix. Commenting the example out here removes the confusing truthy-but-fake
326
+ * value at the source, on top of that fix, while keeping the shape visible
327
+ * for the (uncommon) case a developer needs it — most platforms are
328
+ * auto-detected, and `clivly dev` handles local development.
318
329
  */
319
330
  const TRIGGER_URL_PLACEHOLDER = `https://<yourDomain.com>${CLIVLY_TICK_PATH}`;
320
331
  //#endregion
@@ -1439,8 +1450,8 @@ function credentialEntries(credentials) {
1439
1450
  },
1440
1451
  {
1441
1452
  key: "CLIVLY_SYNC_TRIGGER_URL",
1442
- value: TRIGGER_URL_PLACEHOLDER,
1443
- comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1453
+ value: "",
1454
+ comment: `Public URL of your tick route. Only needed when the platform's own URL can't be auto-detected (see \`clivly status\`). Uncomment and set your deployed origin if so:\n# CLIVLY_SYNC_TRIGGER_URL=${TRIGGER_URL_PLACEHOLDER}`
1444
1455
  }
1445
1456
  ];
1446
1457
  }
@@ -1809,8 +1820,7 @@ const CLIVLY_ENV_KEYS = [
1809
1820
  },
1810
1821
  {
1811
1822
  key: "CLIVLY_SYNC_TRIGGER_URL",
1812
- value: TRIGGER_URL_PLACEHOLDER,
1813
- comment: "Public URL of your tick route — replace <yourDomain.com> once deployed. Local dev: run `clivly dev` instead."
1823
+ comment: `Public URL of your tick route. Only needed when the platform's own URL can't be auto-detected (see \`clivly status\`). Uncomment and set your deployed origin if so:\n# CLIVLY_SYNC_TRIGGER_URL=${TRIGGER_URL_PLACEHOLDER}`
1814
1824
  },
1815
1825
  {
1816
1826
  key: "CLIVLY_SYNC_TRIGGER_SECRET",
@@ -1870,6 +1880,7 @@ function renderImports(plan) {
1870
1880
  "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
1871
1881
  "import { fromDrizzle } from \"clivly/drizzle\";",
1872
1882
  "import { discoverFromDrizzle } from \"clivly/drizzle\";",
1883
+ "import { drizzleIntrospector } from \"clivly/drizzle\";",
1873
1884
  `import * as schema from "${plan.importPath}";`
1874
1885
  ];
1875
1886
  if (plan.db === null) {
@@ -1902,6 +1913,9 @@ export default createClivlySDK({
1902
1913
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
1903
1914
  // all when you add a mapping — not just the ones wired up as sources below.
1904
1915
  schema: discoverFromDrizzle(schema),
1916
+ // Lets derived objects authored in the dashboard run against this database.
1917
+ // Without it they are fetched, validated and then skipped.
1918
+ introspector: drizzleIntrospector(${dbExpr}),
1905
1919
  source: {
1906
1920
  // Keyed by CRM concept — contacts/companies/deals/custom.
1907
1921
  // NOT by your entity key. The entity's own \`source:\` above is a table name.
@@ -3200,6 +3214,32 @@ const STATUS_STAGES = [
3200
3214
  }
3201
3215
  },
3202
3216
  sdkChecksStage,
3217
+ {
3218
+ id: "derived-objects",
3219
+ label: "Dashboard-authored objects",
3220
+ needs: [configStage.id],
3221
+ run(ctx) {
3222
+ const remote = ctx.sdk?.describeRemoteProjections?.();
3223
+ if (!remote) return Promise.resolve({
3224
+ state: "skip",
3225
+ detail: "This app's installed clivly predates dashboard-authored objects."
3226
+ });
3227
+ if (!remote.capable) return Promise.resolve({
3228
+ state: "warn",
3229
+ detail: "Objects authored in the dashboard cannot run against this app.",
3230
+ hint: "Add `introspector: drizzleIntrospector(db)` to clivly.config.ts."
3231
+ });
3232
+ if (!remote.ran) return Promise.resolve({
3233
+ state: "pass",
3234
+ detail: "Ready; no sync has run in this process yet."
3235
+ });
3236
+ const broken = remote.invalid + remote.failed;
3237
+ return Promise.resolve({
3238
+ state: broken > 0 ? "warn" : "pass",
3239
+ detail: broken > 0 ? `${remote.ok} running, ${broken} not — open the object in Clivly for the reason.` : `${remote.ok} running.`
3240
+ });
3241
+ }
3242
+ },
3203
3243
  triggerUrlStage
3204
3244
  ];
3205
3245
  function defaultLoadSdk(path) {
@@ -3253,7 +3293,8 @@ const ALIAS_STAGE_IDS = {
3253
3293
  "config",
3254
3294
  "runtime",
3255
3295
  "boot-hook",
3256
- "setup-checks"
3296
+ "setup-checks",
3297
+ "derived-objects"
3257
3298
  ],
3258
3299
  connect: [
3259
3300
  "api-key",