@memberjunction/integration-engine 5.37.0 → 5.39.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/dist/ActionMetadataGenerator.d.ts +8 -1
- package/dist/ActionMetadataGenerator.d.ts.map +1 -1
- package/dist/ActionMetadataGenerator.js +22 -3
- package/dist/ActionMetadataGenerator.js.map +1 -1
- package/dist/AdaptiveConcurrency.d.ts +85 -0
- package/dist/AdaptiveConcurrency.d.ts.map +1 -0
- package/dist/AdaptiveConcurrency.js +148 -0
- package/dist/AdaptiveConcurrency.js.map +1 -0
- package/dist/BaseIntegrationConnector.d.ts +127 -3
- package/dist/BaseIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseIntegrationConnector.js +126 -11
- package/dist/BaseIntegrationConnector.js.map +1 -1
- package/dist/BaseRESTIntegrationConnector.d.ts +80 -15
- package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseRESTIntegrationConnector.js +314 -64
- package/dist/BaseRESTIntegrationConnector.js.map +1 -1
- package/dist/ConflictRecency.d.ts +24 -0
- package/dist/ConflictRecency.d.ts.map +1 -0
- package/dist/ConflictRecency.js +25 -0
- package/dist/ConflictRecency.js.map +1 -0
- package/dist/ContentHash.d.ts +28 -0
- package/dist/ContentHash.d.ts.map +1 -0
- package/dist/ContentHash.js +54 -0
- package/dist/ContentHash.js.map +1 -0
- package/dist/EnrichSchemaConstraints.d.ts +59 -0
- package/dist/EnrichSchemaConstraints.d.ts.map +1 -0
- package/dist/EnrichSchemaConstraints.js +168 -0
- package/dist/EnrichSchemaConstraints.js.map +1 -0
- package/dist/FieldMappingEngine.d.ts +22 -0
- package/dist/FieldMappingEngine.d.ts.map +1 -1
- package/dist/FieldMappingEngine.js +66 -5
- package/dist/FieldMappingEngine.js.map +1 -1
- package/dist/HashDiff.d.ts +68 -0
- package/dist/HashDiff.d.ts.map +1 -0
- package/dist/HashDiff.js +108 -0
- package/dist/HashDiff.js.map +1 -0
- package/dist/IntegrationActionGenerator.d.ts +93 -0
- package/dist/IntegrationActionGenerator.d.ts.map +1 -0
- package/dist/IntegrationActionGenerator.js +313 -0
- package/dist/IntegrationActionGenerator.js.map +1 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts +86 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -0
- package/dist/IntegrationConnectorCreationPipeline.js +226 -0
- package/dist/IntegrationConnectorCreationPipeline.js.map +1 -0
- package/dist/IntegrationEngine.d.ts +199 -1
- package/dist/IntegrationEngine.d.ts.map +1 -1
- package/dist/IntegrationEngine.js +1592 -109
- package/dist/IntegrationEngine.js.map +1 -1
- package/dist/IntegrationSchemaSync.d.ts +82 -0
- package/dist/IntegrationSchemaSync.d.ts.map +1 -1
- package/dist/IntegrationSchemaSync.js +289 -42
- package/dist/IntegrationSchemaSync.js.map +1 -1
- package/dist/MatchEngine.d.ts.map +1 -1
- package/dist/MatchEngine.js +4 -0
- package/dist/MatchEngine.js.map +1 -1
- package/dist/RateLimiter.d.ts +117 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +159 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/SyncLogger.d.ts +106 -0
- package/dist/SyncLogger.d.ts.map +1 -0
- package/dist/SyncLogger.js +176 -0
- package/dist/SyncLogger.js.map +1 -0
- package/dist/WatermarkService.d.ts +36 -1
- package/dist/WatermarkService.d.ts.map +1 -1
- package/dist/WatermarkService.js +113 -3
- package/dist/WatermarkService.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +52 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -6
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name of the per-record content-hash mirror column. Written on every integration
|
|
3
|
+
* table alongside the other `__mj_integration_*` columns when present. Holds the
|
|
4
|
+
* SHA-256 (hex) of the last-synced external field values.
|
|
5
|
+
*
|
|
6
|
+
* This is the change-detection lever for connectors that have NO usable watermark
|
|
7
|
+
* (the source can't tell us "what changed since T"), e.g. YourMembership: every
|
|
8
|
+
* sync re-fetches every record. Comparing the freshly-computed hash of an incoming
|
|
9
|
+
* record against the stored hash lets the engine skip the per-record DB load and
|
|
10
|
+
* write entirely for the unchanged majority — turning a 50k-record no-op re-sync
|
|
11
|
+
* from 50k loads into ~0.
|
|
12
|
+
*
|
|
13
|
+
* Gated everywhere by EntityInfo field presence, so it is a no-op on tables that
|
|
14
|
+
* predate the column (the engine still syncs correctly via the dirty-flag path).
|
|
15
|
+
*/
|
|
16
|
+
export declare const CONTENT_HASH_COLUMN = "__mj_integration_ContentHash";
|
|
17
|
+
/**
|
|
18
|
+
* Deterministic content hash of a mapped-field record. Canonicalizes by SORTING keys
|
|
19
|
+
* (so field-order variation between fetches never produces a spurious "changed"), then
|
|
20
|
+
* SHA-256s the canonical JSON. `undefined` values are dropped (treated as absent) so a
|
|
21
|
+
* field that is sometimes omitted and sometimes explicitly undefined hashes the same.
|
|
22
|
+
*
|
|
23
|
+
* Note: this hashes the MAPPED field values (what actually lands in MJ), not the raw
|
|
24
|
+
* external payload — so a change in a source field we don't map won't (correctly) count
|
|
25
|
+
* as a change for sync purposes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function computeContentHash(fields: Record<string, unknown>): string;
|
|
28
|
+
//# sourceMappingURL=ContentHash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContentHash.d.ts","sourceRoot":"","sources":["../src/ContentHash.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,iCAAiC,CAAC;AAElE;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAG1E"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Name of the per-record content-hash mirror column. Written on every integration
|
|
4
|
+
* table alongside the other `__mj_integration_*` columns when present. Holds the
|
|
5
|
+
* SHA-256 (hex) of the last-synced external field values.
|
|
6
|
+
*
|
|
7
|
+
* This is the change-detection lever for connectors that have NO usable watermark
|
|
8
|
+
* (the source can't tell us "what changed since T"), e.g. YourMembership: every
|
|
9
|
+
* sync re-fetches every record. Comparing the freshly-computed hash of an incoming
|
|
10
|
+
* record against the stored hash lets the engine skip the per-record DB load and
|
|
11
|
+
* write entirely for the unchanged majority — turning a 50k-record no-op re-sync
|
|
12
|
+
* from 50k loads into ~0.
|
|
13
|
+
*
|
|
14
|
+
* Gated everywhere by EntityInfo field presence, so it is a no-op on tables that
|
|
15
|
+
* predate the column (the engine still syncs correctly via the dirty-flag path).
|
|
16
|
+
*/
|
|
17
|
+
export const CONTENT_HASH_COLUMN = '__mj_integration_ContentHash';
|
|
18
|
+
/**
|
|
19
|
+
* Deterministic content hash of a mapped-field record. Canonicalizes by SORTING keys
|
|
20
|
+
* (so field-order variation between fetches never produces a spurious "changed"), then
|
|
21
|
+
* SHA-256s the canonical JSON. `undefined` values are dropped (treated as absent) so a
|
|
22
|
+
* field that is sometimes omitted and sometimes explicitly undefined hashes the same.
|
|
23
|
+
*
|
|
24
|
+
* Note: this hashes the MAPPED field values (what actually lands in MJ), not the raw
|
|
25
|
+
* external payload — so a change in a source field we don't map won't (correctly) count
|
|
26
|
+
* as a change for sync purposes.
|
|
27
|
+
*/
|
|
28
|
+
export function computeContentHash(fields) {
|
|
29
|
+
const canonical = canonicalize(fields);
|
|
30
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Stable JSON serialization: object keys sorted recursively, arrays kept in order
|
|
34
|
+
* (array order is semantically meaningful), `undefined` entries omitted. Dates and
|
|
35
|
+
* other non-plain values fall back to their JSON form.
|
|
36
|
+
*/
|
|
37
|
+
function canonicalize(value) {
|
|
38
|
+
if (value === null)
|
|
39
|
+
return 'null';
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return 'null'; // top-level undefined — shouldn't happen, but stay total
|
|
42
|
+
if (typeof value !== 'object')
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
if (Array.isArray(value)) {
|
|
45
|
+
return `[${value.map(v => (v === undefined ? 'null' : canonicalize(v))).join(',')}]`;
|
|
46
|
+
}
|
|
47
|
+
if (value instanceof Date)
|
|
48
|
+
return JSON.stringify(value.toISOString());
|
|
49
|
+
const obj = value;
|
|
50
|
+
const keys = Object.keys(obj).filter(k => obj[k] !== undefined).sort();
|
|
51
|
+
const body = keys.map(k => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',');
|
|
52
|
+
return `{${body}}`;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ContentHash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContentHash.js","sourceRoot":"","sources":["../src/ContentHash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,8BAA8B,CAAC;AAElE;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA+B;IAC9D,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,KAAc;IAChC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,CAAC,yDAAyD;IACjG,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACzF,CAAC;IACD,IAAI,KAAK,YAAY,IAAI;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC;IACvE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrF,OAAO,IAAI,IAAI,GAAG,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SourceObjectInfo } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Optional one-shot description generator. When supplied, objects/fields with no
|
|
4
|
+
* Description get ONE best-effort call (no iterate/converge). Omitted on AI-less
|
|
5
|
+
* instances → descriptions are left as honest gaps, never fabricated. Return null to
|
|
6
|
+
* leave the description unset.
|
|
7
|
+
*/
|
|
8
|
+
export type DescribeFn = (input: {
|
|
9
|
+
kind: 'object' | 'field';
|
|
10
|
+
objectName: string;
|
|
11
|
+
fieldName?: string;
|
|
12
|
+
sourceType?: string;
|
|
13
|
+
}) => Promise<string | null>;
|
|
14
|
+
export interface EnrichOptions {
|
|
15
|
+
describeFn?: DescribeFn;
|
|
16
|
+
}
|
|
17
|
+
export interface EnrichResult {
|
|
18
|
+
inferredForeignKeys: number;
|
|
19
|
+
descriptionsAdded: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Lightweight post-discovery enrichment — the runtime "LightweightConstraintDiscovery"
|
|
23
|
+
* from the integration design. Fills PROVABLE gaps with NO hard AI dependency:
|
|
24
|
+
*
|
|
25
|
+
* - Deterministic, naming-based FK inference: for a field the source did NOT declare
|
|
26
|
+
* as a foreign key, infer `ForeignKeyTarget` only when the column name unambiguously
|
|
27
|
+
* points at exactly one sibling object (e.g. `CompanyId` → `companies`, or a field
|
|
28
|
+
* whose name equals a sibling's PK field name). Ambiguous or self-referential
|
|
29
|
+
* matches are left alone — provable-only, never guess.
|
|
30
|
+
* - Optional one-shot descriptions when a `describeFn` is supplied (AI-optional; a
|
|
31
|
+
* no-op on instances without an AI provider, which is the common BI case).
|
|
32
|
+
*
|
|
33
|
+
* Operates on the discovered SourceObjectInfo[] in place, BEFORE persistence, so the
|
|
34
|
+
* inferred FKs flow through IntegrationSchemaSync's normal Declared/Discovered merge
|
|
35
|
+
* (they land as `MetadataSource='Discovered'`, soft FKs). Never throws.
|
|
36
|
+
*/
|
|
37
|
+
export declare class EnrichSchemaConstraints {
|
|
38
|
+
static Enrich(objects: SourceObjectInfo[], options?: EnrichOptions): Promise<EnrichResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Deterministic naming-based FK inference. Mutates fields in place
|
|
41
|
+
* (IsForeignKey + ForeignKeyTarget + the object's Relationships). Returns the count
|
|
42
|
+
* inferred. Provable-only: a field is inferred to be an FK ONLY when its name maps
|
|
43
|
+
* to exactly one OTHER object.
|
|
44
|
+
*/
|
|
45
|
+
static InferForeignKeys(objects: SourceObjectInfo[]): number;
|
|
46
|
+
/** Fills missing Descriptions via a single describeFn call each. Best-effort, never throws. */
|
|
47
|
+
private static FillDescriptions;
|
|
48
|
+
private static safeDescribe;
|
|
49
|
+
/** Resolves the target object name a field's name points at, or null / AMBIGUOUS. */
|
|
50
|
+
private static inferFieldTarget;
|
|
51
|
+
private static lookupVariant;
|
|
52
|
+
/** Map of normalized object-name variants → ExternalName (or AMBIGUOUS when >1 object shares it). */
|
|
53
|
+
private static buildObjectNameIndex;
|
|
54
|
+
/** Map of lowercased PK field name → the single object declaring it (or AMBIGUOUS). */
|
|
55
|
+
private static buildPKNameIndex;
|
|
56
|
+
/** Conservative singular/plural + lowercase variants of a name for matching. */
|
|
57
|
+
private static normalizedVariants;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=EnrichSchemaConstraints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnrichSchemaConstraints.d.ts","sourceRoot":"","sources":["../src/EnrichSchemaConstraints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,MAAM,YAAY,CAAC;AAKpE;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE;IAC7B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC1B,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAuB;WACZ,MAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAS3G;;;;;OAKG;WACW,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,MAAM;IAoCnE,+FAA+F;mBAC1E,gBAAgB;mBAiBhB,YAAY;IAIjC,qFAAqF;IACrF,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAiB/B,OAAO,CAAC,MAAM,CAAC,aAAa;IAQ5B,qGAAqG;IACrG,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAYnC,uFAAuF;IACvF,OAAO,CAAC,MAAM,CAAC,gBAAgB;IAa/B,gFAAgF;IAChF,OAAO,CAAC,MAAM,CAAC,kBAAkB;CAUpC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Sentinel for a normalized name that maps to more than one object (ambiguous). */
|
|
2
|
+
const AMBIGUOUS = 'ambiguous';
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight post-discovery enrichment — the runtime "LightweightConstraintDiscovery"
|
|
5
|
+
* from the integration design. Fills PROVABLE gaps with NO hard AI dependency:
|
|
6
|
+
*
|
|
7
|
+
* - Deterministic, naming-based FK inference: for a field the source did NOT declare
|
|
8
|
+
* as a foreign key, infer `ForeignKeyTarget` only when the column name unambiguously
|
|
9
|
+
* points at exactly one sibling object (e.g. `CompanyId` → `companies`, or a field
|
|
10
|
+
* whose name equals a sibling's PK field name). Ambiguous or self-referential
|
|
11
|
+
* matches are left alone — provable-only, never guess.
|
|
12
|
+
* - Optional one-shot descriptions when a `describeFn` is supplied (AI-optional; a
|
|
13
|
+
* no-op on instances without an AI provider, which is the common BI case).
|
|
14
|
+
*
|
|
15
|
+
* Operates on the discovered SourceObjectInfo[] in place, BEFORE persistence, so the
|
|
16
|
+
* inferred FKs flow through IntegrationSchemaSync's normal Declared/Discovered merge
|
|
17
|
+
* (they land as `MetadataSource='Discovered'`, soft FKs). Never throws.
|
|
18
|
+
*/
|
|
19
|
+
export class EnrichSchemaConstraints {
|
|
20
|
+
static async Enrich(objects, options = {}) {
|
|
21
|
+
const inferredForeignKeys = this.InferForeignKeys(objects);
|
|
22
|
+
let descriptionsAdded = 0;
|
|
23
|
+
if (options.describeFn) {
|
|
24
|
+
descriptionsAdded = await this.FillDescriptions(objects, options.describeFn);
|
|
25
|
+
}
|
|
26
|
+
return { inferredForeignKeys, descriptionsAdded };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Deterministic naming-based FK inference. Mutates fields in place
|
|
30
|
+
* (IsForeignKey + ForeignKeyTarget + the object's Relationships). Returns the count
|
|
31
|
+
* inferred. Provable-only: a field is inferred to be an FK ONLY when its name maps
|
|
32
|
+
* to exactly one OTHER object.
|
|
33
|
+
*/
|
|
34
|
+
static InferForeignKeys(objects) {
|
|
35
|
+
const byName = this.buildObjectNameIndex(objects);
|
|
36
|
+
const pkNameToObject = this.buildPKNameIndex(objects);
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (const obj of objects) {
|
|
39
|
+
// A single (surrogate) PK is never an FK to itself, so it is skipped. But a
|
|
40
|
+
// COMPOSITE PK is the junction/association pattern where each PK part is
|
|
41
|
+
// typically an FK to a parent — e.g. a contacts↔companies association whose PK
|
|
42
|
+
// is [contact_id, company_id]. Those MUST be inferred so the object becomes
|
|
43
|
+
// FK-reachable and the sync DAG orders it AFTER its parents; without it the
|
|
44
|
+
// association table never orders after its parents and silently fills in nothing.
|
|
45
|
+
const isCompositePK = obj.PrimaryKeyFields.length > 1;
|
|
46
|
+
for (const field of obj.Fields) {
|
|
47
|
+
if (field.IsForeignKey && field.ForeignKeyTarget)
|
|
48
|
+
continue; // already declared
|
|
49
|
+
if (field.IsPrimaryKey && !isCompositePK)
|
|
50
|
+
continue; // surrogate PK ≠ FK; composite-PK parts may be
|
|
51
|
+
const target = this.inferFieldTarget(field, byName, pkNameToObject);
|
|
52
|
+
if (!target || target === AMBIGUOUS || target === obj.ExternalName)
|
|
53
|
+
continue; // skip ambiguous / self
|
|
54
|
+
field.IsForeignKey = true;
|
|
55
|
+
field.ForeignKeyTarget = target;
|
|
56
|
+
if (!obj.Relationships.some(r => r.FieldName === field.Name)) {
|
|
57
|
+
const targetObj = objects.find(o => o.ExternalName === target);
|
|
58
|
+
obj.Relationships.push({
|
|
59
|
+
FieldName: field.Name,
|
|
60
|
+
TargetObject: target,
|
|
61
|
+
TargetField: targetObj?.PrimaryKeyFields[0] ?? 'id',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
count++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
/** Fills missing Descriptions via a single describeFn call each. Best-effort, never throws. */
|
|
70
|
+
static async FillDescriptions(objects, describe) {
|
|
71
|
+
let added = 0;
|
|
72
|
+
for (const obj of objects) {
|
|
73
|
+
if (!obj.Description) {
|
|
74
|
+
const d = await this.safeDescribe(describe, { kind: 'object', objectName: obj.ExternalName });
|
|
75
|
+
if (d) {
|
|
76
|
+
obj.Description = d;
|
|
77
|
+
added++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const field of obj.Fields) {
|
|
81
|
+
if (!field.Description) {
|
|
82
|
+
const d = await this.safeDescribe(describe, { kind: 'field', objectName: obj.ExternalName, fieldName: field.Name, sourceType: field.SourceType });
|
|
83
|
+
if (d) {
|
|
84
|
+
field.Description = d;
|
|
85
|
+
added++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return added;
|
|
91
|
+
}
|
|
92
|
+
static async safeDescribe(describe, input) {
|
|
93
|
+
try {
|
|
94
|
+
return await describe(input);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Resolves the target object name a field's name points at, or null / AMBIGUOUS. */
|
|
101
|
+
static inferFieldTarget(field, byName, pkNameToObject) {
|
|
102
|
+
// 1) Strip a trailing id-suffix and match the base against object-name variants.
|
|
103
|
+
const base = field.Name.replace(/[_-]?id$/i, '');
|
|
104
|
+
if (base && base.toLowerCase() !== field.Name.toLowerCase()) {
|
|
105
|
+
const hit = this.lookupVariant(base, byName);
|
|
106
|
+
if (hit)
|
|
107
|
+
return hit; // may be AMBIGUOUS
|
|
108
|
+
}
|
|
109
|
+
// 2) Field name equals a sibling object's PK field name (e.g. ProfileID → object whose PK is ProfileID).
|
|
110
|
+
const pkHit = pkNameToObject.get(field.Name.toLowerCase());
|
|
111
|
+
if (pkHit)
|
|
112
|
+
return pkHit; // may be AMBIGUOUS
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
static lookupVariant(base, byName) {
|
|
116
|
+
for (const v of this.normalizedVariants(base)) {
|
|
117
|
+
const hit = byName.get(v);
|
|
118
|
+
if (hit)
|
|
119
|
+
return hit;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Map of normalized object-name variants → ExternalName (or AMBIGUOUS when >1 object shares it). */
|
|
124
|
+
static buildObjectNameIndex(objects) {
|
|
125
|
+
const map = new Map();
|
|
126
|
+
for (const o of objects) {
|
|
127
|
+
for (const v of this.normalizedVariants(o.ExternalName)) {
|
|
128
|
+
const existing = map.get(v);
|
|
129
|
+
if (existing === undefined)
|
|
130
|
+
map.set(v, o.ExternalName);
|
|
131
|
+
else if (existing !== o.ExternalName)
|
|
132
|
+
map.set(v, AMBIGUOUS);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return map;
|
|
136
|
+
}
|
|
137
|
+
/** Map of lowercased PK field name → the single object declaring it (or AMBIGUOUS). */
|
|
138
|
+
static buildPKNameIndex(objects) {
|
|
139
|
+
const map = new Map();
|
|
140
|
+
for (const o of objects) {
|
|
141
|
+
for (const pk of o.PrimaryKeyFields) {
|
|
142
|
+
const key = pk.toLowerCase();
|
|
143
|
+
const existing = map.get(key);
|
|
144
|
+
if (existing === undefined)
|
|
145
|
+
map.set(key, o.ExternalName);
|
|
146
|
+
else if (existing !== o.ExternalName)
|
|
147
|
+
map.set(key, AMBIGUOUS);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return map;
|
|
151
|
+
}
|
|
152
|
+
/** Conservative singular/plural + lowercase variants of a name for matching. */
|
|
153
|
+
static normalizedVariants(name) {
|
|
154
|
+
const lower = name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
155
|
+
const set = new Set([lower]);
|
|
156
|
+
if (lower.endsWith('ies'))
|
|
157
|
+
set.add(lower.slice(0, -3) + 'y'); // companies → company
|
|
158
|
+
if (lower.endsWith('es'))
|
|
159
|
+
set.add(lower.slice(0, -2)); // boxes → box
|
|
160
|
+
if (lower.endsWith('s'))
|
|
161
|
+
set.add(lower.slice(0, -1)); // deals → deal
|
|
162
|
+
set.add(lower + 's'); // company → companys (loose)
|
|
163
|
+
if (lower.endsWith('y'))
|
|
164
|
+
set.add(lower.slice(0, -1) + 'ies'); // company → companies
|
|
165
|
+
return Array.from(set).filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=EnrichSchemaConstraints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EnrichSchemaConstraints.js","sourceRoot":"","sources":["../src/EnrichSchemaConstraints.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,17 @@ import type { ExternalRecord, MappedRecord } from './types.js';
|
|
|
5
5
|
* from external records to MJ entity fields.
|
|
6
6
|
*/
|
|
7
7
|
export declare class FieldMappingEngine {
|
|
8
|
+
/**
|
|
9
|
+
* Cache of compiled custom-expression functions, keyed by the expression string, so an
|
|
10
|
+
* identical expression is compiled once instead of once per record in a mapping batch.
|
|
11
|
+
*
|
|
12
|
+
* A failed compile is cached as the `Error` it threw — a malformed expression is therefore
|
|
13
|
+
* compiled a single time (then re-thrown from cache on every record) rather than recompiled
|
|
14
|
+
* on the per-record error path. Bounded via {@link MJLruCache} because the owning
|
|
15
|
+
* `IntegrationEngine` is a process-lifetime singleton; expression cardinality is normally
|
|
16
|
+
* tiny (it tracks configured field maps), so the 1000-entry default is comfortably ample.
|
|
17
|
+
*/
|
|
18
|
+
private readonly customFunctionCache;
|
|
8
19
|
/**
|
|
9
20
|
* Applies field mappings to a batch of external records, producing mapped records
|
|
10
21
|
* ready for match resolution and persistence.
|
|
@@ -90,5 +101,16 @@ export declare class FieldMappingEngine {
|
|
|
90
101
|
* The expression has access to `value` (current field value) and `fields` (all record fields).
|
|
91
102
|
*/
|
|
92
103
|
private ApplyCustom;
|
|
104
|
+
/**
|
|
105
|
+
* Returns the compiled function for an expression, compiling and caching on first use.
|
|
106
|
+
* If the expression failed to compile previously, the cached error is re-thrown — a
|
|
107
|
+
* malformed expression is compiled exactly once across an entire batch.
|
|
108
|
+
*/
|
|
109
|
+
private GetCompiledExpression;
|
|
110
|
+
/**
|
|
111
|
+
* Compiles a custom expression into a callable, returning the thrown `Error` instead of
|
|
112
|
+
* propagating it so the caller can cache compile failures alongside successes.
|
|
113
|
+
*/
|
|
114
|
+
private compileExpression;
|
|
93
115
|
}
|
|
94
116
|
//# sourceMappingURL=FieldMappingEngine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldMappingEngine.d.ts","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FieldMappingEngine.d.ts","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAe/D;;;GAGG;AACH,qBAAa,kBAAkB;IAC3B;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAwD;IAE5F;;;;;;;;OAQG;IACI,KAAK,CACR,OAAO,EAAE,cAAc,EAAE,EACzB,SAAS,EAAE,2BAA2B,EAAE,EACxC,UAAU,EAAE,MAAM,GACnB,YAAY,EAAE;IAKjB;;OAEG;IACH,OAAO,CAAC,eAAe;IAsBvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA6BzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,OAAO,CAAC,WAAW;IAOnB;;OAEG;IACH,OAAO,CAAC,UAAU;IAMlB;;OAEG;IACH,OAAO,CAAC,UAAU;IAMlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAKpB;;OAEG;IACH,OAAO,CAAC,WAAW;IASnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAkBnB;;OAEG;IACH,OAAO,CAAC,UAAU;IAOlB;;OAEG;IACH,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,cAAc;IAQtB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAcnB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAY7B;;;OAGG;IACH,OAAO,CAAC,iBAAiB;CAQ5B"}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { MJLruCache } from '@memberjunction/global';
|
|
1
2
|
/**
|
|
2
3
|
* Engine responsible for applying field-level mappings and transformations
|
|
3
4
|
* from external records to MJ entity fields.
|
|
4
5
|
*/
|
|
5
6
|
export class FieldMappingEngine {
|
|
7
|
+
constructor() {
|
|
8
|
+
/**
|
|
9
|
+
* Cache of compiled custom-expression functions, keyed by the expression string, so an
|
|
10
|
+
* identical expression is compiled once instead of once per record in a mapping batch.
|
|
11
|
+
*
|
|
12
|
+
* A failed compile is cached as the `Error` it threw — a malformed expression is therefore
|
|
13
|
+
* compiled a single time (then re-thrown from cache on every record) rather than recompiled
|
|
14
|
+
* on the per-record error path. Bounded via {@link MJLruCache} because the owning
|
|
15
|
+
* `IntegrationEngine` is a process-lifetime singleton; expression cardinality is normally
|
|
16
|
+
* tiny (it tracks configured field maps), so the 1000-entry default is comfortably ample.
|
|
17
|
+
*/
|
|
18
|
+
this.customFunctionCache = new MJLruCache();
|
|
19
|
+
}
|
|
6
20
|
/**
|
|
7
21
|
* Applies field mappings to a batch of external records, producing mapped records
|
|
8
22
|
* ready for match resolution and persistence.
|
|
@@ -70,7 +84,10 @@ export class FieldMappingEngine {
|
|
|
70
84
|
* and handling errors according to the step's OnError strategy.
|
|
71
85
|
*/
|
|
72
86
|
ExecuteTransformStep(step, value, allFields) {
|
|
73
|
-
|
|
87
|
+
// Grace by default (§8 "still get as much data"): an unconfigured transform that errors on one
|
|
88
|
+
// record's value nulls that FIELD and lets the record sync, rather than failing the record/batch.
|
|
89
|
+
// An author can still opt into strict 'Fail' or 'Skip' explicitly per step.
|
|
90
|
+
const onError = step.OnError ?? 'Null';
|
|
74
91
|
try {
|
|
75
92
|
const transformed = this.DispatchTransform(step, value, allFields);
|
|
76
93
|
return { Value: transformed, Skipped: false };
|
|
@@ -166,10 +183,18 @@ export class FieldMappingEngine {
|
|
|
166
183
|
ApplyFormat(value, config) {
|
|
167
184
|
if (config.FormatType === 'date') {
|
|
168
185
|
const dateVal = value instanceof Date ? value : new Date(String(value));
|
|
186
|
+
// Throw a CONTROLLED error on an unparseable date — vs the raw RangeError FormatDate→toISOString()
|
|
187
|
+
// would throw — so ExecuteTransformStep's OnError strategy handles it uniformly (default 'Null').
|
|
188
|
+
if (isNaN(dateVal.getTime()))
|
|
189
|
+
throw new Error(`Cannot format "${String(value)}" as a date`);
|
|
169
190
|
return this.FormatDate(dateVal, config.FormatString);
|
|
170
191
|
}
|
|
171
192
|
if (config.FormatType === 'number') {
|
|
172
193
|
const numVal = Number(value);
|
|
194
|
+
// Throw on non-numeric — was: emitted the literal string "NaN", which BYPASSED OnError and then
|
|
195
|
+
// failed SQL numeric binding (corrupting the write). OnError now handles it (default 'Null').
|
|
196
|
+
if (!Number.isFinite(numVal))
|
|
197
|
+
throw new Error(`Cannot format "${String(value)}" as a number`);
|
|
173
198
|
return numVal.toFixed(Number(config.FormatString) || 0);
|
|
174
199
|
}
|
|
175
200
|
return String(value ?? '');
|
|
@@ -194,8 +219,14 @@ export class FieldMappingEngine {
|
|
|
194
219
|
return this.CoerceToNumber(value);
|
|
195
220
|
case 'boolean':
|
|
196
221
|
return this.CoerceToBoolean(value);
|
|
197
|
-
case 'date':
|
|
198
|
-
|
|
222
|
+
case 'date': {
|
|
223
|
+
// Throw on unparseable — was: returned an Invalid Date that corrupted the write, bypassing
|
|
224
|
+
// OnError. OnError handles it now (default 'Null' → field nulled, record syncs).
|
|
225
|
+
const d = new Date(String(value));
|
|
226
|
+
if (isNaN(d.getTime()))
|
|
227
|
+
throw new Error(`Cannot coerce "${String(value)}" to date`);
|
|
228
|
+
return d;
|
|
229
|
+
}
|
|
199
230
|
default:
|
|
200
231
|
throw new Error(`Unknown coerce target type: ${config.TargetType}`);
|
|
201
232
|
}
|
|
@@ -205,6 +236,8 @@ export class FieldMappingEngine {
|
|
|
205
236
|
*/
|
|
206
237
|
CoerceToNumber(value) {
|
|
207
238
|
const num = Number(value);
|
|
239
|
+
// Throw on unparseable so OnError decides (default 'Null' → field nulled, record syncs; 'Fail' → strict).
|
|
240
|
+
// The throw is caught by ExecuteTransformStep and never escapes to fail the record/batch.
|
|
208
241
|
if (isNaN(num)) {
|
|
209
242
|
throw new Error(`Cannot coerce "${String(value)}" to number`);
|
|
210
243
|
}
|
|
@@ -237,8 +270,7 @@ export class FieldMappingEngine {
|
|
|
237
270
|
*/
|
|
238
271
|
ApplyCustom(value, allFields, config) {
|
|
239
272
|
try {
|
|
240
|
-
|
|
241
|
-
const fn = new Function('value', 'fields', `return (${config.Expression});`);
|
|
273
|
+
const fn = this.GetCompiledExpression(config.Expression);
|
|
242
274
|
return fn(value, allFields);
|
|
243
275
|
}
|
|
244
276
|
catch (err) {
|
|
@@ -246,5 +278,34 @@ export class FieldMappingEngine {
|
|
|
246
278
|
throw new Error(`Custom transform expression failed: ${message}`);
|
|
247
279
|
}
|
|
248
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Returns the compiled function for an expression, compiling and caching on first use.
|
|
283
|
+
* If the expression failed to compile previously, the cached error is re-thrown — a
|
|
284
|
+
* malformed expression is compiled exactly once across an entire batch.
|
|
285
|
+
*/
|
|
286
|
+
GetCompiledExpression(expression) {
|
|
287
|
+
let cached = this.customFunctionCache.Get(expression);
|
|
288
|
+
if (cached === undefined) {
|
|
289
|
+
cached = this.compileExpression(expression);
|
|
290
|
+
this.customFunctionCache.Set(expression, cached);
|
|
291
|
+
}
|
|
292
|
+
if (cached instanceof Error) {
|
|
293
|
+
throw cached;
|
|
294
|
+
}
|
|
295
|
+
return cached;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Compiles a custom expression into a callable, returning the thrown `Error` instead of
|
|
299
|
+
* propagating it so the caller can cache compile failures alongside successes.
|
|
300
|
+
*/
|
|
301
|
+
compileExpression(expression) {
|
|
302
|
+
try {
|
|
303
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
304
|
+
return new Function('value', 'fields', `return (${expression});`);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
249
310
|
}
|
|
250
311
|
//# sourceMappingURL=FieldMappingEngine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldMappingEngine.js","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FieldMappingEngine.js","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAiBpD;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IAA/B;QACI;;;;;;;;;WASG;QACc,wBAAmB,GAAG,IAAI,UAAU,EAAsC,CAAC;IA2UhG,CAAC;IAzUG;;;;;;;;OAQG;IACI,KAAK,CACR,OAAyB,EACzB,SAAwC,EACxC,UAAkB;QAElB,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;IACvF,CAAC;IAED;;OAEG;IACK,eAAe,CACnB,MAAsB,EACtB,SAAwC,EACxC,UAAkB;QAElB,MAAM,YAAY,GAA4B,EAAE,CAAC;QAEjD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YACvD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACtB,YAAY,CAAC,QAAQ,CAAC,oBAAoB,CAAC,GAAG,KAAK,CAAC;YACxD,CAAC;QACL,CAAC;QAED,OAAO;YACH,cAAc,EAAE,MAAM;YACtB,YAAY,EAAE,UAAU;YACxB,YAAY,EAAE,YAAY;YAC1B,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;SACrD,CAAC;IACN,CAAC;IAED;;;OAGG;IACK,iBAAiB,CACrB,MAAsB,EACtB,QAAqC;QAErC,IAAI,KAAK,GAAY,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;QAEzE,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YACrE,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,SAAS,CAAC;YACrC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QACzB,CAAC;QAED,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;OAEG;IACK,sBAAsB,CAAC,YAA2B;QACtD,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QAE3D,IAAI,CAAC;YACD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACjD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,CAAC;YACtC,OAAO,MAAyB,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,EAAE,CAAC;QACd,CAAC;IACL,CAAC;IAED;;;OAGG;IACK,oBAAoB,CACxB,IAAmB,EACnB,KAAc,EACd,SAAkC;QAElC,+FAA+F;QAC/F,kGAAkG;QAClG,4EAA4E;QAC5E,MAAM,OAAO,GAAqB,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC;QAEzD,IAAI,CAAC;YACD,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;YACnE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,IAAI,CAAC,oBAAoB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACnD,CAAC;IACL,CAAC;IAED;;OAEG;IACK,iBAAiB,CACrB,IAAmB,EACnB,KAAc,EACd,SAAkC;QAElC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YAChB,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,MAAsB,CAAC,CAAC;YAChE,KAAK,OAAO;gBACR,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,MAAqB,CAAC,CAAC;YAC9D,KAAK,OAAO;gBACR,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,MAAqB,CAAC,CAAC;YAC9D,KAAK,SAAS;gBACV,OAAO,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,CAAC,MAAuB,CAAC,CAAC;YACtE,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,MAAsB,CAAC,CAAC;YAChE,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,MAAsB,CAAC,CAAC;YAChE,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,MAAsB,CAAC,CAAC;YAChE,KAAK,WAAW;gBACZ,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,MAAyB,CAAC,CAAC;YACtE,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,MAAsB,CAAC,CAAC;YAC3E;gBACI,MAAM,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAChE,CAAC;IACL,CAAC;IAED;;OAEG;IACK,oBAAoB,CAAC,OAAyB,EAAE,GAAY;QAChE,QAAQ,OAAO,EAAE,CAAC;YACd,KAAK,MAAM;gBACP,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC/C,KAAK,MAAM;gBACP,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC3C,KAAK,MAAM;gBACP,MAAM,GAAG,CAAC;QAClB,CAAC;IACL,CAAC;IAED;;OAEG;IACK,WAAW,CAAC,KAAc,EAAE,MAAoB;QACpD,IAAI,KAAK,IAAI,IAAI,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACrD,OAAO,MAAM,CAAC,YAAY,CAAC;QAC/B,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,KAAc,EAAE,MAAmB;QAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC7D,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACvD,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,KAAc,EAAE,MAAmB;QAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/C,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;IACvC,CAAC;IAED;;OAEG;IACK,YAAY,CAAC,SAAkC,EAAE,MAAqB;QAC1E,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACxE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC;IAED;;OAEG;IACK,WAAW,CAAC,KAAc,EAAE,MAAoB;QACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CACzC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,QAAQ,CAC5C,CAAC;QACF,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;IACvD,CAAC;IAED;;OAEG;IACK,WAAW,CAAC,KAAc,EAAE,MAAoB;QACpD,IAAI,MAAM,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACxE,mGAAmG;YACnG,kGAAkG;YAClG,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAC5F,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,MAAM,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;YAC7B,gGAAgG;YAChG,8FAA8F;YAC9F,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAC9F,OAAO,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAC/B,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAU,EAAE,YAAoB;QAC/C,IAAI,YAAY,CAAC,WAAW,EAAE,KAAK,KAAK,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YACrE,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;QAC9B,CAAC;QACD,OAAO,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACK,WAAW,CAAC,KAAc,EAAE,MAAoB;QACpD,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;YACxB,KAAK,QAAQ;gBACT,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;YAC/B,KAAK,QAAQ;gBACT,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YACtC,KAAK,SAAS;gBACV,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YACvC,KAAK,MAAM,CAAC,CAAC,CAAC;gBACV,2FAA2F;gBAC3F,iFAAiF;gBACjF,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAClC,IAAI,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;oBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBACpF,OAAO,CAAC,CAAC;YACb,CAAC;YACD;gBACI,MAAM,IAAI,KAAK,CAAC,+BAA+B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QAC5E,CAAC;IACL,CAAC;IAED;;OAEG;IACK,cAAc,CAAC,KAAc;QACjC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,0GAA0G;QAC1G,0FAA0F;QAC1F,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAClE,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,KAAc;QAClC,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,KAAK,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAClD,OAAO,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC;IACnE,CAAC;IAED;;OAEG;IACK,cAAc,CAAC,KAAc,EAAE,MAAuB;QAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,OAAO,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;IAED;;;OAGG;IACK,WAAW,CACf,KAAc,EACd,SAAkC,EAClC,MAAoB;QAEpB,IAAI,CAAC;YACD,MAAM,EAAE,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YACzD,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,MAAM,IAAI,KAAK,CAAC,uCAAuC,OAAO,EAAE,CAAC,CAAC;QACtE,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,qBAAqB,CAAC,UAAkB;QAC5C,IAAI,MAAM,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACtD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;YAC5C,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,MAAM,YAAY,KAAK,EAAE,CAAC;YAC1B,MAAM,MAAM,CAAC;QACjB,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED;;;OAGG;IACK,iBAAiB,CAAC,UAAkB;QACxC,IAAI,CAAC;YACD,8DAA8D;YAC9D,OAAO,IAAI,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,UAAU,IAAI,CAAuB,CAAC;QAC5F,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,CAAC;IACL,CAAC;CACJ"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partitioned / Merkle-style hash-diff (§7 "hash-diff / full-table compare to find changed
|
|
3
|
+
* partitions cheaply when no incremental cursor exists").
|
|
4
|
+
*
|
|
5
|
+
* The problem: some sources have NO usable watermark — they can't tell us "what changed since T"
|
|
6
|
+
* (e.g. YourMembership re-fetches every record every time). Per-record content hashing
|
|
7
|
+
* (`computeContentHash`) already lets us skip the per-record load+write for the unchanged majority,
|
|
8
|
+
* but we still have to FETCH every record to hash it. When the remote can expose a cheap per-PARTITION
|
|
9
|
+
* rollup hash (a folder digest, an aggregated checksum, or just a previously-stored snapshot of one),
|
|
10
|
+
* we can compare partition rollups first and only deep-fetch the partitions whose rollup moved.
|
|
11
|
+
*
|
|
12
|
+
* This module builds the local side of that comparison ON TOP of `computeContentHash`:
|
|
13
|
+
* 1. `partitionRecords` — bucket records by an arbitrary partition key.
|
|
14
|
+
* 2. `partitionRollupHash` — fold each bucket's per-record hashes into ONE order-independent digest.
|
|
15
|
+
* 3. `diffPartitions` — compare two partition→rollup maps; only the differing partitions
|
|
16
|
+
* (changed/added/removed) need a deep re-sync.
|
|
17
|
+
*
|
|
18
|
+
* Everything here is pure, deterministic, and free of DB/network — it operates on in-memory
|
|
19
|
+
* record arrays and plain string→string maps, so it is cheaply unit-testable and reusable on either
|
|
20
|
+
* side of the comparison.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Stable partition bucket for a record IDENTITY (e.g. its ExternalID). Hashing the identity (not the
|
|
24
|
+
* content) keeps a record in the SAME partition across syncs even when its content changes — so a
|
|
25
|
+
* content edit shows up as a *changed partition* rather than a record hopping buckets. SHA-256 of the
|
|
26
|
+
* id, first 4 bytes folded modulo `partitionCount`, gives an even, deterministic spread. Default 256
|
|
27
|
+
* buckets is a sane balance (few enough rollups to store, fine-grained enough to skip most work).
|
|
28
|
+
*/
|
|
29
|
+
export declare function partitionKeyForIdentity(identity: string, partitionCount?: number): string;
|
|
30
|
+
/** Outcome of comparing a local partition→rollup map against a remote one. */
|
|
31
|
+
export type PartitionDiff = {
|
|
32
|
+
/** Partitions present on BOTH sides whose rollup hash differs — need a deep re-sync. */
|
|
33
|
+
changed: string[];
|
|
34
|
+
/** Partitions present ONLY in the local map — new since the remote snapshot. */
|
|
35
|
+
added: string[];
|
|
36
|
+
/** Partitions present ONLY in the remote map — removed since the remote snapshot. */
|
|
37
|
+
removed: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Buckets records by a partition key. `getKey` is accepted to keep the call shape symmetric with
|
|
41
|
+
* the rest of the diff pipeline (callers thread a stable record identity through partition + rollup);
|
|
42
|
+
* partitioning itself only needs `getPartition`. Returns a Map keyed by partition string, each value
|
|
43
|
+
* the array of records that fall in that partition. Records arrive in the order encountered — order
|
|
44
|
+
* is preserved within a bucket and never relied upon by the rollup (see `partitionRollupHash`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function partitionRecords<T>(records: readonly T[], getKey: (record: T) => string, getPartition: (record: T) => string): Map<string, T[]>;
|
|
47
|
+
/**
|
|
48
|
+
* ORDER-INDEPENDENT rollup of a partition's records into a single hash. Each record's mapped fields
|
|
49
|
+
* are content-hashed via `computeContentHash`, the per-record hashes are SORTED, then the sorted list
|
|
50
|
+
* is concatenated and SHA-256'd. Sorting before combining is what makes the rollup stable regardless
|
|
51
|
+
* of the order records were fetched in — `[a,b]` and `[b,a]` produce the identical partition digest,
|
|
52
|
+
* so a re-fetch that merely reorders rows never spuriously flags the partition as changed.
|
|
53
|
+
*
|
|
54
|
+
* An empty partition rolls up to the SHA-256 of the empty string, a stable sentinel distinct from any
|
|
55
|
+
* non-empty partition.
|
|
56
|
+
*/
|
|
57
|
+
export declare function partitionRollupHash<T>(records: readonly T[], fieldsOf: (record: T) => Record<string, unknown>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Compares two partition→rollup maps and reports which partitions diverge. Only `changed`/`added`/
|
|
60
|
+
* `removed` partitions need a deep re-sync; partitions whose rollup matches on both sides are proven
|
|
61
|
+
* identical and can be skipped entirely. The result arrays are sorted for deterministic output.
|
|
62
|
+
*
|
|
63
|
+
* - `changed` — key in BOTH maps, rollup values differ.
|
|
64
|
+
* - `added` — key in `local` only.
|
|
65
|
+
* - `removed` — key in `remote` only.
|
|
66
|
+
*/
|
|
67
|
+
export declare function diffPartitions(local: ReadonlyMap<string, string>, remote: ReadonlyMap<string, string>): PartitionDiff;
|
|
68
|
+
//# sourceMappingURL=HashDiff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HashDiff.d.ts","sourceRoot":"","sources":["../src/HashDiff.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,SAAM,GAAG,MAAM,CAGtF;AAED,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG;IACxB,wFAAwF;IACxF,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gFAAgF;IAChF,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qFAAqF;IACrF,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAC9B,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,EAC7B,YAAY,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,GACpC,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAelB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EACjC,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjD,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,aAAa,CAoBf"}
|