@memberjunction/integration-engine 5.49.0 → 5.51.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.
@@ -21,6 +21,52 @@ export declare class MatchEngine {
21
21
  * @returns Updated mapped records with resolved ChangeType and MatchedMJRecordID
22
22
  */
23
23
  Resolve(records: MappedRecord[], entityMap: ICompanyIntegrationEntityMap, fieldMaps: ICompanyIntegrationFieldMap[], contextUser: UserInfo, provider?: IMetadataProvider): Promise<MappedRecord[]>;
24
+ /**
25
+ * Issues every lookup this batch needs as ONE batched read, and builds both indexes from it.
26
+ *
27
+ * The legs are independent — the record-map read and each criteria-shape group's read share
28
+ * no data dependency — so they go out together via {@link RunView.RunViews} rather than as
29
+ * separate awaits. Result order matches param order, which is how each leg is attributed back.
30
+ *
31
+ * A `null` index means "the read failed, fall back to the per-record query" and is NOT the
32
+ * same as an empty index, which means "asked, and there is nothing". Conflating them would
33
+ * treat every record as unmapped and turn an incremental sync into a batch of duplicate
34
+ * creates, so the two are kept distinct all the way down.
35
+ */
36
+ private PrefetchBatchLookups;
37
+ /**
38
+ * The read that resolves an entire batch of external IDs against the record map, or null when
39
+ * the batch carries no usable external ID (nothing to ask, so no query is issued).
40
+ */
41
+ private buildRecordMapViewParams;
42
+ /**
43
+ * Builds the record-map index from its leg of the batched read.
44
+ *
45
+ * Returns an index containing only the IDs that HAVE a mapping, so a miss is represented by
46
+ * absence — exactly what the per-record lookup needed to distinguish. Returns null if the read
47
+ * failed, which makes callers fall back to their original per-record query rather than
48
+ * silently treating every record as unmapped.
49
+ *
50
+ * The index carries a second, normalized view of the same rows. The `IN (…)` read is evaluated
51
+ * by the database under its own collation, so a row stored as `abc` comes back for a requested
52
+ * `ABC`; pairing the result up in JavaScript with `===` would then miss it. See
53
+ * {@link normalizeExternalID}.
54
+ */
55
+ private buildRecordMapIndexFromResult;
56
+ /** Builds the exact + normalized views of a record-map read, separating out ambiguous folds. */
57
+ private buildRecordMapIndex;
58
+ /**
59
+ * True when the platform's `=` on string columns is case- and trailing-blank-insensitive, so
60
+ * the JS-side fold in {@link normalizeExternalID} reproduces what the database already did.
61
+ *
62
+ * SQL Server, under the default `SQL_Latin1_General_CP1_CI_AS` collation, is. PostgreSQL is
63
+ * not: `text`/`varchar` equality there is exact on both counts. Folding on PG would make the
64
+ * batch index return a mapping the per-record query would never return — the record would be
65
+ * UPDATED onto someone else's row instead of created, which is the one error direction this
66
+ * whole index is built to avoid. A provider with no dialect (client-side) gets `false`, the
67
+ * conservative answer.
68
+ */
69
+ private get platformFoldsStringEquality();
24
70
  /**
25
71
  * Resolves a single record by checking for an existing MJ match.
26
72
  */
@@ -34,30 +80,126 @@ export declare class MatchEngine {
34
80
  */
35
81
  private ResolveExistingRecord;
36
82
  /**
37
- * Attempts to find an existing MJ record by key field matching, then falls back
38
- * to the CompanyIntegrationRecordMap.
83
+ * Attempts to find an existing MJ record by identity (PK) or, failing that, by the
84
+ * configured key fields — then falls back to the CompanyIntegrationRecordMap.
39
85
  *
40
- * For composite-PK entities (no auto-generated ID), key-field matching is always
41
- * attempted (even when no key fields are configured) so that PK fields themselves
42
- * serve as the unique match criteria. This handles entities like InsightTopics
43
- * where (person_id, topic) together form the natural key.
86
+ * The direct lookup is attempted whenever it CAN resolve something:
87
+ * - the mapped record carries a complete PK (single OR composite) the identity case,
88
+ * and the same key the apply path will address the row by; or
89
+ * - key fields are configured the fallback case, for mapping external records onto
90
+ * pre-existing MJ rows whose PK the external system does not know.
91
+ *
92
+ * Previously a single-PK entity with no configured key fields skipped this entirely and
93
+ * relied on the record map alone — so a record whose map row was missing (or truncated;
94
+ * see `LoadAllRecordMaps`) re-CREATED a row whose PK already existed. Integration shadow
95
+ * tables are exactly this shape: a single soft PK holding the external ID.
44
96
  */
45
97
  private FindExistingRecord;
46
98
  /**
47
- * Searches for an existing MJ record using key field values.
99
+ * Answers the identity/key lookup from the batch prefetch when it can, and only issues the
100
+ * per-record query when the prefetch neither found the record nor proved it absent.
101
+ */
102
+ private LookupByKeyFields;
103
+ /**
104
+ * Returns the entity's primary-key fields (soft PKs included — integration shadow tables
105
+ * carry no physical key, so `PrimaryKeys` is populated from `additionalSchemaInfo` and is
106
+ * still the natural identity).
107
+ */
108
+ private primaryKeyFieldsFor;
109
+ /** True when the mapped data carries a value for EVERY PK field — i.e. it asserts an identity. */
110
+ private hasCompleteMappedPrimaryKey;
111
+ /**
112
+ * Searches for an existing MJ record, using ONE definition of identity.
113
+ *
114
+ * **The unified rule (tasks.md PR 2 item 3).** The entity's primary key — including a
115
+ * soft PK, which is what integration shadow tables carry — is the single definition of
116
+ * record identity, for matching AND for saving. Configured key fields (`IsKeyField`) are
117
+ * a *fallback lookup* for records whose PK the mapped data does not carry, never a
118
+ * competing identity.
48
119
  *
49
- * For composite-PK entities, all PK fields are added to the WHERE filter to
50
- * guarantee a unique match, and the returned ID is a '|'-delimited composite
51
- * of all PK field values (matching the ExternalID format used by connectors).
120
+ * Concretely: when the mapped record carries a COMPLETE PK, match on the PK alone. The
121
+ * apply path already addresses the record by its PK (`CreateRecord.extractMappedPrimaryKey`
122
+ * `InnerLoad`), so matching on anything else is exactly how a record could match one row
123
+ * and then be written to another — or match a row whose PK then collides on save. When the
124
+ * PK is absent or partial (e.g. mapping external records onto pre-existing MJ rows keyed by
125
+ * a server-assigned UUID), fall back to the configured key fields, plus whatever PK parts
126
+ * ARE present to narrow the result.
52
127
  *
53
- * For single-PK entities, behaviour is unchanged: filter by configured key fields,
54
- * return the single PK value as a plain string.
128
+ * The returned ID is always the PK values in `PrimaryKeys` order, '|'-joined — the same
129
+ * format `ExternalID`/`EntityRecordID` use — so the caller can load the row directly.
55
130
  */
56
131
  private FindByKeyFields;
57
132
  /**
58
- * Builds a SQL filter clause from key field values on a mapped record.
133
+ * Builds the field/value criteria that identify a record, applying the item-3 identity rule:
134
+ * a COMPLETE primary key matches on the PK alone; otherwise the configured key fields plus
135
+ * whatever PK parts are present. Returns null when nothing can be matched on.
136
+ *
137
+ * Field/value pairs (rather than pre-rendered SQL) so the same criteria can be rendered as a
138
+ * filter clause AND used as a local lookup key when a whole batch is resolved in one query.
139
+ */
140
+ private BuildMatchCriteria;
141
+ /**
142
+ * Quotes a value as a SQL string literal for a `RunView` `ExtraFilter`.
143
+ *
144
+ * `ExtraFilter` is SQL text, not a parameter list, so literals must be escaped here rather
145
+ * than bound. Where the provider is a database provider this defers to its dialect — the same
146
+ * rule `RecordMapBatch`'s read path uses, so the two cannot disagree about a value.
147
+ * `IMetadataProvider` in general carries no dialect (a client-side provider has none), so the
148
+ * fallback is ANSI quote-doubling, which is what both supported platforms do.
149
+ *
150
+ * Every value quoted here — external IDs and key-field values alike — is compared against an
151
+ * nvarchar column, so it goes through {@link quoteTextLiteral} rather than the dialect's bare
152
+ * `QuoteStringLiteral`: on SQL Server the bare form is a *varchar* literal and silently drops
153
+ * any character outside the database's collation codepage before the comparison.
154
+ */
155
+ private quoteLiteral;
156
+ /**
157
+ * Renders criteria as a filter clause.
158
+ *
159
+ * ANSI double-quoted identifiers — portable across SQL Server (QUOTED_IDENTIFIER ON, the
160
+ * driver default) and Postgres (exact-case; integration columns are lowercase). Plain
161
+ * identifiers break on a column named for a reserved word (e.g. a soft PK named
162
+ * `open`/`order`); brackets would fix SQL Server but break Postgres, so double-quote.
163
+ */
164
+ private CriteriaToSQL;
165
+ /**
166
+ * Order-independent local lookup key for a criteria set (and for a row read back for it).
167
+ *
168
+ * `\0` delimits both within and between pairs, because neither a field name nor a value can
169
+ * contain it and no printable delimiter can make that guarantee. With a space, field `A B`
170
+ * = `C` and field `A` = `B C` both render `A B C` — and since this key is what decides which
171
+ * row answers which criteria, a collision attributes a row to the wrong record.
172
+ */
173
+ private CriteriaKey;
174
+ /**
175
+ * Resolves the identity/key lookup for a WHOLE batch in one query per criteria shape,
176
+ * instead of one query per record (tasks.md PR 2 item 5).
177
+ *
178
+ * Records are grouped by entity + the exact set of fields they match on, so every record in
179
+ * a group produces a structurally identical AND-clause; the group's clauses are OR-ed into a
180
+ * single read and the rows attributed back locally by the same field/value pairs that built
181
+ * the filter. A 500-record batch of an integration shadow table (single soft PK) collapses
182
+ * from 500 reads to 1.
183
+ *
184
+ * Attribution is deliberately conservative. `Matched` holds records we found a row for;
185
+ * `Unmatched` holds records we can PROVE have no row — only populated when the group's read
186
+ * returned nothing at all, so a record whose row exists but whose local key comparison
187
+ * differs from the database's (case-insensitive collation, numeric/date formatting) is left
188
+ * in neither set and falls back to its own query. A false "unmatched" would create a
189
+ * duplicate, so it is never inferred.
190
+ *
191
+ * This half is pure: it builds the reads without issuing them, so every group goes out as one
192
+ * leg of the batch's single {@link RunView.RunViews} call. {@link MatchEngine.buildKeyMatchIndex}
193
+ * attributes the results back.
194
+ */
195
+ private buildKeyMatchGroups;
196
+ /**
197
+ * Attributes the batched group reads back to their criteria, positionally, in the order
198
+ * {@link MatchEngine.buildKeyMatchGroups} emitted them.
199
+ *
200
+ * Returns null if any group read failed — callers then use the original per-record path.
59
201
  */
60
- private BuildKeyFieldFilter;
202
+ private buildKeyMatchIndex;
61
203
  /**
62
204
  * Checks the CompanyIntegrationRecordMap for a previous external↔MJ mapping.
63
205
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MatchEngine.d.ts","sourceRoot":"","sources":["../src/MatchEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAqB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,KAAK,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACnG,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AAGnE;;;GAGG;AACH,qBAAa,WAAW;IACpB,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAAC,CAAoB;IAEtC,4FAA4F;IAC5F,SAAS,KAAK,aAAa,IAAI,iBAAiB,CAE/C;IAED;;;;;;;;;OASG;IACU,OAAO,CAChB,OAAO,EAAE,YAAY,EAAE,EACvB,SAAS,EAAE,4BAA4B,EACvC,SAAS,EAAE,2BAA2B,EAAE,EACxC,WAAW,EAAE,QAAQ,EACrB,QAAQ,CAAC,EAAE,iBAAiB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC;IAmB1B;;OAEG;YACW,mBAAmB;IAsBjC;;OAEG;YACW,oBAAoB;IAmBlC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;;;;;;OAQG;YACW,kBAAkB;IAuBhC;;;;;;;;;OASG;YACW,eAAe;IAqD7B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;OAEG;YACW,kBAAkB;CA0BnC"}
1
+ {"version":3,"file":"MatchEngine.d.ts","sourceRoot":"","sources":["../src/MatchEngine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,iBAAiB,EAA6D,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACzJ,OAAO,KAAK,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AACnG,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,YAAY,CAAC;AA8EnE;;;GAGG;AACH,qBAAa,WAAW;IACpB,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAAC,CAAoB;IAEtC,4FAA4F;IAC5F,SAAS,KAAK,aAAa,IAAI,iBAAiB,CAE/C;IAED;;;;;;;;;OASG;IACU,OAAO,CAChB,OAAO,EAAE,YAAY,EAAE,EACvB,SAAS,EAAE,4BAA4B,EACvC,SAAS,EAAE,2BAA2B,EAAE,EACxC,WAAW,EAAE,QAAQ,EACrB,QAAQ,CAAC,EAAE,iBAAiB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC;IAkC1B;;;;;;;;;;;OAWG;YACW,oBAAoB;IA+BlC;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAyBhC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,6BAA6B;IAOrC,gGAAgG;IAChG,OAAO,CAAC,mBAAmB;IA6B3B;;;;;;;;;;OAUG;IACH,OAAO,KAAK,2BAA2B,GAMtC;IAED;;OAEG;YACW,mBAAmB;IAwBjC;;OAEG;YACW,oBAAoB;IAqBlC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;;;;;;;;;;;;OAcG;YACW,kBAAkB;IAsBhC;;;OAGG;YACW,iBAAiB;IAkB/B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAK3B,kGAAkG;IAClG,OAAO,CAAC,2BAA2B;IAMnC;;;;;;;;;;;;;;;;;;;OAmBG;YACW,eAAe;IAyB7B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAyC1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,YAAY;IAOpB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAMrB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;IAQnB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,mBAAmB;IAgD3B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;OAEG;YACW,kBAAkB;CA8CnC"}