clivly 0.9.5 → 0.9.7

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 (78) hide show
  1. package/README.md +22 -3
  2. package/core-auth-adapter.cjs +1 -1
  3. package/core-auth-adapter.mjs +1 -1
  4. package/core-drizzle.cjs +1 -1
  5. package/core-drizzle.d.cts +1 -1
  6. package/core-drizzle.d.mts +1 -1
  7. package/core-drizzle.mjs +1 -1
  8. package/core-entity-config.cjs +1 -1
  9. package/core-entity-config.mjs +1 -1
  10. package/core-mapping-form.cjs +1 -1
  11. package/core-mapping-form.mjs +1 -1
  12. package/core-mappings-config.cjs +1 -1
  13. package/core-mappings-config.mjs +1 -1
  14. package/core-sync-engine.cjs +1 -1
  15. package/core-sync-engine.mjs +1 -1
  16. package/core-view-compiler.cjs +1 -1
  17. package/core-view-compiler.mjs +1 -1
  18. package/core.cjs +14 -9
  19. package/core.d.cts +2 -2
  20. package/core.d.mts +2 -2
  21. package/core.mjs +9 -9
  22. package/{drizzle-CiK8MMEZ.d.mts → drizzle-BmDDgAIF.d.mts} +3 -1
  23. package/{drizzle-bfjVzYR6.cjs → drizzle-DcB7OfBp.cjs} +9 -1
  24. package/{drizzle-DEtRrqtc.d.cts → drizzle-ixVV4pPN.d.cts} +3 -1
  25. package/{drizzle-3kvhkwO7.mjs → drizzle-wbcuG-cy.mjs} +9 -1
  26. package/drizzle.cjs +61 -10
  27. package/drizzle.d.cts +9 -3
  28. package/drizzle.d.mts +9 -3
  29. package/drizzle.mjs +61 -10
  30. package/{entity-config-B74SPOUZ.mjs → entity-config-C8IIkrmX.mjs} +34 -5
  31. package/{entity-config-7sGCk_nw.cjs → entity-config-CPM-Tp-d.cjs} +34 -5
  32. package/index.cjs +41 -9
  33. package/index.mjs +41 -9
  34. package/internal-BLKMnIIZ.cjs +259 -0
  35. package/internal-BpVh_J77.mjs +212 -0
  36. package/{internal-o2M7FGlR.d.mts → internal-CvlFk3KC.d.mts} +82 -2
  37. package/{internal-wCXSELDN.d.cts → internal-DGUOLQAq.d.cts} +82 -2
  38. package/internal-drizzle.cjs +1 -1
  39. package/internal-drizzle.d.cts +1 -1
  40. package/internal-drizzle.d.mts +1 -1
  41. package/internal-drizzle.mjs +1 -1
  42. package/internal-entity-config.cjs +1 -1
  43. package/internal-entity-config.mjs +1 -1
  44. package/internal-entity-heuristics.cjs +1 -1
  45. package/internal-entity-heuristics.mjs +1 -1
  46. package/internal-mappings-config.cjs +1 -1
  47. package/internal-mappings-config.mjs +1 -1
  48. package/internal-sync-engine.cjs +1 -1
  49. package/internal-sync-engine.mjs +1 -1
  50. package/internal-view-compiler.cjs +1 -1
  51. package/internal-view-compiler.mjs +1 -1
  52. package/internal.cjs +13 -7
  53. package/internal.d.cts +2 -2
  54. package/internal.d.mts +2 -2
  55. package/internal.mjs +8 -8
  56. package/{mappings-config-CLyEg0sf.cjs → mappings-config-CBwL3FVh.cjs} +1 -1
  57. package/{mappings-config-DQS_s4dg.mjs → mappings-config-DUKBe6OS.mjs} +1 -1
  58. package/{namespace-probe-Cn7voAd0.d.mts → namespace-probe-BQ4imnV7.d.mts} +85 -2
  59. package/{namespace-probe-BoGI18dQ.d.cts → namespace-probe-DZIGdvlm.d.cts} +85 -2
  60. package/package.json +1 -1
  61. package/{projection-Dz6_i1-w.cjs → projection-DEwL-D8R.cjs} +6 -0
  62. package/{projection-CSGJbZlZ.mjs → projection-hnPSgH6G.mjs} +1 -1
  63. package/{projection-sql-BgnfiGqF.mjs → projection-sql-9zzTVGvf.mjs} +34 -2
  64. package/{projection-sql-Cl1qIn8m.cjs → projection-sql-iHy-Cx6V.cjs} +45 -1
  65. package/{sdk-CM1UO-I7.cjs → sdk-BL9BCzyD.cjs} +354 -20
  66. package/{sdk-DYBDR939.mjs → sdk-CubnsyD9.mjs} +354 -20
  67. package/sdk.cjs +2 -2
  68. package/sdk.d.cts +2 -2
  69. package/sdk.d.mts +2 -2
  70. package/sdk.mjs +2 -2
  71. package/{sync-engine-C4A5jgA4.cjs → sync-engine-BLgwLpR6.cjs} +1 -1
  72. package/{sync-engine-Dt5QQSKO.mjs → sync-engine-B_IzoUSx.mjs} +1 -1
  73. package/{view-compiler-B5nDal14.cjs → view-compiler-BHI5E4QI.cjs} +2 -2
  74. package/{view-compiler-CSYwEWf5.mjs → view-compiler-D_6gfXFr.mjs} +2 -2
  75. package/vite.d.cts +1 -1
  76. package/vite.d.mts +1 -1
  77. package/internal-CykXpEGk.mjs +0 -68
  78. package/internal-DqduZU3l.cjs +0 -97
