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
@@ -1,5 +1,6 @@
1
- const require_entity_config = require("./entity-config-DE30_RpU.cjs");
2
- const require_mappings_config = require("./mappings-config-CjCmT-aU.cjs");
1
+ const require_projection = require("./projection-Dz6_i1-w.cjs");
2
+ const require_entity_config = require("./entity-config-DgAe0_YD.cjs");
3
+ const require_mappings_config = require("./mappings-config-Dx9ldgdf.cjs");
3
4
  const require_drizzle_meta = require("./drizzle-meta-Bb1JWYqf.cjs");
4
5
  //#region ../sdk/src/deployment-identity.ts
5
6
  const MAX_DEPLOYMENT_ID_LENGTH = 128;
@@ -989,6 +990,240 @@ function describeSilentPush(target, body) {
989
990
  return `Clivly sync: pushed ${received} row(s) for ${objectType ? `"${source}" → "${objectType}"` : `"${source}"`} and none were stored — no active mapping in this workspace targets it, so the rows were dropped. Map it in Integrations → Mapping, then sync again.`;
990
991
  }
991
992
  //#endregion
993
+ //#region ../sdk/src/remote-projections.ts
994
+ const SYNC_CUSTOM_PATH$1 = "/clivly/sdk/sync/custom";
995
+ const MISSING_INTROSPECTOR = "this app cannot run dashboard-authored derived objects yet — add `introspector: drizzleIntrospector(db)` to clivly.config.ts";
996
+ /**
997
+ * Turn definitions fetched from the cloud into runnable sync sources.
998
+ *
999
+ * Revalidates every projection locally against the schema THIS app discovered,
1000
+ * never against the cloud's stored copy. Two reasons, and both matter:
1001
+ *
1002
+ * 1. Security. `checkTablesExist`/`checkRefs` bound every table and column ref
1003
+ * to the schema passed in, so validating against the locally-discovered
1004
+ * schema makes the blast radius exactly the namespace the developer handed
1005
+ * to `discoverFromDrizzle`. Trusting the cloud's verdict instead would let a
1006
+ * compromised cloud read any table in the database.
1007
+ * 2. Truth. The stored copy can be stale. A column dropped in the host app
1008
+ * becomes a readable quarantine here instead of a raw Postgres error out of
1009
+ * `sql.raw` at execution time.
1010
+ */
1011
+ function resolveRemoteProjections(args) {
1012
+ const { entries, introspector, schema } = args;
1013
+ return entries.map((entry) => resolveOne(entry, schema, introspector));
1014
+ }
1015
+ /**
1016
+ * The base table's PK, resolved from the schema THIS app discovered — never
1017
+ * from the cloud's copy.
1018
+ *
1019
+ * `validateProjection` proved its uniqueness case against this exact source
1020
+ * (`primaryKeyColumns` over the locally-discovered schema, see
1021
+ * `projection-validate.ts`). Compiling a *different* column — the cloud's
1022
+ * `basePkColumn`, which can go stale the moment a host renames a PK and the
1023
+ * `updatedAt`-only ETag never invalidates — would defeat that proof and
1024
+ * revive the raw-Postgres-error failure mode local revalidation exists to
1025
+ * prevent. Only relevant when the identity is `base_pk` with no explicit
1026
+ * column; every other identity kind ignores `basePkColumn` entirely.
1027
+ */
1028
+ function resolveLocalBasePk(projection, schema) {
1029
+ if (projection.identity.kind !== "base_pk" || projection.identity.column) return;
1030
+ const baseTable = schema.find((t) => t.name === projection.baseTable);
1031
+ if (!baseTable) return;
1032
+ return require_entity_config.primaryKeyColumns(baseTable)[0];
1033
+ }
1034
+ function resolveOne(entry, schema, introspector) {
1035
+ const parsed = require_projection.projectionSchema.safeParse(entry.projection);
1036
+ if (!parsed.success) return {
1037
+ id: entry.id,
1038
+ status: "invalid",
1039
+ reason: `projection did not parse: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1040
+ };
1041
+ const projection = parsed.data;
1042
+ const errors = require_entity_config.validateProjection(projection, schema, entry.entityKey);
1043
+ if (errors.length > 0) return {
1044
+ id: entry.id,
1045
+ status: "invalid",
1046
+ reason: errors.map((e) => `${e.path}: ${e.message}`).join("; ")
1047
+ };
1048
+ if (!introspector?.projectionSource) return {
1049
+ id: entry.id,
1050
+ status: "skipped",
1051
+ reason: MISSING_INTROSPECTOR
1052
+ };
1053
+ try {
1054
+ const basePkColumn = resolveLocalBasePk(projection, schema) ?? entry.basePkColumn;
1055
+ const source = introspector.projectionSource(projection, {
1056
+ objectType: entry.objectType,
1057
+ ...basePkColumn ? { basePkColumn } : {}
1058
+ });
1059
+ return {
1060
+ id: entry.id,
1061
+ status: "ok",
1062
+ source
1063
+ };
1064
+ } catch (error) {
1065
+ return {
1066
+ id: entry.id,
1067
+ status: "failed",
1068
+ reason: error instanceof Error ? error.message : String(error)
1069
+ };
1070
+ }
1071
+ }
1072
+ const PROJECTIONS_PATH = "/clivly/sdk/projections";
1073
+ const PROJECTIONS_REPORT_PATH = "/clivly/sdk/projections/report";
1074
+ const MAX_REASON = 2e3;
1075
+ /**
1076
+ * Merge what resolution decided with what the push actually did.
1077
+ *
1078
+ * A `status: "ok"` outcome means the projection *resolved*, not that its rows
1079
+ * landed. If the push then failed, the object did not sync and must not be
1080
+ * reported as healthy — so the push result wins for those.
1081
+ */
1082
+ function buildRemoteProjectionReport(args) {
1083
+ return {
1084
+ ...args.runId ? { runId: args.runId } : {},
1085
+ results: args.outcomes.map((outcome) => {
1086
+ if (outcome.status !== "ok") return {
1087
+ id: outcome.id,
1088
+ status: outcome.status,
1089
+ reason: outcome.reason.slice(0, MAX_REASON)
1090
+ };
1091
+ const result = args.pushed.get(outcome.source.entity);
1092
+ if (result?.ok) return {
1093
+ id: outcome.id,
1094
+ status: "ok",
1095
+ rows: result.count
1096
+ };
1097
+ return {
1098
+ id: outcome.id,
1099
+ status: "failed",
1100
+ reason: (result?.reason ?? "the push to Clivly failed; rows were read but not stored").slice(0, MAX_REASON)
1101
+ };
1102
+ })
1103
+ };
1104
+ }
1105
+ function createRemoteProjectionFetcher(args) {
1106
+ let etag = null;
1107
+ let cached = [];
1108
+ return { async fetch() {
1109
+ try {
1110
+ const response = await args.fetcher(`${args.apiUrl}${PROJECTIONS_PATH}`, { headers: {
1111
+ authorization: `Bearer ${args.apiKey}`,
1112
+ ...etag ? { "if-none-match": etag } : {}
1113
+ } });
1114
+ if (response.status === 304) return cached;
1115
+ if (!response.ok) return [];
1116
+ const body = await response.json();
1117
+ etag = body.etag ?? null;
1118
+ cached = body.projections ?? [];
1119
+ return cached;
1120
+ } catch {
1121
+ return [];
1122
+ }
1123
+ } };
1124
+ }
1125
+ const REMOTE_PAGE_SIZE = 500;
1126
+ /**
1127
+ * Page one source to exhaustion, keyset-paging on identity alone (the id
1128
+ * field, no timestamp). Split out of `pushRemoteProjections` so that
1129
+ * function's own cognitive complexity stays under the lint threshold — this
1130
+ * is the piece that can throw (a `fetchPage` execution error), kept separate
1131
+ * from push-failure handling below.
1132
+ */
1133
+ async function fetchAllPages(source, pageSize) {
1134
+ const idField = source.idField ?? "id";
1135
+ const all = [];
1136
+ let since = null;
1137
+ for (;;) {
1138
+ const rows = await source.fetchPage({
1139
+ since,
1140
+ limit: pageSize
1141
+ });
1142
+ if (rows.length === 0) break;
1143
+ all.push(...rows);
1144
+ const last = rows.at(-1);
1145
+ const nextId = String(last?.[idField]);
1146
+ if (since && nextId === since.id) break;
1147
+ since = { id: nextId };
1148
+ if (rows.length < pageSize) break;
1149
+ }
1150
+ return all;
1151
+ }
1152
+ /**
1153
+ * Page a remote projection by identity and push one `full` batch.
1154
+ *
1155
+ * `full` rather than `delta` because only a full push lets the server archive
1156
+ * rows that dropped out when a join or filter stopped matching — a delta cannot
1157
+ * express absence. And identity rather than a timestamp because a
1158
+ * dashboard-authored projection has no watermark column, while validation has
1159
+ * already proven the identity unique.
1160
+ *
1161
+ * `pageSize` defaults to `REMOTE_PAGE_SIZE` and exists so a test can force a
1162
+ * short page without either mutating the exported constant or constructing a
1163
+ * fixture hundreds of rows deep.
1164
+ *
1165
+ * Returns one entry per source, keyed by `source.entity`. `count` and `ok`
1166
+ * are independent: `count` is how many rows were pushed (0 for a projection
1167
+ * that legitimately matches nothing this run), `ok` is whether the push
1168
+ * itself succeeded. A caller that folds failures into a report must branch
1169
+ * on `ok`, never on `count === 0` — a correctly-working, legitimately-empty
1170
+ * projection has `ok: true, count: 0` and is not a failure.
1171
+ *
1172
+ * Each source's paging-and-push is wrapped individually: a projection can
1173
+ * validate cleanly and still fail at execution — permission denied, a type
1174
+ * error, a column dropped between validation and this run — and `fetchPage`
1175
+ * throwing is the realistic shape of that. Quarantine that one source as
1176
+ * `{ count: 0, ok: false, reason }` and keep going. The design's promise is
1177
+ * one bad object never takes down the others or the run's report; letting a
1178
+ * throw here escape the loop would break that promise for every source
1179
+ * queued after the failing one.
1180
+ *
1181
+ * Paging and pushing are caught separately, on purpose: `reason` is set only
1182
+ * when paging itself threw (no rows were ever read), so the reported cause
1183
+ * names execution — not the push, which is what actually failed when
1184
+ * `args.push` returns `false`. Conflating the two sent an author whose
1185
+ * projection can't even run to look at their push credentials instead of
1186
+ * their SQL.
1187
+ */
1188
+ async function pushRemoteProjections(args) {
1189
+ const pageSize = args.pageSize ?? REMOTE_PAGE_SIZE;
1190
+ const pushed = /* @__PURE__ */ new Map();
1191
+ for (const source of args.sources) {
1192
+ let all;
1193
+ try {
1194
+ all = await fetchAllPages(source, pageSize);
1195
+ } catch (error) {
1196
+ const message = error instanceof Error ? error.message : String(error);
1197
+ pushed.set(source.entity, {
1198
+ count: 0,
1199
+ ok: false,
1200
+ reason: `the projection failed to execute: ${message}`.slice(0, MAX_REASON)
1201
+ });
1202
+ continue;
1203
+ }
1204
+ try {
1205
+ const ok = await args.push(SYNC_CUSTOM_PATH$1, {
1206
+ source: source.entity,
1207
+ rows: all,
1208
+ mode: "full",
1209
+ fetched: all.length,
1210
+ cursor: null,
1211
+ objectType: source.objectType
1212
+ });
1213
+ pushed.set(source.entity, {
1214
+ count: all.length,
1215
+ ok
1216
+ });
1217
+ } catch {
1218
+ pushed.set(source.entity, {
1219
+ count: 0,
1220
+ ok: false
1221
+ });
1222
+ }
1223
+ }
1224
+ return pushed;
1225
+ }
1226
+ //#endregion
992
1227
  //#region ../sdk/src/self-check.ts
993
1228
  function selfCheck(input) {
994
1229
  const issues = [];
@@ -1337,12 +1572,25 @@ function resolveContactEntity(config) {
1337
1572
  }
1338
1573
  function collectSchemaMeta(config) {
1339
1574
  const byTable = /* @__PURE__ */ new Map();
1340
- for (const table of config.schema ?? []) if (table.columnsMeta) byTable.set(table.name, table.columnsMeta);
1575
+ const merge = (name, meta) => {
1576
+ const existing = byTable.get(name);
1577
+ byTable.set(name, {
1578
+ columnsMeta: meta.columnsMeta ?? existing?.columnsMeta,
1579
+ uniqueConstraints: meta.uniqueConstraints ?? existing?.uniqueConstraints
1580
+ });
1581
+ };
1582
+ for (const table of config.schema ?? []) if (table.columnsMeta || table.uniqueConstraints) merge(table.name, {
1583
+ columnsMeta: table.columnsMeta,
1584
+ uniqueConstraints: table.uniqueConstraints
1585
+ });
1341
1586
  const sources = [];
1342
1587
  if (config.source) sources.push(config.source.contacts, config.source.companies, config.source.deals, ...config.source.custom ?? []);
1343
1588
  for (const source of sources) {
1344
1589
  const meta = source?.[require_drizzle_meta.DRIZZLE_META];
1345
- if (meta?.tableName && meta.columnsMeta) byTable.set(meta.tableName, meta.columnsMeta);
1590
+ if (meta?.tableName && (meta.columnsMeta || meta.uniqueConstraints)) merge(meta.tableName, {
1591
+ columnsMeta: meta.columnsMeta,
1592
+ uniqueConstraints: meta.uniqueConstraints
1593
+ });
1346
1594
  }
1347
1595
  return byTable;
1348
1596
  }
@@ -1357,12 +1605,13 @@ function buildSchema(config) {
1357
1605
  if (config.contactEntity && !byName.has(config.contactEntity)) byName.set(config.contactEntity, new Set(config.contactFields ?? []));
1358
1606
  const metaByTable = collectSchemaMeta(config);
1359
1607
  return [...byName].map(([name, columns]) => {
1360
- const columnsMeta = metaByTable.get(name);
1361
- if (columnsMeta) for (const col of columnsMeta) columns.add(col.name);
1608
+ const meta = metaByTable.get(name);
1609
+ for (const col of meta?.columnsMeta ?? []) columns.add(col.name);
1362
1610
  return {
1363
1611
  name,
1364
1612
  columns: [...columns],
1365
- ...columnsMeta ? { columnsMeta } : {}
1613
+ ...meta?.columnsMeta ? { columnsMeta: meta.columnsMeta } : {},
1614
+ ...meta?.uniqueConstraints ? { uniqueConstraints: meta.uniqueConstraints } : {}
1366
1615
  };
1367
1616
  });
1368
1617
  }
@@ -1397,6 +1646,24 @@ function timingSafeEqual(a, b) {
1397
1646
  }
1398
1647
  const RUN_ID_HEADER = "x-clivly-run-id";
1399
1648
  const SIGNATURE_HEADER = "x-clivly-signature";
1649
+ /**
1650
+ * An error the mounted trigger route should answer with a specific HTTP
1651
+ * status, rather than the generic 500 it uses for a genuinely unhandled
1652
+ * error. A fail-safe rejection (e.g. an unresolvable sample `entityKey`) is a
1653
+ * configuration mismatch the caller can fix — surfacing it as "your app
1654
+ * crashed" sends a developer hunting through their own sync handler instead.
1655
+ */
1656
+ var TriggerRequestError = class extends Error {
1657
+ status;
1658
+ constructor(status, message) {
1659
+ super(message);
1660
+ this.name = new.target.name;
1661
+ this.status = status;
1662
+ }
1663
+ };
1664
+ function triggerErrorStatus(error) {
1665
+ return error instanceof TriggerRequestError ? error.status : 500;
1666
+ }
1400
1667
  const SIGNATURE_TOLERANCE_SECONDS = 300;
1401
1668
  const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
1402
1669
  const SECONDS_PER_MS = 1e3;
@@ -1578,9 +1845,15 @@ function createClivlySDK(config) {
1578
1845
  const cursorStore = config.cursorStore ?? createMemoryCursorStore();
1579
1846
  const fetcher = createFetcher(config.retry);
1580
1847
  let warnedUnsignedTrigger = false;
1848
+ const remoteProjections = createRemoteProjectionFetcher({
1849
+ apiKey: config.apiKey,
1850
+ apiUrl,
1851
+ fetcher
1852
+ });
1581
1853
  let observedUrl;
1582
1854
  let lastPushFailure = null;
1583
1855
  let lastPush = { failed: false };
1856
+ let lastRemoteOutcomes = [];
1584
1857
  const warnedSilentTargets = /* @__PURE__ */ new Set();
1585
1858
  const warnIfSilent = async (target, response) => {
1586
1859
  if (typeof response.json !== "function") return;
@@ -1652,18 +1925,58 @@ function createClivlySDK(config) {
1652
1925
  for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1653
1926
  return failed;
1654
1927
  };
1928
+ const syncRemoteSources = async (sources, runId) => {
1929
+ const pushed = await pushRemoteProjections({
1930
+ push: createPush(runId),
1931
+ sources
1932
+ });
1933
+ const failed = [];
1934
+ for (const [entity, result] of pushed) if (!result.ok) failed.push(entity);
1935
+ return {
1936
+ failed,
1937
+ pushed
1938
+ };
1939
+ };
1940
+ const reportRemoteOutcomes = async (outcomes, pushed, runId) => {
1941
+ if (outcomes.length === 0) return;
1942
+ try {
1943
+ const report = buildRemoteProjectionReport({
1944
+ outcomes,
1945
+ pushed,
1946
+ runId
1947
+ });
1948
+ await fetcher(`${apiUrl}${PROJECTIONS_REPORT_PATH}`, {
1949
+ method: "POST",
1950
+ headers: {
1951
+ authorization: `Bearer ${config.apiKey}`,
1952
+ "content-type": "application/json"
1953
+ },
1954
+ body: JSON.stringify(report)
1955
+ });
1956
+ } catch {}
1957
+ };
1655
1958
  const runSync = async (options) => {
1656
- if (!config.source) return;
1959
+ const remoteOutcomes = resolveRemoteProjections({
1960
+ entries: await remoteProjections.fetch(),
1961
+ introspector: config.introspector,
1962
+ schema: buildSchema(config)
1963
+ });
1964
+ lastRemoteOutcomes = remoteOutcomes;
1965
+ const remoteSources = remoteOutcomes.filter((o) => o.status === "ok").map((o) => o.source);
1966
+ if (!config.source && remoteSources.length === 0 && remoteOutcomes.length === 0) return;
1657
1967
  lastPushFailure = null;
1658
1968
  const mode = options?.mode ?? "delta";
1659
1969
  const runId = options?.runId;
1660
1970
  const failed = [];
1661
- if (config.source.companies) {
1971
+ if (config.source?.companies) {
1662
1972
  if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode, void 0, runId)) failed.push(config.source.companies.entity);
1663
1973
  }
1664
- if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
1665
- if (config.source.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
1666
- failed.push(...await syncCustomSources(config.source.custom, mode, runId));
1974
+ if (config.source?.contacts && !await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
1975
+ if (config.source?.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
1976
+ failed.push(...await syncCustomSources(config.source?.custom, mode, runId));
1977
+ const remote = await syncRemoteSources(remoteSources, runId);
1978
+ failed.push(...remote.failed);
1979
+ await reportRemoteOutcomes(lastRemoteOutcomes, remote.pushed, runId);
1667
1980
  if (failed.length > 0) {
1668
1981
  const message = describePushFailure(failed);
1669
1982
  lastPush = {
@@ -1676,7 +1989,7 @@ function createClivlySDK(config) {
1676
1989
  };
1677
1990
  const runSampleSync = async (entityKey, runId) => {
1678
1991
  const resolved = resolveSampleSource(config, entityKey);
1679
- if (!resolved) throw new Error(`Clivly sync: sample entityKey "${entityKey}" did not resolve to a configured source`);
1992
+ if (!resolved) throw new TriggerRequestError(400, `Clivly sync: no source is configured for entity "${entityKey}". The dashboard maps this table, but clivly.config declares no matching entity — add it to \`entities\`, or remove the mapping in Integrations → Mapping.`);
1680
1993
  lastPushFailure = null;
1681
1994
  if (!await syncOne(resolved.source, resolved.path, "delta", resolved.objectType, runId, SAMPLE_BATCH_SIZE, SAMPLE_BATCH_SIZE, false)) throw new Error(describePushFailure([resolved.source.entity]));
1682
1995
  };
