@clivly/sdk 0.7.0 → 0.8.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 +1 -1
- package/dist/index.cjs +158 -23
- package/dist/index.mjs +158 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -124,7 +124,7 @@ the heartbeat and sync loop. It is dev-server only; use `runScheduled()` or
|
|
|
124
124
|
Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`), then:
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
npx clivly ping #
|
|
127
|
+
npx clivly ping # CLIVLY_SECRET_KEY + API reachability
|
|
128
128
|
npx clivly doctor # env → config → schema → columns → sample fetch → heartbeat
|
|
129
129
|
```
|
|
130
130
|
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,85 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
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
|
+
//#region src/entity-source.ts
|
|
6
|
+
const CONCEPT_KEYS = {
|
|
7
|
+
contact: "contacts",
|
|
8
|
+
company: "companies",
|
|
9
|
+
deal: "deals"
|
|
10
|
+
};
|
|
11
|
+
function resolveEntitySource(config, entityKey) {
|
|
12
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
13
|
+
const src = config.source;
|
|
14
|
+
if (!(entity && src)) return null;
|
|
15
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
16
|
+
if (conceptKey) {
|
|
17
|
+
const source = src[conceptKey];
|
|
18
|
+
return source ? {
|
|
19
|
+
concept: entity.concept,
|
|
20
|
+
expectedKey: conceptKey,
|
|
21
|
+
source
|
|
22
|
+
} : null;
|
|
23
|
+
}
|
|
24
|
+
if (entity.concept === "custom") {
|
|
25
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
26
|
+
return match ? {
|
|
27
|
+
concept: "custom",
|
|
28
|
+
expectedKey: "custom",
|
|
29
|
+
source: match
|
|
30
|
+
} : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
35
|
+
function expectedSourceKey(concept) {
|
|
36
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/concept-coverage.ts
|
|
40
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
41
|
+
function configuredKeys(config) {
|
|
42
|
+
const src = config.source;
|
|
43
|
+
if (!src) return [];
|
|
44
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
48
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
49
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
50
|
+
* succeeds with zero rows.
|
|
51
|
+
*/
|
|
52
|
+
function checkConceptCoverage(config) {
|
|
53
|
+
const entities = config.entities?.entities ?? {};
|
|
54
|
+
const keys = Object.keys(entities);
|
|
55
|
+
if (keys.length === 0) return {
|
|
56
|
+
name: CHECK_NAME,
|
|
57
|
+
ok: false,
|
|
58
|
+
skipped: true,
|
|
59
|
+
detail: "No entities configured — nothing to cross-check."
|
|
60
|
+
};
|
|
61
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
62
|
+
if (unresolved.length === 0) return {
|
|
63
|
+
name: CHECK_NAME,
|
|
64
|
+
ok: true,
|
|
65
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
66
|
+
};
|
|
67
|
+
const available = configuredKeys(config);
|
|
68
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
69
|
+
const problems = unresolved.map((key) => {
|
|
70
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
71
|
+
if (concept === "custom") {
|
|
72
|
+
const custom = config.source?.custom ?? [];
|
|
73
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
74
|
+
}
|
|
75
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
name: CHECK_NAME,
|
|
79
|
+
ok: false,
|
|
80
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
5
84
|
//#region src/custom-slug.ts
|
|
6
85
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
7
86
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -82,6 +161,56 @@ function describeFetchError(entity, error) {
|
|
|
82
161
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
83
162
|
}
|
|
84
163
|
//#endregion
|
|
164
|
+
//#region src/mapping-dry-run.ts
|
|
165
|
+
/**
|
|
166
|
+
* Fetch one real row and push it through the entity's field map.
|
|
167
|
+
*
|
|
168
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
169
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
170
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
171
|
+
* rather than an error, so it has to be caught here.
|
|
172
|
+
*
|
|
173
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
174
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
175
|
+
*/
|
|
176
|
+
async function runMappingDryRun(config, entityKey) {
|
|
177
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
178
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
179
|
+
if (!resolved) return {
|
|
180
|
+
name,
|
|
181
|
+
ok: false,
|
|
182
|
+
skipped: true,
|
|
183
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
184
|
+
};
|
|
185
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
186
|
+
let rows;
|
|
187
|
+
try {
|
|
188
|
+
rows = await resolved.source.fetchPage({
|
|
189
|
+
since: null,
|
|
190
|
+
limit: 1
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
name,
|
|
195
|
+
ok: false,
|
|
196
|
+
detail: describeFetchError(entityKey, error)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const row = rows[0];
|
|
200
|
+
if (row === void 0) return {
|
|
201
|
+
name,
|
|
202
|
+
ok: false,
|
|
203
|
+
skipped: true,
|
|
204
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
205
|
+
};
|
|
206
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
207
|
+
return {
|
|
208
|
+
name,
|
|
209
|
+
ok: missing.length === 0,
|
|
210
|
+
detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
85
214
|
//#region src/namespace-probe.ts
|
|
86
215
|
/**
|
|
87
216
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -172,6 +301,11 @@ async function namespaceCheck(config) {
|
|
|
172
301
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
173
302
|
};
|
|
174
303
|
}
|
|
304
|
+
async function mappingDryRunChecks(config) {
|
|
305
|
+
const checks = [];
|
|
306
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
307
|
+
return checks;
|
|
308
|
+
}
|
|
175
309
|
/**
|
|
176
310
|
* Run the read-only setup checks and return a structured report. Every check
|
|
177
311
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -198,6 +332,7 @@ async function runDoctor(args) {
|
|
|
198
332
|
ok: sources.length > 0,
|
|
199
333
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
200
334
|
});
|
|
335
|
+
checks.push(checkConceptCoverage(config));
|
|
201
336
|
for (const source of sources) {
|
|
202
337
|
const meta = source[require_drizzle_meta.DRIZZLE_META];
|
|
203
338
|
if (!meta) {
|
|
@@ -236,6 +371,7 @@ async function runDoctor(args) {
|
|
|
236
371
|
detail: describeFetchError(source.entity, error)
|
|
237
372
|
});
|
|
238
373
|
}
|
|
374
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
239
375
|
const result = await connect();
|
|
240
376
|
checks.push({
|
|
241
377
|
name: "Heartbeat",
|
|
@@ -279,6 +415,7 @@ async function syncSource(args) {
|
|
|
279
415
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
280
416
|
let cursor = since;
|
|
281
417
|
let pushed = 0;
|
|
418
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
282
419
|
for (;;) {
|
|
283
420
|
const rows = await source.fetchPage({
|
|
284
421
|
since: cursor,
|
|
@@ -290,6 +427,8 @@ async function syncSource(args) {
|
|
|
290
427
|
source: source.entity,
|
|
291
428
|
rows: capped,
|
|
292
429
|
mode: "delta",
|
|
430
|
+
fetched: rows.length,
|
|
431
|
+
cursor: resumedFrom,
|
|
293
432
|
...objectType ? { objectType } : {}
|
|
294
433
|
})) return {
|
|
295
434
|
pushed,
|
|
@@ -332,6 +471,8 @@ async function fullSyncSource(args) {
|
|
|
332
471
|
source: source.entity,
|
|
333
472
|
rows: all,
|
|
334
473
|
mode: "full",
|
|
474
|
+
fetched: all.length,
|
|
475
|
+
cursor: null,
|
|
335
476
|
...objectType ? { objectType } : {}
|
|
336
477
|
});
|
|
337
478
|
return {
|
|
@@ -622,31 +763,25 @@ function createFetcher(retry) {
|
|
|
622
763
|
}
|
|
623
764
|
};
|
|
624
765
|
}
|
|
766
|
+
const CONCEPT_PATHS = {
|
|
767
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
768
|
+
companies: SYNC_COMPANIES_PATH,
|
|
769
|
+
deals: SYNC_DEALS_PATH,
|
|
770
|
+
custom: SYNC_CUSTOM_PATH
|
|
771
|
+
};
|
|
625
772
|
function resolveSampleSource(config, entityKey) {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
638
|
-
source: src.deals,
|
|
639
|
-
path: SYNC_DEALS_PATH
|
|
773
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
774
|
+
if (!resolved) return null;
|
|
775
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
776
|
+
if (!path) return null;
|
|
777
|
+
return resolved.expectedKey === "custom" ? {
|
|
778
|
+
source: resolved.source,
|
|
779
|
+
path,
|
|
780
|
+
objectType: resolved.source.objectType
|
|
781
|
+
} : {
|
|
782
|
+
source: resolved.source,
|
|
783
|
+
path
|
|
640
784
|
};
|
|
641
|
-
if (entity.concept === "custom") {
|
|
642
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
643
|
-
return custom ? {
|
|
644
|
-
source: custom,
|
|
645
|
-
path: SYNC_CUSTOM_PATH,
|
|
646
|
-
objectType: custom.objectType
|
|
647
|
-
} : null;
|
|
648
|
-
}
|
|
649
|
-
return null;
|
|
650
785
|
}
|
|
651
786
|
const PREVIEW_LIMIT = 5;
|
|
652
787
|
function resolvePreviewSource(config, sourceTable) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
3
|
import { defineClivlyConfig } from "@clivly/core/entity-config";
|
|
4
|
+
//#region src/entity-source.ts
|
|
5
|
+
const CONCEPT_KEYS = {
|
|
6
|
+
contact: "contacts",
|
|
7
|
+
company: "companies",
|
|
8
|
+
deal: "deals"
|
|
9
|
+
};
|
|
10
|
+
function resolveEntitySource(config, entityKey) {
|
|
11
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
12
|
+
const src = config.source;
|
|
13
|
+
if (!(entity && src)) return null;
|
|
14
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
15
|
+
if (conceptKey) {
|
|
16
|
+
const source = src[conceptKey];
|
|
17
|
+
return source ? {
|
|
18
|
+
concept: entity.concept,
|
|
19
|
+
expectedKey: conceptKey,
|
|
20
|
+
source
|
|
21
|
+
} : null;
|
|
22
|
+
}
|
|
23
|
+
if (entity.concept === "custom") {
|
|
24
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
25
|
+
return match ? {
|
|
26
|
+
concept: "custom",
|
|
27
|
+
expectedKey: "custom",
|
|
28
|
+
source: match
|
|
29
|
+
} : null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
34
|
+
function expectedSourceKey(concept) {
|
|
35
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/concept-coverage.ts
|
|
39
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
40
|
+
function configuredKeys(config) {
|
|
41
|
+
const src = config.source;
|
|
42
|
+
if (!src) return [];
|
|
43
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
47
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
48
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
49
|
+
* succeeds with zero rows.
|
|
50
|
+
*/
|
|
51
|
+
function checkConceptCoverage(config) {
|
|
52
|
+
const entities = config.entities?.entities ?? {};
|
|
53
|
+
const keys = Object.keys(entities);
|
|
54
|
+
if (keys.length === 0) return {
|
|
55
|
+
name: CHECK_NAME,
|
|
56
|
+
ok: false,
|
|
57
|
+
skipped: true,
|
|
58
|
+
detail: "No entities configured — nothing to cross-check."
|
|
59
|
+
};
|
|
60
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
61
|
+
if (unresolved.length === 0) return {
|
|
62
|
+
name: CHECK_NAME,
|
|
63
|
+
ok: true,
|
|
64
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
65
|
+
};
|
|
66
|
+
const available = configuredKeys(config);
|
|
67
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
68
|
+
const problems = unresolved.map((key) => {
|
|
69
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
70
|
+
if (concept === "custom") {
|
|
71
|
+
const custom = config.source?.custom ?? [];
|
|
72
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
73
|
+
}
|
|
74
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
name: CHECK_NAME,
|
|
78
|
+
ok: false,
|
|
79
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
4
83
|
//#region src/custom-slug.ts
|
|
5
84
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
6
85
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -81,6 +160,56 @@ function describeFetchError(entity, error) {
|
|
|
81
160
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
82
161
|
}
|
|
83
162
|
//#endregion
|
|
163
|
+
//#region src/mapping-dry-run.ts
|
|
164
|
+
/**
|
|
165
|
+
* Fetch one real row and push it through the entity's field map.
|
|
166
|
+
*
|
|
167
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
168
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
169
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
170
|
+
* rather than an error, so it has to be caught here.
|
|
171
|
+
*
|
|
172
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
173
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
174
|
+
*/
|
|
175
|
+
async function runMappingDryRun(config, entityKey) {
|
|
176
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
177
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
178
|
+
if (!resolved) return {
|
|
179
|
+
name,
|
|
180
|
+
ok: false,
|
|
181
|
+
skipped: true,
|
|
182
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
183
|
+
};
|
|
184
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
185
|
+
let rows;
|
|
186
|
+
try {
|
|
187
|
+
rows = await resolved.source.fetchPage({
|
|
188
|
+
since: null,
|
|
189
|
+
limit: 1
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
ok: false,
|
|
195
|
+
detail: describeFetchError(entityKey, error)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const row = rows[0];
|
|
199
|
+
if (row === void 0) return {
|
|
200
|
+
name,
|
|
201
|
+
ok: false,
|
|
202
|
+
skipped: true,
|
|
203
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
204
|
+
};
|
|
205
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
ok: missing.length === 0,
|
|
209
|
+
detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
84
213
|
//#region src/namespace-probe.ts
|
|
85
214
|
/**
|
|
86
215
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -171,6 +300,11 @@ async function namespaceCheck(config) {
|
|
|
171
300
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
172
301
|
};
|
|
173
302
|
}
|
|
303
|
+
async function mappingDryRunChecks(config) {
|
|
304
|
+
const checks = [];
|
|
305
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
306
|
+
return checks;
|
|
307
|
+
}
|
|
174
308
|
/**
|
|
175
309
|
* Run the read-only setup checks and return a structured report. Every check
|
|
176
310
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -197,6 +331,7 @@ async function runDoctor(args) {
|
|
|
197
331
|
ok: sources.length > 0,
|
|
198
332
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
199
333
|
});
|
|
334
|
+
checks.push(checkConceptCoverage(config));
|
|
200
335
|
for (const source of sources) {
|
|
201
336
|
const meta = source[DRIZZLE_META];
|
|
202
337
|
if (!meta) {
|
|
@@ -235,6 +370,7 @@ async function runDoctor(args) {
|
|
|
235
370
|
detail: describeFetchError(source.entity, error)
|
|
236
371
|
});
|
|
237
372
|
}
|
|
373
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
238
374
|
const result = await connect();
|
|
239
375
|
checks.push({
|
|
240
376
|
name: "Heartbeat",
|
|
@@ -278,6 +414,7 @@ async function syncSource(args) {
|
|
|
278
414
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
279
415
|
let cursor = since;
|
|
280
416
|
let pushed = 0;
|
|
417
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
281
418
|
for (;;) {
|
|
282
419
|
const rows = await source.fetchPage({
|
|
283
420
|
since: cursor,
|
|
@@ -289,6 +426,8 @@ async function syncSource(args) {
|
|
|
289
426
|
source: source.entity,
|
|
290
427
|
rows: capped,
|
|
291
428
|
mode: "delta",
|
|
429
|
+
fetched: rows.length,
|
|
430
|
+
cursor: resumedFrom,
|
|
292
431
|
...objectType ? { objectType } : {}
|
|
293
432
|
})) return {
|
|
294
433
|
pushed,
|
|
@@ -331,6 +470,8 @@ async function fullSyncSource(args) {
|
|
|
331
470
|
source: source.entity,
|
|
332
471
|
rows: all,
|
|
333
472
|
mode: "full",
|
|
473
|
+
fetched: all.length,
|
|
474
|
+
cursor: null,
|
|
334
475
|
...objectType ? { objectType } : {}
|
|
335
476
|
});
|
|
336
477
|
return {
|
|
@@ -621,31 +762,25 @@ function createFetcher(retry) {
|
|
|
621
762
|
}
|
|
622
763
|
};
|
|
623
764
|
}
|
|
765
|
+
const CONCEPT_PATHS = {
|
|
766
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
767
|
+
companies: SYNC_COMPANIES_PATH,
|
|
768
|
+
deals: SYNC_DEALS_PATH,
|
|
769
|
+
custom: SYNC_CUSTOM_PATH
|
|
770
|
+
};
|
|
624
771
|
function resolveSampleSource(config, entityKey) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
637
|
-
source: src.deals,
|
|
638
|
-
path: SYNC_DEALS_PATH
|
|
772
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
773
|
+
if (!resolved) return null;
|
|
774
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
775
|
+
if (!path) return null;
|
|
776
|
+
return resolved.expectedKey === "custom" ? {
|
|
777
|
+
source: resolved.source,
|
|
778
|
+
path,
|
|
779
|
+
objectType: resolved.source.objectType
|
|
780
|
+
} : {
|
|
781
|
+
source: resolved.source,
|
|
782
|
+
path
|
|
639
783
|
};
|
|
640
|
-
if (entity.concept === "custom") {
|
|
641
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
642
|
-
return custom ? {
|
|
643
|
-
source: custom,
|
|
644
|
-
path: SYNC_CUSTOM_PATH,
|
|
645
|
-
objectType: custom.objectType
|
|
646
|
-
} : null;
|
|
647
|
-
}
|
|
648
|
-
return null;
|
|
649
784
|
}
|
|
650
785
|
const PREVIEW_LIMIT = 5;
|
|
651
786
|
function resolvePreviewSource(config, sourceTable) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@clivly/core": "0.
|
|
58
|
+
"@clivly/core": "0.8.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"drizzle-orm": ">=0.30",
|
|
61
|
+
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|
|
62
62
|
"vite": ">=5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|