package/drizzle.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { t as BASE_REF } from "./projection-CSGJbZlZ.mjs";
1
+ import { t as BASE_REF } from "./projection-hnPSgH6G.mjs";
2
2
  import { t as DRIZZLE_META } from "./drizzle-meta-D-5k3n0i.mjs";
3
- import { a as quoteIdent, n as compileProjectionIdentity, r as compileProjectionJoins, t as compileProjectionFrom } from "./projection-sql-BgnfiGqF.mjs";
4
- import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-3kvhkwO7.mjs";
3
+ import { a as compileProjectionPreview, i as compileProjectionJoins, n as compileProjectionFrom, r as compileProjectionIdentity, s as quoteIdent } from "./projection-sql-9zzTVGvf.mjs";
4
+ import { n as buildUniqueConstraints, r as discoverFromDrizzle, t as buildColumnsMeta } from "./drizzle-wbcuG-cy.mjs";
5
5
  import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
6
6
  //#region ../sdk/src/drizzle-projection.ts
7
7
  /** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
@@ -39,10 +39,12 @@ function fromDrizzleProjection(db, projection, opts) {
39
39
  const client = db;
40
40
  const known = /* @__PURE__ */ new Set([BASE_REF, ...projection.joins.map((j) => j.key)]);
41
41
  if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
42
- const cursorRef = parseRef(opts.cursorField);
43
- assertKnownRef(cursorRef, known, "cursorField");
44
42
  for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
45
43
  const identityExpr = compileProjectionIdentity(projection, opts.basePkColumn ?? "");
44
+ const fromClause = [compileProjectionFrom(projection), ...compileProjectionJoins(projection)].join(" ");
45
+ if (opts.cursorField === void 0) return fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromClause, opts);
46
+ const cursorRef = parseRef(opts.cursorField);
47
+ assertKnownRef(cursorRef, known, "cursorField");
46
48
  const cursorToken = refToken(cursorRef);
47
49
  const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
48
50
  const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
