@memberjunction/integration-engine 5.41.0 → 5.43.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 +51 -0
- package/dist/BaseIntegrationConnector.d.ts +109 -0
- package/dist/BaseIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseIntegrationConnector.js +308 -93
- package/dist/BaseIntegrationConnector.js.map +1 -1
- package/dist/BaseRESTIntegrationConnector.d.ts +36 -0
- package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseRESTIntegrationConnector.js +228 -28
- package/dist/BaseRESTIntegrationConnector.js.map +1 -1
- package/dist/ContentHash.d.ts +22 -0
- package/dist/ContentHash.d.ts.map +1 -1
- package/dist/ContentHash.js +35 -0
- package/dist/ContentHash.js.map +1 -1
- package/dist/CustomColumnPromotion.d.ts +108 -0
- package/dist/CustomColumnPromotion.d.ts.map +1 -0
- package/dist/CustomColumnPromotion.js +202 -0
- package/dist/CustomColumnPromotion.js.map +1 -0
- package/dist/CustomOverflow.d.ts +64 -0
- package/dist/CustomOverflow.d.ts.map +1 -0
- package/dist/CustomOverflow.js +74 -0
- package/dist/CustomOverflow.js.map +1 -0
- package/dist/FieldMappingEngine.d.ts +16 -90
- package/dist/FieldMappingEngine.d.ts.map +1 -1
- package/dist/FieldMappingEngine.js +42 -257
- package/dist/FieldMappingEngine.js.map +1 -1
- package/dist/IntegrationConnectorCreationPipeline.d.ts +33 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -1
- package/dist/IntegrationConnectorCreationPipeline.js +157 -0
- package/dist/IntegrationConnectorCreationPipeline.js.map +1 -1
- package/dist/IntegrationEngine.d.ts +120 -6
- package/dist/IntegrationEngine.d.ts.map +1 -1
- package/dist/IntegrationEngine.js +890 -168
- package/dist/IntegrationEngine.js.map +1 -1
- package/dist/IntegrationSchemaSync.d.ts +84 -13
- package/dist/IntegrationSchemaSync.d.ts.map +1 -1
- package/dist/IntegrationSchemaSync.js +198 -36
- package/dist/IntegrationSchemaSync.js.map +1 -1
- package/dist/KeySerialization.d.ts +21 -0
- package/dist/KeySerialization.d.ts.map +1 -0
- package/dist/KeySerialization.js +29 -0
- package/dist/KeySerialization.js.map +1 -0
- package/dist/MatchEngine.d.ts.map +1 -1
- package/dist/MatchEngine.js +12 -4
- package/dist/MatchEngine.js.map +1 -1
- package/dist/RecordFlatten.d.ts +49 -0
- package/dist/RecordFlatten.d.ts.map +1 -0
- package/dist/RecordFlatten.js +50 -0
- package/dist/RecordFlatten.js.map +1 -0
- package/dist/StreamingDiscovery.d.ts +145 -0
- package/dist/StreamingDiscovery.d.ts.map +1 -0
- package/dist/StreamingDiscovery.js +306 -0
- package/dist/StreamingDiscovery.js.map +1 -0
- package/dist/SyncLogger.d.ts +1 -1
- package/dist/SyncLogger.d.ts.map +1 -1
- package/dist/SyncLogger.js +19 -1
- package/dist/SyncLogger.js.map +1 -1
- package/dist/auth-helpers/BasicAuthHeaderBuilder.d.ts +36 -0
- package/dist/auth-helpers/BasicAuthHeaderBuilder.d.ts.map +1 -0
- package/dist/auth-helpers/BasicAuthHeaderBuilder.js +38 -0
- package/dist/auth-helpers/BasicAuthHeaderBuilder.js.map +1 -0
- package/dist/auth-helpers/OAuth1aSigner.d.ts +56 -0
- package/dist/auth-helpers/OAuth1aSigner.d.ts.map +1 -0
- package/dist/auth-helpers/OAuth1aSigner.js +91 -0
- package/dist/auth-helpers/OAuth1aSigner.js.map +1 -0
- package/dist/auth-helpers/OAuth2TokenManager.d.ts +103 -0
- package/dist/auth-helpers/OAuth2TokenManager.d.ts.map +1 -0
- package/dist/auth-helpers/OAuth2TokenManager.js +143 -0
- package/dist/auth-helpers/OAuth2TokenManager.js.map +1 -0
- package/dist/auth-helpers/index.d.ts +11 -0
- package/dist/auth-helpers/index.d.ts.map +1 -0
- package/dist/auth-helpers/index.js +8 -0
- package/dist/auth-helpers/index.js.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FieldTransformEngine } from '@memberjunction/global';
|
|
2
|
+
import { computeUnmappedFields } from './CustomOverflow.js';
|
|
3
|
+
import { flattenRecord, hasNestedObject } from './RecordFlatten.js';
|
|
2
4
|
/**
|
|
3
|
-
* Engine responsible for applying field-level mappings and transformations
|
|
4
|
-
*
|
|
5
|
+
* Engine responsible for applying field-level mappings and transformations from external records to
|
|
6
|
+
* MJ entity fields.
|
|
7
|
+
*
|
|
8
|
+
* The per-value transform pipeline (direct / regex / split / combine / lookup / format / coerce /
|
|
9
|
+
* substring / custom) is owned by the shared {@link FieldTransformEngine} in `@memberjunction/global`
|
|
10
|
+
* and reused here, so integration sync and the rules-based bulk-update processor run the *exact same*
|
|
11
|
+
* transform implementation — one place to fix, one place to extend. This engine keeps only the
|
|
12
|
+
* integration-specific concerns: source flattening, field-map iteration, and unmapped-field overflow
|
|
13
|
+
* capture. See the Field Rules guide in `@memberjunction/global` for the shared engine, and
|
|
14
|
+
* `EntityFieldRules` in `@memberjunction/core` for the metadata-aware entity-update sibling.
|
|
5
15
|
*/
|
|
6
16
|
export class FieldMappingEngine {
|
|
7
17
|
constructor() {
|
|
8
|
-
/**
|
|
9
|
-
|
|
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();
|
|
18
|
+
/** The shared transform pipeline. Holds its own LRU cache of compiled custom expressions. */
|
|
19
|
+
this.transformEngine = new FieldTransformEngine();
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
|
-
* Applies field mappings to a batch of external records, producing mapped records
|
|
22
|
-
*
|
|
22
|
+
* Applies field mappings to a batch of external records, producing mapped records ready for match
|
|
23
|
+
* resolution and persistence.
|
|
23
24
|
*
|
|
24
25
|
* @param records - External records to map
|
|
25
26
|
* @param fieldMaps - Active field map entities defining source→destination mappings
|
|
@@ -34,34 +35,46 @@ export class FieldMappingEngine {
|
|
|
34
35
|
* Maps a single external record through all active field mappings.
|
|
35
36
|
*/
|
|
36
37
|
MapSingleRecord(record, fieldMaps, entityName) {
|
|
38
|
+
// Flatten nested source objects to scalar columns (e.g. checkin_question.id →
|
|
39
|
+
// checkin_question_id) BEFORE mapping. The match keys on the mapped PK VALUE
|
|
40
|
+
// (MatchEngine.FindByKeyFields), so an object-valued source field would produce a
|
|
41
|
+
// per-occurrence / per-version key → duplicate rows. Discovery flattens identically, so
|
|
42
|
+
// the field maps reference the flattened scalar names. A record with no nested objects
|
|
43
|
+
// passes through unchanged — every flat-record connector is a no-op here.
|
|
44
|
+
const ext = hasNestedObject(record.Fields)
|
|
45
|
+
? { ...record, Fields: flattenRecord(record.Fields) }
|
|
46
|
+
: record;
|
|
37
47
|
const mappedFields = {};
|
|
48
|
+
const mappedSourceNames = new Set();
|
|
38
49
|
for (const fieldMap of fieldMaps) {
|
|
39
|
-
|
|
50
|
+
mappedSourceNames.add(fieldMap.SourceFieldName);
|
|
51
|
+
const value = this.ApplyFieldMapping(ext, fieldMap);
|
|
40
52
|
if (value !== undefined) {
|
|
41
53
|
mappedFields[fieldMap.DestinationFieldName] = value;
|
|
42
54
|
}
|
|
43
55
|
}
|
|
56
|
+
// Capture the source keys with no field map (gaps.md §2). Cheap O(columns) diff;
|
|
57
|
+
// the result is empty (and discarded by the writer) in the common all-mapped case,
|
|
58
|
+
// so this adds no measurable cost to a customs-free sync. The engine parks any extras
|
|
59
|
+
// in the __mj_integration_CustomOverflow system column — see {@link CustomOverflow}.
|
|
60
|
+
const unmappedFields = computeUnmappedFields(ext.Fields, mappedSourceNames);
|
|
44
61
|
return {
|
|
45
|
-
ExternalRecord:
|
|
62
|
+
ExternalRecord: ext,
|
|
46
63
|
MJEntityName: entityName,
|
|
47
64
|
MappedFields: mappedFields,
|
|
48
|
-
ChangeType:
|
|
65
|
+
ChangeType: ext.IsDeleted ? 'Delete' : 'Create',
|
|
66
|
+
UnmappedFields: unmappedFields,
|
|
49
67
|
};
|
|
50
68
|
}
|
|
51
69
|
/**
|
|
52
|
-
* Applies a single field mapping, including the full transform pipeline
|
|
53
|
-
* Returns undefined if the field should be skipped (OnError: Skip).
|
|
70
|
+
* Applies a single field mapping, including the full transform pipeline (delegated to the shared
|
|
71
|
+
* {@link FieldTransformEngine}). Returns undefined if the field should be skipped (OnError: Skip).
|
|
54
72
|
*/
|
|
55
73
|
ApplyFieldMapping(record, fieldMap) {
|
|
56
|
-
|
|
74
|
+
const value = record.Fields[fieldMap.SourceFieldName];
|
|
57
75
|
const pipeline = this.ParseTransformPipeline(fieldMap.TransformPipeline);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (result.Skipped)
|
|
61
|
-
return undefined;
|
|
62
|
-
value = result.Value;
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
76
|
+
const result = this.transformEngine.ExecutePipeline(value, record.Fields, pipeline);
|
|
77
|
+
return result.Skipped ? undefined : result.Value;
|
|
65
78
|
}
|
|
66
79
|
/**
|
|
67
80
|
* Parses the JSON transform pipeline string into typed TransformStep objects.
|
|
@@ -79,233 +92,5 @@ export class FieldMappingEngine {
|
|
|
79
92
|
return [];
|
|
80
93
|
}
|
|
81
94
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Executes a single transform step, applying the configured transformation
|
|
84
|
-
* and handling errors according to the step's OnError strategy.
|
|
85
|
-
*/
|
|
86
|
-
ExecuteTransformStep(step, value, allFields) {
|
|
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';
|
|
91
|
-
try {
|
|
92
|
-
const transformed = this.DispatchTransform(step, value, allFields);
|
|
93
|
-
return { Value: transformed, Skipped: false };
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
return this.HandleTransformError(onError, err);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Dispatches to the appropriate transform handler based on step type.
|
|
101
|
-
*/
|
|
102
|
-
DispatchTransform(step, value, allFields) {
|
|
103
|
-
switch (step.Type) {
|
|
104
|
-
case 'direct':
|
|
105
|
-
return this.ApplyDirect(value, step.Config);
|
|
106
|
-
case 'regex':
|
|
107
|
-
return this.ApplyRegex(value, step.Config);
|
|
108
|
-
case 'split':
|
|
109
|
-
return this.ApplySplit(value, step.Config);
|
|
110
|
-
case 'combine':
|
|
111
|
-
return this.ApplyCombine(allFields, step.Config);
|
|
112
|
-
case 'lookup':
|
|
113
|
-
return this.ApplyLookup(value, step.Config);
|
|
114
|
-
case 'format':
|
|
115
|
-
return this.ApplyFormat(value, step.Config);
|
|
116
|
-
case 'coerce':
|
|
117
|
-
return this.ApplyCoerce(value, step.Config);
|
|
118
|
-
case 'substring':
|
|
119
|
-
return this.ApplySubstring(value, step.Config);
|
|
120
|
-
case 'custom':
|
|
121
|
-
return this.ApplyCustom(value, allFields, step.Config);
|
|
122
|
-
default:
|
|
123
|
-
throw new Error(`Unknown transform type: ${step.Type}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Handles a transform error according to the OnError strategy.
|
|
128
|
-
*/
|
|
129
|
-
HandleTransformError(onError, err) {
|
|
130
|
-
switch (onError) {
|
|
131
|
-
case 'Skip':
|
|
132
|
-
return { Value: undefined, Skipped: true };
|
|
133
|
-
case 'Null':
|
|
134
|
-
return { Value: null, Skipped: false };
|
|
135
|
-
case 'Fail':
|
|
136
|
-
throw err;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Direct pass-through, applying default value if source is null/undefined.
|
|
141
|
-
*/
|
|
142
|
-
ApplyDirect(value, config) {
|
|
143
|
-
if (value == null && config.DefaultValue !== undefined) {
|
|
144
|
-
return config.DefaultValue;
|
|
145
|
-
}
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Applies a regex replacement to a string value.
|
|
150
|
-
*/
|
|
151
|
-
ApplyRegex(value, config) {
|
|
152
|
-
const strValue = String(value ?? '');
|
|
153
|
-
const regex = new RegExp(config.Pattern, config.Flags ?? '');
|
|
154
|
-
return strValue.replace(regex, config.Replacement);
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Splits a string value and extracts a part by index.
|
|
158
|
-
*/
|
|
159
|
-
ApplySplit(value, config) {
|
|
160
|
-
const strValue = String(value ?? '');
|
|
161
|
-
const parts = strValue.split(config.Delimiter);
|
|
162
|
-
return parts[config.Index] ?? null;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Combines multiple source fields with a separator.
|
|
166
|
-
*/
|
|
167
|
-
ApplyCombine(allFields, config) {
|
|
168
|
-
const values = config.SourceFields.map(f => String(allFields[f] ?? ''));
|
|
169
|
-
return values.join(config.Separator);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Performs a case-insensitive value lookup/mapping.
|
|
173
|
-
*/
|
|
174
|
-
ApplyLookup(value, config) {
|
|
175
|
-
const strValue = String(value ?? '');
|
|
176
|
-
const lowerKey = strValue.toLowerCase();
|
|
177
|
-
const entry = Object.entries(config.Map).find(([key]) => key.toLowerCase() === lowerKey);
|
|
178
|
-
return entry ? entry[1] : (config.Default ?? null);
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Applies date/number/string formatting.
|
|
182
|
-
*/
|
|
183
|
-
ApplyFormat(value, config) {
|
|
184
|
-
if (config.FormatType === 'date') {
|
|
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`);
|
|
190
|
-
return this.FormatDate(dateVal, config.FormatString);
|
|
191
|
-
}
|
|
192
|
-
if (config.FormatType === 'number') {
|
|
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`);
|
|
198
|
-
return numVal.toFixed(Number(config.FormatString) || 0);
|
|
199
|
-
}
|
|
200
|
-
return String(value ?? '');
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Basic date formatting supporting ISO and common format tokens.
|
|
204
|
-
*/
|
|
205
|
-
FormatDate(date, formatString) {
|
|
206
|
-
if (formatString.toLowerCase() === 'iso' || formatString === 'ISO8601') {
|
|
207
|
-
return date.toISOString();
|
|
208
|
-
}
|
|
209
|
-
return date.toLocaleDateString('en-US');
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Coerces a value to the specified target type.
|
|
213
|
-
*/
|
|
214
|
-
ApplyCoerce(value, config) {
|
|
215
|
-
switch (config.TargetType) {
|
|
216
|
-
case 'string':
|
|
217
|
-
return String(value ?? '');
|
|
218
|
-
case 'number':
|
|
219
|
-
return this.CoerceToNumber(value);
|
|
220
|
-
case 'boolean':
|
|
221
|
-
return this.CoerceToBoolean(value);
|
|
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
|
-
}
|
|
230
|
-
default:
|
|
231
|
-
throw new Error(`Unknown coerce target type: ${config.TargetType}`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Coerces a value to a number, throwing on NaN.
|
|
236
|
-
*/
|
|
237
|
-
CoerceToNumber(value) {
|
|
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.
|
|
241
|
-
if (isNaN(num)) {
|
|
242
|
-
throw new Error(`Cannot coerce "${String(value)}" to number`);
|
|
243
|
-
}
|
|
244
|
-
return num;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Coerces a value to boolean, supporting common truthy/falsy strings.
|
|
248
|
-
*/
|
|
249
|
-
CoerceToBoolean(value) {
|
|
250
|
-
if (typeof value === 'boolean')
|
|
251
|
-
return value;
|
|
252
|
-
if (typeof value === 'number')
|
|
253
|
-
return value !== 0;
|
|
254
|
-
const strVal = String(value).toLowerCase().trim();
|
|
255
|
-
return strVal === 'true' || strVal === '1' || strVal === 'yes';
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Extracts a substring from a string value.
|
|
259
|
-
*/
|
|
260
|
-
ApplySubstring(value, config) {
|
|
261
|
-
const strValue = String(value ?? '');
|
|
262
|
-
if (config.Length !== undefined) {
|
|
263
|
-
return strValue.substring(config.Start, config.Start + config.Length);
|
|
264
|
-
}
|
|
265
|
-
return strValue.substring(config.Start);
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Evaluates a custom JavaScript expression.
|
|
269
|
-
* The expression has access to `value` (current field value) and `fields` (all record fields).
|
|
270
|
-
*/
|
|
271
|
-
ApplyCustom(value, allFields, config) {
|
|
272
|
-
try {
|
|
273
|
-
const fn = this.GetCompiledExpression(config.Expression);
|
|
274
|
-
return fn(value, allFields);
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
278
|
-
throw new Error(`Custom transform expression failed: ${message}`);
|
|
279
|
-
}
|
|
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
|
-
}
|
|
310
95
|
}
|
|
311
96
|
//# sourceMappingURL=FieldMappingEngine.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FieldMappingEngine.js","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"FieldMappingEngine.js","sourceRoot":"","sources":["../src/FieldMappingEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAKpE;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,kBAAkB;IAA/B;QACI,6FAA6F;QAC5E,oBAAe,GAAG,IAAI,oBAAoB,EAAE,CAAC;IA4FlE,CAAC;IA1FG;;;;;;;;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,8EAA8E;QAC9E,6EAA6E;QAC7E,kFAAkF;QAClF,wFAAwF;QACxF,uFAAuF;QACvF,0EAA0E;QAC1E,MAAM,GAAG,GAAmB,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC;YACtD,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACrD,CAAC,CAAC,MAAM,CAAC;QAEb,MAAM,YAAY,GAA4B,EAAE,CAAC;QACjD,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAU,CAAC;QAE5C,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YAC/B,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;YAChD,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACtB,YAAY,CAAC,QAAQ,CAAC,oBAAoB,CAAC,GAAG,KAAK,CAAC;YACxD,CAAC;QACL,CAAC;QAED,iFAAiF;QACjF,mFAAmF;QACnF,sFAAsF;QACtF,qFAAqF;QACrF,MAAM,cAAc,GAAG,qBAAqB,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;QAE5E,OAAO;YACH,cAAc,EAAE,GAAG;YACnB,YAAY,EAAE,UAAU;YACxB,YAAY,EAAE,YAAY;YAC1B,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ;YAC/C,cAAc,EAAE,cAAc;SACjC,CAAC;IACN,CAAC;IAED;;;OAGG;IACK,iBAAiB,CACrB,MAAsB,EACtB,QAAqC;QAErC,MAAM,KAAK,GAAY,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,sBAAsB,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACpF,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;IACrD,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;CACJ"}
|
|
@@ -38,6 +38,13 @@ export interface ConnectorCreationPipelineOptions {
|
|
|
38
38
|
RunID?: string;
|
|
39
39
|
/** Trigger reason recorded in the manifest. */
|
|
40
40
|
TriggerType?: 'Manual' | 'Scheduled' | 'Webhook' | 'Pipeline' | 'Restart';
|
|
41
|
+
/**
|
|
42
|
+
* §7 — when true, this is a COMPREHENSIVE re-discovery: declared/discovered objects (and their
|
|
43
|
+
* fields) ABSENT from this run are deactivated (Status='Disabled', never deleted; reversible on a
|
|
44
|
+
* later rediscovery). Only set on a full-surface refresh — a scoped/partial discovery must leave it
|
|
45
|
+
* false so it never disables what it didn't look at. Threaded to PersistDiscoveredSchema.
|
|
46
|
+
*/
|
|
47
|
+
DeactivateAbsent?: boolean;
|
|
41
48
|
}
|
|
42
49
|
/** Outcome of a single pipeline invocation. */
|
|
43
50
|
export interface ConnectorCreationPipelineResult {
|
|
@@ -77,7 +84,33 @@ export interface ConnectorCreationPipelineResult {
|
|
|
77
84
|
* `__mj.Entity`. The pipeline emits `entity.skipped-no-pk` events for visibility.
|
|
78
85
|
*/
|
|
79
86
|
export declare class IntegrationConnectorCreationPipeline {
|
|
87
|
+
/** In-flight runs by CompanyIntegrationID — coalesces a concurrent duplicate onto the same promise. */
|
|
88
|
+
private static readonly inFlightRuns;
|
|
89
|
+
/** Just-completed runs by CompanyIntegrationID — coalesces a *sequential* duplicate within the window. */
|
|
90
|
+
private static readonly recentRuns;
|
|
91
|
+
/** Default coalesce window (ms) when the env override is unset/invalid. */
|
|
92
|
+
private static readonly DEFAULT_COALESCE_WINDOW_MS;
|
|
93
|
+
/**
|
|
94
|
+
* How long a just-completed run is reused for a duplicate invocation of the SAME CompanyIntegration.
|
|
95
|
+
* Sized only to absorb the create-time double-fire (the `IsActive` Save-hook runs the pipeline, then
|
|
96
|
+
* `IntegrationCreateConnection` calls it again milliseconds later) — short enough that a genuine,
|
|
97
|
+
* later operator-initiated re-refresh always runs fresh. Override via the
|
|
98
|
+
* `MJ_CONNECTOR_PIPELINE_COALESCE_WINDOW_MS` env var (a positive integer of milliseconds; set 0/unset
|
|
99
|
+
* to use the default). Env-based, matching the RSU env-var convention (RSU_WORK_DIR, etc.).
|
|
100
|
+
*/
|
|
101
|
+
private static get COALESCE_WINDOW_MS();
|
|
102
|
+
/**
|
|
103
|
+
* Public entry. De-dups the known create-time double-invocation: the connection's `IsActive`
|
|
104
|
+
* false→true Save fires the entity-server hook (which runs this pipeline WITH LLM PK inference),
|
|
105
|
+
* and the create resolver then calls it again. Both converge here, so we run the pipeline ONCE
|
|
106
|
+
* per CompanyIntegration and hand both callers the same result — no double introspect/persist/
|
|
107
|
+
* classify, no double live API calls, and the resolver still gets a real summary. A legitimate
|
|
108
|
+
* re-refresh later (outside the window) runs fresh.
|
|
109
|
+
*/
|
|
80
110
|
Run(opts: ConnectorCreationPipelineOptions): Promise<ConnectorCreationPipelineResult>;
|
|
111
|
+
/** Drops recent-run entries older than the coalesce window so the map stays bounded. */
|
|
112
|
+
private static pruneRecentRuns;
|
|
113
|
+
private runInternal;
|
|
81
114
|
private StageConnectionTest;
|
|
82
115
|
private StageIntrospect;
|
|
83
116
|
private StagePersist;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntegrationConnectorCreationPipeline.d.ts","sourceRoot":"","sources":["../src/IntegrationConnectorCreationPipeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EACR,0BAA0B,EAG7B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAEH,KAAK,sBAAsB,EAC9B,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAEH,KAAK,kBAAkB,EAC1B,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,wBAAwB,
|
|
1
|
+
{"version":3,"file":"IntegrationConnectorCreationPipeline.d.ts","sourceRoot":"","sources":["../src/IntegrationConnectorCreationPipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,7 +21,65 @@ import { IntegrationSchemaSync } from './IntegrationSchemaSync.js';
|
|
|
21
21
|
* `__mj.Entity`. The pipeline emits `entity.skipped-no-pk` events for visibility.
|
|
22
22
|
*/
|
|
23
23
|
export class IntegrationConnectorCreationPipeline {
|
|
24
|
+
/** In-flight runs by CompanyIntegrationID — coalesces a concurrent duplicate onto the same promise. */
|
|
25
|
+
static { this.inFlightRuns = new Map(); }
|
|
26
|
+
/** Just-completed runs by CompanyIntegrationID — coalesces a *sequential* duplicate within the window. */
|
|
27
|
+
static { this.recentRuns = new Map(); }
|
|
28
|
+
/** Default coalesce window (ms) when the env override is unset/invalid. */
|
|
29
|
+
static { this.DEFAULT_COALESCE_WINDOW_MS = 5000; }
|
|
30
|
+
/**
|
|
31
|
+
* How long a just-completed run is reused for a duplicate invocation of the SAME CompanyIntegration.
|
|
32
|
+
* Sized only to absorb the create-time double-fire (the `IsActive` Save-hook runs the pipeline, then
|
|
33
|
+
* `IntegrationCreateConnection` calls it again milliseconds later) — short enough that a genuine,
|
|
34
|
+
* later operator-initiated re-refresh always runs fresh. Override via the
|
|
35
|
+
* `MJ_CONNECTOR_PIPELINE_COALESCE_WINDOW_MS` env var (a positive integer of milliseconds; set 0/unset
|
|
36
|
+
* to use the default). Env-based, matching the RSU env-var convention (RSU_WORK_DIR, etc.).
|
|
37
|
+
*/
|
|
38
|
+
static get COALESCE_WINDOW_MS() {
|
|
39
|
+
const override = Number(process.env.MJ_CONNECTOR_PIPELINE_COALESCE_WINDOW_MS);
|
|
40
|
+
return Number.isFinite(override) && override > 0 ? override : IntegrationConnectorCreationPipeline.DEFAULT_COALESCE_WINDOW_MS;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Public entry. De-dups the known create-time double-invocation: the connection's `IsActive`
|
|
44
|
+
* false→true Save fires the entity-server hook (which runs this pipeline WITH LLM PK inference),
|
|
45
|
+
* and the create resolver then calls it again. Both converge here, so we run the pipeline ONCE
|
|
46
|
+
* per CompanyIntegration and hand both callers the same result — no double introspect/persist/
|
|
47
|
+
* classify, no double live API calls, and the resolver still gets a real summary. A legitimate
|
|
48
|
+
* re-refresh later (outside the window) runs fresh.
|
|
49
|
+
*/
|
|
24
50
|
async Run(opts) {
|
|
51
|
+
const ciID = opts.CompanyIntegration?.ID;
|
|
52
|
+
if (!ciID)
|
|
53
|
+
return this.runInternal(opts); // no key to de-dup on — run directly
|
|
54
|
+
const cls = IntegrationConnectorCreationPipeline;
|
|
55
|
+
const inFlight = cls.inFlightRuns.get(ciID);
|
|
56
|
+
if (inFlight)
|
|
57
|
+
return inFlight; // a concurrent run is already going — share it
|
|
58
|
+
cls.pruneRecentRuns();
|
|
59
|
+
const recent = cls.recentRuns.get(ciID);
|
|
60
|
+
if (recent)
|
|
61
|
+
return recent.result; // a run just completed (within the window) — reuse it
|
|
62
|
+
const promise = this.runInternal(opts);
|
|
63
|
+
cls.inFlightRuns.set(ciID, promise);
|
|
64
|
+
try {
|
|
65
|
+
const result = await promise;
|
|
66
|
+
cls.recentRuns.set(ciID, { result, at: Date.now() });
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
cls.inFlightRuns.delete(ciID);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Drops recent-run entries older than the coalesce window so the map stays bounded. */
|
|
74
|
+
static pruneRecentRuns() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
for (const [key, entry] of IntegrationConnectorCreationPipeline.recentRuns) {
|
|
77
|
+
if (now - entry.at >= IntegrationConnectorCreationPipeline.COALESCE_WINDOW_MS) {
|
|
78
|
+
IntegrationConnectorCreationPipeline.recentRuns.delete(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async runInternal(opts) {
|
|
25
83
|
const runID = opts.RunID ?? IntegrationProgressEmitter.newRunID('connector');
|
|
26
84
|
const manifest = {
|
|
27
85
|
runID,
|
|
@@ -95,6 +153,103 @@ export class IntegrationConnectorCreationPipeline {
|
|
|
95
153
|
const startMs = Date.now();
|
|
96
154
|
try {
|
|
97
155
|
const schema = await opts.Connector.IntrospectSchema(opts.CompanyIntegration, opts.ContextUser, opts.IntrospectOptions);
|
|
156
|
+
// UNIVERSAL additive runtime-object discovery — the single chokepoint EVERY connector
|
|
157
|
+
// funnels through, regardless of which base it extends or whether that base's
|
|
158
|
+
// IntrospectSchema only reflects already-declared metadata. IntrospectSchema gives the
|
|
159
|
+
// rich declared/persisted catalog; we then ADD any objects the connector surfaces at
|
|
160
|
+
// runtime that aren't already in it, via the abstract DiscoverObjects/DiscoverFields
|
|
161
|
+
// primitives that EVERY connector implements (so a future connector on a different base
|
|
162
|
+
// can't silently lose runtime discovery). PersistDiscoveredSchema is additive, so
|
|
163
|
+
// declared objects are preserved and runtime-only objects (e.g. an auth-gated file
|
|
164
|
+
// feed's streams) get created as Discovered. Errors are SURFACED, never swallowed.
|
|
165
|
+
const seen = new Set(schema.Objects.map(o => o.ExternalName.toLowerCase()));
|
|
166
|
+
let runtimeObjects = [];
|
|
167
|
+
try {
|
|
168
|
+
runtimeObjects = await opts.Connector.DiscoverObjects(opts.CompanyIntegration, opts.ContextUser);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
+
emitter.stageError('Introspect', `DiscoverObjects failed: ${msg}`, { code: 'discover-objects-failed' });
|
|
173
|
+
console.error(`[IntrospectPipeline] DiscoverObjects failed: ${msg}`);
|
|
174
|
+
}
|
|
175
|
+
let runtimeAdded = 0;
|
|
176
|
+
for (const d of runtimeObjects) {
|
|
177
|
+
const key = d.Name.toLowerCase();
|
|
178
|
+
if (seen.has(key)) {
|
|
179
|
+
// §case-3 (data-only-discoverable): a DECLARED object the connector ALSO surfaces at
|
|
180
|
+
// runtime, but the declared form carries NO fields (e.g. a file-feed stream declared by
|
|
181
|
+
// NAME only — its columns are knowable solely from the records). Without this, the loop
|
|
182
|
+
// skipped it, it stayed field-less + PK-less, and ApplyAll dropped it — so the user's data
|
|
183
|
+
// for that object never landed. Discover its fields over the read path and populate the
|
|
184
|
+
// existing declared object IN PLACE so it becomes syncable.
|
|
185
|
+
const existing = schema.Objects.find(o => o.ExternalName.toLowerCase() === key);
|
|
186
|
+
if (existing && existing.Fields.length === 0) {
|
|
187
|
+
try {
|
|
188
|
+
const dfields = await opts.Connector.DiscoverFieldsViaFetch(opts.CompanyIntegration, d.Name, opts.ContextUser);
|
|
189
|
+
existing.Fields = dfields.map(f => ({
|
|
190
|
+
Name: f.Name, Label: f.Label, Description: f.Description, SourceType: f.DataType,
|
|
191
|
+
IsRequired: f.IsRequired, AllowsNull: f.AllowsNull, MaxLength: f.MaxLength ?? null,
|
|
192
|
+
Precision: f.Precision ?? null, Scale: f.Scale ?? null, DefaultValue: f.DefaultValue ?? null,
|
|
193
|
+
IsPrimaryKey: f.IsPrimaryKey ?? false, IsUniqueKey: f.IsUniqueKey, IsReadOnly: f.IsReadOnly,
|
|
194
|
+
IsForeignKey: f.IsForeignKey ?? false, ForeignKeyTarget: f.ForeignKeyTarget ?? null,
|
|
195
|
+
}));
|
|
196
|
+
existing.PrimaryKeyFields = dfields.filter(f => f.IsPrimaryKey).map(f => f.Name);
|
|
197
|
+
existing.Relationships = dfields
|
|
198
|
+
.filter(f => (f.IsForeignKey ?? false) && f.ForeignKeyTarget)
|
|
199
|
+
.map(f => ({ FieldName: f.Name, TargetObject: f.ForeignKeyTarget, TargetField: 'ID' }));
|
|
200
|
+
console.log(`[IntrospectPipeline] declared field-less object "${d.Name}" → discovered ${dfields.length} fields via read path`);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
emitter.stageError('Introspect', `DiscoverFieldsViaFetch failed for declared field-less "${d.Name}": ${msg}`, { code: 'discover-fields-failed' });
|
|
205
|
+
console.error(`[IntrospectPipeline] DiscoverFieldsViaFetch failed for declared "${d.Name}": ${msg}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
seen.add(key);
|
|
211
|
+
let fields = [];
|
|
212
|
+
try {
|
|
213
|
+
// Discover fields + PROVABLE PK over the READ PATH (FetchChanges), time-bounded and
|
|
214
|
+
// SAVE-LESS — stream as much of the feed as the budget allows so the PK decision is
|
|
215
|
+
// made on a statistically-significant sample, not a single (possibly tiny) file. No
|
|
216
|
+
// DB write happens here; the real save is the later ApplyAll → StartSync.
|
|
217
|
+
fields = await opts.Connector.DiscoverFieldsViaFetch(opts.CompanyIntegration, d.Name, opts.ContextUser);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
221
|
+
emitter.stageError('Introspect', `DiscoverFieldsViaFetch failed for "${d.Name}": ${msg}`, { code: 'discover-fields-failed' });
|
|
222
|
+
console.error(`[IntrospectPipeline] DiscoverFieldsViaFetch failed for "${d.Name}": ${msg}`);
|
|
223
|
+
}
|
|
224
|
+
schema.Objects.push({
|
|
225
|
+
ExternalName: d.Name,
|
|
226
|
+
ExternalLabel: d.Label,
|
|
227
|
+
Description: d.Description,
|
|
228
|
+
Fields: fields.map(f => ({
|
|
229
|
+
Name: f.Name,
|
|
230
|
+
Label: f.Label,
|
|
231
|
+
Description: f.Description,
|
|
232
|
+
SourceType: f.DataType,
|
|
233
|
+
IsRequired: f.IsRequired,
|
|
234
|
+
AllowsNull: f.AllowsNull,
|
|
235
|
+
MaxLength: f.MaxLength ?? null,
|
|
236
|
+
Precision: f.Precision ?? null,
|
|
237
|
+
Scale: f.Scale ?? null,
|
|
238
|
+
DefaultValue: f.DefaultValue ?? null,
|
|
239
|
+
IsPrimaryKey: f.IsPrimaryKey ?? false,
|
|
240
|
+
IsUniqueKey: f.IsUniqueKey,
|
|
241
|
+
IsReadOnly: f.IsReadOnly,
|
|
242
|
+
IsForeignKey: f.IsForeignKey ?? false,
|
|
243
|
+
ForeignKeyTarget: f.ForeignKeyTarget ?? null,
|
|
244
|
+
})),
|
|
245
|
+
PrimaryKeyFields: fields.filter(f => f.IsPrimaryKey).map(f => f.Name),
|
|
246
|
+
Relationships: fields
|
|
247
|
+
.filter(f => (f.IsForeignKey ?? false) && f.ForeignKeyTarget)
|
|
248
|
+
.map(f => ({ FieldName: f.Name, TargetObject: f.ForeignKeyTarget, TargetField: 'ID' })),
|
|
249
|
+
});
|
|
250
|
+
runtimeAdded++;
|
|
251
|
+
}
|
|
252
|
+
console.log(`[IntrospectPipeline] declared=${seen.size - runtimeAdded} runtime-added=${runtimeAdded} total=${schema.Objects.length}`);
|
|
98
253
|
const fieldCount = schema.Objects.reduce((acc, o) => acc + o.Fields.length, 0);
|
|
99
254
|
emitter.stageComplete('Introspect', {
|
|
100
255
|
processed: schema.Objects.length,
|
|
@@ -124,6 +279,8 @@ export class IntegrationConnectorCreationPipeline {
|
|
|
124
279
|
ContextUser: opts.ContextUser,
|
|
125
280
|
Provider: opts.Provider,
|
|
126
281
|
UseTransactionGroup: true,
|
|
282
|
+
// §7 comprehensive-refresh deactivation (objects + fields absent from this discovery).
|
|
283
|
+
DeactivateAbsent: opts.DeactivateAbsent ?? false,
|
|
127
284
|
});
|
|
128
285
|
// Emit per-object + per-field structural-transparency events so the UI/audit
|
|
129
286
|
// trail captures WHO won each attribute.
|