@memberjunction/server 5.26.0 → 5.27.1

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.
@@ -1,274 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
-
3
- /**
4
- * Tests for the organic-key packing logic in SkipSDK.
5
- *
6
- * The packing helpers (`packSingleSkipOrganicKey`, `packSingleSkipOrganicKeyRelatedEntity`,
7
- * and the organic-key block inside `packSingleSkipEntityInfo`) are private methods on the
8
- * SkipSDK class and the SDK module pulls in a large dependency graph (mssql, http, MJ
9
- * config, AI engine, rxjs). To keep these as focused unit tests we mock every transitive
10
- * dependency aggressively and reach the private methods via bracket notation.
11
- */
12
-
13
- // ---- Mutable state shared with the @memberjunction/core mock ----------------
14
-
15
- const mockMetadataEntities: Array<{ ID: string; Name: string; SchemaName: string; BaseView: string }> = [];
16
-
17
- // ---- Module mocks (must be defined before importing skip-sdk) ---------------
18
-
19
- vi.mock('@memberjunction/core', () => ({
20
- LogStatus: vi.fn(),
21
- LogError: vi.fn(),
22
- Metadata: class {
23
- static get Provider() {
24
- return { Entities: mockMetadataEntities };
25
- }
26
- },
27
- RunQuery: class {},
28
- EntityInfo: class {},
29
- EntityFieldInfo: class {},
30
- EntityRelationshipInfo: class {},
31
- EntityOrganicKeyInfo: class {},
32
- EntityOrganicKeyRelatedEntityInfo: class {},
33
- UserInfo: class {},
34
- }));
35
-
36
- vi.mock('@memberjunction/global', () => ({
37
- CopyScalarsAndArrays: (o: unknown) => o,
38
- // The packing helpers compare entity IDs with UUIDsEqual; for the test we
39
- // can use simple string equality.
40
- UUIDsEqual: (a: string, b: string) => a === b,
41
- }));
42
-
43
- vi.mock('@memberjunction/ai', () => ({
44
- GetAIAPIKey: vi.fn(() => 'test-key'),
45
- }));
46
-
47
- vi.mock('@memberjunction/aiengine', () => ({
48
- AIEngine: { Instance: { Config: () => Promise.resolve() } },
49
- }));
50
-
51
- vi.mock('@memberjunction/data-context', () => ({
52
- DataContext: class {},
53
- }));
54
-
55
- vi.mock('mssql', () => ({
56
- default: {},
57
- ConnectionPool: class {},
58
- }));
59
-
60
- vi.mock('http', () => ({ request: vi.fn() }));
61
- vi.mock('https', () => ({ request: vi.fn() }));
62
- vi.mock('zlib', () => ({ gzip: vi.fn(), createGunzip: vi.fn() }));
63
-
64
- vi.mock('rxjs', () => ({
65
- BehaviorSubject: class {
66
- private value: unknown;
67
- constructor(initial: unknown) { this.value = initial; }
68
- next(v: unknown) { this.value = v; }
69
- pipe() { return this; }
70
- },
71
- }));
72
- vi.mock('rxjs/operators', () => ({ take: vi.fn() }));
73
-
74
- // Skip SDK pulls server config and a couple of resolver internals; stub all of them.
75
- vi.mock('../config.js', () => ({
76
- configInfo: { askSkip: { chatURL: '', apiKey: '', orgID: '', organizationInfo: '' } },
77
- baseUrl: '',
78
- publicUrl: '',
79
- graphqlPort: 4000,
80
- graphqlRootPath: '/graphql',
81
- apiKey: '',
82
- }));
83
-
84
- vi.mock('../index.js', () => ({
85
- getDbType: vi.fn(() => 'mssql'),
86
- }));
87
-
88
- vi.mock('../resolvers/GetDataResolver.js', () => ({
89
- registerAccessToken: vi.fn(),
90
- GetDataAccessToken: vi.fn(),
91
- }));
92
-
93
- // ---- Imports under test (must come AFTER vi.mock calls) ---------------------
94
-
95
- import { SkipSDK } from '../agents/skip-sdk';
96
- import type { SkipEntityOrganicKeyInfo } from '@memberjunction/skip-types';
97
-
98
- // ---- Helpers to fabricate runtime-shaped MJ organic key objects -------------
99
-
100
- function makeOrganicKey(overrides: Record<string, unknown> = {}) {
101
- const base = {
102
- ID: 'ok-1',
103
- EntityID: 'ent-source',
104
- Name: 'EmailMatch',
105
- Description: 'Match members across systems by email address',
106
- MatchFieldNames: 'EmailAddress',
107
- NormalizationStrategy: 'LowerCaseTrim' as const,
108
- CustomNormalizationExpression: null,
109
- Sequence: 0,
110
- Status: 'Active' as const,
111
- RelatedEntities: [] as unknown[],
112
- };
113
- const merged = { ...base, ...overrides };
114
- Object.defineProperty(merged, 'MatchFieldNamesArray', {
115
- get() {
116
- return this.MatchFieldNames ? this.MatchFieldNames.split(',').map((f: string) => f.trim()) : [];
117
- },
118
- });
119
- return merged;
120
- }
121
-
122
- function makeOrganicKeyRelatedEntity(overrides: Record<string, unknown> = {}) {
123
- const base = {
124
- ID: 'okre-1',
125
- EntityOrganicKeyID: 'ok-1',
126
- RelatedEntityID: 'ent-target',
127
- RelatedEntityFieldNames: null as string | null,
128
- TransitiveObjectName: null as string | null,
129
- TransitiveObjectMatchFieldNames: null as string | null,
130
- TransitiveObjectOutputFieldName: null as string | null,
131
- RelatedEntityJoinFieldName: null as string | null,
132
- DisplayName: null as string | null,
133
- Sequence: 0,
134
- };
135
- const merged = { ...base, ...overrides };
136
- Object.defineProperty(merged, 'IsDirectMatch', {
137
- get() { return this.RelatedEntityFieldNames != null; },
138
- });
139
- Object.defineProperty(merged, 'IsTransitiveMatch', {
140
- get() { return this.TransitiveObjectName != null; },
141
- });
142
- Object.defineProperty(merged, 'RelatedEntityFieldNamesArray', {
143
- get() {
144
- return this.RelatedEntityFieldNames ? this.RelatedEntityFieldNames.split(',').map((f: string) => f.trim()) : [];
145
- },
146
- });
147
- Object.defineProperty(merged, 'TransitiveObjectMatchFieldNamesArray', {
148
- get() {
149
- return this.TransitiveObjectMatchFieldNames ? this.TransitiveObjectMatchFieldNames.split(',').map((f: string) => f.trim()) : [];
150
- },
151
- });
152
- return merged;
153
- }
154
-
155
- // Bracket-notation accessors so the tests can reach private methods without
156
- // adopting a // @ts-expect-error per call site.
157
- type SkipSDKPrivate = SkipSDK & {
158
- packSingleSkipOrganicKey: (ok: unknown) => SkipEntityOrganicKeyInfo | null;
159
- packSingleSkipOrganicKeyRelatedEntity: (re: unknown) => unknown | null;
160
- };
161
-
162
- describe('SkipSDK organic key packing', () => {
163
- let sdk: SkipSDKPrivate;
164
-
165
- beforeEach(() => {
166
- mockMetadataEntities.length = 0;
167
- sdk = new SkipSDK() as SkipSDKPrivate;
168
- });
169
-
170
- describe('packSingleSkipOrganicKeyRelatedEntity', () => {
171
- it('should pack a direct-match related entity using metadata for schema/baseView', () => {
172
- mockMetadataEntities.push({ ID: 'ent-target', Name: 'Members', SchemaName: 'ym', BaseView: 'vwMembers' });
173
- const re = makeOrganicKeyRelatedEntity({
174
- ID: 'okre-direct',
175
- RelatedEntityID: 'ent-target',
176
- RelatedEntityFieldNames: 'EmailAddress',
177
- DisplayName: 'Members by Email',
178
- Sequence: 1,
179
- });
180
-
181
- const result = sdk['packSingleSkipOrganicKeyRelatedEntity'](re) as Record<string, unknown> | null;
182
- expect(result).not.toBeNull();
183
- expect(result!.id).toBe('okre-direct');
184
- expect(result!.relatedEntityName).toBe('Members');
185
- expect(result!.relatedEntitySchemaName).toBe('ym');
186
- expect(result!.relatedEntityBaseView).toBe('vwMembers');
187
- expect(result!.isDirectMatch).toBe(true);
188
- expect(result!.isTransitiveMatch).toBe(false);
189
- expect(result!.relatedEntityFieldNames).toEqual(['EmailAddress']);
190
- expect(result!.transitiveObjectName).toBeUndefined();
191
- });
192
-
193
- it('should pack a transitive-match related entity through a bridge view', () => {
194
- mockMetadataEntities.push({ ID: 'ent-target', Name: 'Members', SchemaName: 'ym', BaseView: 'vwMembers' });
195
- const re = makeOrganicKeyRelatedEntity({
196
- RelatedEntityID: 'ent-target',
197
- TransitiveObjectName: 'ym.vwAcronymToMember',
198
- TransitiveObjectMatchFieldNames: 'Acronym',
199
- TransitiveObjectOutputFieldName: 'MemberID',
200
- RelatedEntityJoinFieldName: 'ID',
201
- });
202
-
203
- const result = sdk['packSingleSkipOrganicKeyRelatedEntity'](re) as Record<string, unknown> | null;
204
- expect(result).not.toBeNull();
205
- expect(result!.isDirectMatch).toBe(false);
206
- expect(result!.isTransitiveMatch).toBe(true);
207
- expect(result!.relatedEntityFieldNames).toBeUndefined();
208
- expect(result!.transitiveObjectName).toBe('ym.vwAcronymToMember');
209
- expect(result!.transitiveObjectMatchFieldNames).toEqual(['Acronym']);
210
- expect(result!.transitiveObjectOutputFieldName).toBe('MemberID');
211
- expect(result!.relatedEntityJoinFieldName).toBe('ID');
212
- });
213
-
214
- it('should return null when the related entity is not in metadata', () => {
215
- // mockMetadataEntities is empty
216
- const re = makeOrganicKeyRelatedEntity({
217
- RelatedEntityID: 'ent-missing',
218
- RelatedEntityFieldNames: 'EmailAddress',
219
- });
220
-
221
- const result = sdk['packSingleSkipOrganicKeyRelatedEntity'](re);
222
- expect(result).toBeNull();
223
- });
224
- });
225
-
226
- describe('packSingleSkipOrganicKey', () => {
227
- beforeEach(() => {
228
- mockMetadataEntities.push({ ID: 'ent-target', Name: 'Members', SchemaName: 'ym', BaseView: 'vwMembers' });
229
- });
230
-
231
- it('should pack a basic organic key with parsed match field names', () => {
232
- const ok = makeOrganicKey({
233
- ID: 'ok-acronym',
234
- Name: 'AcronymMatch',
235
- MatchFieldNames: 'MemberOrganization',
236
- Sequence: 5,
237
- RelatedEntities: [makeOrganicKeyRelatedEntity({ RelatedEntityID: 'ent-target', RelatedEntityFieldNames: 'EmailAddress' })],
238
- });
239
-
240
- const result = sdk['packSingleSkipOrganicKey'](ok);
241
- expect(result).not.toBeNull();
242
- expect(result!.id).toBe('ok-acronym');
243
- expect(result!.name).toBe('AcronymMatch');
244
- expect(result!.matchFieldNames).toEqual(['MemberOrganization']);
245
- expect(result!.normalizationStrategy).toBe('LowerCaseTrim');
246
- expect(result!.sequence).toBe(5);
247
- expect(result!.relatedEntities).toHaveLength(1);
248
- });
249
-
250
- it('should preserve a custom normalization expression', () => {
251
- const ok = makeOrganicKey({
252
- NormalizationStrategy: 'Custom',
253
- CustomNormalizationExpression: 'REPLACE(LOWER({{FieldName}}), \' \', \'\')',
254
- });
255
-
256
- const result = sdk['packSingleSkipOrganicKey'](ok);
257
- expect(result!.normalizationStrategy).toBe('Custom');
258
- expect(result!.customNormalizationExpression).toBe('REPLACE(LOWER({{FieldName}}), \' \', \'\')');
259
- });
260
-
261
- it('should drop related entities that cannot be resolved against metadata', () => {
262
- const ok = makeOrganicKey({
263
- RelatedEntities: [
264
- makeOrganicKeyRelatedEntity({ ID: 're-good', RelatedEntityID: 'ent-target', RelatedEntityFieldNames: 'EmailAddress' }),
265
- makeOrganicKeyRelatedEntity({ ID: 're-bad', RelatedEntityID: 'ent-missing', RelatedEntityFieldNames: 'EmailAddress' }),
266
- ],
267
- });
268
-
269
- const result = sdk['packSingleSkipOrganicKey'](ok);
270
- expect(result!.relatedEntities).toHaveLength(1);
271
- expect(result!.relatedEntities[0].id).toBe('re-good');
272
- });
273
- });
274
- });