@@ -1689,6 +2002,20 @@ function createClivlySDK(config) {
1689
2002
  observedUrl
1690
2003
  })
1691
2004
  });
2005
+ const describeRemoteProjections = () => {
2006
+ const counts = {
2007
+ failed: 0,
2008
+ invalid: 0,
2009
+ ok: 0,
2010
+ skipped: 0
2011
+ };
2012
+ for (const outcome of lastRemoteOutcomes) counts[outcome.status] += 1;
2013
+ return {
2014
+ capable: Boolean(config.introspector?.projectionSource),
2015
+ ran: lastRemoteOutcomes.length > 0,
2016
+ ...counts
2017
+ };
2018
+ };
1692
2019
  const sendHeartbeat = () => {
1693
2020
  const triggerUrl = describeSyncTrigger().resolved;
1694
2021
  return fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
@@ -1887,7 +2214,7 @@ function createClivlySDK(config) {
1887
2214
  });
1888
2215
  const runScheduled = async (options) => {
1889
2216
  await heartbeat();
1890
- if (config.source) await runSync(options);
2217
+ if (config.source || config.introspector) await runSync(options);
1891
2218
  };
1892
2219
  const triggerRequestAllowed = async (req, rawBody) => {
1893
2220
  const secret = config.syncTrigger?.secret;
@@ -1901,7 +2228,9 @@ function createClivlySDK(config) {
1901
2228
  const parseTriggerBody = (rawBody) => {
1902
2229
  if (!rawBody) return {};
1903
2230
  try {
1904
- return JSON.parse(rawBody);
2231
+ const parsed = JSON.parse(rawBody);
2232
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2233
+ return parsed;
1905
2234
  } catch {
1906
2235
  return {};
1907
2236
  }
@@ -1934,7 +2263,7 @@ function createClivlySDK(config) {
1934
2263
  ok: false,
1935
2264
  runId,
1936
2265
  error: error instanceof Error ? error.message : String(error)
1937
- }, 500);
2266
+ }, triggerErrorStatus(error));
1938
2267
  }
1939
2268
  };
1940
2269
  const start = async () => {
@@ -1981,7 +2310,7 @@ function createClivlySDK(config) {
1981
2310
  presenceClient = null;
1982
2311
  console.warn("Clivly presence socket unavailable; using HTTP heartbeat", error);
1983
2312
  }
1984
- if (!timers.sync && config.source) {
2313
+ if (!timers.sync && (config.source || config.introspector)) {
1985
2314
  timers.sync = setInterval(() => {
1986
2315
  runSync().catch(() => {});
1987
2316
  }, syncIntervalMs);
@@ -2015,6 +2344,7 @@ function createClivlySDK(config) {
2015
2344
  ping,
2016
2345
  doctor,
2017
2346
  describeSyncTrigger,
2347
+ describeRemoteProjections,
2018
2348
  runScheduled,
2019
2349
  createClivlyHandler,
2020
2350
  sync: runSync,