@clivly/sdk 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.
- package/README.md +34 -13
- package/dist/chat-session.d.cts +1 -1
- package/dist/chat-session.d.mts +1 -1
- package/dist/drizzle.cjs +56 -22
- package/dist/drizzle.d.cts +8 -2
- package/dist/drizzle.d.mts +9 -3
- package/dist/drizzle.mjs +56 -22
- package/dist/index.cjs +345 -15
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +345 -15
- package/dist/{types-CiFEalbP.d.cts → types-B7HKG2Ph.d.cts} +41 -1
- package/dist/{types-CiFEalbP.d.mts → types-B7HKG2Ph.d.mts} +41 -1
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.mts +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,7 @@ const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
|
|
|
3
3
|
const require_chat_session = require("./chat-session.cjs");
|
|
4
4
|
let _clivly_core_entity_config = require("@clivly/core/entity-config");
|
|
5
5
|
let _clivly_core_mappings_config = require("@clivly/core/mappings-config");
|
|
6
|
+
let _clivly_core = require("@clivly/core");
|
|
6
7
|
//#region src/deployment-identity.ts
|
|
7
8
|
const MAX_DEPLOYMENT_ID_LENGTH = 128;
|
|
8
9
|
const PLATFORM_DEPLOYMENT_KEYS = [
|
|
@@ -991,6 +992,240 @@ function describeSilentPush(target, body) {
|
|
|
991
992
|
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.`;
|
|
992
993
|
}
|
|
993
994
|
//#endregion
|
|
995
|
+
//#region src/remote-projections.ts
|
|
996
|
+
const SYNC_CUSTOM_PATH$1 = "/clivly/sdk/sync/custom";
|
|
997
|
+
const MISSING_INTROSPECTOR = "this app cannot run dashboard-authored derived objects yet — add `introspector: drizzleIntrospector(db)` to clivly.config.ts";
|
|
998
|
+
/**
|
|
999
|
+
* Turn definitions fetched from the cloud into runnable sync sources.
|
|
1000
|
+
*
|
|
1001
|
+
* Revalidates every projection locally against the schema THIS app discovered,
|
|
1002
|
+
* never against the cloud's stored copy. Two reasons, and both matter:
|
|
1003
|
+
*
|
|
1004
|
+
* 1. Security. `checkTablesExist`/`checkRefs` bound every table and column ref
|
|
1005
|
+
* to the schema passed in, so validating against the locally-discovered
|
|
1006
|
+
* schema makes the blast radius exactly the namespace the developer handed
|
|
1007
|
+
* to `discoverFromDrizzle`. Trusting the cloud's verdict instead would let a
|
|
1008
|
+
* compromised cloud read any table in the database.
|
|
1009
|
+
* 2. Truth. The stored copy can be stale. A column dropped in the host app
|
|
1010
|
+
* becomes a readable quarantine here instead of a raw Postgres error out of
|
|
1011
|
+
* `sql.raw` at execution time.
|
|
1012
|
+
*/
|
|
1013
|
+
function resolveRemoteProjections(args) {
|
|
1014
|
+
const { entries, introspector, schema } = args;
|
|
1015
|
+
return entries.map((entry) => resolveOne(entry, schema, introspector));
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* The base table's PK, resolved from the schema THIS app discovered — never
|
|
1019
|
+
* from the cloud's copy.
|
|
1020
|
+
*
|
|
1021
|
+
* `validateProjection` proved its uniqueness case against this exact source
|
|
1022
|
+
* (`primaryKeyColumns` over the locally-discovered schema, see
|
|
1023
|
+
* `projection-validate.ts`). Compiling a *different* column — the cloud's
|
|
1024
|
+
* `basePkColumn`, which can go stale the moment a host renames a PK and the
|
|
1025
|
+
* `updatedAt`-only ETag never invalidates — would defeat that proof and
|
|
1026
|
+
* revive the raw-Postgres-error failure mode local revalidation exists to
|
|
1027
|
+
* prevent. Only relevant when the identity is `base_pk` with no explicit
|
|
1028
|
+
* column; every other identity kind ignores `basePkColumn` entirely.
|
|
1029
|
+
*/
|
|
1030
|
+
function resolveLocalBasePk(projection, schema) {
|
|
1031
|
+
if (projection.identity.kind !== "base_pk" || projection.identity.column) return;
|
|
1032
|
+
const baseTable = schema.find((t) => t.name === projection.baseTable);
|
|
1033
|
+
if (!baseTable) return;
|
|
1034
|
+
return (0, _clivly_core.primaryKeyColumns)(baseTable)[0];
|
|
1035
|
+
}
|
|
1036
|
+
function resolveOne(entry, schema, introspector) {
|
|
1037
|
+
const parsed = _clivly_core.projectionSchema.safeParse(entry.projection);
|
|
1038
|
+
if (!parsed.success) return {
|
|
1039
|
+
id: entry.id,
|
|
1040
|
+
status: "invalid",
|
|
1041
|
+
reason: `projection did not parse: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1042
|
+
};
|
|
1043
|
+
const projection = parsed.data;
|
|
1044
|
+
const errors = (0, _clivly_core.validateProjection)(projection, schema, entry.entityKey);
|
|
1045
|
+
if (errors.length > 0) return {
|
|
1046
|
+
id: entry.id,
|
|
1047
|
+
status: "invalid",
|
|
1048
|
+
reason: errors.map((e) => `${e.path}: ${e.message}`).join("; ")
|
|
1049
|
+
};
|
|
1050
|
+
if (!introspector?.projectionSource) return {
|
|
1051
|
+
id: entry.id,
|
|
1052
|
+
status: "skipped",
|
|
1053
|
+
reason: MISSING_INTROSPECTOR
|
|
1054
|
+
};
|
|
1055
|
+
try {
|
|
1056
|
+
const basePkColumn = resolveLocalBasePk(projection, schema) ?? entry.basePkColumn;
|
|
1057
|
+
const source = introspector.projectionSource(projection, {
|
|
1058
|
+
objectType: entry.objectType,
|
|
1059
|
+
...basePkColumn ? { basePkColumn } : {}
|
|
1060
|
+
});
|
|
1061
|
+
return {
|
|
1062
|
+
id: entry.id,
|
|
1063
|
+
status: "ok",
|
|
1064
|
+
source
|
|
1065
|
+
};
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
return {
|
|
1068
|
+
id: entry.id,
|
|
1069
|
+
status: "failed",
|
|
1070
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const PROJECTIONS_PATH = "/clivly/sdk/projections";
|
|
1075
|
+
const PROJECTIONS_REPORT_PATH = "/clivly/sdk/projections/report";
|
|
1076
|
+
const MAX_REASON = 2e3;
|
|
1077
|
+
/**
|
|
1078
|
+
* Merge what resolution decided with what the push actually did.
|
|
1079
|
+
*
|
|
1080
|
+
* A `status: "ok"` outcome means the projection *resolved*, not that its rows
|
|
1081
|
+
* landed. If the push then failed, the object did not sync and must not be
|
|
1082
|
+
* reported as healthy — so the push result wins for those.
|
|
1083
|
+
*/
|
|
1084
|
+
function buildRemoteProjectionReport(args) {
|
|
1085
|
+
return {
|
|
1086
|
+
...args.runId ? { runId: args.runId } : {},
|
|
1087
|
+
results: args.outcomes.map((outcome) => {
|
|
1088
|
+
if (outcome.status !== "ok") return {
|
|
1089
|
+
id: outcome.id,
|
|
1090
|
+
status: outcome.status,
|
|
1091
|
+
reason: outcome.reason.slice(0, MAX_REASON)
|
|
1092
|
+
};
|
|
1093
|
+
const result = args.pushed.get(outcome.source.entity);
|
|
1094
|
+
if (result?.ok) return {
|
|
1095
|
+
id: outcome.id,
|
|
1096
|
+
status: "ok",
|
|
1097
|
+
rows: result.count
|
|
1098
|
+
};
|
|
1099
|
+
return {
|
|
1100
|
+
id: outcome.id,
|
|
1101
|
+
status: "failed",
|
|
1102
|
+
reason: (result?.reason ?? "the push to Clivly failed; rows were read but not stored").slice(0, MAX_REASON)
|
|
1103
|
+
};
|
|
1104
|
+
})
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function createRemoteProjectionFetcher(args) {
|
|
1108
|
+
let etag = null;
|
|
1109
|
+
let cached = [];
|
|
1110
|
+
return { async fetch() {
|
|
1111
|
+
try {
|
|
1112
|
+
const response = await args.fetcher(`${args.apiUrl}${PROJECTIONS_PATH}`, { headers: {
|
|
1113
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
1114
|
+
...etag ? { "if-none-match": etag } : {}
|
|
1115
|
+
} });
|
|
1116
|
+
if (response.status === 304) return cached;
|
|
1117
|
+
if (!response.ok) return [];
|
|
1118
|
+
const body = await response.json();
|
|
1119
|
+
etag = body.etag ?? null;
|
|
1120
|
+
cached = body.projections ?? [];
|
|
1121
|
+
return cached;
|
|
1122
|
+
} catch {
|
|
1123
|
+
return [];
|
|
1124
|
+
}
|
|
1125
|
+
} };
|
|
1126
|
+
}
|
|
1127
|
+
const REMOTE_PAGE_SIZE = 500;
|
|
1128
|
+
/**
|
|
1129
|
+
* Page one source to exhaustion, keyset-paging on identity alone (the id
|
|
1130
|
+
* field, no timestamp). Split out of `pushRemoteProjections` so that
|
|
1131
|
+
* function's own cognitive complexity stays under the lint threshold — this
|
|
1132
|
+
* is the piece that can throw (a `fetchPage` execution error), kept separate
|
|
1133
|
+
* from push-failure handling below.
|
|
1134
|
+
*/
|
|
1135
|
+
async function fetchAllPages(source, pageSize) {
|
|
1136
|
+
const idField = source.idField ?? "id";
|
|
1137
|
+
const all = [];
|
|
1138
|
+
let since = null;
|
|
1139
|
+
for (;;) {
|
|
1140
|
+
const rows = await source.fetchPage({
|
|
1141
|
+
since,
|
|
1142
|
+
limit: pageSize
|
|
1143
|
+
});
|
|
1144
|
+
if (rows.length === 0) break;
|
|
1145
|
+
all.push(...rows);
|
|
1146
|
+
const last = rows.at(-1);
|
|
1147
|
+
const nextId = String(last?.[idField]);
|
|
1148
|
+
if (since && nextId === since.id) break;
|
|
1149
|
+
since = { id: nextId };
|
|
1150
|
+
if (rows.length < pageSize) break;
|
|
1151
|
+
}
|
|
1152
|
+
return all;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Page a remote projection by identity and push one `full` batch.
|
|
1156
|
+
*
|
|
1157
|
+
* `full` rather than `delta` because only a full push lets the server archive
|
|
1158
|
+
* rows that dropped out when a join or filter stopped matching — a delta cannot
|
|
1159
|
+
* express absence. And identity rather than a timestamp because a
|
|
1160
|
+
* dashboard-authored projection has no watermark column, while validation has
|
|
1161
|
+
* already proven the identity unique.
|
|
1162
|
+
*
|
|
1163
|
+
* `pageSize` defaults to `REMOTE_PAGE_SIZE` and exists so a test can force a
|
|
1164
|
+
* short page without either mutating the exported constant or constructing a
|
|
1165
|
+
* fixture hundreds of rows deep.
|
|
1166
|
+
*
|
|
1167
|
+
* Returns one entry per source, keyed by `source.entity`. `count` and `ok`
|
|
1168
|
+
* are independent: `count` is how many rows were pushed (0 for a projection
|
|
1169
|
+
* that legitimately matches nothing this run), `ok` is whether the push
|
|
1170
|
+
* itself succeeded. A caller that folds failures into a report must branch
|
|
1171
|
+
* on `ok`, never on `count === 0` — a correctly-working, legitimately-empty
|
|
1172
|
+
* projection has `ok: true, count: 0` and is not a failure.
|
|
1173
|
+
*
|
|
1174
|
+
* Each source's paging-and-push is wrapped individually: a projection can
|
|
1175
|
+
* validate cleanly and still fail at execution — permission denied, a type
|
|
1176
|
+
* error, a column dropped between validation and this run — and `fetchPage`
|
|
1177
|
+
* throwing is the realistic shape of that. Quarantine that one source as
|
|
1178
|
+
* `{ count: 0, ok: false, reason }` and keep going. The design's promise is
|
|
1179
|
+
* one bad object never takes down the others or the run's report; letting a
|
|
1180
|
+
* throw here escape the loop would break that promise for every source
|
|
1181
|
+
* queued after the failing one.
|
|
1182
|
+
*
|
|
1183
|
+
* Paging and pushing are caught separately, on purpose: `reason` is set only
|
|
1184
|
+
* when paging itself threw (no rows were ever read), so the reported cause
|
|
1185
|
+
* names execution — not the push, which is what actually failed when
|
|
1186
|
+
* `args.push` returns `false`. Conflating the two sent an author whose
|
|
1187
|
+
* projection can't even run to look at their push credentials instead of
|
|
1188
|
+
* their SQL.
|
|
1189
|
+
*/
|
|
1190
|
+
async function pushRemoteProjections(args) {
|
|
1191
|
+
const pageSize = args.pageSize ?? REMOTE_PAGE_SIZE;
|
|
1192
|
+
const pushed = /* @__PURE__ */ new Map();
|
|
1193
|
+
for (const source of args.sources) {
|
|
1194
|
+
let all;
|
|
1195
|
+
try {
|
|
1196
|
+
all = await fetchAllPages(source, pageSize);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1199
|
+
pushed.set(source.entity, {
|
|
1200
|
+
count: 0,
|
|
1201
|
+
ok: false,
|
|
1202
|
+
reason: `the projection failed to execute: ${message}`.slice(0, MAX_REASON)
|
|
1203
|
+
});
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1208
|
+
source: source.entity,
|
|
1209
|
+
rows: all,
|
|
1210
|
+
mode: "full",
|
|
1211
|
+
fetched: all.length,
|
|
1212
|
+
cursor: null,
|
|
1213
|
+
objectType: source.objectType
|
|
1214
|
+
});
|
|
1215
|
+
pushed.set(source.entity, {
|
|
1216
|
+
count: all.length,
|
|
1217
|
+
ok
|
|
1218
|
+
});
|
|
1219
|
+
} catch {
|
|
1220
|
+
pushed.set(source.entity, {
|
|
1221
|
+
count: 0,
|
|
1222
|
+
ok: false
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return pushed;
|
|
1227
|
+
}
|
|
1228
|
+
//#endregion
|
|
994
1229
|
//#region src/self-check.ts
|
|
995
1230
|
function selfCheck(input) {
|
|
996
1231
|
const issues = [];
|
|
@@ -1250,12 +1485,25 @@ function resolveContactEntity(config) {
|
|
|
1250
1485
|
}
|
|
1251
1486
|
function collectSchemaMeta(config) {
|
|
1252
1487
|
const byTable = /* @__PURE__ */ new Map();
|
|
1253
|
-
|
|
1488
|
+
const merge = (name, meta) => {
|
|
1489
|
+
const existing = byTable.get(name);
|
|
1490
|
+
byTable.set(name, {
|
|
1491
|
+
columnsMeta: meta.columnsMeta ?? existing?.columnsMeta,
|
|
1492
|
+
uniqueConstraints: meta.uniqueConstraints ?? existing?.uniqueConstraints
|
|
1493
|
+
});
|
|
1494
|
+
};
|
|
1495
|
+
for (const table of config.schema ?? []) if (table.columnsMeta || table.uniqueConstraints) merge(table.name, {
|
|
1496
|
+
columnsMeta: table.columnsMeta,
|
|
1497
|
+
uniqueConstraints: table.uniqueConstraints
|
|
1498
|
+
});
|
|
1254
1499
|
const sources = [];
|
|
1255
1500
|
if (config.source) sources.push(config.source.contacts, config.source.companies, config.source.deals, ...config.source.custom ?? []);
|
|
1256
1501
|
for (const source of sources) {
|
|
1257
1502
|
const meta = source?.[require_drizzle_meta.DRIZZLE_META];
|
|
1258
|
-
if (meta?.tableName && meta.columnsMeta)
|
|
1503
|
+
if (meta?.tableName && (meta.columnsMeta || meta.uniqueConstraints)) merge(meta.tableName, {
|
|
1504
|
+
columnsMeta: meta.columnsMeta,
|
|
1505
|
+
uniqueConstraints: meta.uniqueConstraints
|
|
1506
|
+
});
|
|
1259
1507
|
}
|
|
1260
1508
|
return byTable;
|
|
1261
1509
|
}
|
|
@@ -1270,12 +1518,13 @@ function buildSchema(config) {
|
|
|
1270
1518
|
if (config.contactEntity && !byName.has(config.contactEntity)) byName.set(config.contactEntity, new Set(config.contactFields ?? []));
|
|
1271
1519
|
const metaByTable = collectSchemaMeta(config);
|
|
1272
1520
|
return [...byName].map(([name, columns]) => {
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1521
|
+
const meta = metaByTable.get(name);
|
|
1522
|
+
for (const col of meta?.columnsMeta ?? []) columns.add(col.name);
|
|
1275
1523
|
return {
|
|
1276
1524
|
name,
|
|
1277
1525
|
columns: [...columns],
|
|
1278
|
-
...columnsMeta ? { columnsMeta } : {}
|
|
1526
|
+
...meta?.columnsMeta ? { columnsMeta: meta.columnsMeta } : {},
|
|
1527
|
+
...meta?.uniqueConstraints ? { uniqueConstraints: meta.uniqueConstraints } : {}
|
|
1279
1528
|
};
|
|
1280
1529
|
});
|
|
1281
1530
|
}
|
|
@@ -1310,6 +1559,24 @@ function timingSafeEqual(a, b) {
|
|
|
1310
1559
|
}
|
|
1311
1560
|
const RUN_ID_HEADER = "x-clivly-run-id";
|
|
1312
1561
|
const SIGNATURE_HEADER = "x-clivly-signature";
|
|
1562
|
+
/**
|
|
1563
|
+
* An error the mounted trigger route should answer with a specific HTTP
|
|
1564
|
+
* status, rather than the generic 500 it uses for a genuinely unhandled
|
|
1565
|
+
* error. A fail-safe rejection (e.g. an unresolvable sample `entityKey`) is a
|
|
1566
|
+
* configuration mismatch the caller can fix — surfacing it as "your app
|
|
1567
|
+
* crashed" sends a developer hunting through their own sync handler instead.
|
|
1568
|
+
*/
|
|
1569
|
+
var TriggerRequestError = class extends Error {
|
|
1570
|
+
status;
|
|
1571
|
+
constructor(status, message) {
|
|
1572
|
+
super(message);
|
|
1573
|
+
this.name = new.target.name;
|
|
1574
|
+
this.status = status;
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
function triggerErrorStatus(error) {
|
|
1578
|
+
return error instanceof TriggerRequestError ? error.status : 500;
|
|
1579
|
+
}
|
|
1313
1580
|
const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
1314
1581
|
const SIGNATURE_HEADER_RE = /^t=(\d+),v1=([0-9a-f]+)$/;
|
|
1315
1582
|
const SECONDS_PER_MS = 1e3;
|
|
@@ -1491,9 +1758,15 @@ function createClivlySDK(config) {
|
|
|
1491
1758
|
const cursorStore = config.cursorStore ?? createMemoryCursorStore();
|
|
1492
1759
|
const fetcher = createFetcher(config.retry);
|
|
1493
1760
|
let warnedUnsignedTrigger = false;
|
|
1761
|
+
const remoteProjections = createRemoteProjectionFetcher({
|
|
1762
|
+
apiKey: config.apiKey,
|
|
1763
|
+
apiUrl,
|
|
1764
|
+
fetcher
|
|
1765
|
+
});
|
|
1494
1766
|
let observedUrl;
|
|
1495
1767
|
let lastPushFailure = null;
|
|
1496
1768
|
let lastPush = { failed: false };
|
|
1769
|
+
let lastRemoteOutcomes = [];
|
|
1497
1770
|
const warnedSilentTargets = /* @__PURE__ */ new Set();
|
|
1498
1771
|
const warnIfSilent = async (target, response) => {
|
|
1499
1772
|
if (typeof response.json !== "function") return;
|
|
@@ -1565,18 +1838,58 @@ function createClivlySDK(config) {
|
|
|
1565
1838
|
for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
1566
1839
|
return failed;
|
|
1567
1840
|
};
|
|
1841
|
+
const syncRemoteSources = async (sources, runId) => {
|
|
1842
|
+
const pushed = await pushRemoteProjections({
|
|
1843
|
+
push: createPush(runId),
|
|
1844
|
+
sources
|
|
1845
|
+
});
|
|
1846
|
+
const failed = [];
|
|
1847
|
+
for (const [entity, result] of pushed) if (!result.ok) failed.push(entity);
|
|
1848
|
+
return {
|
|
1849
|
+
failed,
|
|
1850
|
+
pushed
|
|
1851
|
+
};
|
|
1852
|
+
};
|
|
1853
|
+
const reportRemoteOutcomes = async (outcomes, pushed, runId) => {
|
|
1854
|
+
if (outcomes.length === 0) return;
|
|
1855
|
+
try {
|
|
1856
|
+
const report = buildRemoteProjectionReport({
|
|
1857
|
+
outcomes,
|
|
1858
|
+
pushed,
|
|
1859
|
+
runId
|
|
1860
|
+
});
|
|
1861
|
+
await fetcher(`${apiUrl}${PROJECTIONS_REPORT_PATH}`, {
|
|
1862
|
+
method: "POST",
|
|
1863
|
+
headers: {
|
|
1864
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
1865
|
+
"content-type": "application/json"
|
|
1866
|
+
},
|
|
1867
|
+
body: JSON.stringify(report)
|
|
1868
|
+
});
|
|
1869
|
+
} catch {}
|
|
1870
|
+
};
|
|
1568
1871
|
const runSync = async (options) => {
|
|
1569
|
-
|
|
1872
|
+
const remoteOutcomes = resolveRemoteProjections({
|
|
1873
|
+
entries: await remoteProjections.fetch(),
|
|
1874
|
+
introspector: config.introspector,
|
|
1875
|
+
schema: buildSchema(config)
|
|
1876
|
+
});
|
|
1877
|
+
lastRemoteOutcomes = remoteOutcomes;
|
|
1878
|
+
const remoteSources = remoteOutcomes.filter((o) => o.status === "ok").map((o) => o.source);
|
|
1879
|
+
if (!config.source && remoteSources.length === 0 && remoteOutcomes.length === 0) return;
|
|
1570
1880
|
lastPushFailure = null;
|
|
1571
1881
|
const mode = options?.mode ?? "delta";
|
|
1572
1882
|
const runId = options?.runId;
|
|
1573
1883
|
const failed = [];
|
|
1574
|
-
if (config.source
|
|
1884
|
+
if (config.source?.companies) {
|
|
1575
1885
|
if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode, void 0, runId)) failed.push(config.source.companies.entity);
|
|
1576
1886
|
}
|
|
1577
|
-
if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1578
|
-
if (config.source
|
|
1579
|
-
failed.push(...await syncCustomSources(config.source
|
|
1887
|
+
if (config.source?.contacts && !await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1888
|
+
if (config.source?.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
|
|
1889
|
+
failed.push(...await syncCustomSources(config.source?.custom, mode, runId));
|
|
1890
|
+
const remote = await syncRemoteSources(remoteSources, runId);
|
|
1891
|
+
failed.push(...remote.failed);
|
|
1892
|
+
await reportRemoteOutcomes(lastRemoteOutcomes, remote.pushed, runId);
|
|
1580
1893
|
if (failed.length > 0) {
|
|
1581
1894
|
const message = describePushFailure(failed);
|
|
1582
1895
|
lastPush = {
|
|
@@ -1589,7 +1902,7 @@ function createClivlySDK(config) {
|
|
|
1589
1902
|
};
|
|
1590
1903
|
const runSampleSync = async (entityKey, runId) => {
|
|
1591
1904
|
const resolved = resolveSampleSource(config, entityKey);
|
|
1592
|
-
if (!resolved) throw new
|
|
1905
|
+
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.`);
|
|
1593
1906
|
lastPushFailure = null;
|
|
1594
1907
|
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]));
|
|
1595
1908
|
};
|
|
@@ -1602,6 +1915,20 @@ function createClivlySDK(config) {
|
|
|
1602
1915
|
observedUrl
|
|
1603
1916
|
})
|
|
1604
1917
|
});
|
|
1918
|
+
const describeRemoteProjections = () => {
|
|
1919
|
+
const counts = {
|
|
1920
|
+
failed: 0,
|
|
1921
|
+
invalid: 0,
|
|
1922
|
+
ok: 0,
|
|
1923
|
+
skipped: 0
|
|
1924
|
+
};
|
|
1925
|
+
for (const outcome of lastRemoteOutcomes) counts[outcome.status] += 1;
|
|
1926
|
+
return {
|
|
1927
|
+
capable: Boolean(config.introspector?.projectionSource),
|
|
1928
|
+
ran: lastRemoteOutcomes.length > 0,
|
|
1929
|
+
...counts
|
|
1930
|
+
};
|
|
1931
|
+
};
|
|
1605
1932
|
const sendHeartbeat = () => {
|
|
1606
1933
|
const triggerUrl = describeSyncTrigger().resolved;
|
|
1607
1934
|
return fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
|
|
@@ -1800,7 +2127,7 @@ function createClivlySDK(config) {
|
|
|
1800
2127
|
});
|
|
1801
2128
|
const runScheduled = async (options) => {
|
|
1802
2129
|
await heartbeat();
|
|
1803
|
-
if (config.source) await runSync(options);
|
|
2130
|
+
if (config.source || config.introspector) await runSync(options);
|
|
1804
2131
|
};
|
|
1805
2132
|
const triggerRequestAllowed = async (req, rawBody) => {
|
|
1806
2133
|
const secret = config.syncTrigger?.secret;
|
|
@@ -1814,7 +2141,9 @@ function createClivlySDK(config) {
|
|
|
1814
2141
|
const parseTriggerBody = (rawBody) => {
|
|
1815
2142
|
if (!rawBody) return {};
|
|
1816
2143
|
try {
|
|
1817
|
-
|
|
2144
|
+
const parsed = JSON.parse(rawBody);
|
|
2145
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2146
|
+
return parsed;
|
|
1818
2147
|
} catch {
|
|
1819
2148
|
return {};
|
|
1820
2149
|
}
|
|
@@ -1847,7 +2176,7 @@ function createClivlySDK(config) {
|
|
|
1847
2176
|
ok: false,
|
|
1848
2177
|
runId,
|
|
1849
2178
|
error: error instanceof Error ? error.message : String(error)
|
|
1850
|
-
},
|
|
2179
|
+
}, triggerErrorStatus(error));
|
|
1851
2180
|
}
|
|
1852
2181
|
};
|
|
1853
2182
|
const start = async () => {
|
|
@@ -1894,7 +2223,7 @@ function createClivlySDK(config) {
|
|
|
1894
2223
|
presenceClient = null;
|
|
1895
2224
|
console.warn("Clivly presence socket unavailable; using HTTP heartbeat", error);
|
|
1896
2225
|
}
|
|
1897
|
-
if (!timers.sync && config.source) {
|
|
2226
|
+
if (!timers.sync && (config.source || config.introspector)) {
|
|
1898
2227
|
timers.sync = setInterval(() => {
|
|
1899
2228
|
runSync().catch(() => {});
|
|
1900
2229
|
}, syncIntervalMs);
|
|
@@ -1928,6 +2257,7 @@ function createClivlySDK(config) {
|
|
|
1928
2257
|
ping,
|
|
1929
2258
|
doctor,
|
|
1930
2259
|
describeSyncTrigger,
|
|
2260
|
+
describeRemoteProjections,
|
|
1931
2261
|
runScheduled,
|
|
1932
2262
|
createClivlyHandler,
|
|
1933
2263
|
sync: runSync,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-B7HKG2Ph.cjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.cjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/trigger-url.d.ts
|
|
@@ -34,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
34
34
|
*/
|
|
35
35
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
36
36
|
//#endregion
|
|
37
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, type SyncTriggerDescription, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
|
37
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, type RemoteProjectionsDescription, type SyncTriggerDescription, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, g as RemoteProjectionsDescription, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable, v as SyncTriggerDescription } from "./types-B7HKG2Ph.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/trigger-url.d.ts
|
|
@@ -34,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
34
34
|
*/
|
|
35
35
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
36
36
|
//#endregion
|
|
37
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, type SyncTriggerDescription, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
|
37
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, type RemoteProjectionsDescription, type SyncTriggerDescription, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|