@@ -54,11 +56,7 @@ function fromDrizzleProjection(db, projection, opts) {
54
56
  emitted.add(token);
55
57
  selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(token)}`);
56
58
  }
57
- const head = [
58
- `select ${selectItems.join(", ")}`,
59
- compileProjectionFrom(projection),
60
- ...compileProjectionJoins(projection)
61
- ].join(" ");
59
+ const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
62
60
  return {
63
61
  entity: opts.entity,
64
62
  cursorField: cursorToken,
@@ -71,6 +69,49 @@ function fromDrizzleProjection(db, projection, opts) {
71
69
  }
72
70
  };
73
71
  }
72
+ /**
73
+ * The identity-only paging path `fromDrizzleProjection` takes when
74
+ * `cursorField` is omitted. Deliberately not a generalisation of the
75
+ * timestamp path above — it selects no timestamp cursor, pages on the
76
+ * identity expression alone, and always reports `cursorField`/`idField` as
77
+ * `"id"`, the alias every row carries either way.
78
+ */
79
+ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromClause, opts) {
80
+ const selectItems = [`${identityExpr} AS ${quoteIdent("id")}`];
81
+ const emitted = /* @__PURE__ */ new Set();
82
+ for (const binding of Object.values(projection.fieldBindings)) {
83
+ const token = refToken(binding.from);
84
+ if (emitted.has(token)) continue;
85
+ emitted.add(token);
86
+ selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(token)}`);
87
+ }
88
+ const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
89
+ return {
90
+ entity: opts.entity,
91
+ cursorField: "id",
92
+ idField: "id",
93
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
94
+ fetchPage: async ({ since, limit }) => {
95
+ const where = since ? sql` where ${sql.raw(identityExpr)} > ${since.id}` : sql``;
96
+ const query = sql`${sql.raw(head)}${where} order by ${sql.raw(identityExpr)} asc limit ${limit}`;
97
+ return normalizeRows(await client.execute(query));
98
+ }
99
+ };
100
+ }
101
+ //#endregion
102
+ //#region ../sdk/src/projection-preview.ts
103
+ /** `db.execute` returns either an array or `{ rows }`, depending on driver. */
104
+ function rowsOf(result) {
105
+ if (Array.isArray(result)) return result;
106
+ const rows = result.rows;
107
+ if (Array.isArray(rows)) return rows;
108
+ throw new Error("previewProjection: db.execute() returned neither an array of rows nor { rows }.");
109
+ }
110
+ async function previewProjection(db, projection, opts) {
111
+ const client = db;
112
+ const { sql } = await import("drizzle-orm");
113
+ return rowsOf(await client.execute(sql.raw(compileProjectionPreview(projection, opts.limit))));
114
+ }
74
115
  //#endregion
75
116
  //#region ../sdk/src/drizzle.ts
76
117
  function inferIdField(table) {
@@ -134,6 +175,16 @@ function drizzleIntrospector(db) {
134
175
  },
135
176
  async sampleSelect(table) {
136
177
  await client.execute(sql`select 1 from ${sql.identifier(table)} limit 1`);
178
+ },
179
+ runProjection(projection, opts) {
180
+ return previewProjection(db, projection, opts);
181
+ },
182
+ projectionSource(projection, opts) {
183
+ return fromDrizzleProjection(db, projection, {
184
+ entity: opts.objectType,
185
+ objectType: opts.objectType,
186
+ ...opts.basePkColumn ? { basePkColumn: opts.basePkColumn } : {}
187
+ });
137
188
  }
138
189
  };
139
190
  }
@@ -1,4 +1,4 @@
1
- import { a as _enum, c as literal, d as strictObject, f as string, i as projectionSchema, l as number, o as array, p as union, r as identityRefs, s as boolean, t as BASE_REF, u as record } from "./projection-CSGJbZlZ.mjs";
1
+ import { a as projectionSchema, c as boolean, d as record, f as strictObject, i as identityRefs, l as literal, m as union, o as _enum, p as string, s as array, t as BASE_REF, u as number } from "./projection-hnPSgH6G.mjs";
2
2
  //#region ../core/src/schema-meta.ts
