@memberjunction/record-comparison 0.0.1 → 5.44.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 CHANGED
@@ -1,45 +1,53 @@
1
1
  # @memberjunction/record-comparison
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/record-comparison`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
3
+ Framework-agnostic **record comparison** primitive for MemberJunction.
4
+
5
+ `RecordComparisonEngine` loads a set of records for a single entity and computes a
6
+ structured, field-level delta between them — a survivor candidate plus its potential
7
+ matches diffed column-by-column. It performs a read-only load (`RunView` with
8
+ `ResultType: 'simple'` and a targeted field list) and never mutates records.
9
+
10
+ ## Why this package exists
11
+
12
+ The comparison logic is needed by two callers that sit on **opposite sides** of the
13
+ dependency graph:
14
+
15
+ - The **LLM duplicate-detection reasoning path** (`@memberjunction/ai-vector-dupe`),
16
+ which feeds the differing-field deltas to the reasoning provider as context.
17
+ - The **server/UI side-by-side comparison panel**
18
+ (`@memberjunction/core-entities-server` `MJServer` resolver GraphQL client
19
+ Angular `record-merge`).
20
+
21
+ `@memberjunction/core-entities-server` depends on `@memberjunction/ai-vector-dupe`, so
22
+ the dupe package sits *lower* in the graph and cannot reach an engine that lives in the
23
+ server package without creating a build cycle. Hoisting the engine into this low-level
24
+ package (its only dependency is `@memberjunction/core`) gives both sides a single
25
+ implementation with no cycle and no duplicated delta logic.
26
+
27
+ ## Usage
28
+
29
+ ```typescript
30
+ import { RecordComparisonEngine } from '@memberjunction/record-comparison';
31
+ import { CompositeKey } from '@memberjunction/core';
32
+
33
+ const engine = new RecordComparisonEngine();
34
+
35
+ // By entity name (resolver / UI path) — resolves the entity from metadata.
36
+ const result = await engine.CompareRecords(
37
+ { EntityName: 'Accounts', Keys: [survivorKey, candidateKey] },
38
+ contextUser,
39
+ provider // request-scoped IMetadataProvider (multi-provider safety)
40
+ );
41
+
42
+ // With an already-resolved EntityInfo + an injected RunView (dupe path) — skips the
43
+ // by-name lookup and reuses the caller's request-scoped RunView.
44
+ const result2 = await engine.CompareRecordsForEntity(
45
+ entityInfo,
46
+ [sourceKey, ...candidateKeys],
47
+ contextUser,
48
+ { runView }
49
+ );
50
+ ```
51
+
52
+ `result.Fields` is a plain-data delta matrix (no `BaseEntity` instances), so it
53
+ serializes cleanly across GraphQL and into a reasoning prompt.
@@ -0,0 +1,193 @@
1
+ import { RunView, CompositeKey, EntityInfo, EntityFieldInfo, UserInfo, IMetadataProvider } from '@memberjunction/core';
2
+ /**
3
+ * Input to {@link RecordComparisonEngine.CompareRecords}.
4
+ *
5
+ * Describes the set of records (a survivor candidate plus its potential matches)
6
+ * to load and diff field-by-field for a single entity.
7
+ */
8
+ export interface RecordComparisonInput {
9
+ /** Registered entity name (e.g. "Accounts"), NOT the physical table name. */
10
+ EntityName: string;
11
+ /**
12
+ * The composite keys of the records to compare. By convention the first key is
13
+ * the survivor candidate and the remainder are potential matches, but the engine
14
+ * treats all records uniformly — ordering is preserved in the output column index.
15
+ */
16
+ Keys: CompositeKey[];
17
+ /**
18
+ * Optional include-list of field names to restrict the comparison to. When omitted,
19
+ * all visible (non-PK, non-system) fields are compared. Field names are matched
20
+ * case-insensitively against the entity's field metadata.
21
+ */
22
+ IncludeFields?: string[];
23
+ }
24
+ /**
25
+ * One record loaded for comparison. The `Key` correlates this record back to the
26
+ * input column index; `Values` is a plain map of fieldName → value for that record.
27
+ */
28
+ export interface RecordComparisonRecord {
29
+ /** Zero-based index aligned with the input `Keys` array (the comparison column). */
30
+ ColumnIndex: number;
31
+ /** The composite key identifying this record (as supplied in the input). */
32
+ Key: CompositeKey;
33
+ /** A human-readable label for the record (name-field value when available, else the key). */
34
+ Label: string;
35
+ /** Plain field-name → value map for this record (only the compared fields). */
36
+ Values: Record<string, RecordFieldValue>;
37
+ }
38
+ /** A scalar field value as loaded from the database (never a BaseEntity). */
39
+ export type RecordFieldValue = string | number | boolean | null;
40
+ /**
41
+ * Options for {@link RecordComparisonEngine.CompareRecordsForEntity}.
42
+ */
43
+ export interface RecordComparisonOptions {
44
+ /**
45
+ * Optional include-list of field names. When omitted, all visible (non-PK, non-system)
46
+ * fields are compared. Matched case-insensitively against the entity's field metadata.
47
+ */
48
+ IncludeFields?: string[];
49
+ /**
50
+ * An already-constructed `RunView` to load the records through. When supplied, it is used
51
+ * verbatim, so callers can thread their own request-scoped/provider-bound RunView (e.g. the
52
+ * duplicate detector). When omitted, a RunView is built from {@link RecordComparisonOptions.Provider}.
53
+ */
54
+ RunViewInstance?: RunView;
55
+ /** Request-scoped metadata provider (multi-provider safety) used to build a RunView when one isn't supplied. */
56
+ Provider?: IMetadataProvider;
57
+ }
58
+ /**
59
+ * The per-record value of a single field within a {@link RecordComparisonFieldDelta}.
60
+ */
61
+ export interface RecordComparisonFieldCell {
62
+ /** Column index aligned with {@link RecordComparisonRecord.ColumnIndex}. */
63
+ ColumnIndex: number;
64
+ /** The field's value for this record. */
65
+ Value: RecordFieldValue;
66
+ /**
67
+ * True when this cell's value is equal (case-insensitive, trimmed) to the
68
+ * reference cell (column 0, the survivor candidate). Column 0 is always true.
69
+ */
70
+ EqualsReference: boolean;
71
+ }
72
+ /**
73
+ * The delta for a single field across all compared records.
74
+ */
75
+ export interface RecordComparisonFieldDelta {
76
+ /** Field name (matches the entity field metadata Name). */
77
+ FieldName: string;
78
+ /** Display name for the field (falls back to FieldName). */
79
+ DisplayName: string;
80
+ /** Optional grouping category from the field metadata. */
81
+ Category: string | null;
82
+ /** Per-record values for this field, in column order. */
83
+ Cells: RecordComparisonFieldCell[];
84
+ /**
85
+ * True when at least one record's value differs from the reference (column 0).
86
+ * False when every record shares the same value.
87
+ */
88
+ Differs: boolean;
89
+ }
90
+ /**
91
+ * The serializable result of comparing a set of records: the loaded records plus a
92
+ * per-field delta matrix. This is the LLM "deltas" payload and (eventually) the UI
93
+ * side-by-side model — it is plain-data only and contains no BaseEntity instances.
94
+ */
95
+ export interface RecordComparisonResult {
96
+ /** Whether the comparison succeeded. */
97
+ Success: boolean;
98
+ /** Populated when {@link Success} is false. */
99
+ ErrorMessage?: string;
100
+ /** Registered entity name that was compared. */
101
+ EntityName: string;
102
+ /** The loaded records, in input column order. */
103
+ Records: RecordComparisonRecord[];
104
+ /** The per-field delta matrix. Only fields with at least one non-empty value are included. */
105
+ Fields: RecordComparisonFieldDelta[];
106
+ }
107
+ /**
108
+ * Framework-agnostic engine that loads a set of records for one entity and computes a
109
+ * structured field-level delta between them.
110
+ *
111
+ * This is the shared comparison primitive used by the LLM reasoning path (it feeds the
112
+ * "deltas" context to the reasoning provider) and, opportunistically, by the UI
113
+ * side-by-side comparison panel. It performs a read-only load (`RunView` with
114
+ * `ResultType: 'simple'` + targeted `Fields`) — it never mutates records.
115
+ *
116
+ * No Angular, no Router, no resolver coupling. Server-side: always thread `contextUser`
117
+ * and (in multi-provider scenarios) the request-scoped `provider`.
118
+ */
119
+ export declare class RecordComparisonEngine {
120
+ /**
121
+ * Loads the requested records and computes the field-delta matrix.
122
+ *
123
+ * @param input the entity, keys, and optional field include-list to compare
124
+ * @param contextUser the server-side context user (REQUIRED for correct isolation)
125
+ * @param provider optional request-scoped metadata provider (multi-provider safety)
126
+ */
127
+ CompareRecords(input: RecordComparisonInput, contextUser: UserInfo, provider?: IMetadataProvider): Promise<RecordComparisonResult>;
128
+ /**
129
+ * The lower-level comparison entry point: given an already-resolved {@link EntityInfo}
130
+ * (no by-name metadata lookup), load the keyed records and compute the field-delta matrix.
131
+ *
132
+ * Callers that already hold the entity and a request-scoped `RunView` (e.g. the duplicate
133
+ * detector) use this directly via {@link RecordComparisonOptions.RunViewInstance}, avoiding a
134
+ * redundant metadata resolution. The by-name {@link RecordComparisonEngine.CompareRecords}
135
+ * resolves the entity and delegates here.
136
+ *
137
+ * @param entity the resolved entity to compare records within
138
+ * @param keys the composite keys to compare (column 0 is the reference/survivor candidate)
139
+ * @param contextUser the server-side context user (thread for correct isolation)
140
+ * @param options include-list, an injected RunView, and/or the request-scoped provider
141
+ */
142
+ CompareRecordsForEntity(entity: EntityInfo, keys: CompositeKey[], contextUser?: UserInfo, options?: RecordComparisonOptions): Promise<RecordComparisonResult>;
143
+ /**
144
+ * Resolves the set of fields to compare: the include-list (case-insensitive) when
145
+ * supplied, otherwise all non-PK, non-system fields. Sorted name-field → DefaultInView → Sequence.
146
+ */
147
+ protected selectFields(entity: EntityInfo, includeFields?: string[]): EntityFieldInfo[];
148
+ /** True when a field should participate in the comparison. */
149
+ protected isComparableField(field: EntityFieldInfo, includeSet: Set<string> | null): boolean;
150
+ /** True for MemberJunction system/internal columns that should not be diffed. */
151
+ protected isSystemField(fieldName: string): boolean;
152
+ /** Orders fields name-field first, then DefaultInView, then by Sequence/Name. */
153
+ protected compareFieldOrder(a: EntityFieldInfo, b: EntityFieldInfo): number;
154
+ /**
155
+ * Loads the records via a single read-only RunView OR-ing every supplied key.
156
+ * Returns plain rows (ResultType 'simple') or null on failure.
157
+ */
158
+ protected loadRecords(entity: EntityInfo, keys: CompositeKey[], fields: EntityFieldInfo[], contextUser: UserInfo | undefined, options?: RecordComparisonOptions): Promise<Record<string, RecordFieldValue>[] | null>;
159
+ /** Builds the field-name select list: PK fields + the compared fields (deduped). */
160
+ protected buildSelectFieldNames(entity: EntityInfo, fields: EntityFieldInfo[]): string[];
161
+ /** Builds an OR-ed WHERE clause across all keys, e.g. "(ID='a') OR (ID='b')". */
162
+ protected buildKeysFilter(keys: CompositeKey[]): string | null;
163
+ /**
164
+ * Correlates each input key (by column index) with its loaded row and builds the
165
+ * per-record value maps. Missing rows yield a record with all-null values.
166
+ */
167
+ protected buildRecords(entity: EntityInfo, keys: CompositeKey[], fields: EntityFieldInfo[], rawRecords: Record<string, RecordFieldValue>[]): RecordComparisonRecord[];
168
+ /** Finds the loaded row whose primary-key values match the supplied composite key. */
169
+ protected findRowForKey(entity: EntityInfo, key: CompositeKey, rawRecords: Record<string, RecordFieldValue>[]): Record<string, RecordFieldValue> | null;
170
+ /** True when every PK value on the row equals the key's value (case-insensitive). */
171
+ protected rowMatchesKey(entity: EntityInfo, key: CompositeKey, row: Record<string, RecordFieldValue>): boolean;
172
+ /** Extracts the compared-field values from a loaded row (null when row missing). */
173
+ protected extractFieldValues(fields: EntityFieldInfo[], row: Record<string, RecordFieldValue> | null): Record<string, RecordFieldValue>;
174
+ /** Produces a readable label: the name-field value when present, else the key string. */
175
+ protected buildRecordLabel(entity: EntityInfo, key: CompositeKey, row: Record<string, RecordFieldValue> | null): string;
176
+ /**
177
+ * Builds the per-field delta matrix from the loaded records. Column 0 is the
178
+ * reference (survivor candidate); every other cell is compared against it.
179
+ * Fields where every record's value is empty are dropped.
180
+ */
181
+ protected buildFieldDeltas(fields: EntityFieldInfo[], records: RecordComparisonRecord[]): RecordComparisonFieldDelta[];
182
+ /** Builds the per-record cells for one field, flagging equality against column 0. */
183
+ protected buildFieldCells(field: EntityFieldInfo, records: RecordComparisonRecord[]): RecordComparisonFieldCell[];
184
+ /** True when every cell's value is null/empty (such a field carries no signal). */
185
+ protected allCellsEmpty(cells: RecordComparisonFieldCell[]): boolean;
186
+ /** Case-insensitive, trimmed equality used for both diff highlighting and PK matching. */
187
+ protected valuesEqual(a: RecordFieldValue, b: RecordFieldValue): boolean;
188
+ /** Coerces a loaded value into a comparable scalar; passes scalars through, else null. */
189
+ protected normalizeValue(value: RecordFieldValue | undefined): RecordFieldValue;
190
+ /** Builds a failed-shape result. */
191
+ protected errorResult(entityName: string, message: string): RecordComparisonResult;
192
+ }
193
+ //# sourceMappingURL=RecordComparisonEngine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordComparisonEngine.d.ts","sourceRoot":"","sources":["../src/RecordComparisonEngine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,268 @@
1
+ import { LogError, Metadata, RunView } from '@memberjunction/core';
2
+ /**
3
+ * Framework-agnostic engine that loads a set of records for one entity and computes a
4
+ * structured field-level delta between them.
5
+ *
6
+ * This is the shared comparison primitive used by the LLM reasoning path (it feeds the
7
+ * "deltas" context to the reasoning provider) and, opportunistically, by the UI
8
+ * side-by-side comparison panel. It performs a read-only load (`RunView` with
9
+ * `ResultType: 'simple'` + targeted `Fields`) — it never mutates records.
10
+ *
11
+ * No Angular, no Router, no resolver coupling. Server-side: always thread `contextUser`
12
+ * and (in multi-provider scenarios) the request-scoped `provider`.
13
+ */
14
+ export class RecordComparisonEngine {
15
+ /**
16
+ * Loads the requested records and computes the field-delta matrix.
17
+ *
18
+ * @param input the entity, keys, and optional field include-list to compare
19
+ * @param contextUser the server-side context user (REQUIRED for correct isolation)
20
+ * @param provider optional request-scoped metadata provider (multi-provider safety)
21
+ */
22
+ async CompareRecords(input, contextUser, provider) {
23
+ const md = provider ?? Metadata.Provider;
24
+ const entity = md?.EntityByName(input.EntityName);
25
+ if (!entity) {
26
+ return this.errorResult(input.EntityName, `Entity '${input.EntityName}' not found in metadata`);
27
+ }
28
+ return this.CompareRecordsForEntity(entity, input.Keys, contextUser, {
29
+ IncludeFields: input.IncludeFields,
30
+ Provider: provider
31
+ });
32
+ }
33
+ /**
34
+ * The lower-level comparison entry point: given an already-resolved {@link EntityInfo}
35
+ * (no by-name metadata lookup), load the keyed records and compute the field-delta matrix.
36
+ *
37
+ * Callers that already hold the entity and a request-scoped `RunView` (e.g. the duplicate
38
+ * detector) use this directly via {@link RecordComparisonOptions.RunViewInstance}, avoiding a
39
+ * redundant metadata resolution. The by-name {@link RecordComparisonEngine.CompareRecords}
40
+ * resolves the entity and delegates here.
41
+ *
42
+ * @param entity the resolved entity to compare records within
43
+ * @param keys the composite keys to compare (column 0 is the reference/survivor candidate)
44
+ * @param contextUser the server-side context user (thread for correct isolation)
45
+ * @param options include-list, an injected RunView, and/or the request-scoped provider
46
+ */
47
+ async CompareRecordsForEntity(entity, keys, contextUser, options) {
48
+ try {
49
+ if (!keys || keys.length === 0) {
50
+ return this.errorResult(entity.Name, 'No keys supplied to compare');
51
+ }
52
+ const fields = this.selectFields(entity, options?.IncludeFields);
53
+ const rawRecords = await this.loadRecords(entity, keys, fields, contextUser, options);
54
+ if (rawRecords === null) {
55
+ return this.errorResult(entity.Name, 'Failed to load records for comparison');
56
+ }
57
+ const records = this.buildRecords(entity, keys, fields, rawRecords);
58
+ const deltas = this.buildFieldDeltas(fields, records);
59
+ return {
60
+ Success: true,
61
+ EntityName: entity.Name,
62
+ Records: records,
63
+ Fields: deltas
64
+ };
65
+ }
66
+ catch (e) {
67
+ LogError(e);
68
+ return this.errorResult(entity.Name, e instanceof Error ? e.message : String(e));
69
+ }
70
+ }
71
+ /**
72
+ * Resolves the set of fields to compare: the include-list (case-insensitive) when
73
+ * supplied, otherwise all non-PK, non-system fields. Sorted name-field → DefaultInView → Sequence.
74
+ */
75
+ selectFields(entity, includeFields) {
76
+ const includeSet = includeFields && includeFields.length > 0
77
+ ? new Set(includeFields.map(f => f.trim().toLowerCase()))
78
+ : null;
79
+ const candidates = entity.Fields.filter(f => this.isComparableField(f, includeSet));
80
+ return candidates.sort((a, b) => this.compareFieldOrder(a, b));
81
+ }
82
+ /** True when a field should participate in the comparison. */
83
+ isComparableField(field, includeSet) {
84
+ if (includeSet) {
85
+ return includeSet.has(field.Name.toLowerCase());
86
+ }
87
+ if (field.IsPrimaryKey) {
88
+ return false;
89
+ }
90
+ return !this.isSystemField(field.Name);
91
+ }
92
+ /** True for MemberJunction system/internal columns that should not be diffed. */
93
+ isSystemField(fieldName) {
94
+ return fieldName.startsWith('__mj_');
95
+ }
96
+ /** Orders fields name-field first, then DefaultInView, then by Sequence/Name. */
97
+ compareFieldOrder(a, b) {
98
+ if (a.IsNameField !== b.IsNameField) {
99
+ return a.IsNameField ? -1 : 1;
100
+ }
101
+ if (a.DefaultInView !== b.DefaultInView) {
102
+ return a.DefaultInView ? -1 : 1;
103
+ }
104
+ const seqA = a.Sequence ?? 0;
105
+ const seqB = b.Sequence ?? 0;
106
+ if (seqA !== seqB) {
107
+ return seqA - seqB;
108
+ }
109
+ return a.Name.localeCompare(b.Name);
110
+ }
111
+ /**
112
+ * Loads the records via a single read-only RunView OR-ing every supplied key.
113
+ * Returns plain rows (ResultType 'simple') or null on failure.
114
+ */
115
+ async loadRecords(entity, keys, fields, contextUser, options) {
116
+ const filter = this.buildKeysFilter(keys);
117
+ if (!filter) {
118
+ return null;
119
+ }
120
+ // Prefer a caller-supplied RunView (already bound to the request-scoped provider).
121
+ // Otherwise build one from the provider: concrete MJ providers implement both
122
+ // IMetadataProvider and IRunViewProvider, so the cast threads it into RunView for
123
+ // multi-provider safety.
124
+ const rv = options?.RunViewInstance ?? new RunView(options?.Provider ?? null);
125
+ const result = await rv.RunView({
126
+ EntityName: entity.Name,
127
+ ExtraFilter: filter,
128
+ Fields: this.buildSelectFieldNames(entity, fields),
129
+ ResultType: 'simple',
130
+ MaxRows: keys.length
131
+ }, contextUser);
132
+ if (!result.Success) {
133
+ LogError(`RecordComparisonEngine.loadRecords failed: ${result.ErrorMessage}`);
134
+ return null;
135
+ }
136
+ return result.Results ?? [];
137
+ }
138
+ /** Builds the field-name select list: PK fields + the compared fields (deduped). */
139
+ buildSelectFieldNames(entity, fields) {
140
+ const names = new Set();
141
+ for (const pk of entity.PrimaryKeys) {
142
+ names.add(pk.Name);
143
+ }
144
+ for (const f of fields) {
145
+ names.add(f.Name);
146
+ }
147
+ return Array.from(names);
148
+ }
149
+ /** Builds an OR-ed WHERE clause across all keys, e.g. "(ID='a') OR (ID='b')". */
150
+ buildKeysFilter(keys) {
151
+ const clauses = keys
152
+ .filter(k => k.HasValue)
153
+ .map(k => `(${k.ToWhereClause()})`);
154
+ return clauses.length > 0 ? clauses.join(' OR ') : null;
155
+ }
156
+ /**
157
+ * Correlates each input key (by column index) with its loaded row and builds the
158
+ * per-record value maps. Missing rows yield a record with all-null values.
159
+ */
160
+ buildRecords(entity, keys, fields, rawRecords) {
161
+ return keys.map((key, columnIndex) => {
162
+ const row = this.findRowForKey(entity, key, rawRecords);
163
+ const values = this.extractFieldValues(fields, row);
164
+ return {
165
+ ColumnIndex: columnIndex,
166
+ Key: key,
167
+ Label: this.buildRecordLabel(entity, key, row),
168
+ Values: values
169
+ };
170
+ });
171
+ }
172
+ /** Finds the loaded row whose primary-key values match the supplied composite key. */
173
+ findRowForKey(entity, key, rawRecords) {
174
+ return rawRecords.find(row => this.rowMatchesKey(entity, key, row)) ?? null;
175
+ }
176
+ /** True when every PK value on the row equals the key's value (case-insensitive). */
177
+ rowMatchesKey(entity, key, row) {
178
+ return entity.PrimaryKeys.every(pk => {
179
+ const keyValue = key.GetValueByFieldName(pk.Name);
180
+ return this.valuesEqual(row[pk.Name], keyValue);
181
+ });
182
+ }
183
+ /** Extracts the compared-field values from a loaded row (null when row missing). */
184
+ extractFieldValues(fields, row) {
185
+ const values = {};
186
+ for (const f of fields) {
187
+ values[f.Name] = row ? this.normalizeValue(row[f.Name]) : null;
188
+ }
189
+ return values;
190
+ }
191
+ /** Produces a readable label: the name-field value when present, else the key string. */
192
+ buildRecordLabel(entity, key, row) {
193
+ const nameField = entity.Fields.find(f => f.IsNameField);
194
+ if (row && nameField) {
195
+ const v = row[nameField.Name];
196
+ if (v !== null && v !== undefined && String(v).trim().length > 0) {
197
+ return String(v);
198
+ }
199
+ }
200
+ return key.Values();
201
+ }
202
+ /**
203
+ * Builds the per-field delta matrix from the loaded records. Column 0 is the
204
+ * reference (survivor candidate); every other cell is compared against it.
205
+ * Fields where every record's value is empty are dropped.
206
+ */
207
+ buildFieldDeltas(fields, records) {
208
+ const deltas = [];
209
+ for (const field of fields) {
210
+ const cells = this.buildFieldCells(field, records);
211
+ if (this.allCellsEmpty(cells)) {
212
+ continue;
213
+ }
214
+ deltas.push({
215
+ FieldName: field.Name,
216
+ DisplayName: field.DisplayNameOrName,
217
+ Category: field.Category ?? null,
218
+ Cells: cells,
219
+ Differs: cells.some(c => !c.EqualsReference)
220
+ });
221
+ }
222
+ return deltas;
223
+ }
224
+ /** Builds the per-record cells for one field, flagging equality against column 0. */
225
+ buildFieldCells(field, records) {
226
+ const referenceValue = records.length > 0 ? records[0].Values[field.Name] ?? null : null;
227
+ return records.map(record => {
228
+ const value = record.Values[field.Name] ?? null;
229
+ return {
230
+ ColumnIndex: record.ColumnIndex,
231
+ Value: value,
232
+ EqualsReference: this.valuesEqual(value, referenceValue)
233
+ };
234
+ });
235
+ }
236
+ /** True when every cell's value is null/empty (such a field carries no signal). */
237
+ allCellsEmpty(cells) {
238
+ return cells.every(c => c.Value === null || c.Value === '' || c.Value === undefined);
239
+ }
240
+ /** Case-insensitive, trimmed equality used for both diff highlighting and PK matching. */
241
+ valuesEqual(a, b) {
242
+ if (a === null || a === undefined) {
243
+ return b === null || b === undefined;
244
+ }
245
+ if (b === null || b === undefined) {
246
+ return false;
247
+ }
248
+ return String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
249
+ }
250
+ /** Coerces a loaded value into a comparable scalar; passes scalars through, else null. */
251
+ normalizeValue(value) {
252
+ if (value === undefined) {
253
+ return null;
254
+ }
255
+ return value;
256
+ }
257
+ /** Builds a failed-shape result. */
258
+ errorResult(entityName, message) {
259
+ return {
260
+ Success: false,
261
+ ErrorMessage: message,
262
+ EntityName: entityName,
263
+ Records: [],
264
+ Fields: []
265
+ };
266
+ }
267
+ }
268
+ //# sourceMappingURL=RecordComparisonEngine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordComparisonEngine.js","sourceRoot":"","sources":["../src/RecordComparisonEngine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export * from './RecordComparisonEngine.js';
2
+ export * from './operations/RecordComparisonCompareOperation.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,+CAA+C,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './RecordComparisonEngine.js';
2
+ export * from './operations/RecordComparisonCompareOperation.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,+CAA+C,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { IMetadataProvider, UserInfo } from '@memberjunction/core';
2
+ import { RecordComparisonCompareOperation, type RecordComparisonCompareInput, type RecordComparisonCompareOutput } from '@memberjunction/core-entities';
3
+ export declare class RecordComparisonCompareServerOperation extends RecordComparisonCompareOperation {
4
+ protected InternalExecute(input: RecordComparisonCompareInput, provider: IMetadataProvider, user: UserInfo): Promise<RecordComparisonCompareOutput>;
5
+ /** Builds a {@link CompositeKey} from a wire-form comparison key. */
6
+ private toCompositeKey;
7
+ }
8
+ /** Tree-shaking anchor — referenced so bundlers retain the `@RegisterClass` registration. */
9
+ export declare function LoadRecordComparisonCompareOperation(): void;
10
+ //# sourceMappingURL=RecordComparisonCompareOperation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordComparisonCompareOperation.d.ts","sourceRoot":"","sources":["../../src/operations/RecordComparisonCompareOperation.ts"],"names":[],"mappings":"AAaA,OAAO,EAAwC,iBAAiB,EAAgB,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACvH,OAAO,EACH,gCAAgC,EAChC,KAAK,4BAA4B,EACjC,KAAK,6BAA6B,EAErC,MAAM,+BAA+B,CAAC;AAGvC,qBACa,sCAAuC,SAAQ,gCAAgC;cACxE,eAAe,CAC3B,KAAK,EAAE,4BAA4B,EACnC,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,GACf,OAAO,CAAC,6BAA6B,CAAC;IAwBzC,qEAAqE;IACrE,OAAO,CAAC,cAAc;CASzB;AAED,6FAA6F;AAC7F,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D"}
@@ -0,0 +1,63 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ /**
8
+ * @fileoverview Server implementation of the `RecordComparison.Compare` Remote Operation — loads a
9
+ * set of an entity's records by composite key and computes the field-level delta matrix between them.
10
+ *
11
+ * Extends the CodeGen-emitted {@link RecordComparisonCompareOperation} base in
12
+ * `@memberjunction/core-entities` (`generated/remote_operations.ts` — operation key + typed I/O, from
13
+ * the `MJ: Remote Operations` row) and supplies the server body via {@link RecordComparisonEngine}.
14
+ * Registered under `RecordComparison.Compare`; replaces the bespoke `GetRecordComparison` GraphQL
15
+ * resolver + the hand-written `GraphQLRecordComparisonClient` transport.
16
+ *
17
+ * @module @memberjunction/record-comparison
18
+ */
19
+ import { RegisterClass } from '@memberjunction/global';
20
+ import { BaseRemotableOperation, CompositeKey, KeyValuePair } from '@memberjunction/core';
21
+ import { RecordComparisonCompareOperation, } from '@memberjunction/core-entities';
22
+ import { RecordComparisonEngine } from '../RecordComparisonEngine.js';
23
+ let RecordComparisonCompareServerOperation = class RecordComparisonCompareServerOperation extends RecordComparisonCompareOperation {
24
+ async InternalExecute(input, provider, user) {
25
+ if (!input?.EntityName) {
26
+ throw new Error('EntityName is required');
27
+ }
28
+ const engineInput = {
29
+ EntityName: input.EntityName,
30
+ Keys: (input.Keys ?? []).map((k) => this.toCompositeKey(k)),
31
+ IncludeFields: input.IncludeFields,
32
+ };
33
+ const result = await new RecordComparisonEngine().CompareRecords(engineInput, user, provider);
34
+ if (!result.Success) {
35
+ // Surface as a logical failure on the wrapping RemoteOpResult.
36
+ throw new Error(result.ErrorMessage ?? 'Record comparison failed');
37
+ }
38
+ return {
39
+ EntityName: result.EntityName,
40
+ Records: result.Records ?? [],
41
+ Fields: result.Fields ?? [],
42
+ };
43
+ }
44
+ /** Builds a {@link CompositeKey} from a wire-form comparison key. */
45
+ toCompositeKey(key) {
46
+ const pairs = (key.KeyValuePairs ?? []).map((kvp) => {
47
+ const pair = new KeyValuePair();
48
+ pair.FieldName = kvp.FieldName;
49
+ pair.Value = kvp.Value;
50
+ return pair;
51
+ });
52
+ return CompositeKey.FromKeyValuePairs(pairs);
53
+ }
54
+ };
55
+ RecordComparisonCompareServerOperation = __decorate([
56
+ RegisterClass(BaseRemotableOperation, 'RecordComparison.Compare')
57
+ ], RecordComparisonCompareServerOperation);
58
+ export { RecordComparisonCompareServerOperation };
59
+ /** Tree-shaking anchor — referenced so bundlers retain the `@RegisterClass` registration. */
60
+ export function LoadRecordComparisonCompareOperation() {
61
+ // intentionally empty
62
+ }
63
+ //# sourceMappingURL=RecordComparisonCompareOperation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecordComparisonCompareOperation.js","sourceRoot":"","sources":["../../src/operations/RecordComparisonCompareOperation.ts"],"names":[],"mappings":";;;;;;AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAqB,YAAY,EAAY,MAAM,sBAAsB,CAAC;AACvH,OAAO,EACH,gCAAgC,GAInC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,sBAAsB,EAAyB,MAAM,2BAA2B,CAAC;AAGnF,IAAM,sCAAsC,GAA5C,MAAM,sCAAuC,SAAQ,gCAAgC;IAC9E,KAAK,CAAC,eAAe,CAC3B,KAAmC,EACnC,QAA2B,EAC3B,IAAc;QAEd,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC9C,CAAC;QAED,MAAM,WAAW,GAA0B;YACvC,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;YAC3D,aAAa,EAAE,KAAK,CAAC,aAAa;SACrC,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,sBAAsB,EAAE,CAAC,cAAc,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC9F,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAClB,+DAA+D;YAC/D,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,YAAY,IAAI,0BAA0B,CAAC,CAAC;QACvE,CAAC;QAED,OAAO;YACH,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;SAC9B,CAAC;IACN,CAAC;IAED,qEAAqE;IAC7D,cAAc,CAAC,GAAwB;QAC3C,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YAChD,MAAM,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;YAC/B,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;YACvB,OAAO,IAAI,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,OAAO,YAAY,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;CACJ,CAAA;AAvCY,sCAAsC;IADlD,aAAa,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;GACrD,sCAAsC,CAuClD;;AAED,6FAA6F;AAC7F,MAAM,UAAU,oCAAoC;IAChD,sBAAsB;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,31 @@
1
1
  {
2
2
  "name": "@memberjunction/record-comparison",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/record-comparison",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "5.44.0",
5
+ "description": "MemberJunction: Framework-agnostic record comparison engine — loads a set of records for one entity and computes a structured field-level delta. Shared by the duplicate-detection reasoning path and the UI side-by-side comparison panel.",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc && tsc-alias -f",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest"
15
+ },
16
+ "author": "MemberJunction.com",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "@memberjunction/core": "5.44.0",
20
+ "@memberjunction/core-entities": "5.44.0",
21
+ "@memberjunction/global": "5.44.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "24.10.11",
25
+ "typescript": "^5.9.3"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/MemberJunction/MJ"
30
+ }
10
31
  }