@clivly/sdk 0.9.6 → 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 +33 -12
- 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 +322 -12
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +322 -12
- 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.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
|
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
3
|
import { customObjectDeclarations, customObjectDeclarations as customObjectDeclarations$1, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
4
|
import { entitiesToMappings } from "@clivly/core/mappings-config";
|
|
5
|
+
import { primaryKeyColumns, projectionSchema, validateProjection } from "@clivly/core";
|
|
5
6
|
//#region src/deployment-identity.ts
|
|
6
7
|
const MAX_DEPLOYMENT_ID_LENGTH = 128;
|
|
7
8
|
const PLATFORM_DEPLOYMENT_KEYS = [
|
|
@@ -990,6 +991,240 @@ function describeSilentPush(target, body) {
|
|
|
990
991
|
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.`;
|
|
991
992
|
}
|
|
992
993
|
//#endregion
|
|
994
|
+
//#region src/remote-projections.ts
|
|
995
|
+
const SYNC_CUSTOM_PATH$1 = "/clivly/sdk/sync/custom";
|
|
996
|
+
const MISSING_INTROSPECTOR = "this app cannot run dashboard-authored derived objects yet — add `introspector: drizzleIntrospector(db)` to clivly.config.ts";
|
|
997
|
+
/**
|
|
998
|
+
* Turn definitions fetched from the cloud into runnable sync sources.
|
|
999
|
+
*
|
|
1000
|
+
* Revalidates every projection locally against the schema THIS app discovered,
|
|
1001
|
+
* never against the cloud's stored copy. Two reasons, and both matter:
|
|
1002
|
+
*
|
|
1003
|
+
* 1. Security. `checkTablesExist`/`checkRefs` bound every table and column ref
|
|
1004
|
+
* to the schema passed in, so validating against the locally-discovered
|
|
1005
|
+
* schema makes the blast radius exactly the namespace the developer handed
|
|
1006
|
+
* to `discoverFromDrizzle`. Trusting the cloud's verdict instead would let a
|
|
1007
|
+
* compromised cloud read any table in the database.
|
|
1008
|
+
* 2. Truth. The stored copy can be stale. A column dropped in the host app
|
|
1009
|
+
* becomes a readable quarantine here instead of a raw Postgres error out of
|
|
1010
|
+
* `sql.raw` at execution time.
|
|
1011
|
+
*/
|
|
1012
|
+
function resolveRemoteProjections(args) {
|
|
1013
|
+
const { entries, introspector, schema } = args;
|
|
1014
|
+
return entries.map((entry) => resolveOne(entry, schema, introspector));
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* The base table's PK, resolved from the schema THIS app discovered — never
|
|
1018
|
+
* from the cloud's copy.
|
|
1019
|
+
*
|
|
1020
|
+
* `validateProjection` proved its uniqueness case against this exact source
|
|
1021
|
+
* (`primaryKeyColumns` over the locally-discovered schema, see
|
|
1022
|
+
* `projection-validate.ts`). Compiling a *different* column — the cloud's
|
|
1023
|
+
* `basePkColumn`, which can go stale the moment a host renames a PK and the
|
|
1024
|
+
* `updatedAt`-only ETag never invalidates — would defeat that proof and
|
|
1025
|
+
* revive the raw-Postgres-error failure mode local revalidation exists to
|
|
1026
|
+
* prevent. Only relevant when the identity is `base_pk` with no explicit
|
|
1027
|
+
* column; every other identity kind ignores `basePkColumn` entirely.
|
|
1028
|
+
*/
|
|
1029
|
+
function resolveLocalBasePk(projection, schema) {
|
|
1030
|
+
if (projection.identity.kind !== "base_pk" || projection.identity.column) return;
|
|
1031
|
+
const baseTable = schema.find((t) => t.name === projection.baseTable);
|
|
1032
|
+
if (!baseTable) return;
|
|
1033
|
+
return primaryKeyColumns(baseTable)[0];
|
|
1034
|
+
}
|
|
1035
|
+
function resolveOne(entry, schema, introspector) {
|
|
1036
|
+
const parsed = projectionSchema.safeParse(entry.projection);
|
|
1037
|
+
if (!parsed.success) return {
|
|
1038
|
+
id: entry.id,
|
|
1039
|
+
status: "invalid",
|
|
1040
|
+
reason: `projection did not parse: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
|
|
1041
|
+
};
|
|
1042
|
+
const projection = parsed.data;
|
|
1043
|
+
const errors = validateProjection(projection, schema, entry.entityKey);
|
|
1044
|
+
if (errors.length > 0) return {
|
|
1045
|
+
id: entry.id,
|
|
1046
|
+
status: "invalid",
|
|
1047
|
+
reason: errors.map((e) => `${e.path}: ${e.message}`).join("; ")
|
|
1048
|
+
};
|
|
1049
|
+
if (!introspector?.projectionSource) return {
|
|
1050
|
+
id: entry.id,
|
|
1051
|
+
status: "skipped",
|
|
1052
|
+
reason: MISSING_INTROSPECTOR
|
|
1053
|
+
};
|
|
1054
|
+
try {
|
|
1055
|
+
const basePkColumn = resolveLocalBasePk(projection, schema) ?? entry.basePkColumn;
|
|
1056
|
+
const source = introspector.projectionSource(projection, {
|
|
1057
|
+
objectType: entry.objectType,
|
|
1058
|
+
...basePkColumn ? { basePkColumn } : {}
|
|
1059
|
+
});
|
|
1060
|
+
return {
|
|
1061
|
+
id: entry.id,
|
|
1062
|
+
status: "ok",
|
|
1063
|
+
source
|
|
1064
|
+
};
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
return {
|
|
1067
|
+
id: entry.id,
|
|
1068
|
+
status: "failed",
|
|
1069
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const PROJECTIONS_PATH = "/clivly/sdk/projections";
|
|
1074
|
+
const PROJECTIONS_REPORT_PATH = "/clivly/sdk/projections/report";
|
|
1075
|
+
const MAX_REASON = 2e3;
|
|
1076
|
+
/**
|
|
1077
|
+
* Merge what resolution decided with what the push actually did.
|
|
1078
|
+
*
|
|
1079
|
+
* A `status: "ok"` outcome means the projection *resolved*, not that its rows
|
|
1080
|
+
* landed. If the push then failed, the object did not sync and must not be
|
|
1081
|
+
* reported as healthy — so the push result wins for those.
|
|
1082
|
+
*/
|
|
1083
|
+
function buildRemoteProjectionReport(args) {
|
|
1084
|
+
return {
|
|
1085
|
+
...args.runId ? { runId: args.runId } : {},
|
|
1086
|
+
results: args.outcomes.map((outcome) => {
|
|
1087
|
+
if (outcome.status !== "ok") return {
|
|
1088
|
+
id: outcome.id,
|
|
1089
|
+
status: outcome.status,
|
|
1090
|
+
reason: outcome.reason.slice(0, MAX_REASON)
|
|
1091
|
+
};
|
|
1092
|
+
const result = args.pushed.get(outcome.source.entity);
|
|
1093
|
+
if (result?.ok) return {
|
|
1094
|
+
id: outcome.id,
|
|
1095
|
+
status: "ok",
|
|
1096
|
+
rows: result.count
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
id: outcome.id,
|
|
1100
|
+
status: "failed",
|
|
1101
|
+
reason: (result?.reason ?? "the push to Clivly failed; rows were read but not stored").slice(0, MAX_REASON)
|
|
1102
|
+
};
|
|
1103
|
+
})
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
function createRemoteProjectionFetcher(args) {
|
|
1107
|
+
let etag = null;
|
|
1108
|
+
let cached = [];
|
|
1109
|
+
return { async fetch() {
|
|
1110
|
+
try {
|
|
1111
|
+
const response = await args.fetcher(`${args.apiUrl}${PROJECTIONS_PATH}`, { headers: {
|
|
1112
|
+
authorization: `Bearer ${args.apiKey}`,
|
|
1113
|
+
...etag ? { "if-none-match": etag } : {}
|
|
1114
|
+
} });
|
|
1115
|
+
if (response.status === 304) return cached;
|
|
1116
|
+
if (!response.ok) return [];
|
|
1117
|
+
const body = await response.json();
|
|
1118
|
+
etag = body.etag ?? null;
|
|
1119
|
+
cached = body.projections ?? [];
|
|
1120
|
+
return cached;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
} };
|
|
1125
|
+
}
|
|
1126
|
+
const REMOTE_PAGE_SIZE = 500;
|
|
1127
|
+
/**
|
|
1128
|
+
* Page one source to exhaustion, keyset-paging on identity alone (the id
|
|
1129
|
+
* field, no timestamp). Split out of `pushRemoteProjections` so that
|
|
1130
|
+
* function's own cognitive complexity stays under the lint threshold — this
|
|
1131
|
+
* is the piece that can throw (a `fetchPage` execution error), kept separate
|
|
1132
|
+
* from push-failure handling below.
|
|
1133
|
+
*/
|
|
1134
|
+
async function fetchAllPages(source, pageSize) {
|
|
1135
|
+
const idField = source.idField ?? "id";
|
|
1136
|
+
const all = [];
|
|
1137
|
+
let since = null;
|
|
1138
|
+
for (;;) {
|
|
1139
|
+
const rows = await source.fetchPage({
|
|
1140
|
+
since,
|
|
1141
|
+
limit: pageSize
|
|
1142
|
+
});
|
|
1143
|
+
if (rows.length === 0) break;
|
|
1144
|
+
all.push(...rows);
|
|
1145
|
+
const last = rows.at(-1);
|
|
1146
|
+
const nextId = String(last?.[idField]);
|
|
1147
|
+
if (since && nextId === since.id) break;
|
|
1148
|
+
since = { id: nextId };
|
|
1149
|
+
if (rows.length < pageSize) break;
|
|
1150
|
+
}
|
|
1151
|
+
return all;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Page a remote projection by identity and push one `full` batch.
|
|
1155
|
+
*
|
|
1156
|
+
* `full` rather than `delta` because only a full push lets the server archive
|
|
1157
|
+
* rows that dropped out when a join or filter stopped matching — a delta cannot
|
|
1158
|
+
* express absence. And identity rather than a timestamp because a
|
|
1159
|
+
* dashboard-authored projection has no watermark column, while validation has
|
|
1160
|
+
* already proven the identity unique.
|
|
1161
|
+
*
|
|
1162
|
+
* `pageSize` defaults to `REMOTE_PAGE_SIZE` and exists so a test can force a
|
|
1163
|
+
* short page without either mutating the exported constant or constructing a
|
|
1164
|
+
* fixture hundreds of rows deep.
|
|
1165
|
+
*
|
|
1166
|
+
* Returns one entry per source, keyed by `source.entity`. `count` and `ok`
|
|
1167
|
+
* are independent: `count` is how many rows were pushed (0 for a projection
|
|
1168
|
+
* that legitimately matches nothing this run), `ok` is whether the push
|
|
1169
|
+
* itself succeeded. A caller that folds failures into a report must branch
|
|
1170
|
+
* on `ok`, never on `count === 0` — a correctly-working, legitimately-empty
|
|
1171
|
+
* projection has `ok: true, count: 0` and is not a failure.
|
|
1172
|
+
*
|
|
1173
|
+
* Each source's paging-and-push is wrapped individually: a projection can
|
|
1174
|
+
* validate cleanly and still fail at execution — permission denied, a type
|
|
1175
|
+
* error, a column dropped between validation and this run — and `fetchPage`
|
|
1176
|
+
* throwing is the realistic shape of that. Quarantine that one source as
|
|
1177
|
+
* `{ count: 0, ok: false, reason }` and keep going. The design's promise is
|
|
1178
|
+
* one bad object never takes down the others or the run's report; letting a
|
|
1179
|
+
* throw here escape the loop would break that promise for every source
|
|
1180
|
+
* queued after the failing one.
|
|
1181
|
+
*
|
|
1182
|
+
* Paging and pushing are caught separately, on purpose: `reason` is set only
|
|
1183
|
+
* when paging itself threw (no rows were ever read), so the reported cause
|
|
1184
|
+
* names execution — not the push, which is what actually failed when
|
|
1185
|
+
* `args.push` returns `false`. Conflating the two sent an author whose
|
|
1186
|
+
* projection can't even run to look at their push credentials instead of
|
|
1187
|
+
* their SQL.
|
|
1188
|
+
*/
|
|
1189
|
+
async function pushRemoteProjections(args) {
|
|
1190
|
+
const pageSize = args.pageSize ?? REMOTE_PAGE_SIZE;
|
|
1191
|
+
const pushed = /* @__PURE__ */ new Map();
|
|
1192
|
+
for (const source of args.sources) {
|
|
1193
|
+
let all;
|
|
1194
|
+
try {
|
|
1195
|
+
all = await fetchAllPages(source, pageSize);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1198
|
+
pushed.set(source.entity, {
|
|
1199
|
+
count: 0,
|
|
1200
|
+
ok: false,
|
|
1201
|
+
reason: `the projection failed to execute: ${message}`.slice(0, MAX_REASON)
|
|
1202
|
+
});
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
const ok = await args.push(SYNC_CUSTOM_PATH$1, {
|
|
1207
|
+
source: source.entity,
|
|
1208
|
+
rows: all,
|
|
1209
|
+
mode: "full",
|
|
1210
|
+
fetched: all.length,
|
|
1211
|
+
cursor: null,
|
|
1212
|
+
objectType: source.objectType
|
|
1213
|
+
});
|
|
1214
|
+
pushed.set(source.entity, {
|
|
1215
|
+
count: all.length,
|
|
1216
|
+
ok
|
|
1217
|
+
});
|
|
1218
|
+
} catch {
|
|
1219
|
+
pushed.set(source.entity, {
|
|
1220
|
+
count: 0,
|
|
1221
|
+
ok: false
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return pushed;
|
|
1226
|
+
}
|
|
1227
|
+
//#endregion
|
|
993
1228
|
//#region src/self-check.ts
|
|
994
1229
|
function selfCheck(input) {
|
|
995
1230
|
const issues = [];
|
|
@@ -1249,12 +1484,25 @@ function resolveContactEntity(config) {
|
|
|
1249
1484
|
}
|
|
1250
1485
|
function collectSchemaMeta(config) {
|
|
1251
1486
|
const byTable = /* @__PURE__ */ new Map();
|
|
1252
|
-
|
|
1487
|
+
const merge = (name, meta) => {
|
|
1488
|
+
const existing = byTable.get(name);
|
|
1489
|
+
byTable.set(name, {
|
|
1490
|
+
columnsMeta: meta.columnsMeta ?? existing?.columnsMeta,
|
|
1491
|
+
uniqueConstraints: meta.uniqueConstraints ?? existing?.uniqueConstraints
|
|
1492
|
+
});
|
|
1493
|
+
};
|
|
1494
|
+
for (const table of config.schema ?? []) if (table.columnsMeta || table.uniqueConstraints) merge(table.name, {
|
|
1495
|
+
columnsMeta: table.columnsMeta,
|
|
1496
|
+
uniqueConstraints: table.uniqueConstraints
|
|
1497
|
+
});
|
|
1253
1498
|
const sources = [];
|
|
1254
1499
|
if (config.source) sources.push(config.source.contacts, config.source.companies, config.source.deals, ...config.source.custom ?? []);
|
|
1255
1500
|
for (const source of sources) {
|
|
1256
1501
|
const meta = source?.[DRIZZLE_META];
|
|
1257
|
-
if (meta?.tableName && meta.columnsMeta)
|
|
1502
|
+
if (meta?.tableName && (meta.columnsMeta || meta.uniqueConstraints)) merge(meta.tableName, {
|
|
1503
|
+
columnsMeta: meta.columnsMeta,
|
|
1504
|
+
uniqueConstraints: meta.uniqueConstraints
|
|
1505
|
+
});
|
|
1258
1506
|
}
|
|
1259
1507
|
return byTable;
|
|
1260
1508
|
}
|
|
@@ -1269,12 +1517,13 @@ function buildSchema(config) {
|
|
|
1269
1517
|
if (config.contactEntity && !byName.has(config.contactEntity)) byName.set(config.contactEntity, new Set(config.contactFields ?? []));
|
|
1270
1518
|
const metaByTable = collectSchemaMeta(config);
|
|
1271
1519
|
return [...byName].map(([name, columns]) => {
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1520
|
+
const meta = metaByTable.get(name);
|
|
1521
|
+
for (const col of meta?.columnsMeta ?? []) columns.add(col.name);
|
|
1274
1522
|
return {
|
|
1275
1523
|
name,
|
|
1276
1524
|
columns: [...columns],
|
|
1277
|
-
...columnsMeta ? { columnsMeta } : {}
|
|
1525
|
+
...meta?.columnsMeta ? { columnsMeta: meta.columnsMeta } : {},
|
|
1526
|
+
...meta?.uniqueConstraints ? { uniqueConstraints: meta.uniqueConstraints } : {}
|
|
1278
1527
|
};
|
|
1279
1528
|
});
|
|
1280
1529
|
}
|
|
@@ -1490,9 +1739,15 @@ function createClivlySDK(config) {
|
|
|
1490
1739
|
const cursorStore = config.cursorStore ?? createMemoryCursorStore();
|
|
1491
1740
|
const fetcher = createFetcher(config.retry);
|
|
1492
1741
|
let warnedUnsignedTrigger = false;
|
|
1742
|
+
const remoteProjections = createRemoteProjectionFetcher({
|
|
1743
|
+
apiKey: config.apiKey,
|
|
1744
|
+
apiUrl,
|
|
1745
|
+
fetcher
|
|
1746
|
+
});
|
|
1493
1747
|
let observedUrl;
|
|
1494
1748
|
let lastPushFailure = null;
|
|
1495
1749
|
let lastPush = { failed: false };
|
|
1750
|
+
let lastRemoteOutcomes = [];
|
|
1496
1751
|
const warnedSilentTargets = /* @__PURE__ */ new Set();
|
|
1497
1752
|
const warnIfSilent = async (target, response) => {
|
|
1498
1753
|
if (typeof response.json !== "function") return;
|
|
@@ -1564,18 +1819,58 @@ function createClivlySDK(config) {
|
|
|
1564
1819
|
for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
|
|
1565
1820
|
return failed;
|
|
1566
1821
|
};
|
|
1822
|
+
const syncRemoteSources = async (sources, runId) => {
|
|
1823
|
+
const pushed = await pushRemoteProjections({
|
|
1824
|
+
push: createPush(runId),
|
|
1825
|
+
sources
|
|
1826
|
+
});
|
|
1827
|
+
const failed = [];
|
|
1828
|
+
for (const [entity, result] of pushed) if (!result.ok) failed.push(entity);
|
|
1829
|
+
return {
|
|
1830
|
+
failed,
|
|
1831
|
+
pushed
|
|
1832
|
+
};
|
|
1833
|
+
};
|
|
1834
|
+
const reportRemoteOutcomes = async (outcomes, pushed, runId) => {
|
|
1835
|
+
if (outcomes.length === 0) return;
|
|
1836
|
+
try {
|
|
1837
|
+
const report = buildRemoteProjectionReport({
|
|
1838
|
+
outcomes,
|
|
1839
|
+
pushed,
|
|
1840
|
+
runId
|
|
1841
|
+
});
|
|
1842
|
+
await fetcher(`${apiUrl}${PROJECTIONS_REPORT_PATH}`, {
|
|
1843
|
+
method: "POST",
|
|
1844
|
+
headers: {
|
|
1845
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
1846
|
+
"content-type": "application/json"
|
|
1847
|
+
},
|
|
1848
|
+
body: JSON.stringify(report)
|
|
1849
|
+
});
|
|
1850
|
+
} catch {}
|
|
1851
|
+
};
|
|
1567
1852
|
const runSync = async (options) => {
|
|
1568
|
-
|
|
1853
|
+
const remoteOutcomes = resolveRemoteProjections({
|
|
1854
|
+
entries: await remoteProjections.fetch(),
|
|
1855
|
+
introspector: config.introspector,
|
|
1856
|
+
schema: buildSchema(config)
|
|
1857
|
+
});
|
|
1858
|
+
lastRemoteOutcomes = remoteOutcomes;
|
|
1859
|
+
const remoteSources = remoteOutcomes.filter((o) => o.status === "ok").map((o) => o.source);
|
|
1860
|
+
if (!config.source && remoteSources.length === 0 && remoteOutcomes.length === 0) return;
|
|
1569
1861
|
lastPushFailure = null;
|
|
1570
1862
|
const mode = options?.mode ?? "delta";
|
|
1571
1863
|
const runId = options?.runId;
|
|
1572
1864
|
const failed = [];
|
|
1573
|
-
if (config.source
|
|
1865
|
+
if (config.source?.companies) {
|
|
1574
1866
|
if (!await syncOne(config.source.companies, SYNC_COMPANIES_PATH, mode, void 0, runId)) failed.push(config.source.companies.entity);
|
|
1575
1867
|
}
|
|
1576
|
-
if (!await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1577
|
-
if (config.source
|
|
1578
|
-
failed.push(...await syncCustomSources(config.source
|
|
1868
|
+
if (config.source?.contacts && !await syncOne(config.source.contacts, SYNC_CONTACTS_PATH, mode, void 0, runId)) failed.push(config.source.contacts.entity);
|
|
1869
|
+
if (config.source?.deals && !await syncOne(config.source.deals, SYNC_DEALS_PATH, mode, void 0, runId)) failed.push(config.source.deals.entity);
|
|
1870
|
+
failed.push(...await syncCustomSources(config.source?.custom, mode, runId));
|
|
1871
|
+
const remote = await syncRemoteSources(remoteSources, runId);
|
|
1872
|
+
failed.push(...remote.failed);
|
|
1873
|
+
await reportRemoteOutcomes(lastRemoteOutcomes, remote.pushed, runId);
|
|
1579
1874
|
if (failed.length > 0) {
|
|
1580
1875
|
const message = describePushFailure(failed);
|
|
1581
1876
|
lastPush = {
|
|
@@ -1601,6 +1896,20 @@ function createClivlySDK(config) {
|
|
|
1601
1896
|
observedUrl
|
|
1602
1897
|
})
|
|
1603
1898
|
});
|
|
1899
|
+
const describeRemoteProjections = () => {
|
|
1900
|
+
const counts = {
|
|
1901
|
+
failed: 0,
|
|
1902
|
+
invalid: 0,
|
|
1903
|
+
ok: 0,
|
|
1904
|
+
skipped: 0
|
|
1905
|
+
};
|
|
1906
|
+
for (const outcome of lastRemoteOutcomes) counts[outcome.status] += 1;
|
|
1907
|
+
return {
|
|
1908
|
+
capable: Boolean(config.introspector?.projectionSource),
|
|
1909
|
+
ran: lastRemoteOutcomes.length > 0,
|
|
1910
|
+
...counts
|
|
1911
|
+
};
|
|
1912
|
+
};
|
|
1604
1913
|
const sendHeartbeat = () => {
|
|
1605
1914
|
const triggerUrl = describeSyncTrigger().resolved;
|
|
1606
1915
|
return fetcher(`${apiUrl}/clivly/sdk/heartbeat`, {
|
|
@@ -1799,7 +2108,7 @@ function createClivlySDK(config) {
|
|
|
1799
2108
|
});
|
|
1800
2109
|
const runScheduled = async (options) => {
|
|
1801
2110
|
await heartbeat();
|
|
1802
|
-
if (config.source) await runSync(options);
|
|
2111
|
+
if (config.source || config.introspector) await runSync(options);
|
|
1803
2112
|
};
|
|
1804
2113
|
const triggerRequestAllowed = async (req, rawBody) => {
|
|
1805
2114
|
const secret = config.syncTrigger?.secret;
|
|
@@ -1893,7 +2202,7 @@ function createClivlySDK(config) {
|
|
|
1893
2202
|
presenceClient = null;
|
|
1894
2203
|
console.warn("Clivly presence socket unavailable; using HTTP heartbeat", error);
|
|
1895
2204
|
}
|
|
1896
|
-
if (!timers.sync && config.source) {
|
|
2205
|
+
if (!timers.sync && (config.source || config.introspector)) {
|
|
1897
2206
|
timers.sync = setInterval(() => {
|
|
1898
2207
|
runSync().catch(() => {});
|
|
1899
2208
|
}, syncIntervalMs);
|
|
@@ -1927,6 +2236,7 @@ function createClivlySDK(config) {
|
|
|
1927
2236
|
ping,
|
|
1928
2237
|
doctor,
|
|
1929
2238
|
describeSyncTrigger,
|
|
2239
|
+
describeRemoteProjections,
|
|
1930
2240
|
runScheduled,
|
|
1931
2241
|
createClivlyHandler,
|
|
1932
2242
|
sync: runSync,
|
|
@@ -18,6 +18,26 @@ interface DrizzleSourceMeta {
|
|
|
18
18
|
interface NamespaceIntrospector {
|
|
19
19
|
/** All base table names in the reachable schema. */
|
|
20
20
|
listTables(): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Build a runnable sync source from a projection, for derived objects
|
|
23
|
+
* authored in the dashboard rather than in `clivly.config.ts`.
|
|
24
|
+
*
|
|
25
|
+
* Optional for the same reason `runProjection` is: an introspector may be
|
|
26
|
+
* hand-written. Absent, remote projections report `skipped` with a reason
|
|
27
|
+
* naming this method, rather than failing the run.
|
|
28
|
+
*
|
|
29
|
+
* This is deliberately the only route to a database handle for remote
|
|
30
|
+
* projections. `drizzleIntrospector(db)` already closes over the
|
|
31
|
+
* connection, so `introspector:` is the single config line that turns
|
|
32
|
+
* dashboard-authored objects on — and the SDK core never holds a raw
|
|
33
|
+
* client.
|
|
34
|
+
*/
|
|
35
|
+
projectionSource?(projection: ProjectionMapping, opts: {
|
|
36
|
+
basePkColumn?: string;
|
|
37
|
+
objectType: string;
|
|
38
|
+
}): SyncSource & {
|
|
39
|
+
objectType: string;
|
|
40
|
+
};
|
|
21
41
|
/**
|
|
22
42
|
* Read a few rows through an unsaved projection, for the dashboard's derived
|
|
23
43
|
* object preview. Rows are keyed by field name.
|
|
@@ -378,6 +398,17 @@ interface SyncTriggerDescription {
|
|
|
378
398
|
*/
|
|
379
399
|
resolved?: string;
|
|
380
400
|
}
|
|
401
|
+
/** @see ClivlySDK.describeRemoteProjections */
|
|
402
|
+
interface RemoteProjectionsDescription {
|
|
403
|
+
/** False when no `introspector` is configured — nothing can run. */
|
|
404
|
+
capable: boolean;
|
|
405
|
+
failed: number;
|
|
406
|
+
invalid: number;
|
|
407
|
+
ok: number;
|
|
408
|
+
/** False until a sync has run in this process. */
|
|
409
|
+
ran: boolean;
|
|
410
|
+
skipped: number;
|
|
411
|
+
}
|
|
381
412
|
interface ClivlySDK {
|
|
382
413
|
/**
|
|
383
414
|
* Send a single heartbeat and report the outcome richly: `ok`, the HTTP
|
|
@@ -413,6 +444,15 @@ interface ClivlySDK {
|
|
|
413
444
|
* anyone who can reach it — set a secret if it's publicly routable.
|
|
414
445
|
*/
|
|
415
446
|
createClivlyHandler(): (req: Request) => Promise<Response>;
|
|
447
|
+
/**
|
|
448
|
+
* What this app would tell Clivly about dashboard-authored derived objects,
|
|
449
|
+
* right now. Counts rather than the outcomes themselves: this reports
|
|
450
|
+
* health, and the per-object reasons already travel to the dashboard, which
|
|
451
|
+
* is where an author fixes them.
|
|
452
|
+
*
|
|
453
|
+
* Read by `clivly status`.
|
|
454
|
+
*/
|
|
455
|
+
describeRemoteProjections(): RemoteProjectionsDescription;
|
|
416
456
|
/**
|
|
417
457
|
* What this app would tell Clivly about its sync trigger route, right now.
|
|
418
458
|
*
|
|
@@ -497,4 +537,4 @@ interface ClivlySDK {
|
|
|
497
537
|
}): Promise<void>;
|
|
498
538
|
}
|
|
499
539
|
//#endregion
|
|
500
|
-
export {
|
|
540
|
+
export { SyncSource as _, ClivlySDK as a, CreateChatSessionHandlerOptions as c, DoctorCheck as d, DoctorReport as f, RemoteProjectionsDescription as g, EnsuredObjectTypeResult as h, ClivlyConnectResult as i, CursorStore as l, EnsuredMappingResult as m, ChatWidgetVisitor as n, ClivlySDKConfig as o, EnsureSchemaResult as p, ClivlyConnectReason as r, ClivlyUser as s, ChatSessionRequestBody as t, DiscoveredTable as u, SyncTriggerDescription as v, NamespaceIntrospector as y };
|
|
@@ -18,6 +18,26 @@ interface DrizzleSourceMeta {
|
|
|
18
18
|
interface NamespaceIntrospector {
|
|
19
19
|
/** All base table names in the reachable schema. */
|
|
20
20
|
listTables(): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Build a runnable sync source from a projection, for derived objects
|
|
23
|
+
* authored in the dashboard rather than in `clivly.config.ts`.
|
|
24
|
+
*
|
|
25
|
+
* Optional for the same reason `runProjection` is: an introspector may be
|
|
26
|
+
* hand-written. Absent, remote projections report `skipped` with a reason
|
|
27
|
+
* naming this method, rather than failing the run.
|
|
28
|
+
*
|
|
29
|
+
* This is deliberately the only route to a database handle for remote
|
|
30
|
+
* projections. `drizzleIntrospector(db)` already closes over the
|
|
31
|
+
* connection, so `introspector:` is the single config line that turns
|
|
32
|
+
* dashboard-authored objects on — and the SDK core never holds a raw
|
|
33
|
+
* client.
|
|
34
|
+
*/
|
|
35
|
+
projectionSource?(projection: ProjectionMapping, opts: {
|
|
36
|
+
basePkColumn?: string;
|
|
37
|
+
objectType: string;
|
|
38
|
+
}): SyncSource & {
|
|
39
|
+
objectType: string;
|
|
40
|
+
};
|
|
21
41
|
/**
|
|
22
42
|
* Read a few rows through an unsaved projection, for the dashboard's derived
|
|
23
43
|
* object preview. Rows are keyed by field name.
|
|
@@ -378,6 +398,17 @@ interface SyncTriggerDescription {
|
|
|
378
398
|
*/
|
|
379
399
|
resolved?: string;
|
|
380
400
|
}
|
|
401
|
+
/** @see ClivlySDK.describeRemoteProjections */
|
|
402
|
+
interface RemoteProjectionsDescription {
|
|
403
|
+
/** False when no `introspector` is configured — nothing can run. */
|
|
404
|
+
capable: boolean;
|
|
405
|
+
failed: number;
|
|
406
|
+
invalid: number;
|
|
407
|
+
ok: number;
|
|
408
|
+
/** False until a sync has run in this process. */
|
|
409
|
+
ran: boolean;
|
|
410
|
+
skipped: number;
|
|
411
|
+
}
|
|
381
412
|
interface ClivlySDK {
|
|
382
413
|
/**
|
|
383
414
|
* Send a single heartbeat and report the outcome richly: `ok`, the HTTP
|
|
@@ -413,6 +444,15 @@ interface ClivlySDK {
|
|
|
413
444
|
* anyone who can reach it — set a secret if it's publicly routable.
|
|
414
445
|
*/
|
|
415
446
|
createClivlyHandler(): (req: Request) => Promise<Response>;
|
|
447
|
+
/**
|
|
448
|
+
* What this app would tell Clivly about dashboard-authored derived objects,
|
|
449
|
+
* right now. Counts rather than the outcomes themselves: this reports
|
|
450
|
+
* health, and the per-object reasons already travel to the dashboard, which
|
|
451
|
+
* is where an author fixes them.
|
|
452
|
+
*
|
|
453
|
+
* Read by `clivly status`.
|
|
454
|
+
*/
|
|
455
|
+
describeRemoteProjections(): RemoteProjectionsDescription;
|
|
416
456
|
/**
|
|
417
457
|
* What this app would tell Clivly about its sync trigger route, right now.
|
|
418
458
|
*
|
|
@@ -497,4 +537,4 @@ interface ClivlySDK {
|
|
|
497
537
|
}): Promise<void>;
|
|
498
538
|
}
|
|
499
539
|
//#endregion
|
|
500
|
-
export {
|
|
540
|
+
export { SyncSource as _, ClivlySDK as a, CreateChatSessionHandlerOptions as c, DoctorCheck as d, DoctorReport as f, RemoteProjectionsDescription as g, EnsuredObjectTypeResult as h, ClivlyConnectResult as i, CursorStore as l, EnsuredMappingResult as m, ChatWidgetVisitor as n, ClivlySDKConfig as o, EnsureSchemaResult as p, ClivlyConnectReason as r, ClivlyUser as s, ChatSessionRequestBody as t, DiscoveredTable as u, SyncTriggerDescription as v, NamespaceIntrospector as y };
|
package/dist/vite.d.cts
CHANGED
package/dist/vite.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@clivly/core": "0.9.
|
|
58
|
+
"@clivly/core": "0.9.7"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|