3
3
  /**
4
4
  * Accessors over a discovered table's schema-v2 metadata.
@@ -137,7 +137,7 @@ function checkTablesExist(errors, projection, resolver, base) {
137
137
  if (!hasRichMeta(table)) {
138
138
  errors.push({
139
139
  path,
140
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
140
+ 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
141
  });
142
142
  ok = false;
143
143
  }
@@ -182,10 +182,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
182
182
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
183
183
  for (const ref of identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
184
184
  }
185
+ /**
186
+ * Prove that a single-column identity (`column`, or `base_pk` with an
187
+ * explicit column) is actually unique.
188
+ *
189
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
190
+ * check in this file: a column not provably covered by a unique key is
191
+ * rejected, never assumed safe.
192
+ */
193
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
194
+ const table = resolver.tableFor(ref.table);
195
+ if (!table) return;
196
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
197
+ path,
198
+ 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`
199
+ });
200
+ }
185
201
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
186
202
  const path = `${base}.projection.identity`;
187
203
  const pk = primaryKeyColumns(baseTable);
188
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
204
+ const { identity } = projection;
205
+ if (identity.kind === "base_pk" && !identity.column) {
189
206
  if (pk.length === 0) {
190
207
  errors.push({
191
208
  path,
@@ -200,9 +217,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
200
217
  });
201
218
  return;
202
219
  }
220
+ return;
221
+ }
222
+ if (identity.kind === "base_pk" && identity.column) {
223
+ checkSingleColumnIdentity(errors, resolver, path, {
224
+ table: BASE_REF,
225
+ column: identity.column
226
+ });
227
+ return;
228
+ }
229
+ if (identity.kind === "column") {
230
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
231
+ return;
203
232
  }
204
- if (projection.identity.kind !== "composite") return;
205
- for (const part of projection.identity.parts) {
233
+ if (identity.kind !== "composite") return;
234
+ for (const part of identity.parts) {
206
235
  const table = resolver.tableFor(part.table);
207
236
  const meta = table && columnMetaFor(table, part.column);
208
237
  if (!meta) continue;
@@ -1,4 +1,4 @@
1
- const require_projection = require("./projection-Dz6_i1-w.cjs");
1
+ const require_projection = require("./projection-DEwL-D8R.cjs");
2
2
  //#region ../core/src/schema-meta.ts
3
3
  /**
4
4
  * Accessors over a discovered table's schema-v2 metadata.
@@ -137,7 +137,7 @@ function checkTablesExist(errors, projection, resolver, base) {
137
137
  if (!hasRichMeta(table)) {
138
138
  errors.push({
139
139
  path,
140
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
140
+ 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
141
  });
142
142
  ok = false;
143
143
  }
@@ -182,10 +182,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
182
182
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
183
183
  for (const ref of require_projection.identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
184
184
  }
185
+ /**
186
+ * Prove that a single-column identity (`column`, or `base_pk` with an
187
+ * explicit column) is actually unique.
188
+ *
189
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
190
+ * check in this file: a column not provably covered by a unique key is
191
+ * rejected, never assumed safe.
192
+ */
193
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
194
+ const table = resolver.tableFor(ref.table);
195
+ if (!table) return;
196
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
197
+ path,
198
+ 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`
199
+ });
200
+ }
185
201
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
186
202
  const path = `${base}.projection.identity`;
187
203
  const pk = primaryKeyColumns(baseTable);
188
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
204
+ const { identity } = projection;
205
+ if (identity.kind === "base_pk" && !identity.column) {
189
206
  if (pk.length === 0) {
190
207
  errors.push({
191
208
  path,
@@ -200,9 +217,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
200
217
  });
201
218
  return;
202
219
  }
220
+ return;
221
+ }
222
+ if (identity.kind === "base_pk" && identity.column) {
223
+ checkSingleColumnIdentity(errors, resolver, path, {
224
+ table: require_projection.BASE_REF,
225
+ column: identity.column
226
+ });
227
+ return;
228
+ }
229
+ if (identity.kind === "column") {
230
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
231
+ return;
203
232
  }
