@memberjunction/integration-engine 6.1.0-edge.3 → 6.1.0-edge.5
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/BaseIntegrationConnector.d.ts +26 -2
- package/dist/BaseIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseIntegrationConnector.js +145 -23
- package/dist/BaseIntegrationConnector.js.map +1 -1
- package/dist/BaseRESTIntegrationConnector.d.ts +62 -8
- package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseRESTIntegrationConnector.js +200 -20
- package/dist/BaseRESTIntegrationConnector.js.map +1 -1
- package/dist/BatchIdentity.d.ts +47 -0
- package/dist/BatchIdentity.d.ts.map +1 -0
- package/dist/BatchIdentity.js +78 -0
- package/dist/BatchIdentity.js.map +1 -0
- package/dist/DeclaredSampleMerge.d.ts +74 -0
- package/dist/DeclaredSampleMerge.d.ts.map +1 -0
- package/dist/DeclaredSampleMerge.js +104 -0
- package/dist/DeclaredSampleMerge.js.map +1 -0
- package/dist/DiscoveryWatchdog.d.ts +103 -0
- package/dist/DiscoveryWatchdog.d.ts.map +1 -0
- package/dist/DiscoveryWatchdog.js +166 -0
- package/dist/DiscoveryWatchdog.js.map +1 -0
- package/dist/FieldMapValidation.d.ts +40 -0
- package/dist/FieldMapValidation.d.ts.map +1 -0
- package/dist/FieldMapValidation.js +58 -0
- package/dist/FieldMapValidation.js.map +1 -0
- package/dist/FieldMappingEngine.d.ts +1 -13
- package/dist/FieldMappingEngine.d.ts.map +1 -1
- package/dist/FieldMappingEngine.js +18 -3
- package/dist/FieldMappingEngine.js.map +1 -1
- package/dist/IntegrationConnectorCreationPipeline.d.ts +20 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -1
- package/dist/IntegrationConnectorCreationPipeline.js +203 -30
- package/dist/IntegrationConnectorCreationPipeline.js.map +1 -1
- package/dist/IntegrationEngine.d.ts +204 -1
- package/dist/IntegrationEngine.d.ts.map +1 -1
- package/dist/IntegrationEngine.js +1224 -262
- package/dist/IntegrationEngine.js.map +1 -1
- package/dist/IntegrationSchemaSync.d.ts +54 -2
- package/dist/IntegrationSchemaSync.d.ts.map +1 -1
- package/dist/IntegrationSchemaSync.js +122 -16
- package/dist/IntegrationSchemaSync.js.map +1 -1
- package/dist/KeylessRecordGuard.d.ts +56 -0
- package/dist/KeylessRecordGuard.d.ts.map +1 -0
- package/dist/KeylessRecordGuard.js +80 -0
- package/dist/KeylessRecordGuard.js.map +1 -0
- package/dist/ResumeConcurrency.d.ts +46 -0
- package/dist/ResumeConcurrency.d.ts.map +1 -0
- package/dist/ResumeConcurrency.js +74 -0
- package/dist/ResumeConcurrency.js.map +1 -0
- package/dist/RetryAfter.d.ts +50 -0
- package/dist/RetryAfter.d.ts.map +1 -0
- package/dist/RetryAfter.js +127 -0
- package/dist/RetryAfter.js.map +1 -0
- package/dist/RetryRunner.d.ts +26 -2
- package/dist/RetryRunner.d.ts.map +1 -1
- package/dist/RetryRunner.js +11 -3
- package/dist/RetryRunner.js.map +1 -1
- package/dist/StreamingDiscovery.d.ts +11 -0
- package/dist/StreamingDiscovery.d.ts.map +1 -1
- package/dist/StreamingDiscovery.js +13 -3
- package/dist/StreamingDiscovery.js.map +1 -1
- package/dist/SyncDirectives.d.ts +66 -0
- package/dist/SyncDirectives.d.ts.map +1 -0
- package/dist/SyncDirectives.js +123 -0
- package/dist/SyncDirectives.js.map +1 -0
- package/dist/SyncLogger.d.ts +2 -1
- package/dist/SyncLogger.d.ts.map +1 -1
- package/dist/SyncLogger.js +1 -0
- package/dist/SyncLogger.js.map +1 -1
- package/dist/WatermarkService.d.ts +15 -0
- package/dist/WatermarkService.d.ts.map +1 -1
- package/dist/WatermarkService.js +26 -0
- package/dist/WatermarkService.js.map +1 -1
- package/dist/WriteSerializer.d.ts +44 -0
- package/dist/WriteSerializer.d.ts.map +1 -0
- package/dist/WriteSerializer.js +70 -0
- package/dist/WriteSerializer.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +45 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +66 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Within-batch identity: no two records in one batch may share an ExternalID.
|
|
3
|
+
*
|
|
4
|
+
* ExternalID *is* the identity the record map keys on, so two records carrying the same one are two
|
|
5
|
+
* observations of a single source record, never two rows. The write path cannot discover this on its
|
|
6
|
+
* own: it decides insert-vs-update by asking whether the identity already exists in the DATABASE,
|
|
7
|
+
* and for a first-time record neither copy does — so both insert, and the pair re-inserts on every
|
|
8
|
+
* subsequent sync. Nothing downstream can repair it either, because both rows then map to the same
|
|
9
|
+
* ExternalID and no later run can tell which to keep.
|
|
10
|
+
*
|
|
11
|
+
* Observed in the field: an object whose duplicate groups were byte-identical on every captured
|
|
12
|
+
* column AND written inside the same second — meaning the source itself listed the same element
|
|
13
|
+
* twice within one batch, rather than a re-read delivering it again. The whole-batch fingerprint
|
|
14
|
+
* guard nearby catches a batch REPEATED in full (the infinite-loop case); it cannot see duplicates
|
|
15
|
+
* inside a single batch.
|
|
16
|
+
*
|
|
17
|
+
* Last-wins matches upsert semantics: when two entries share an identity but differ, the later
|
|
18
|
+
* entry is the more recent observation of that record's state, which is exactly what a single
|
|
19
|
+
* upsert would have left behind had they arrived in separate batches.
|
|
20
|
+
*/
|
|
21
|
+
/** How many repeated identities to name in the warning. */
|
|
22
|
+
const SAMPLE_LIMIT = 5;
|
|
23
|
+
/**
|
|
24
|
+
* Collapses records sharing an ExternalID within one batch, keeping the LAST occurrence in its
|
|
25
|
+
* original position. Returns the input array unchanged (same reference) when every identity is
|
|
26
|
+
* already unique, so the ordinary path allocates nothing.
|
|
27
|
+
*
|
|
28
|
+
* A record with an empty or missing ExternalID is passed through untouched: identity-less records
|
|
29
|
+
* are a separate defect with a separate guard, and silently collapsing them here would merge
|
|
30
|
+
* unrelated rows.
|
|
31
|
+
*/
|
|
32
|
+
export function CollapseDuplicateIdentities(records) {
|
|
33
|
+
if (records.length < 2) {
|
|
34
|
+
return { Records: records, Collapsed: 0, SampleIDs: [] };
|
|
35
|
+
}
|
|
36
|
+
// First pass: find which identities repeat, without allocating a copy of the batch.
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
let duplicates = 0;
|
|
39
|
+
for (const r of records) {
|
|
40
|
+
const id = r?.ExternalID;
|
|
41
|
+
if (!id)
|
|
42
|
+
continue;
|
|
43
|
+
const seen = counts.get(id);
|
|
44
|
+
if (seen === undefined) {
|
|
45
|
+
counts.set(id, 1);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
counts.set(id, seen + 1);
|
|
49
|
+
duplicates++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (duplicates === 0) {
|
|
53
|
+
return { Records: records, Collapsed: 0, SampleIDs: [] };
|
|
54
|
+
}
|
|
55
|
+
const sample = [];
|
|
56
|
+
for (const [id, n] of counts) {
|
|
57
|
+
if (n > 1 && sample.length < SAMPLE_LIMIT)
|
|
58
|
+
sample.push(id);
|
|
59
|
+
if (sample.length >= SAMPLE_LIMIT)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
// Second pass: keep the LAST occurrence of each repeated identity, in its original slot.
|
|
63
|
+
const lastIndexByID = new Map();
|
|
64
|
+
for (let i = 0; i < records.length; i++) {
|
|
65
|
+
const id = records[i]?.ExternalID;
|
|
66
|
+
if (id)
|
|
67
|
+
lastIndexByID.set(id, i);
|
|
68
|
+
}
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < records.length; i++) {
|
|
71
|
+
const r = records[i];
|
|
72
|
+
const id = r?.ExternalID;
|
|
73
|
+
if (!id || lastIndexByID.get(id) === i)
|
|
74
|
+
out.push(r);
|
|
75
|
+
}
|
|
76
|
+
return { Records: out, Collapsed: records.length - out.length, SampleIDs: sample };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=BatchIdentity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BatchIdentity.js","sourceRoot":"","sources":["../src/BatchIdentity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoBH,2DAA2D;AAC3D,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,UAAU,2BAA2B,CAA0B,OAAyB;IAC1F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,OAAO,EAAE,OAAc,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,oFAAoF;IACpF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACtB,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC;QACzB,IAAI,CAAC,EAAE;YAAE,SAAS;QAClB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACJ,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC;YACzB,UAAU,EAAE,CAAC;QACjB,CAAC;IACL,CAAC;IACD,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,OAAc,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,YAAY;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3D,IAAI,MAAM,CAAC,MAAM,IAAI,YAAY;YAAE,MAAM;IAC7C,CAAC;IAED,yFAAyF;IACzF,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC;QAClC,IAAI,EAAE;YAAE,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,GAAG,GAAQ,EAAE,CAAC;IACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,EAAE,GAAG,CAAC,EAAE,UAAU,CAAC;QACzB,IAAI,CAAC,EAAE,IAAI,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging what a connector DECLARED about an object with what STREAMING THE SOURCE observed.
|
|
3
|
+
*
|
|
4
|
+
* ## Why sampling has to be unconditional
|
|
5
|
+
*
|
|
6
|
+
* Discovery used to stream records only when an object arrived with no fields — later widened to
|
|
7
|
+
* "no fields, or no key". Both are gates on the wrong question, because sampling answers three
|
|
8
|
+
* questions and a declaration can only pre-answer one of them:
|
|
9
|
+
*
|
|
10
|
+
* | question | can a declaration answer it? |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | what is the primary key? | **yes** — a declared key is authoritative |
|
|
13
|
+
* | which fields does the source actually send? | no — a catalog lists what the vendor documents |
|
|
14
|
+
* | how wide are the values? | no — only the data knows |
|
|
15
|
+
*
|
|
16
|
+
* So an object declared with fields AND a key was never sampled, and its undeclared columns arrived
|
|
17
|
+
* later through the overflow path one sync at a time, while its widths were whatever the catalog
|
|
18
|
+
* guessed. A column declared 100 wide against data that is 900 wide is not a slow discovery — it is
|
|
19
|
+
* a truncation or a migration written by hand afterwards.
|
|
20
|
+
*
|
|
21
|
+
* ## The merge is per-attribute, and it is one-directional
|
|
22
|
+
*
|
|
23
|
+
* Sampling never overrides a deliberate declaration; it only fills gaps and widens:
|
|
24
|
+
*
|
|
25
|
+
* - **Primary key** — declared wins outright. An observed key is adopted only when none was declared.
|
|
26
|
+
* - **New fields** — an observed field absent from the declaration is ADDED, so the column exists at
|
|
27
|
+
* RSU time instead of appearing in overflow after a sync.
|
|
28
|
+
* - **Widths** — effective length is `max(declared, observed)`, and an unbounded declaration always
|
|
29
|
+
* wins over any measured number. Never shrink: shrinking is the one outcome that loses data.
|
|
30
|
+
* - **Everything else** — labels, descriptions, types, nullability, FK targets — the declaration
|
|
31
|
+
* stands. A sampled type is inferred from a handful of values; a declared one was written down.
|
|
32
|
+
*
|
|
33
|
+
* The asymmetry is deliberate. A declaration that is WRONG about a width is corrected by the data;
|
|
34
|
+
* a declaration that is RIGHT about a description is not second-guessed by a sample.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The only three attributes the merge reasons about. Declared fields and sampled fields are
|
|
38
|
+
* different shapes elsewhere in the pipeline (`SourceType` vs `DataType`), and neither shape is
|
|
39
|
+
* this module's business — the caller normalizes to one and gets the same one back.
|
|
40
|
+
*/
|
|
41
|
+
export interface MergeableField {
|
|
42
|
+
Name: string;
|
|
43
|
+
MaxLength?: number | null;
|
|
44
|
+
IsPrimaryKey?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** A field's length where `null` means unbounded — the widest thing there is, never narrowed. */
|
|
47
|
+
export type FieldLength = number | null | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Effective length for a field the source both declared and was observed producing.
|
|
50
|
+
*
|
|
51
|
+
* `null` (unbounded) beats every number, on either side: a declaration of "unbounded" is a
|
|
52
|
+
* deliberate statement that no width is safe, and an observation that exceeded the bounded ceiling
|
|
53
|
+
* is the data saying the same thing. Otherwise the larger wins, and a missing side yields to the
|
|
54
|
+
* present one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function MergeLength(declared: FieldLength, observed: FieldLength): FieldLength;
|
|
57
|
+
/** The outcome of merging one object's declared fields with a sample, for logging and tests. */
|
|
58
|
+
export interface MergeOutcome<TField extends MergeableField> {
|
|
59
|
+
Fields: TField[];
|
|
60
|
+
/** Fields the sample found that the declaration never mentioned. */
|
|
61
|
+
AddedFieldNames: string[];
|
|
62
|
+
/** Fields whose declared length was too narrow for the observed data. */
|
|
63
|
+
WidenedFieldNames: string[];
|
|
64
|
+
/** Key columns adopted from the sample because none was declared. */
|
|
65
|
+
AdoptedKeyNames: string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Merges an observed field set into a declared one under the rules above.
|
|
69
|
+
*
|
|
70
|
+
* `declared` is returned unchanged in shape and order, with new fields appended — order carries
|
|
71
|
+
* meaning to a reader comparing a catalog against a table, so observed fields do not interleave.
|
|
72
|
+
*/
|
|
73
|
+
export declare function MergeDeclaredWithSample<TField extends MergeableField>(declared: ReadonlyArray<TField>, observed: ReadonlyArray<TField>): MergeOutcome<TField>;
|
|
74
|
+
//# sourceMappingURL=DeclaredSampleMerge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeclaredSampleMerge.d.ts","sourceRoot":"","sources":["../src/DeclaredSampleMerge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,iGAAiG;AACjG,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,GAAG,WAAW,CAKrF;AAED,gGAAgG;AAChG,MAAM,WAAW,YAAY,CAAC,MAAM,SAAS,cAAc;IACvD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oEAAoE;IACpE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,qEAAqE;IACrE,eAAe,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAS,cAAc,EACjE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,EAC/B,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAChC,YAAY,CAAC,MAAM,CAAC,CA6CtB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merging what a connector DECLARED about an object with what STREAMING THE SOURCE observed.
|
|
3
|
+
*
|
|
4
|
+
* ## Why sampling has to be unconditional
|
|
5
|
+
*
|
|
6
|
+
* Discovery used to stream records only when an object arrived with no fields — later widened to
|
|
7
|
+
* "no fields, or no key". Both are gates on the wrong question, because sampling answers three
|
|
8
|
+
* questions and a declaration can only pre-answer one of them:
|
|
9
|
+
*
|
|
10
|
+
* | question | can a declaration answer it? |
|
|
11
|
+
* |---|---|
|
|
12
|
+
* | what is the primary key? | **yes** — a declared key is authoritative |
|
|
13
|
+
* | which fields does the source actually send? | no — a catalog lists what the vendor documents |
|
|
14
|
+
* | how wide are the values? | no — only the data knows |
|
|
15
|
+
*
|
|
16
|
+
* So an object declared with fields AND a key was never sampled, and its undeclared columns arrived
|
|
17
|
+
* later through the overflow path one sync at a time, while its widths were whatever the catalog
|
|
18
|
+
* guessed. A column declared 100 wide against data that is 900 wide is not a slow discovery — it is
|
|
19
|
+
* a truncation or a migration written by hand afterwards.
|
|
20
|
+
*
|
|
21
|
+
* ## The merge is per-attribute, and it is one-directional
|
|
22
|
+
*
|
|
23
|
+
* Sampling never overrides a deliberate declaration; it only fills gaps and widens:
|
|
24
|
+
*
|
|
25
|
+
* - **Primary key** — declared wins outright. An observed key is adopted only when none was declared.
|
|
26
|
+
* - **New fields** — an observed field absent from the declaration is ADDED, so the column exists at
|
|
27
|
+
* RSU time instead of appearing in overflow after a sync.
|
|
28
|
+
* - **Widths** — effective length is `max(declared, observed)`, and an unbounded declaration always
|
|
29
|
+
* wins over any measured number. Never shrink: shrinking is the one outcome that loses data.
|
|
30
|
+
* - **Everything else** — labels, descriptions, types, nullability, FK targets — the declaration
|
|
31
|
+
* stands. A sampled type is inferred from a handful of values; a declared one was written down.
|
|
32
|
+
*
|
|
33
|
+
* The asymmetry is deliberate. A declaration that is WRONG about a width is corrected by the data;
|
|
34
|
+
* a declaration that is RIGHT about a description is not second-guessed by a sample.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Effective length for a field the source both declared and was observed producing.
|
|
38
|
+
*
|
|
39
|
+
* `null` (unbounded) beats every number, on either side: a declaration of "unbounded" is a
|
|
40
|
+
* deliberate statement that no width is safe, and an observation that exceeded the bounded ceiling
|
|
41
|
+
* is the data saying the same thing. Otherwise the larger wins, and a missing side yields to the
|
|
42
|
+
* present one.
|
|
43
|
+
*/
|
|
44
|
+
export function MergeLength(declared, observed) {
|
|
45
|
+
if (declared === null || observed === null)
|
|
46
|
+
return null;
|
|
47
|
+
if (declared === undefined)
|
|
48
|
+
return observed;
|
|
49
|
+
if (observed === undefined)
|
|
50
|
+
return declared;
|
|
51
|
+
return Math.max(declared, observed);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Merges an observed field set into a declared one under the rules above.
|
|
55
|
+
*
|
|
56
|
+
* `declared` is returned unchanged in shape and order, with new fields appended — order carries
|
|
57
|
+
* meaning to a reader comparing a catalog against a table, so observed fields do not interleave.
|
|
58
|
+
*/
|
|
59
|
+
export function MergeDeclaredWithSample(declared, observed) {
|
|
60
|
+
const observedByLower = new Map(observed.map(f => [f.Name.toLowerCase(), f]));
|
|
61
|
+
const declaredNames = new Set(declared.map(f => f.Name.toLowerCase()));
|
|
62
|
+
const declaredHasKey = declared.some(f => f.IsPrimaryKey === true);
|
|
63
|
+
const widened = [];
|
|
64
|
+
const adoptedKeys = [];
|
|
65
|
+
const merged = declared.map(field => {
|
|
66
|
+
const seen = observedByLower.get(field.Name.toLowerCase());
|
|
67
|
+
if (!seen)
|
|
68
|
+
return field;
|
|
69
|
+
const length = MergeLength(field.MaxLength, seen.MaxLength);
|
|
70
|
+
const widenedHere = length !== field.MaxLength;
|
|
71
|
+
if (widenedHere)
|
|
72
|
+
widened.push(field.Name);
|
|
73
|
+
// The key is adopted only into a declaration that named none. A declared key is
|
|
74
|
+
// authoritative even when the sample would have nominated a different column.
|
|
75
|
+
const adoptKey = !declaredHasKey && seen.IsPrimaryKey === true;
|
|
76
|
+
if (adoptKey)
|
|
77
|
+
adoptedKeys.push(field.Name);
|
|
78
|
+
if (!widenedHere && !adoptKey)
|
|
79
|
+
return field;
|
|
80
|
+
return {
|
|
81
|
+
...field,
|
|
82
|
+
MaxLength: length ?? null,
|
|
83
|
+
IsPrimaryKey: adoptKey ? true : field.IsPrimaryKey,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
// Fields the vendor sends but never documented. Appended as-is: the sample is the only thing
|
|
87
|
+
// that knows anything about them, so there is nothing to defer to.
|
|
88
|
+
const added = [];
|
|
89
|
+
for (const seen of observed) {
|
|
90
|
+
if (declaredNames.has(seen.Name.toLowerCase()))
|
|
91
|
+
continue;
|
|
92
|
+
added.push(seen.Name);
|
|
93
|
+
merged.push({
|
|
94
|
+
...seen,
|
|
95
|
+
// A key nominated on a field the declaration never mentioned is still only adopted
|
|
96
|
+
// when nothing was declared — same rule, applied to the appended half.
|
|
97
|
+
IsPrimaryKey: declaredHasKey ? false : seen.IsPrimaryKey,
|
|
98
|
+
});
|
|
99
|
+
if (!declaredHasKey && seen.IsPrimaryKey === true)
|
|
100
|
+
adoptedKeys.push(seen.Name);
|
|
101
|
+
}
|
|
102
|
+
return { Fields: merged, AddedFieldNames: added, WidenedFieldNames: widened, AdoptedKeyNames: adoptedKeys };
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=DeclaredSampleMerge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeclaredSampleMerge.js","sourceRoot":"","sources":["../src/DeclaredSampleMerge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registry of in-flight discovery samples, reported periodically while any are running.
|
|
3
|
+
*
|
|
4
|
+
* From outside the process, a discovery that is working and one that is hung look identical:
|
|
5
|
+
* nothing is written between the run's start and its end, so a long-but-healthy sample of a slow
|
|
6
|
+
* source and a walk that will never terminate produce the same evidence — none. That is the whole
|
|
7
|
+
* difficulty of diagnosing discovery: the failure mode is silence, and silence is also what
|
|
8
|
+
* success looks like until it finishes.
|
|
9
|
+
*
|
|
10
|
+
* The watchdog turns silence into a statement. Every sample registers itself, notes what stage it
|
|
11
|
+
* is in as it goes, and deregisters when it ends; while anything is registered, a ticker names
|
|
12
|
+
* each object still in flight, how long it has been there, what it is waiting on, and how much it
|
|
13
|
+
* has actually pulled. Silence then means the process is gone — anything still running says so
|
|
14
|
+
* under its own name.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here changes what discovery does; it only makes it visible. The ticker is unref'd, so it
|
|
17
|
+
* can never be the reason Node stays alive, and it exists only while samples are in flight.
|
|
18
|
+
*/
|
|
19
|
+
import { BaseSingleton, IShutdownable } from '@memberjunction/global';
|
|
20
|
+
/** What one in-flight sample is doing right now. */
|
|
21
|
+
export type DiscoveryWatchEntry = {
|
|
22
|
+
/** The external object being sampled. */
|
|
23
|
+
Object: string;
|
|
24
|
+
/** Epoch ms the sample started. */
|
|
25
|
+
StartedMs: number;
|
|
26
|
+
/** Free-text phase, e.g. `FetchChanges#3`. */
|
|
27
|
+
Stage: string;
|
|
28
|
+
/** Pages pulled from the source so far. */
|
|
29
|
+
Pages: number;
|
|
30
|
+
/** Records yielded to the inference so far. */
|
|
31
|
+
Records: number;
|
|
32
|
+
/** Epoch ms the sample must not outlive, when it has a deadline. */
|
|
33
|
+
DeadlineMs?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Mutable fields a caller may update mid-flight. */
|
|
36
|
+
export type DiscoveryWatchPatch = Partial<Pick<DiscoveryWatchEntry, 'Stage' | 'Pages' | 'Records'>>;
|
|
37
|
+
export type DiscoveryWatchdogOptions = {
|
|
38
|
+
/** Report interval. `<= 0` disables reporting entirely (tracking still works). */
|
|
39
|
+
IntervalMs?: number;
|
|
40
|
+
/** Clock, injectable for tests. */
|
|
41
|
+
Now?: () => number;
|
|
42
|
+
/** Sink for the report line. */
|
|
43
|
+
Log?: (message: string) => void;
|
|
44
|
+
/** Timer factory, injectable for tests. Must return something `Clear` accepts. */
|
|
45
|
+
SetInterval?: (fn: () => void, ms: number) => unknown;
|
|
46
|
+
Clear?: (handle: unknown) => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the report interval: `MJ_INTEGRATION_DISCOVERY_WATCHDOG_MS`, default 15s, `0` to
|
|
50
|
+
* silence it. A malformed value falls back to the default rather than disabling the reporting an
|
|
51
|
+
* operator was trying to configure.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ResolveWatchdogIntervalMs(env: string | undefined, defaultMs?: number): number;
|
|
54
|
+
export declare class DiscoveryWatchdog extends BaseSingleton<DiscoveryWatchdog> implements IShutdownable {
|
|
55
|
+
/**
|
|
56
|
+
* Process-wide instance, resolved through the Global Object Store rather than a static field.
|
|
57
|
+
*
|
|
58
|
+
* That distinction matters here specifically: this package is one of the ones that can be
|
|
59
|
+
* loaded twice in a process (a bundled connector copy alongside the standalone one). A static
|
|
60
|
+
* field would then give each copy its OWN registry and its OWN ticker, so samples registered
|
|
61
|
+
* through one would be absent from the other's report — a watchdog that silently under-reports
|
|
62
|
+
* what is in flight, which is worse than no watchdog, because the whole contract is that
|
|
63
|
+
* silence means the process is gone.
|
|
64
|
+
*/
|
|
65
|
+
static get Instance(): DiscoveryWatchdog;
|
|
66
|
+
readonly ShutdownName = "DiscoveryWatchdog";
|
|
67
|
+
private readonly inFlight;
|
|
68
|
+
private intervalMs;
|
|
69
|
+
private now;
|
|
70
|
+
private log;
|
|
71
|
+
private setIntervalFn;
|
|
72
|
+
private clearFn;
|
|
73
|
+
private ticker;
|
|
74
|
+
private keySeq;
|
|
75
|
+
protected constructor();
|
|
76
|
+
/**
|
|
77
|
+
* Overrides the defaults — the interval, and the clock/timer/log seams the tests drive.
|
|
78
|
+
* Mirrors {@link AgentRunWatchdog.Configure}: a singleton cannot take constructor arguments,
|
|
79
|
+
* because `BaseSingleton` returns the stored instance and would discard them.
|
|
80
|
+
*
|
|
81
|
+
* Restarts the ticker when one is already running, so a changed interval takes effect at once.
|
|
82
|
+
*/
|
|
83
|
+
Configure(options?: DiscoveryWatchdogOptions): void;
|
|
84
|
+
/** Drops all in-flight state and stops the ticker. For tests, and for a clean shutdown. */
|
|
85
|
+
Reset(): void;
|
|
86
|
+
/** IShutdownable: a diagnostic ticker must not keep firing while the process drains. */
|
|
87
|
+
Shutdown(): Promise<void>;
|
|
88
|
+
/** Registers a sample and returns the key used to update and end it. */
|
|
89
|
+
Start(objectName: string, deadlineMs?: number): string;
|
|
90
|
+
/** Updates an in-flight sample. A key that has already ended is ignored. */
|
|
91
|
+
Note(key: string | undefined, patch: DiscoveryWatchPatch): void;
|
|
92
|
+
/** Deregisters a sample. Stops the ticker once nothing is left in flight. */
|
|
93
|
+
End(key: string | undefined): void;
|
|
94
|
+
/** Read-only view of what is in flight, for callers that want the numbers at the end. */
|
|
95
|
+
Peek(key: string | undefined): Readonly<DiscoveryWatchEntry> | undefined;
|
|
96
|
+
/** The report line, or `null` when nothing is in flight. Pure — this is what the ticker logs. */
|
|
97
|
+
BuildReport(): string | null;
|
|
98
|
+
/** Emits one report immediately. Exposed so the ticker and tests drive the same path. */
|
|
99
|
+
Tick(): void;
|
|
100
|
+
private ensureTicker;
|
|
101
|
+
private stopTicker;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=DiscoveryWatchdog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscoveryWatchdog.d.ts","sourceRoot":"","sources":["../src/DiscoveryWatchdog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAoB,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAExF,oDAAoD;AACpD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC;AAEpG,MAAM,MAAM,wBAAwB,GAAG;IACnC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,gCAAgC;IAChC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,kFAAkF;IAClF,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACtD,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,SAAS,GAAG,MAAM,CAK7F;AAED,qBAAa,iBAAkB,SAAQ,aAAa,CAAC,iBAAiB,CAAE,YAAW,aAAa;IAC5F;;;;;;;;;OASG;IACH,WAAkB,QAAQ,IAAI,iBAAiB,CAE9C;IAED,SAAgB,YAAY,uBAAuB;IAEnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAAe;IAC1B,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,MAAM,CAAK;IAEnB,SAAS;IAeT;;;;;;OAMG;IACI,SAAS,CAAC,OAAO,GAAE,wBAA6B,GAAG,IAAI;IAY9D,2FAA2F;IACpF,KAAK,IAAI,IAAI;IAMpB,wFAAwF;IAC3E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,wEAAwE;IACjE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM;IAc7D,4EAA4E;IACrE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAMtE,6EAA6E;IACtE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAMzC,yFAAyF;IAClF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC,mBAAmB,CAAC,GAAG,SAAS;IAI/E,iGAAiG;IAC1F,WAAW,IAAI,MAAM,GAAG,IAAI;IAWnC,yFAAyF;IAClF,IAAI,IAAI,IAAI;IASnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,UAAU;CAKrB"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registry of in-flight discovery samples, reported periodically while any are running.
|
|
3
|
+
*
|
|
4
|
+
* From outside the process, a discovery that is working and one that is hung look identical:
|
|
5
|
+
* nothing is written between the run's start and its end, so a long-but-healthy sample of a slow
|
|
6
|
+
* source and a walk that will never terminate produce the same evidence — none. That is the whole
|
|
7
|
+
* difficulty of diagnosing discovery: the failure mode is silence, and silence is also what
|
|
8
|
+
* success looks like until it finishes.
|
|
9
|
+
*
|
|
10
|
+
* The watchdog turns silence into a statement. Every sample registers itself, notes what stage it
|
|
11
|
+
* is in as it goes, and deregisters when it ends; while anything is registered, a ticker names
|
|
12
|
+
* each object still in flight, how long it has been there, what it is waiting on, and how much it
|
|
13
|
+
* has actually pulled. Silence then means the process is gone — anything still running says so
|
|
14
|
+
* under its own name.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here changes what discovery does; it only makes it visible. The ticker is unref'd, so it
|
|
17
|
+
* can never be the reason Node stays alive, and it exists only while samples are in flight.
|
|
18
|
+
*/
|
|
19
|
+
import { BaseSingleton, ShutdownRegistry } from '@memberjunction/global';
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the report interval: `MJ_INTEGRATION_DISCOVERY_WATCHDOG_MS`, default 15s, `0` to
|
|
22
|
+
* silence it. A malformed value falls back to the default rather than disabling the reporting an
|
|
23
|
+
* operator was trying to configure.
|
|
24
|
+
*/
|
|
25
|
+
export function ResolveWatchdogIntervalMs(env, defaultMs = 15_000) {
|
|
26
|
+
if (env === undefined || env.trim() === '')
|
|
27
|
+
return defaultMs;
|
|
28
|
+
const parsed = Number.parseInt(env, 10);
|
|
29
|
+
if (!Number.isFinite(parsed))
|
|
30
|
+
return defaultMs;
|
|
31
|
+
return parsed <= 0 ? 0 : parsed;
|
|
32
|
+
}
|
|
33
|
+
export class DiscoveryWatchdog extends BaseSingleton {
|
|
34
|
+
/**
|
|
35
|
+
* Process-wide instance, resolved through the Global Object Store rather than a static field.
|
|
36
|
+
*
|
|
37
|
+
* That distinction matters here specifically: this package is one of the ones that can be
|
|
38
|
+
* loaded twice in a process (a bundled connector copy alongside the standalone one). A static
|
|
39
|
+
* field would then give each copy its OWN registry and its OWN ticker, so samples registered
|
|
40
|
+
* through one would be absent from the other's report — a watchdog that silently under-reports
|
|
41
|
+
* what is in flight, which is worse than no watchdog, because the whole contract is that
|
|
42
|
+
* silence means the process is gone.
|
|
43
|
+
*/
|
|
44
|
+
static get Instance() {
|
|
45
|
+
return super.getInstance();
|
|
46
|
+
}
|
|
47
|
+
constructor() {
|
|
48
|
+
super();
|
|
49
|
+
this.ShutdownName = 'DiscoveryWatchdog';
|
|
50
|
+
this.inFlight = new Map();
|
|
51
|
+
this.ticker = null;
|
|
52
|
+
this.keySeq = 0;
|
|
53
|
+
this.intervalMs = ResolveWatchdogIntervalMs(process.env.MJ_INTEGRATION_DISCOVERY_WATCHDOG_MS);
|
|
54
|
+
this.now = () => Date.now();
|
|
55
|
+
this.log = message => console.log(message);
|
|
56
|
+
this.setIntervalFn = (fn, ms) => {
|
|
57
|
+
const handle = setInterval(fn, ms);
|
|
58
|
+
// Never hold the process open for a diagnostic.
|
|
59
|
+
if (typeof handle.unref === 'function')
|
|
60
|
+
handle.unref();
|
|
61
|
+
return handle;
|
|
62
|
+
};
|
|
63
|
+
this.clearFn = handle => clearInterval(handle);
|
|
64
|
+
ShutdownRegistry.Instance.Register(this);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Overrides the defaults — the interval, and the clock/timer/log seams the tests drive.
|
|
68
|
+
* Mirrors {@link AgentRunWatchdog.Configure}: a singleton cannot take constructor arguments,
|
|
69
|
+
* because `BaseSingleton` returns the stored instance and would discard them.
|
|
70
|
+
*
|
|
71
|
+
* Restarts the ticker when one is already running, so a changed interval takes effect at once.
|
|
72
|
+
*/
|
|
73
|
+
Configure(options = {}) {
|
|
74
|
+
if (options.IntervalMs !== undefined)
|
|
75
|
+
this.intervalMs = options.IntervalMs;
|
|
76
|
+
if (options.Now)
|
|
77
|
+
this.now = options.Now;
|
|
78
|
+
if (options.Log)
|
|
79
|
+
this.log = options.Log;
|
|
80
|
+
if (options.SetInterval)
|
|
81
|
+
this.setIntervalFn = options.SetInterval;
|
|
82
|
+
if (options.Clear)
|
|
83
|
+
this.clearFn = options.Clear;
|
|
84
|
+
if (this.ticker !== null) {
|
|
85
|
+
this.stopTicker();
|
|
86
|
+
this.ensureTicker();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Drops all in-flight state and stops the ticker. For tests, and for a clean shutdown. */
|
|
90
|
+
Reset() {
|
|
91
|
+
this.inFlight.clear();
|
|
92
|
+
this.stopTicker();
|
|
93
|
+
this.keySeq = 0;
|
|
94
|
+
}
|
|
95
|
+
/** IShutdownable: a diagnostic ticker must not keep firing while the process drains. */
|
|
96
|
+
async Shutdown() {
|
|
97
|
+
this.stopTicker();
|
|
98
|
+
}
|
|
99
|
+
/** Registers a sample and returns the key used to update and end it. */
|
|
100
|
+
Start(objectName, deadlineMs) {
|
|
101
|
+
const key = `${objectName}#${++this.keySeq}`;
|
|
102
|
+
this.inFlight.set(key, {
|
|
103
|
+
Object: objectName,
|
|
104
|
+
StartedMs: this.now(),
|
|
105
|
+
Stage: 'starting',
|
|
106
|
+
Pages: 0,
|
|
107
|
+
Records: 0,
|
|
108
|
+
DeadlineMs: deadlineMs,
|
|
109
|
+
});
|
|
110
|
+
this.ensureTicker();
|
|
111
|
+
return key;
|
|
112
|
+
}
|
|
113
|
+
/** Updates an in-flight sample. A key that has already ended is ignored. */
|
|
114
|
+
Note(key, patch) {
|
|
115
|
+
if (!key)
|
|
116
|
+
return;
|
|
117
|
+
const entry = this.inFlight.get(key);
|
|
118
|
+
if (entry)
|
|
119
|
+
Object.assign(entry, patch);
|
|
120
|
+
}
|
|
121
|
+
/** Deregisters a sample. Stops the ticker once nothing is left in flight. */
|
|
122
|
+
End(key) {
|
|
123
|
+
if (!key)
|
|
124
|
+
return;
|
|
125
|
+
this.inFlight.delete(key);
|
|
126
|
+
if (this.inFlight.size === 0)
|
|
127
|
+
this.stopTicker();
|
|
128
|
+
}
|
|
129
|
+
/** Read-only view of what is in flight, for callers that want the numbers at the end. */
|
|
130
|
+
Peek(key) {
|
|
131
|
+
return key ? this.inFlight.get(key) : undefined;
|
|
132
|
+
}
|
|
133
|
+
/** The report line, or `null` when nothing is in flight. Pure — this is what the ticker logs. */
|
|
134
|
+
BuildReport() {
|
|
135
|
+
if (this.inFlight.size === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const now = this.now();
|
|
138
|
+
const entries = [...this.inFlight.values()].sort((a, b) => a.StartedMs - b.StartedMs);
|
|
139
|
+
const oldestSec = Math.round((now - entries[0].StartedMs) / 1000);
|
|
140
|
+
const rows = entries.map(e => `"${e.Object}" ${Math.round((now - e.StartedMs) / 1000)}s stage=${e.Stage} pages=${e.Pages} records=${e.Records}` +
|
|
141
|
+
(e.DeadlineMs !== undefined ? ` deadlineIn=${Math.round((e.DeadlineMs - now) / 1000)}s` : ''));
|
|
142
|
+
return `[DiscoveryWatchdog] ${entries.length} object(s) still in flight, oldest ${oldestSec}s:\n ${rows.join('\n ')}`;
|
|
143
|
+
}
|
|
144
|
+
/** Emits one report immediately. Exposed so the ticker and tests drive the same path. */
|
|
145
|
+
Tick() {
|
|
146
|
+
if (this.inFlight.size === 0) {
|
|
147
|
+
this.stopTicker();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const report = this.BuildReport();
|
|
151
|
+
if (report)
|
|
152
|
+
this.log(report);
|
|
153
|
+
}
|
|
154
|
+
ensureTicker() {
|
|
155
|
+
if (this.intervalMs <= 0 || this.ticker !== null)
|
|
156
|
+
return;
|
|
157
|
+
this.ticker = this.setIntervalFn(() => this.Tick(), this.intervalMs);
|
|
158
|
+
}
|
|
159
|
+
stopTicker() {
|
|
160
|
+
if (this.ticker === null)
|
|
161
|
+
return;
|
|
162
|
+
this.clearFn(this.ticker);
|
|
163
|
+
this.ticker = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=DiscoveryWatchdog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscoveryWatchdog.js","sourceRoot":"","sources":["../src/DiscoveryWatchdog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start-of-run validation for the MJ side of a field map.
|
|
3
|
+
*
|
|
4
|
+
* A field map's `DestinationFieldName` is the MJ column, in BOTH directions — pull writes it,
|
|
5
|
+
* push reads it back out to build the external payload. Nothing checks that the column exists.
|
|
6
|
+
* `BaseEntity.Set` looks the field up and, when it finds nothing, returns without doing anything
|
|
7
|
+
* and without saying anything: the value is dropped for every record, the entity is never dirtied
|
|
8
|
+
* by it, the save succeeds, and the run reports the rows as written. The observable result is a
|
|
9
|
+
* column's worth of data that never arrives, on a sync that reports success — and if that field is
|
|
10
|
+
* a key field, every record is re-created on the next run because it can never be matched again.
|
|
11
|
+
*
|
|
12
|
+
* This is the cheap check that turns that into one warning per map per run: the map is metadata,
|
|
13
|
+
* so it is verifiable the moment the run knows its entity, long before the first record is fetched.
|
|
14
|
+
* The decision is kept pure so it is tested without a provider or an entity.
|
|
15
|
+
*/
|
|
16
|
+
/** The parts of a field map this validation reads. */
|
|
17
|
+
export type ValidatableFieldMap = {
|
|
18
|
+
SourceFieldName: string;
|
|
19
|
+
DestinationFieldName: string;
|
|
20
|
+
Status?: string | null;
|
|
21
|
+
IsKeyField?: boolean | null;
|
|
22
|
+
};
|
|
23
|
+
/** One map that cannot bind, with the detail an operator needs to fix it. */
|
|
24
|
+
export type UnbindableFieldMap = {
|
|
25
|
+
SourceFieldName: string;
|
|
26
|
+
DestinationFieldName: string;
|
|
27
|
+
IsKeyField: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Returns the ACTIVE field maps whose destination column does not exist on the target entity.
|
|
31
|
+
*
|
|
32
|
+
* Matching is case-insensitive, because `BaseEntity` resolves field names case-insensitively — a
|
|
33
|
+
* map that differs only in casing binds fine and is not a finding. An empty `entityFieldNames`
|
|
34
|
+
* means the caller could not resolve the entity at all; that is a different (already reported)
|
|
35
|
+
* condition, so nothing is returned rather than flagging every map.
|
|
36
|
+
*/
|
|
37
|
+
export declare function FindUnbindableFieldMaps(fieldMaps: readonly ValidatableFieldMap[], entityFieldNames: readonly string[]): UnbindableFieldMap[];
|
|
38
|
+
/** The operator-facing sentence for a set of unbindable maps on one entity map. */
|
|
39
|
+
export declare function DescribeUnbindableFieldMaps(unbindable: readonly UnbindableFieldMap[], externalObjectName: string, entityName: string): string;
|
|
40
|
+
//# sourceMappingURL=FieldMapValidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FieldMapValidation.d.ts","sourceRoot":"","sources":["../src/FieldMapValidation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,sDAAsD;AACtD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAAG;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACnC,SAAS,EAAE,SAAS,mBAAmB,EAAE,EACzC,gBAAgB,EAAE,SAAS,MAAM,EAAE,GACpC,kBAAkB,EAAE,CAgBtB;AAED,mFAAmF;AACnF,wBAAgB,2BAA2B,CACvC,UAAU,EAAE,SAAS,kBAAkB,EAAE,EACzC,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,MAAM,GACnB,MAAM,CAaR"}
|