clivly 0.13.0 → 0.15.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 +13 -6
- package/{adapter-DTMeGGsl.d.cts → adapter-BMAUq4hf.d.mts} +28 -1
- package/{adapter-DhPBRNpi.d.mts → adapter-IH3r_I8v.d.cts} +28 -1
- package/{auth-adapter-C6rDx3XH.d.mts → auth-adapter-Bt1GZiag.d.cts} +1 -1
- package/{auth-adapter-Csk9KkvI.d.cts → auth-adapter-CHy273qb.d.mts} +1 -1
- package/auth-better-auth.d.cts +1 -1
- package/auth-better-auth.d.mts +1 -1
- package/auth-clerk.d.cts +1 -1
- package/auth-clerk.d.mts +1 -1
- package/auth-workos.d.cts +1 -1
- package/auth-workos.d.mts +1 -1
- package/core-adapter.d.cts +1 -1
- package/core-adapter.d.mts +1 -1
- package/core-auth-adapter.d.cts +1 -1
- package/core-auth-adapter.d.mts +1 -1
- package/core-sync-engine.cjs +3 -1
- package/core-sync-engine.d.cts +2 -2
- package/core-sync-engine.d.mts +2 -2
- package/core-sync-engine.mjs +2 -2
- package/core-types.d.cts +1 -1
- package/core-types.d.mts +1 -1
- package/core.cjs +4 -2
- package/core.d.cts +6 -6
- package/core.d.mts +6 -6
- package/core.mjs +3 -3
- package/index.cjs +102 -1
- package/index.mjs +102 -1
- package/{internal-Cmx7Zevf.d.mts → internal-C7NlPM3M.d.mts} +3 -1
- package/{internal-u0Bks5Gr.mjs → internal-CLvZdVj5.mjs} +12 -0
- package/{internal-vH5Rlg6Q.d.cts → internal-D2J4VQWT.d.cts} +3 -1
- package/{internal-BxtvxF7Z.cjs → internal-DsArcqkP.cjs} +12 -0
- package/internal-adapter.d.cts +1 -1
- package/internal-adapter.d.mts +1 -1
- package/internal-auth-adapter.d.cts +1 -1
- package/internal-auth-adapter.d.mts +1 -1
- package/internal-sync-engine.cjs +3 -1
- package/internal-sync-engine.d.cts +2 -2
- package/internal-sync-engine.d.mts +2 -2
- package/internal-sync-engine.mjs +2 -2
- package/internal-types.d.cts +1 -1
- package/internal-types.d.mts +1 -1
- package/internal.cjs +4 -2
- package/internal.d.cts +6 -6
- package/internal.d.mts +6 -6
- package/internal.mjs +3 -3
- package/package.json +1 -1
- package/{sdk-B-dp1tZN.mjs → sdk-DLoVPlGB.mjs} +139 -5
- package/{sdk-DKfKhJD_.cjs → sdk-ZMy4C4VK.cjs} +156 -4
- package/sdk.cjs +4 -1
- package/sdk.d.cts +32 -0
- package/sdk.d.mts +32 -0
- package/sdk.mjs +2 -2
- package/{sync-engine-Dwm_KDUS.cjs → sync-engine-BGVDsrOQ.cjs} +131 -71
- package/{sync-engine-V3_UutGv.d.cts → sync-engine-CYsWOujz.d.cts} +63 -22
- package/{sync-engine--M9EaONC.d.mts → sync-engine-C_5TpvfX.d.mts} +63 -22
- package/{sync-engine-Df2MCs_n.mjs → sync-engine-DH058G3w.mjs} +120 -72
- package/{types-B17TWEOL.d.cts → types-CpAtAtxd.d.cts} +35 -0
- package/{types-B17TWEOL.d.mts → types-CpAtAtxd.d.mts} +35 -0
|
@@ -1391,6 +1391,51 @@ async function fullSyncSource(args) {
|
|
|
1391
1391
|
};
|
|
1392
1392
|
}
|
|
1393
1393
|
//#endregion
|
|
1394
|
+
//#region ../sdk/src/trigger-method.ts
|
|
1395
|
+
/** Host-side override, mirroring `CLIVLY_INSTANT_PUSH_DISABLED`. */
|
|
1396
|
+
const REQUIRE_POST_ENV_VAR = "CLIVLY_TRIGGER_REQUIRE_POST";
|
|
1397
|
+
const NON_POST_DEPRECATION_WARNING = `Clivly: this sync trigger was called with GET. It still ran the sync, but a future release will refuse any method except POST with a 405. Change the caller — your cron job or uptime check — to POST. To adopt the new behaviour now, set ${REQUIRE_POST_ENV_VAR}=true.`;
|
|
1398
|
+
const METHOD_NOT_ALLOWED = 405;
|
|
1399
|
+
const NO_CONTENT = 204;
|
|
1400
|
+
const OK = 200;
|
|
1401
|
+
/**
|
|
1402
|
+
* Resolve the phase. Only the exact strings `"true"` and `"false"` count —
|
|
1403
|
+
* anything else (unset, empty, `"1"`, `"TRUE"`) falls through to the default
|
|
1404
|
+
* rather than guessing, so a typo can never silently disable a guard.
|
|
1405
|
+
*/
|
|
1406
|
+
function enforcementEnabled(input) {
|
|
1407
|
+
const override = input.env[REQUIRE_POST_ENV_VAR];
|
|
1408
|
+
if (override === "true") return true;
|
|
1409
|
+
if (override === "false") return false;
|
|
1410
|
+
return input.enforceDefault;
|
|
1411
|
+
}
|
|
1412
|
+
function resolveTriggerMethod(input) {
|
|
1413
|
+
const method = input.method.toUpperCase();
|
|
1414
|
+
if (method === "POST") return {
|
|
1415
|
+
runSync: true,
|
|
1416
|
+
status: OK
|
|
1417
|
+
};
|
|
1418
|
+
if (method === "HEAD") return {
|
|
1419
|
+
runSync: false,
|
|
1420
|
+
status: NO_CONTENT
|
|
1421
|
+
};
|
|
1422
|
+
const refuse = {
|
|
1423
|
+
body: {
|
|
1424
|
+
error: `Method ${method} is not allowed on the Clivly sync trigger. Use POST.`,
|
|
1425
|
+
ok: false
|
|
1426
|
+
},
|
|
1427
|
+
runSync: false,
|
|
1428
|
+
status: METHOD_NOT_ALLOWED
|
|
1429
|
+
};
|
|
1430
|
+
if (method !== "GET") return refuse;
|
|
1431
|
+
if (enforcementEnabled(input)) return refuse;
|
|
1432
|
+
return {
|
|
1433
|
+
runSync: true,
|
|
1434
|
+
status: OK,
|
|
1435
|
+
warn: NON_POST_DEPRECATION_WARNING
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1394
1439
|
//#region ../sdk/src/trigger-url.ts
|
|
1395
1440
|
const SCHEME_REGEX = /^https?:\/\//;
|
|
1396
1441
|
const INVALID_HOST_CHARS_REGEX = /[/\\@?#\s]/;
|
|
@@ -1702,7 +1747,7 @@ function buildSchema(config) {
|
|
|
1702
1747
|
});
|
|
1703
1748
|
}
|
|
1704
1749
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1705
|
-
function
|
|
1750
|
+
function memoryCursorStore() {
|
|
1706
1751
|
const cursors = /* @__PURE__ */ new Map();
|
|
1707
1752
|
return {
|
|
1708
1753
|
get: (entity) => cursors.get(entity) ?? null,
|
|
@@ -1711,6 +1756,56 @@ function createMemoryCursorStore() {
|
|
|
1711
1756
|
}
|
|
1712
1757
|
};
|
|
1713
1758
|
}
|
|
1759
|
+
/** Default key prefix for {@link kvCursorStore}. */
|
|
1760
|
+
const CURSOR_KEY_PREFIX = "clivly:cursor:";
|
|
1761
|
+
/**
|
|
1762
|
+
* A persistent `CursorStore` backed by any key-value store.
|
|
1763
|
+
*
|
|
1764
|
+
* Serverless runtimes discard memory between invocations, so without a
|
|
1765
|
+
* persisted cursor every `delta` sync starts from `null` and re-reads the
|
|
1766
|
+
* whole source table. That is invisible — the run succeeds — and bills like a
|
|
1767
|
+
* full sync forever.
|
|
1768
|
+
*
|
|
1769
|
+
* This exists because the hand-written version has a trap in it:
|
|
1770
|
+
* `SyncCursor.time` is a `Date`, which a JSON round-trip turns into a string,
|
|
1771
|
+
* and `fromDrizzle` then calls `since.time.toISOString()` on it. The cursor is
|
|
1772
|
+
* stored as an ISO string and parsed back to a `Date` here so no caller has to
|
|
1773
|
+
* remember that.
|
|
1774
|
+
*
|
|
1775
|
+
* ```ts
|
|
1776
|
+
* cursorStore: kvCursorStore(env.CLIVLY_CURSORS) // Workers KV binding
|
|
1777
|
+
* ```
|
|
1778
|
+
*
|
|
1779
|
+
* A malformed or partial stored value resolves to `null` rather than throwing:
|
|
1780
|
+
* the worst case is one full re-read, where throwing would wedge sync entirely.
|
|
1781
|
+
*/
|
|
1782
|
+
function kvCursorStore(kv, options) {
|
|
1783
|
+
const prefix = options?.prefix ?? "clivly:cursor:";
|
|
1784
|
+
const keyFor = (entity) => `${prefix}${entity}`;
|
|
1785
|
+
return {
|
|
1786
|
+
get: async (entity) => {
|
|
1787
|
+
const raw = await kv.get(keyFor(entity));
|
|
1788
|
+
if (!raw) return null;
|
|
1789
|
+
try {
|
|
1790
|
+
const parsed = JSON.parse(raw);
|
|
1791
|
+
if (typeof parsed.id !== "string" || typeof parsed.time !== "string") return null;
|
|
1792
|
+
const time = new Date(parsed.time);
|
|
1793
|
+
return Number.isNaN(time.getTime()) ? null : {
|
|
1794
|
+
id: parsed.id,
|
|
1795
|
+
time
|
|
1796
|
+
};
|
|
1797
|
+
} catch {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1801
|
+
set: async (entity, cursor) => {
|
|
1802
|
+
await kv.put(keyFor(entity), cursor === null ? "" : JSON.stringify({
|
|
1803
|
+
id: cursor.id,
|
|
1804
|
+
time: cursor.time.toISOString()
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1714
1809
|
function isLikelyServerless() {
|
|
1715
1810
|
const runtime = globalThis;
|
|
1716
1811
|
if (runtime.navigator?.userAgent === "Cloudflare-Workers") return true;
|
|
@@ -1940,9 +2035,11 @@ function createClivlySDK(config) {
|
|
|
1940
2035
|
let presenceClient = null;
|
|
1941
2036
|
let httpHeartbeatActive = true;
|
|
1942
2037
|
const batchSize = config.sync?.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
1943
|
-
|
|
2038
|
+
if (serverless && !config.cursorStore) console.warn("Clivly: no `cursorStore` is configured and this looks like a serverless/edge runtime, where memory does not survive between invocations. Every `delta` sync will start from scratch and re-read your entire source tables — a full sync, on every run, billed as such. Pass a persistent store: `cursorStore: kvCursorStore(env.MY_KV)` (exported from `@clivly/sdk`). See https://clivly.com/docs/concepts/sync#cursor-stores");
|
|
2039
|
+
const cursorStore = config.cursorStore ?? memoryCursorStore();
|
|
1944
2040
|
const fetcher = createFetcher(config.retry);
|
|
1945
2041
|
let warnedUnsignedTrigger = false;
|
|
2042
|
+
let warnedNonPostTrigger = false;
|
|
1946
2043
|
const remoteProjections = createRemoteProjectionFetcher({
|
|
1947
2044
|
apiKey: config.apiKey,
|
|
1948
2045
|
apiUrl,
|
|
@@ -2368,8 +2465,45 @@ function createClivlySDK(config) {
|
|
|
2368
2465
|
return {};
|
|
2369
2466
|
}
|
|
2370
2467
|
};
|
|
2468
|
+
/**
|
|
2469
|
+
* The response to send instead of running a sync, or null to proceed.
|
|
2470
|
+
*
|
|
2471
|
+
* Split out of the handler so the method decision reads as one thing rather
|
|
2472
|
+
* than three branches ahead of the request plumbing — the same shape as
|
|
2473
|
+
* `triggerRequestAllowed` below.
|
|
2474
|
+
*/
|
|
2475
|
+
const refuseDisallowedMethod = (req) => {
|
|
2476
|
+
const decision = resolveTriggerMethod({
|
|
2477
|
+
enforceDefault: false,
|
|
2478
|
+
env: config.env ?? process.env,
|
|
2479
|
+
method: req.method
|
|
2480
|
+
});
|
|
2481
|
+
if (decision.warn && !warnedNonPostTrigger) {
|
|
2482
|
+
warnedNonPostTrigger = true;
|
|
2483
|
+
console.warn(decision.warn);
|
|
2484
|
+
}
|
|
2485
|
+
if (decision.runSync) return null;
|
|
2486
|
+
return decision.body ? jsonResponse(decision.body, decision.status) : new Response(null, { status: decision.status });
|
|
2487
|
+
};
|
|
2488
|
+
/**
|
|
2489
|
+
* Serve a preview request, or null when this payload is a real sync.
|
|
2490
|
+
*
|
|
2491
|
+
* Preview (T10) is a signed, ZERO-WRITE dry run: it returns ≤5 RAW source
|
|
2492
|
+
* rows, slimmed to only the columns the draft maps or filters on and coerced
|
|
2493
|
+
* to JSON scalars, which the cloud then projects through the same path as a
|
|
2494
|
+
* real sync. It never syncs, never pushes, never writes a cursor and never
|
|
2495
|
+
* ingests. Callers keep it inside their `try` so a malformed body or a
|
|
2496
|
+
* throwing `fetchPage` degrades to the same graceful 500 as the sync paths.
|
|
2497
|
+
*/
|
|
2498
|
+
const handleIfPreview = async (payload) => {
|
|
2499
|
+
if (payload.preview) return await handlePreviewRequest(config, payload.preview);
|
|
2500
|
+
if (payload.previewProjection) return await handleProjectionPreviewRequest(config, payload.previewProjection);
|
|
2501
|
+
return null;
|
|
2502
|
+
};
|
|
2371
2503
|
const createClivlyHandler = () => async (req) => {
|
|
2372
2504
|
observedUrl = observedTriggerUrl(req) ?? observedUrl;
|
|
2505
|
+
const refusal = refuseDisallowedMethod(req);
|
|
2506
|
+
if (refusal) return refusal;
|
|
2373
2507
|
const rawBody = await req.text().catch(() => "");
|
|
2374
2508
|
if (!await triggerRequestAllowed(req, rawBody)) return jsonResponse({
|
|
2375
2509
|
ok: false,
|
|
@@ -2379,8 +2513,8 @@ function createClivlySDK(config) {
|
|
|
2379
2513
|
const runId = payload.runId ?? null;
|
|
2380
2514
|
const startedAt = Date.now();
|
|
2381
2515
|
try {
|
|
2382
|
-
|
|
2383
|
-
if (
|
|
2516
|
+
const previewResponse = await handleIfPreview(payload);
|
|
2517
|
+
if (previewResponse) return previewResponse;
|
|
2384
2518
|
if (payload.sample) await runSampleSync(payload.sample.entityKey, payload.runId);
|
|
2385
2519
|
else await runScheduled({
|
|
2386
2520
|
mode: payload.mode,
|
|
@@ -2486,6 +2620,12 @@ function createClivlySDK(config) {
|
|
|
2486
2620
|
};
|
|
2487
2621
|
}
|
|
2488
2622
|
//#endregion
|
|
2623
|
+
Object.defineProperty(exports, "CURSOR_KEY_PREFIX", {
|
|
2624
|
+
enumerable: true,
|
|
2625
|
+
get: function() {
|
|
2626
|
+
return CURSOR_KEY_PREFIX;
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2489
2629
|
Object.defineProperty(exports, "ClivlyAuthError", {
|
|
2490
2630
|
enumerable: true,
|
|
2491
2631
|
get: function() {
|
|
@@ -2528,3 +2668,15 @@ Object.defineProperty(exports, "isPlaceholderUrl", {
|
|
|
2528
2668
|
return isPlaceholderUrl;
|
|
2529
2669
|
}
|
|
2530
2670
|
});
|
|
2671
|
+
Object.defineProperty(exports, "kvCursorStore", {
|
|
2672
|
+
enumerable: true,
|
|
2673
|
+
get: function() {
|
|
2674
|
+
return kvCursorStore;
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
Object.defineProperty(exports, "memoryCursorStore", {
|
|
2678
|
+
enumerable: true,
|
|
2679
|
+
get: function() {
|
|
2680
|
+
return memoryCursorStore;
|
|
2681
|
+
}
|
|
2682
|
+
});
|
package/sdk.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_entity_config = require("./entity-config-BI4RYNKL.cjs");
|
|
3
|
-
const require_sdk = require("./sdk-
|
|
3
|
+
const require_sdk = require("./sdk-ZMy4C4VK.cjs");
|
|
4
|
+
exports.CURSOR_KEY_PREFIX = require_sdk.CURSOR_KEY_PREFIX;
|
|
4
5
|
exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
|
|
5
6
|
exports.ClivlyError = require_sdk.ClivlyError;
|
|
6
7
|
exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
|
|
@@ -10,4 +11,6 @@ exports.createClivlySDK = require_sdk.createClivlySDK;
|
|
|
10
11
|
exports.customObjectDeclarations = require_entity_config.customObjectDeclarations;
|
|
11
12
|
exports.defineClivlyConfig = require_entity_config.defineClivlyConfig;
|
|
12
13
|
exports.isPlaceholderUrl = require_sdk.isPlaceholderUrl;
|
|
14
|
+
exports.kvCursorStore = require_sdk.kvCursorStore;
|
|
15
|
+
exports.memoryCursorStore = require_sdk.memoryCursorStore;
|
|
13
16
|
exports.validateEntitiesConfig = require_entity_config.validateEntitiesConfig;
|
package/sdk.d.cts
CHANGED
|
@@ -28,6 +28,38 @@ export declare class ClivlyRateLimitError extends ClivlyError {}
|
|
|
28
28
|
export declare class ClivlyNetworkError extends ClivlyError {}
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region ../sdk/src/index.d.ts
|
|
31
|
+
export declare function memoryCursorStore(): CursorStore;
|
|
32
|
+
/** The shape every key-value client shares: Workers KV, Upstash, node-redis. */
|
|
33
|
+
export interface CursorKV {
|
|
34
|
+
get(key: string): Promise<string | null> | string | null;
|
|
35
|
+
put(key: string, value: string): Promise<unknown> | unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Default key prefix for {@link kvCursorStore}. */
|
|
38
|
+
export declare const CURSOR_KEY_PREFIX = "clivly:cursor:";
|
|
39
|
+
/**
|
|
40
|
+
* A persistent `CursorStore` backed by any key-value store.
|
|
41
|
+
*
|
|
42
|
+
* Serverless runtimes discard memory between invocations, so without a
|
|
43
|
+
* persisted cursor every `delta` sync starts from `null` and re-reads the
|
|
44
|
+
* whole source table. That is invisible — the run succeeds — and bills like a
|
|
45
|
+
* full sync forever.
|
|
46
|
+
*
|
|
47
|
+
* This exists because the hand-written version has a trap in it:
|
|
48
|
+
* `SyncCursor.time` is a `Date`, which a JSON round-trip turns into a string,
|
|
49
|
+
* and `fromDrizzle` then calls `since.time.toISOString()` on it. The cursor is
|
|
50
|
+
* stored as an ISO string and parsed back to a `Date` here so no caller has to
|
|
51
|
+
* remember that.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* cursorStore: kvCursorStore(env.CLIVLY_CURSORS) // Workers KV binding
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* A malformed or partial stored value resolves to `null` rather than throwing:
|
|
58
|
+
* the worst case is one full re-read, where throwing would wedge sync entirely.
|
|
59
|
+
*/
|
|
60
|
+
export declare function kvCursorStore(kv: CursorKV, options?: {
|
|
61
|
+
prefix?: string;
|
|
62
|
+
}): CursorStore;
|
|
31
63
|
/**
|
|
32
64
|
* Create the Clivly SDK. The implemented surface — heartbeat/discovery reporting
|
|
33
65
|
* and the auth-verify handler — connects a developer's app to a running Clivly
|
package/sdk.d.mts
CHANGED
|
@@ -28,6 +28,38 @@ export declare class ClivlyRateLimitError extends ClivlyError {}
|
|
|
28
28
|
export declare class ClivlyNetworkError extends ClivlyError {}
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region ../sdk/src/index.d.ts
|
|
31
|
+
export declare function memoryCursorStore(): CursorStore;
|
|
32
|
+
/** The shape every key-value client shares: Workers KV, Upstash, node-redis. */
|
|
33
|
+
export interface CursorKV {
|
|
34
|
+
get(key: string): Promise<string | null> | string | null;
|
|
35
|
+
put(key: string, value: string): Promise<unknown> | unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Default key prefix for {@link kvCursorStore}. */
|
|
38
|
+
export declare const CURSOR_KEY_PREFIX = "clivly:cursor:";
|
|
39
|
+
/**
|
|
40
|
+
* A persistent `CursorStore` backed by any key-value store.
|
|
41
|
+
*
|
|
42
|
+
* Serverless runtimes discard memory between invocations, so without a
|
|
43
|
+
* persisted cursor every `delta` sync starts from `null` and re-reads the
|
|
44
|
+
* whole source table. That is invisible — the run succeeds — and bills like a
|
|
45
|
+
* full sync forever.
|
|
46
|
+
*
|
|
47
|
+
* This exists because the hand-written version has a trap in it:
|
|
48
|
+
* `SyncCursor.time` is a `Date`, which a JSON round-trip turns into a string,
|
|
49
|
+
* and `fromDrizzle` then calls `since.time.toISOString()` on it. The cursor is
|
|
50
|
+
* stored as an ISO string and parsed back to a `Date` here so no caller has to
|
|
51
|
+
* remember that.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* cursorStore: kvCursorStore(env.CLIVLY_CURSORS) // Workers KV binding
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* A malformed or partial stored value resolves to `null` rather than throwing:
|
|
58
|
+
* the worst case is one full re-read, where throwing would wedge sync entirely.
|
|
59
|
+
*/
|
|
60
|
+
export declare function kvCursorStore(kv: CursorKV, options?: {
|
|
61
|
+
prefix?: string;
|
|
62
|
+
}): CursorStore;
|
|
31
63
|
/**
|
|
32
64
|
* Create the Clivly SDK. The implemented surface — heartbeat/discovery reporting
|
|
33
65
|
* and the auth-verify handler — connects a developer's app to a running Clivly
|
package/sdk.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-Ccfb2IL9.mjs";
|
|
2
|
-
import { a as ClivlyRateLimitError, i as
|
|
3
|
-
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
|
2
|
+
import { a as ClivlyAuthError, c as ClivlyRateLimitError, i as memoryCursorStore, l as createChatSessionHandler, n as createClivlySDK, o as ClivlyError, r as kvCursorStore, s as ClivlyNetworkError, t as CURSOR_KEY_PREFIX, u as isPlaceholderUrl } from "./sdk-DLoVPlGB.mjs";
|
|
3
|
+
export { CURSOR_KEY_PREFIX, ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, kvCursorStore, memoryCursorStore, validateEntitiesConfig };
|
|
@@ -1,67 +1,103 @@
|
|
|
1
1
|
const require_view_compiler = require("./view-compiler-xiwm5iar.cjs");
|
|
2
2
|
//#region ../core/src/sync-engine.ts
|
|
3
|
+
/** Actions that remove something — only safe when the source set is complete. */
|
|
4
|
+
function isRemovalAction(action) {
|
|
5
|
+
return action.kind === "archive" || action.kind === "endMembership";
|
|
6
|
+
}
|
|
7
|
+
/** Every distinct non-null company ref in a person's rows, in arrival order. */
|
|
8
|
+
function collectCompanyRefs(rows) {
|
|
9
|
+
const refs = [];
|
|
10
|
+
for (const row of rows) {
|
|
11
|
+
const ref = row.companyRef ?? null;
|
|
12
|
+
if (ref !== null && !refs.includes(ref)) refs.push(ref);
|
|
13
|
+
}
|
|
14
|
+
return refs;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The primary company for a person: the one they already have while it is
|
|
18
|
+
* still one of theirs — so the displayed organisation never flips between
|
|
19
|
+
* syncs — otherwise the lowest ref, which is deterministic for a new person.
|
|
20
|
+
*/
|
|
21
|
+
function pickPrimaryRef(refs, current) {
|
|
22
|
+
if (current !== null && refs.includes(current)) return current;
|
|
23
|
+
return [...refs].sort()[0] ?? null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The mirror row that represents a source identity. Normally there is exactly
|
|
27
|
+
* one; before contact consolidation has run there can be several (one per
|
|
28
|
+
* tenant), and the live, earliest-listed one stands for the person while the
|
|
29
|
+
* rest are left alone for the consolidation script to fold in.
|
|
30
|
+
*/
|
|
31
|
+
function canonicalMirror(rows) {
|
|
32
|
+
return rows.find((row) => !row.archivedAt) ?? rows[0];
|
|
33
|
+
}
|
|
3
34
|
/**
|
|
4
35
|
* Diff a view's rows against the current mirror population for one entity.
|
|
5
36
|
*
|
|
6
|
-
* - source
|
|
7
|
-
* - source
|
|
8
|
-
* - source
|
|
9
|
-
* - live mirror row absent from source
|
|
10
|
-
* - archived mirror row absent from source
|
|
37
|
+
* - source identity with no mirror row → create
|
|
38
|
+
* - source identity with a live mirror row → update (overwrite identity fields)
|
|
39
|
+
* - source identity with an archived mirror → restore (re-activate + update)
|
|
40
|
+
* - live mirror row absent from source → archive (soft delete, keep history)
|
|
41
|
+
* - archived mirror row absent from source → no-op
|
|
11
42
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
43
|
+
* Identity is always the source primary key. A `through` (join-table) view
|
|
44
|
+
* fans one person out into a row per tenant; those rows are ONE person, so they
|
|
45
|
+
* collapse into one action. With `memberships` on (contacts), that action also
|
|
46
|
+
* carries every tenant ref the person was seen with, and each tenant the mirror
|
|
47
|
+
* still has as active but the source no longer lists yields `endMembership` —
|
|
48
|
+
* a person leaving one tenant ends that membership and keeps the others.
|
|
18
49
|
*/
|
|
19
50
|
function reconcile(source, mirror, opts = {}) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const rows = [];
|
|
23
|
-
for (const row of source) {
|
|
24
|
-
const key = keyOf(row.sourceRef, row.companyRef ?? null);
|
|
25
|
-
if (!seen.has(key)) {
|
|
26
|
-
seen.add(key);
|
|
27
|
-
rows.push(row);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const byKey = new Map(mirror.map((m) => [keyOf(m.sourceRef, m.companyRef), m]));
|
|
31
|
-
const liveKeys = /* @__PURE__ */ new Set();
|
|
51
|
+
const rowsByRef = groupBySourceRef(source);
|
|
52
|
+
const mirrorByRef = groupBySourceRef(mirror);
|
|
32
53
|
const actions = [];
|
|
33
|
-
for (const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
else actions.push({
|
|
52
|
-
kind: "update",
|
|
53
|
-
id: existing.id,
|
|
54
|
-
sourceRef: row.sourceRef,
|
|
55
|
-
fields: row.fields,
|
|
56
|
-
companyRef
|
|
57
|
-
});
|
|
54
|
+
for (const [sourceRef, rows] of rowsByRef) {
|
|
55
|
+
const existing = canonicalMirror(mirrorByRef.get(sourceRef) ?? []);
|
|
56
|
+
actions.push(...personActions(sourceRef, rows, existing, opts));
|
|
57
|
+
}
|
|
58
|
+
for (const m of mirror) if (!(rowsByRef.has(m.sourceRef) || m.archivedAt)) actions.push({
|
|
59
|
+
kind: "archive",
|
|
60
|
+
id: m.id,
|
|
61
|
+
sourceRef: m.sourceRef
|
|
62
|
+
});
|
|
63
|
+
return actions;
|
|
64
|
+
}
|
|
65
|
+
function groupBySourceRef(items) {
|
|
66
|
+
const out = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
const list = out.get(item.sourceRef);
|
|
69
|
+
if (list) list.push(item);
|
|
70
|
+
else out.set(item.sourceRef, [item]);
|
|
58
71
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** The create/update/restore for one present person, plus any ended tenants. */
|
|
75
|
+
function personActions(sourceRef, rows, existing, opts) {
|
|
76
|
+
const first = rows[0];
|
|
77
|
+
const refs = collectCompanyRefs(rows);
|
|
78
|
+
const companyRef = opts.memberships ? pickPrimaryRef(refs, existing?.companyRef ?? null) : first.companyRef ?? null;
|
|
79
|
+
const membershipSet = opts.memberships ? { companyRefs: refs } : {};
|
|
80
|
+
if (!existing) return [{
|
|
81
|
+
kind: "create",
|
|
82
|
+
sourceRef,
|
|
83
|
+
fields: first.fields,
|
|
84
|
+
companyRef,
|
|
85
|
+
...membershipSet
|
|
86
|
+
}];
|
|
87
|
+
const actions = [{
|
|
88
|
+
kind: existing.archivedAt ? "restore" : "update",
|
|
89
|
+
id: existing.id,
|
|
90
|
+
sourceRef,
|
|
91
|
+
fields: first.fields,
|
|
92
|
+
companyRef,
|
|
93
|
+
...membershipSet
|
|
94
|
+
}];
|
|
95
|
+
if (opts.memberships) {
|
|
96
|
+
for (const held of existing.companyRefs ?? []) if (!refs.includes(held)) actions.push({
|
|
97
|
+
kind: "endMembership",
|
|
98
|
+
id: existing.id,
|
|
99
|
+
sourceRef,
|
|
100
|
+
companyRef: held
|
|
65
101
|
});
|
|
66
102
|
}
|
|
67
103
|
return actions;
|
|
@@ -74,20 +110,40 @@ const CONCEPT_ORDER = {
|
|
|
74
110
|
function syncOrder(a, b) {
|
|
75
111
|
return (CONCEPT_ORDER[a.concept] ?? 99) - (CONCEPT_ORDER[b.concept] ?? 99);
|
|
76
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve each action's host-side company refs — the primary one and, for
|
|
115
|
+
* contacts, every membership — to `crm_companies.id`s in one lookup. Shared by
|
|
116
|
+
* `runSync` and the cloud push receiver so both persist identical actions.
|
|
117
|
+
*/
|
|
77
118
|
async function resolveActions(store, actions) {
|
|
78
119
|
const refs = /* @__PURE__ */ new Set();
|
|
79
|
-
for (const action of actions)
|
|
120
|
+
for (const action of actions) {
|
|
121
|
+
if (isRemovalAction(action) || !("fields" in action)) continue;
|
|
122
|
+
if (action.companyRef) refs.add(action.companyRef);
|
|
123
|
+
for (const ref of action.companyRefs ?? []) refs.add(ref);
|
|
124
|
+
}
|
|
80
125
|
const resolved = refs.size ? await store.resolveCompanyRefs([...refs]) : /* @__PURE__ */ new Map();
|
|
81
126
|
return actions.map((action) => {
|
|
82
|
-
if (action.kind === "archive") return action;
|
|
127
|
+
if (action.kind === "archive" || action.kind === "endMembership") return action;
|
|
83
128
|
const companyId = action.companyRef ? resolved.get(action.companyRef) ?? null : null;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
129
|
+
const memberships = action.companyRefs?.map((companyRef) => ({
|
|
130
|
+
companyRef,
|
|
131
|
+
companyId: resolved.get(companyRef) ?? null
|
|
132
|
+
}));
|
|
133
|
+
const base = {
|
|
87
134
|
sourceRef: action.sourceRef,
|
|
88
135
|
fields: action.fields,
|
|
89
136
|
companyId,
|
|
90
|
-
companySourceRef: action.companyRef ?? null
|
|
137
|
+
companySourceRef: action.companyRef ?? null,
|
|
138
|
+
...memberships ? { memberships } : {}
|
|
139
|
+
};
|
|
140
|
+
return action.kind === "create" ? {
|
|
141
|
+
kind: "create",
|
|
142
|
+
...base
|
|
143
|
+
} : {
|
|
144
|
+
kind: action.kind,
|
|
145
|
+
id: action.id,
|
|
146
|
+
...base
|
|
91
147
|
};
|
|
92
148
|
});
|
|
93
149
|
}
|
|
@@ -96,19 +152,11 @@ function emptyTally() {
|
|
|
96
152
|
create: 0,
|
|
97
153
|
update: 0,
|
|
98
154
|
restore: 0,
|
|
99
|
-
archive: 0
|
|
155
|
+
archive: 0,
|
|
156
|
+
endMembership: 0
|
|
100
157
|
};
|
|
101
158
|
}
|
|
102
159
|
/**
|
|
103
|
-
* An entity whose company relationship is a `through` (join-table) join fans out
|
|
104
|
-
* into one source row per membership, so its mirror must be keyed compositely.
|
|
105
|
-
*/
|
|
106
|
-
function isCompositeEntity(config, entityKey) {
|
|
107
|
-
const entity = config.entities[entityKey];
|
|
108
|
-
if (!entity?.relationships) return false;
|
|
109
|
-
return Object.values(entity.relationships).some((rel) => "through" in rel.via);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
160
|
* Run a full sync: compile the config's views, then reconcile + persist each,
|
|
113
161
|
* companies first. Returns per-entity and total action counts.
|
|
114
162
|
*/
|
|
@@ -129,7 +177,7 @@ async function runSync(config, store, options = {}) {
|
|
|
129
177
|
role: view.role,
|
|
130
178
|
objectTypeId
|
|
131
179
|
})]);
|
|
132
|
-
const actions = reconcile(source, mirror, {
|
|
180
|
+
const actions = reconcile(source, mirror, { memberships: view.concept === "contact" }).filter((a) => !(options.sample && isRemovalAction(a)));
|
|
133
181
|
const persistActions = await resolveActions(store, actions);
|
|
134
182
|
await store.persist({
|
|
135
183
|
concept: view.concept,
|
|
@@ -217,6 +265,12 @@ Object.defineProperty(exports, "computeSampleWarnings", {
|
|
|
217
265
|
return computeSampleWarnings;
|
|
218
266
|
}
|
|
219
267
|
});
|
|
268
|
+
Object.defineProperty(exports, "isRemovalAction", {
|
|
269
|
+
enumerable: true,
|
|
270
|
+
get: function() {
|
|
271
|
+
return isRemovalAction;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
220
274
|
Object.defineProperty(exports, "previewSync", {
|
|
221
275
|
enumerable: true,
|
|
222
276
|
get: function() {
|
|
@@ -229,6 +283,12 @@ Object.defineProperty(exports, "reconcile", {
|
|
|
229
283
|
return reconcile;
|
|
230
284
|
}
|
|
231
285
|
});
|
|
286
|
+
Object.defineProperty(exports, "resolveActions", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: function() {
|
|
289
|
+
return resolveActions;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
232
292
|
Object.defineProperty(exports, "runSync", {
|
|
233
293
|
enumerable: true,
|
|
234
294
|
get: function() {
|