204
- if (projection.identity.kind !== "composite") return;
205
- for (const part of projection.identity.parts) {
233
+ if (identity.kind !== "composite") return;
234
+ for (const part of identity.parts) {
206
235
  const table = resolver.tableFor(part.table);
207
236
  const meta = table && columnMetaFor(table, part.column);
208
237
  if (!meta) continue;
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-7sGCk_nw.cjs");
3
+ const require_entity_config = require("./entity-config-CPM-Tp-d.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-CM1UO-I7.cjs");
6
+ const require_sdk = require("./sdk-BL9BCzyD.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");
@@ -1596,7 +1596,7 @@ function planInit(schema, overrides = {}) {
1596
1596
  todos.push(...candidateTodos);
1597
1597
  company = candidate;
1598
1598
  if (require_internal_entity_heuristics.matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
1599
- } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain.`);
1599
+ } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain, or build a derived object from it in Clivly (Integrations → Map as custom object) if the fields live in a joined table.`);
1600
1600
  }
1601
1601
  if (contactTable && contact) {
1602
1602
  const scored = require_internal_entity_heuristics.scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
@@ -1871,6 +1871,7 @@ function renderImports(plan) {
1871
1871
  "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
1872
1872
  "import { fromDrizzle } from \"clivly/drizzle\";",
1873
1873
  "import { discoverFromDrizzle } from \"clivly/drizzle\";",
1874
+ "import { drizzleIntrospector } from \"clivly/drizzle\";",
1874
1875
  `import * as schema from "${plan.importPath}";`
1875
1876
  ];
1876
1877
  if (plan.db === null) {
@@ -1903,6 +1904,9 @@ export default createClivlySDK({
1903
1904
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
1904
1905
  // all when you add a mapping — not just the ones wired up as sources below.
1905
1906
  schema: discoverFromDrizzle(schema),
1907
+ // Lets derived objects authored in the dashboard run against this database.
1908
+ // Without it they are fetched, validated and then skipped.
1909
+ introspector: drizzleIntrospector(${dbExpr}),
1906
1910
  source: {
1907
1911
  // Keyed by CRM concept — contacts/companies/deals/custom.
1908
1912
  // NOT by your entity key. The entity's own \`source:\` above is a table name.
@@ -3124,15 +3128,16 @@ const triggerUrlStage = {
3124
3128
  label: "Sync trigger URL",
3125
3129
  needs: [configStage.id],
3126
3130
  run(ctx) {
3127
- const url = ctx.sdk?.syncTrigger?.url;
3128
- if (url && !require_sdk.isPlaceholderUrl(url)) return Promise.resolve({
3131
+ const trigger = ctx.sdk?.describeSyncTrigger?.();
3132
+ if (trigger?.resolved) return Promise.resolve({
3129
3133
  state: "pass",
3130
- detail: url
3134
+ detail: trigger.resolved
3131
3135
  });
3132
- const isPlaceholder = url !== void 0;
3136
+ const configured = trigger?.configured;
3137
+ const isPlaceholder = configured !== void 0 && require_sdk.isPlaceholderUrl(configured);
3133
3138
  return Promise.resolve({
3134
3139
  state: "warn",
3135
- detail: isPlaceholder ? `Still the placeholder (${url}) — remote sync cannot call back into this app.` : "Not set — remote sync cannot call back into this app.",
3140
+ detail: isPlaceholder ? `Still the scaffolded placeholder (${configured}) — remote sync cannot call back into this app.` : "Not set — remote sync cannot call back into this app.",
3136
3141
  hint: "Replace <yourDomain.com> with your deployed origin, or delete the line — on Vercel, Cloudflare Pages, Railway, Render and Fly the SDK derives the URL from the platform. For local development run `clivly dev`: it opens a tunnel and registers the URL for you, outranking whatever is in .env. Push-only setups can ignore this."
3137
3142
  });
3138
3143
  }
@@ -3200,6 +3205,32 @@ const STATUS_STAGES = [
3200
3205
  }
3201
3206
  },
3202
3207
  sdkChecksStage,
3208
+ {
3209
+ id: "derived-objects",
3210
+ label: "Dashboard-authored objects",
3211
+ needs: [configStage.id],
3212
+ run(ctx) {
3213
+ const remote = ctx.sdk?.describeRemoteProjections?.();
3214
+ if (!remote) return Promise.resolve({
3215
+ state: "skip",
3216
+ detail: "This app's installed clivly predates dashboard-authored objects."
3217
+ });
3218
+ if (!remote.capable) return Promise.resolve({
3219
+ state: "warn",
3220
+ detail: "Objects authored in the dashboard cannot run against this app.",
3221
+ hint: "Add `introspector: drizzleIntrospector(db)` to clivly.config.ts."
3222
+ });
3223
+ if (!remote.ran) return Promise.resolve({
3224
+ state: "pass",
3225
+ detail: "Ready; no sync has run in this process yet."
3226
+ });
3227
+ const broken = remote.invalid + remote.failed;
3228
+ return Promise.resolve({
3229
+ state: broken > 0 ? "warn" : "pass",
3230
+ detail: broken > 0 ? `${remote.ok} running, ${broken} not — open the object in Clivly for the reason.` : `${remote.ok} running.`
3231
+ });
3232
+ }
3233
+ },
3203
3234
  triggerUrlStage
3204
3235
  ];
3205
3236
  function defaultLoadSdk(path) {
@@ -3253,7 +3284,8 @@ const ALIAS_STAGE_IDS = {
3253
3284
  "config",
3254
3285
  "runtime",
3255
3286
  "boot-hook",
3256
- "setup-checks"
3287
+ "setup-checks",
3288
+ "derived-objects"
3257
3289
  ],
3258
3290
  connect: [
3259
3291
  "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-B74SPOUZ.mjs";
2
+ import { t as CANONICAL_FIELDS } from "./entity-config-C8IIkrmX.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-DYBDR939.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-CubnsyD9.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";
@@ -1595,7 +1595,7 @@ function planInit(schema, overrides = {}) {
1595
1595
  todos.push(...candidateTodos);
1596
1596
  company = candidate;
1597
1597
  if (matchesConcept(companyTable.tableName, "contact")) todos.push(`"${companyTable.tableName}" matches both contact and company naming — assigned to company. Swap it to your contact entity if that's wrong.`);
1598
- } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain.`);
1598
+ } else todos.push(`"${companyTable.tableName}" looked like a company but no fields could be mapped — left it out. Add it to \`entities\` once you know which columns map to name/domain, or build a derived object from it in Clivly (Integrations → Map as custom object) if the fields live in a joined table.`);
1599
1599
  }
1600
1600
  if (contactTable && contact) {
1601
1601
  const scored = scoreRelationships(toScoredTable(contactTable), schema.tables.map(toScoredTable));
@@ -1870,6 +1870,7 @@ function renderImports(plan) {
1870
1870
  "import { createClivlySDK, defineClivlyConfig } from \"clivly/sdk\";",
1871
1871
  "import { fromDrizzle } from \"clivly/drizzle\";",
1872
1872
  "import { discoverFromDrizzle } from \"clivly/drizzle\";",
1873
+ "import { drizzleIntrospector } from \"clivly/drizzle\";",
1873
1874
  `import * as schema from "${plan.importPath}";`
1874
1875
  ];
1875
1876
  if (plan.db === null) {
@@ -1902,6 +1903,9 @@ export default createClivlySDK({
1902
1903
  // Every table in your Drizzle schema, so the Clivly dashboard can offer them
1903
1904
  // all when you add a mapping — not just the ones wired up as sources below.
1904
1905
  schema: discoverFromDrizzle(schema),
1906
+ // Lets derived objects authored in the dashboard run against this database.
1907
+ // Without it they are fetched, validated and then skipped.
1908
+ introspector: drizzleIntrospector(${dbExpr}),
1905
1909
  source: {
1906
1910
  // Keyed by CRM concept — contacts/companies/deals/custom.
1907
1911
  // NOT by your entity key. The entity's own \`source:\` above is a table name.
@@ -3123,15 +3127,16 @@ const triggerUrlStage = {
3123
3127
  label: "Sync trigger URL",
3124
3128
  needs: [configStage.id],
3125
3129
  run(ctx) {
3126
- const url = ctx.sdk?.syncTrigger?.url;
3127
- if (url && !isPlaceholderUrl(url)) return Promise.resolve({
3130
+ const trigger = ctx.sdk?.describeSyncTrigger?.();
3131
+ if (trigger?.resolved) return Promise.resolve({
3128
3132
  state: "pass",
3129
- detail: url
3133
+ detail: trigger.resolved
3130
3134
  });
3131
- const isPlaceholder = url !== void 0;
3135
+ const configured = trigger?.configured;
3136
+ const isPlaceholder = configured !== void 0 && isPlaceholderUrl(configured);
3132
3137
  return Promise.resolve({
3133
3138
  state: "warn",
3134
- detail: isPlaceholder ? `Still the placeholder (${url}) — remote sync cannot call back into this app.` : "Not set — remote sync cannot call back into this app.",
3139
+ detail: isPlaceholder ? `Still the scaffolded placeholder (${configured}) — remote sync cannot call back into this app.` : "Not set — remote sync cannot call back into this app.",
3135
3140
  hint: "Replace <yourDomain.com> with your deployed origin, or delete the line — on Vercel, Cloudflare Pages, Railway, Render and Fly the SDK derives the URL from the platform. For local development run `clivly dev`: it opens a tunnel and registers the URL for you, outranking whatever is in .env. Push-only setups can ignore this."
3136
3141
  });
3137
3142
  }
@@ -3199,6 +3204,32 @@ const STATUS_STAGES = [
3199
3204
  }
3200
3205
  },
3201
3206
  sdkChecksStage,
3207
+ {
3208
+ id: "derived-objects",
3209
+ label: "Dashboard-authored objects",
3210
+ needs: [configStage.id],
3211
+ run(ctx) {
3212
+ const remote = ctx.sdk?.describeRemoteProjections?.();
3213
+ if (!remote) return Promise.resolve({
3214
+ state: "skip",
3215
+ detail: "This app's installed clivly predates dashboard-authored objects."
3216
+ });
3217
+ if (!remote.capable) return Promise.resolve({
3218
+ state: "warn",
3219
+ detail: "Objects authored in the dashboard cannot run against this app.",
3220
+ hint: "Add `introspector: drizzleIntrospector(db)` to clivly.config.ts."
3221
+ });
3222
+ if (!remote.ran) return Promise.resolve({
3223
+ state: "pass",
3224
+ detail: "Ready; no sync has run in this process yet."
3225
+ });
3226
+ const broken = remote.invalid + remote.failed;
3227
+ return Promise.resolve({
3228
+ state: broken > 0 ? "warn" : "pass",
3229
+ detail: broken > 0 ? `${remote.ok} running, ${broken} not — open the object in Clivly for the reason.` : `${remote.ok} running.`
3230
+ });
3231
+ }
3232
+ },
3202
3233
  triggerUrlStage
3203
3234
  ];
3204
3235
  function defaultLoadSdk(path) {
@@ -3252,7 +3283,8 @@ const ALIAS_STAGE_IDS = {
3252
3283
  "config",
3253
3284
  "runtime",
3254
3285
  "boot-hook",
3255
- "setup-checks"
3286
+ "setup-checks",
3287
+ "derived-objects"
3256
3288
  ],
3257
3289
  connect: [
3258
3290
  "api-key",