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.
- package/README.md +22 -3
- package/core-auth-adapter.cjs +1 -1
- package/core-auth-adapter.mjs +1 -1
- package/core-drizzle.cjs +1 -1
- package/core-drizzle.d.cts +1 -1
- package/core-drizzle.d.mts +1 -1
- package/core-drizzle.mjs +1 -1
- package/core-entity-config.cjs +1 -1
- package/core-entity-config.mjs +1 -1
- package/core-mapping-form.cjs +1 -1
- package/core-mapping-form.mjs +1 -1
- package/core-mappings-config.cjs +1 -1
- package/core-mappings-config.mjs +1 -1
- package/core-sync-engine.cjs +1 -1
- package/core-sync-engine.mjs +1 -1
- package/core-view-compiler.cjs +1 -1
- package/core-view-compiler.mjs +1 -1
- package/core.cjs +14 -9
- package/core.d.cts +2 -2
- package/core.d.mts +2 -2
- package/core.mjs +9 -9
- package/{drizzle-CiK8MMEZ.d.mts → drizzle-BmDDgAIF.d.mts} +3 -1
- package/{drizzle-bfjVzYR6.cjs → drizzle-DcB7OfBp.cjs} +9 -1
- package/{drizzle-DEtRrqtc.d.cts → drizzle-ixVV4pPN.d.cts} +3 -1
- package/{drizzle-3kvhkwO7.mjs → drizzle-wbcuG-cy.mjs} +9 -1
- package/drizzle.cjs +61 -10
- package/drizzle.d.cts +9 -3
- package/drizzle.d.mts +9 -3
- package/drizzle.mjs +61 -10
- package/{entity-config-B74SPOUZ.mjs → entity-config-C8IIkrmX.mjs} +34 -5
- package/{entity-config-7sGCk_nw.cjs → entity-config-CPM-Tp-d.cjs} +34 -5
- package/index.cjs +41 -9
- package/index.mjs +41 -9
- package/internal-BLKMnIIZ.cjs +259 -0
- package/internal-BpVh_J77.mjs +212 -0
- package/{internal-o2M7FGlR.d.mts → internal-CvlFk3KC.d.mts} +82 -2
- package/{internal-wCXSELDN.d.cts → internal-DGUOLQAq.d.cts} +82 -2
- package/internal-drizzle.cjs +1 -1
- package/internal-drizzle.d.cts +1 -1
- package/internal-drizzle.d.mts +1 -1
- package/internal-drizzle.mjs +1 -1
- package/internal-entity-config.cjs +1 -1
- package/internal-entity-config.mjs +1 -1
- package/internal-entity-heuristics.cjs +1 -1
- package/internal-entity-heuristics.mjs +1 -1
- package/internal-mappings-config.cjs +1 -1
- package/internal-mappings-config.mjs +1 -1
- package/internal-sync-engine.cjs +1 -1
- package/internal-sync-engine.mjs +1 -1
- package/internal-view-compiler.cjs +1 -1
- package/internal-view-compiler.mjs +1 -1
- package/internal.cjs +13 -7
- package/internal.d.cts +2 -2
- package/internal.d.mts +2 -2
- package/internal.mjs +8 -8
- package/{mappings-config-CLyEg0sf.cjs → mappings-config-CBwL3FVh.cjs} +1 -1
- package/{mappings-config-DQS_s4dg.mjs → mappings-config-DUKBe6OS.mjs} +1 -1
- package/{namespace-probe-Cn7voAd0.d.mts → namespace-probe-BQ4imnV7.d.mts} +85 -2
- package/{namespace-probe-BoGI18dQ.d.cts → namespace-probe-DZIGdvlm.d.cts} +85 -2
- package/package.json +1 -1
- package/{projection-Dz6_i1-w.cjs → projection-DEwL-D8R.cjs} +6 -0
- package/{projection-CSGJbZlZ.mjs → projection-hnPSgH6G.mjs} +1 -1
- package/{projection-sql-BgnfiGqF.mjs → projection-sql-9zzTVGvf.mjs} +34 -2
- package/{projection-sql-Cl1qIn8m.cjs → projection-sql-iHy-Cx6V.cjs} +45 -1
- package/{sdk-CM1UO-I7.cjs → sdk-BL9BCzyD.cjs} +354 -20
- package/{sdk-DYBDR939.mjs → sdk-CubnsyD9.mjs} +354 -20
- package/sdk.cjs +2 -2
- package/sdk.d.cts +2 -2
- package/sdk.d.mts +2 -2
- package/sdk.mjs +2 -2
- package/{sync-engine-C4A5jgA4.cjs → sync-engine-BLgwLpR6.cjs} +1 -1
- package/{sync-engine-Dt5QQSKO.mjs → sync-engine-B_IzoUSx.mjs} +1 -1
- package/{view-compiler-B5nDal14.cjs → view-compiler-BHI5E4QI.cjs} +2 -2
- package/{view-compiler-CSYwEWf5.mjs → view-compiler-D_6gfXFr.mjs} +2 -2
- package/vite.d.cts +1 -1
- package/vite.d.mts +1 -1
- package/internal-CykXpEGk.mjs +0 -68
- package/internal-DqduZU3l.cjs +0 -97
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const require_projection = require("./projection-DEwL-D8R.cjs");
|
|
2
|
+
const require_entity_config = require("./entity-config-CPM-Tp-d.cjs");
|
|
3
|
+
const require_mappings_config = require("./mappings-config-CBwL3FVh.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;
|
|
@@ -572,12 +573,21 @@ async function runDoctor(args) {
|
|
|
572
573
|
ok: entityCount > 0,
|
|
573
574
|
detail: entityCount > 0 ? `${entityCount} entit${entityCount === 1 ? "y" : "ies"} configured.` : "No `entities` config — pass defineClivlyConfig(...) as `entities`."
|
|
574
575
|
});
|
|
576
|
+
const sourcesDetail = (count, deferred) => {
|
|
577
|
+
if (deferred) return "Deferred — the coverage check below names the entity and the source key it expected.";
|
|
578
|
+
return count === 0 ? "No `source.contacts` — nothing to sync." : `${count} source(s) to mirror.`;
|
|
579
|
+
};
|
|
580
|
+
const coverage = checkConceptCoverage(config);
|
|
581
|
+
const coverageOwnsIt = !(coverage.ok || coverage.skipped);
|
|
582
|
+
const noSources = sources.length === 0;
|
|
583
|
+
const deferred = noSources && coverageOwnsIt;
|
|
575
584
|
checks.push({
|
|
576
585
|
name: "Sources configured",
|
|
577
|
-
ok:
|
|
578
|
-
|
|
586
|
+
ok: !noSources,
|
|
587
|
+
skipped: deferred,
|
|
588
|
+
detail: sourcesDetail(sources.length, deferred)
|
|
579
589
|
});
|
|
580
|
-
checks.push(
|
|
590
|
+
checks.push(coverage);
|
|
581
591
|
checks.push(...cursorColumnChecks(sources));
|
|
582
592
|
for (const source of sources) {
|
|
583
593
|
const check = cursorMonotonicityCheck(source);
|
|
@@ -980,6 +990,240 @@ function describeSilentPush(target, body) {
|
|
|
980
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.`;
|
|
981
991
|
}
|
|
982
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
|
|
983
1227
|
//#region ../sdk/src/self-check.ts
|
|
984
1228
|
function selfCheck(input) {
|
|
985
1229
|
const issues = [];
|
|
@@ -1328,12 +1572,25 @@ function resolveContactEntity(config) {
|
|
|
1328
1572
|
}
|
|
1329
1573
|
function collectSchemaMeta(config) {
|
|
1330
1574
|
const byTable = /* @__PURE__ */ new Map();
|
|
1331
|
-
|
|
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
|
+
});
|
|
1332
1586
|
const sources = [];
|
|
1333
1587
|
if (config.source) sources.push(config.source.contacts, config.source.companies, config.source.deals, ...config.source.custom ?? []);
|
|
1334
1588
|
for (const source of sources) {
|
|
1335
1589
|
const meta = source?.[require_drizzle_meta.DRIZZLE_META];
|
|
1336
|
-
if (meta?.tableName && meta.columnsMeta)
|
|
1590
|
+
if (meta?.tableName && (meta.columnsMeta || meta.uniqueConstraints)) merge(meta.tableName, {
|
|
1591
|
+
columnsMeta: meta.columnsMeta,
|
|
1592
|
+
uniqueConstraints: meta.uniqueConstraints
|
|
1593
|
+
});
|
|
1337
1594
|
}
|
|
1338
1595
|
return byTable;
|
|
1339
1596
|
}
|
|
@@ -1348,12 +1605,13 @@ function buildSchema(config) {
|
|
|
1348
1605
|
if (config.contactEntity && !byName.has(config.contactEntity)) byName.set(config.contactEntity, new Set(config.contactFields ?? []));
|
|
1349
1606
|
const metaByTable = collectSchemaMeta(config);
|
|
1350
1607
|
return [...byName].map(([name, columns]) => {
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1608
|
+
const meta = metaByTable.get(name);
|
|
1609
|
+
for (const col of meta?.columnsMeta ?? []) columns.add(col.name);
|
|
1353
1610
|
return {
|
|
1354
1611
|
name,
|
|
1355
1612
|
columns: [...columns],
|
|
1356
|
-
...columnsMeta ? { columnsMeta } : {}
|
|
1613
|
+
...meta?.columnsMeta ? { columnsMeta: meta.columnsMeta } : {},
|
|
1614
|
+
...meta?.uniqueConstraints ? { uniqueConstraints: meta.uniqueConstraints } : {}
|
|
1357
1615
|
};
|
|
1358
1616
|
});
|
|
1359
1617
|
}
|
|
@@ -1533,6 +1791,15 @@ async function handlePreviewRequest(config, preview) {
|
|
|
1533
1791
|
sampledCount: rows.length
|
|
1534
1792
|
}, 200);
|
|
1535
1793
|
}
|
|
1794
|
+
async function handleProjectionPreviewRequest(config, request) {
|
|
1795
|
+
const run = config.introspector?.runProjection;
|
|
1796
|
+
if (!run) return jsonResponse({ error: "This app cannot preview derived objects. Set `introspector: drizzleIntrospector(db)` in clivly.config.ts, on a clivly version that supports runProjection." }, 400);
|
|
1797
|
+
const rows = await run.call(config.introspector, request.projection, { limit: request.limit });
|
|
1798
|
+
return jsonResponse({
|
|
1799
|
+
rows,
|
|
1800
|
+
sampledCount: rows.length
|
|
1801
|
+
}, 200);
|
|
1802
|
+
}
|
|
1536
1803
|
/**
|
|
1537
1804
|
* Create the Clivly SDK. The implemented surface — heartbeat/discovery reporting
|
|
1538
1805
|
* and the auth-verify handler — connects a developer's app to a running Clivly
|
|
@@ -1560,9 +1827,15 @@ function createClivlySDK(config) {
|
|
|
1560
1827
|
const cursorStore = config.cursorStore ?? createMemoryCursorStore();
|
|
1561
1828
|
const fetcher = createFetcher(config.retry);
|
|
1562
1829
|
let warnedUnsignedTrigger = false;
|
|
1830
|
+
const remoteProjections = createRemoteProjectionFetcher({
|
|
1831
|
+
apiKey: config.apiKey,
|
|
1832
|
+
apiUrl,
|
|
1833
|
+
fetcher
|
|
1834
|
+
});
|
|
1563
1835
|
let observedUrl;
|
|
1564
1836
|
let lastPushFailure = null;
|
|
1565
1837
|
let lastPush = { failed: false };
|
|
1838
|
+
let lastRemoteOutcomes = [];
|
|
1566
1839
|
const warnedSilentTargets = /* @__PURE__ */ new Set();
|
|
1567
1840
|
const warnIfSilent = async (target, response) => {
|
|
1568
1841
|
if (typeof response.json !== "function") return;
|
|
@@ -1634,18 +1907,58 @@ function createClivlySDK(config) {
|
|
|
1634
1907
|
for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
1635
1908
|
return failed;
|
|
1636
1909
|
};
|
|
1910
|
+
const syncRemoteSources = async (sources, runId) => {
|
|
1911
|
+
const pushed = await pushRemoteProjections({
|
|
1912
|
+
push: createPush(runId),
|
|
1913
|
+
sources
|
|
1914
|
+
});
|
|
1915
|
+
const failed = [];
|
|
1916
|
+
for (const [entity, result] of pushed) if (!result.ok) failed.push(entity);
|
|
1917
|
+
return {
|
|
1918
|
+
failed,
|
|
1919
|
+
pushed
|
|
1920
|
+
};
|
|
1921
|
+
};
|
|
1922
|
+
const reportRemoteOutcomes = async (outcomes, pushed, runId) => {
|
|
1923
|
+
if (outcomes.length === 0) return;
|
|
1924
|
+
try {
|
|
1925
|
+
const report = buildRemoteProjectionReport({
|
|
1926
|
+
outcomes,
|
|
1927
|
+
pushed,
|
|
1928
|
+
runId
|
|
1929
|
+
});
|
|
1930
|
+
await fetcher(`${apiUrl}${PROJECTIONS_REPORT_PATH}`, {
|
|
1931
|
+
method: "POST",
|
|
1932
|
+
headers: {
|
|
1933
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
1934
|
+
"content-type": "application/json"
|
|
1935
|
+
},
|
|
1936
|
+
body: JSON.stringify(report)
|
|
1937
|
+
});
|
|
1938
|
+
} catch {}
|
|
1939
|
+
};
|
|
1637
1940
|
const runSync = async (options) => {
|
|
1638
|
-
|
|
1941
|
+
const remoteOutcomes = resolveRemoteProjections({
|
|
1942
|
+
entries: await remoteProjections.fetch(),
|
|
1943
|
+
introspector: config.introspector,
|
|
1944
|
+
schema: buildSchema(config)
|
|
1945
|
+
});
|
|
1946
|
+
lastRemoteOutcomes = remoteOutcomes;
|
|
1947
|
+
const remoteSources = remoteOutcomes.filter((o) => o.status === "ok").map((o) => o.source);
|
|
1948
|
+
if (!config.source && remoteSources.length === 0 && remoteOutcomes.length === 0) return;
|
|
1639
1949
|
lastPushFailure = null;
|
|
1640
1950
|
const mode = options?.mode ?? "delta";
|
|
1641
1951
|
const runId = options?.runId;
|
|
1642
1952
|
const failed = [];
|
|
1643
|
-
if (config.source
|
|
1953
|
+
if (config.source?.companies) {
|
|
1644
1954
|
if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode, void 0, runId)) failed.push(config.source.companies.entity);
|
|
1645
1955
|
}
|
|
1646
|
-
if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1647
|
-
if (config.source
|
|
1648
|
-
failed.push(...await syncCustomSources(config.source
|
|
1956
|
+
if (config.source?.contacts && !await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1957
|
+
if (config.source?.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
|
|
1958
|
+
failed.push(...await syncCustomSources(config.source?.custom, mode, runId));
|
|
1959
|
+
const remote = await syncRemoteSources(remoteSources, runId);
|
|
1960
|
+
failed.push(...remote.failed);
|
|
1961
|
+
await reportRemoteOutcomes(lastRemoteOutcomes, remote.pushed, runId);
|
|
1649
1962
|
if (failed.length > 0) {
|
|
1650
1963
|
const message = describePushFailure(failed);
|
|
1651
1964
|
lastPush = {
|
|
@@ -1662,13 +1975,31 @@ function createClivlySDK(config) {
|
|
|
1662
1975
|
lastPushFailure = null;
|
|
1663
1976
|
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]));
|
|
1664
1977
|
};
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1978
|
+
const describeSyncTrigger = () => ({
|
|
1979
|
+
configured: config.syncTrigger?.url,
|
|
1980
|
+
resolved: resolveTriggerUrl({
|
|
1667
1981
|
configuredUrl: config.syncTrigger?.url,
|
|
1668
1982
|
configuredPath: config.syncTrigger?.path,
|
|
1669
1983
|
env: config.env ?? process.env,
|
|
1670
1984
|
observedUrl
|
|
1671
|
-
})
|
|
1985
|
+
})
|
|
1986
|
+
});
|
|
1987
|
+
const describeRemoteProjections = () => {
|
|
1988
|
+
const counts = {
|
|
1989
|
+
failed: 0,
|
|
1990
|
+
invalid: 0,
|
|
1991
|
+
ok: 0,
|
|
1992
|
+
skipped: 0
|
|
1993
|
+
};
|
|
1994
|
+
for (const outcome of lastRemoteOutcomes) counts[outcome.status] += 1;
|
|
1995
|
+
return {
|
|
1996
|
+
capable: Boolean(config.introspector?.projectionSource),
|
|
1997
|
+
ran: lastRemoteOutcomes.length > 0,
|
|
1998
|
+
...counts
|
|
1999
|
+
};
|
|
2000
|
+
};
|
|
2001
|
+
const sendHeartbeat = () => {
|
|
2002
|
+
const triggerUrl = describeSyncTrigger().resolved;
|
|
1672
2003
|
return fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
|
|
1673
2004
|
method: "POST",
|
|
1674
2005
|
headers: {
|
|
@@ -1865,7 +2196,7 @@ function createClivlySDK(config) {
|
|
|
1865
2196
|
});
|
|
1866
2197
|
const runScheduled = async (options) => {
|
|
1867
2198
|
await heartbeat();
|
|
1868
|
-
if (config.source) await runSync(options);
|
|
2199
|
+
if (config.source || config.introspector) await runSync(options);
|
|
1869
2200
|
};
|
|
1870
2201
|
const triggerRequestAllowed = async (req, rawBody) => {
|
|
1871
2202
|
const secret = config.syncTrigger?.secret;
|
|
@@ -1896,6 +2227,7 @@ function createClivlySDK(config) {
|
|
|
1896
2227
|
const startedAt = Date.now();
|
|
1897
2228
|
try {
|
|
1898
2229
|
if (payload.preview) return await handlePreviewRequest(config, payload.preview);
|
|
2230
|
+
if (payload.previewProjection) return await handleProjectionPreviewRequest(config, payload.previewProjection);
|
|
1899
2231
|
if (payload.sample) await runSampleSync(payload.sample.entityKey, payload.runId);
|
|
1900
2232
|
else await runScheduled({
|
|
1901
2233
|
mode: payload.mode,
|
|
@@ -1958,7 +2290,7 @@ function createClivlySDK(config) {
|
|
|
1958
2290
|
presenceClient = null;
|
|
1959
2291
|
console.warn("Clivly presence socket unavailable; using HTTP heartbeat", error);
|
|
1960
2292
|
}
|
|
1961
|
-
if (!timers.sync && config.source) {
|
|
2293
|
+
if (!timers.sync && (config.source || config.introspector)) {
|
|
1962
2294
|
timers.sync = setInterval(() => {
|
|
1963
2295
|
runSync().catch(() => {});
|
|
1964
2296
|
}, syncIntervalMs);
|
|
@@ -1991,6 +2323,8 @@ function createClivlySDK(config) {
|
|
|
1991
2323
|
connect,
|
|
1992
2324
|
ping,
|
|
1993
2325
|
doctor,
|
|
2326
|
+
describeSyncTrigger,
|
|
2327
|
+
describeRemoteProjections,
|
|
1994
2328
|
runScheduled,
|
|
1995
2329
|
createClivlyHandler,
|
|
1996
2330
|
sync: runSync,
|