@memberjunction/server 5.25.0 → 5.27.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/dist/agents/skip-sdk.d.ts +15 -34
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +58 -209
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/config.d.ts +70 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +6 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +24 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +31 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -0
- package/dist/resolvers/CacheStatsResolver.js +181 -0
- package/dist/resolvers/CacheStatsResolver.js.map +1 -0
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/package.json +66 -63
- package/src/agents/skip-sdk.ts +59 -226
- package/src/config.ts +24 -0
- package/src/generated/generated.ts +18 -0
- package/src/index.ts +16 -2
- package/src/resolvers/CacheStatsResolver.ts +142 -0
- package/src/resolvers/ReportResolver.ts +1 -2
- package/src/__tests__/skip-sdk-organic-keys.test.ts +0 -274
package/src/index.ts
CHANGED
|
@@ -128,6 +128,7 @@ export * from './resolvers/TelemetryResolver.js';
|
|
|
128
128
|
export * from './resolvers/APIKeyResolver.js';
|
|
129
129
|
export * from './resolvers/MCPResolver.js';
|
|
130
130
|
export * from './resolvers/ActionResolver.js';
|
|
131
|
+
export * from './resolvers/CacheStatsResolver.js';
|
|
131
132
|
export * from './resolvers/EntityCommunicationsResolver.js';
|
|
132
133
|
export * from './resolvers/EntityResolver.js';
|
|
133
134
|
export * from './resolvers/ISAEntityResolver.js';
|
|
@@ -451,8 +452,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
451
452
|
}
|
|
452
453
|
// Ensure LocalCacheManager is initialized (no-op if already done during engine loading)
|
|
453
454
|
if (!LocalCacheManager.Instance.IsInitialized) {
|
|
454
|
-
|
|
455
|
-
|
|
455
|
+
// Build cache config from mj.config.cjs cacheSettings
|
|
456
|
+
const cs = configInfo.cacheSettings;
|
|
457
|
+
const cacheConfig = {
|
|
458
|
+
maxSizeBytes: (cs.maxMemoryMB ?? 150) * 1024 * 1024,
|
|
459
|
+
maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
|
|
460
|
+
defaultTTLMs: (cs.defaultTTLSeconds ?? 0) * 1000,
|
|
461
|
+
evictionSweepIntervalMs: (cs.evictionSweepIntervalSeconds ?? 300) * 1000,
|
|
462
|
+
verboseLogging: cs.verboseLogging ?? false,
|
|
463
|
+
};
|
|
464
|
+
await LocalCacheManager.Instance.Initialize(Metadata.Provider.LocalStorageProvider, cacheConfig);
|
|
465
|
+
console.log('LocalCacheManager initialized with cache config:', JSON.stringify({
|
|
466
|
+
maxMemoryMB: cs.maxMemoryMB ?? 150,
|
|
467
|
+
maxPercentOfCachePerEntity: cs.maxPercentOfCachePerEntity ?? 50,
|
|
468
|
+
evictionSweepIntervalSeconds: cs.evictionSweepIntervalSeconds ?? 300,
|
|
469
|
+
}));
|
|
456
470
|
}
|
|
457
471
|
|
|
458
472
|
// Initialize APIKeyEngine singleton — reads apiKeyGeneration from mj.config.cjs automatically
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Field, Float, Int, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { LocalCacheManager, CacheStats, CacheEntryType } from '@memberjunction/core';
|
|
3
|
+
import { RequireSystemUser } from '../directives/index.js';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// GraphQL Object Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
@ObjectType()
|
|
10
|
+
class CacheStatsByTypeGQL {
|
|
11
|
+
@Field(() => String)
|
|
12
|
+
Type: CacheEntryType;
|
|
13
|
+
|
|
14
|
+
@Field(() => Int)
|
|
15
|
+
Count: number;
|
|
16
|
+
|
|
17
|
+
@Field(() => Int)
|
|
18
|
+
SizeBytes: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@ObjectType()
|
|
22
|
+
class CacheStatsGQL {
|
|
23
|
+
@Field(() => Int)
|
|
24
|
+
TotalEntries: number;
|
|
25
|
+
|
|
26
|
+
@Field(() => Int)
|
|
27
|
+
TotalSizeBytes: number;
|
|
28
|
+
|
|
29
|
+
@Field(() => [CacheStatsByTypeGQL])
|
|
30
|
+
ByType: CacheStatsByTypeGQL[];
|
|
31
|
+
|
|
32
|
+
@Field(() => Float)
|
|
33
|
+
OldestEntry: number;
|
|
34
|
+
|
|
35
|
+
@Field(() => Float)
|
|
36
|
+
NewestEntry: number;
|
|
37
|
+
|
|
38
|
+
@Field(() => Int)
|
|
39
|
+
Hits: number;
|
|
40
|
+
|
|
41
|
+
@Field(() => Int)
|
|
42
|
+
Misses: number;
|
|
43
|
+
|
|
44
|
+
@Field(() => Float)
|
|
45
|
+
HitRate: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@ObjectType()
|
|
49
|
+
class CacheEntityBreakdownGQL {
|
|
50
|
+
@Field(() => String)
|
|
51
|
+
EntityName: string;
|
|
52
|
+
|
|
53
|
+
@Field(() => Int)
|
|
54
|
+
EntryCount: number;
|
|
55
|
+
|
|
56
|
+
@Field(() => Int)
|
|
57
|
+
TotalSizeBytes: number;
|
|
58
|
+
|
|
59
|
+
@Field(() => Int)
|
|
60
|
+
TotalAccessCount: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@ObjectType()
|
|
64
|
+
class CacheStatsDetailGQL extends CacheStatsGQL {
|
|
65
|
+
@Field(() => [CacheEntityBreakdownGQL])
|
|
66
|
+
EntityBreakdown: CacheEntityBreakdownGQL[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Helpers (pure core)
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
function toGQL(stats: CacheStats): CacheStatsGQL {
|
|
74
|
+
const gql = new CacheStatsGQL();
|
|
75
|
+
gql.TotalEntries = stats.totalEntries;
|
|
76
|
+
gql.TotalSizeBytes = stats.totalSizeBytes;
|
|
77
|
+
gql.ByType = (['dataset', 'runview', 'runquery'] as const).map(t => {
|
|
78
|
+
const bt = stats.byType[t];
|
|
79
|
+
const entry = new CacheStatsByTypeGQL();
|
|
80
|
+
entry.Type = t;
|
|
81
|
+
entry.Count = bt.count;
|
|
82
|
+
entry.SizeBytes = bt.sizeBytes;
|
|
83
|
+
return entry;
|
|
84
|
+
});
|
|
85
|
+
gql.OldestEntry = stats.oldestEntry;
|
|
86
|
+
gql.NewestEntry = stats.newestEntry;
|
|
87
|
+
gql.Hits = stats.hits;
|
|
88
|
+
gql.Misses = stats.misses;
|
|
89
|
+
gql.HitRate = (stats.hits + stats.misses) > 0
|
|
90
|
+
? (stats.hits / (stats.hits + stats.misses)) * 100
|
|
91
|
+
: 0;
|
|
92
|
+
return gql;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildEntityBreakdown(): CacheEntityBreakdownGQL[] {
|
|
96
|
+
const entries = LocalCacheManager.Instance.GetAllEntries();
|
|
97
|
+
const entityMap = new Map<string, { count: number; sizeBytes: number; accessCount: number }>();
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.type !== 'runview' || !entry.name) continue;
|
|
101
|
+
const existing = entityMap.get(entry.name) ?? { count: 0, sizeBytes: 0, accessCount: 0 };
|
|
102
|
+
existing.count++;
|
|
103
|
+
existing.sizeBytes += entry.sizeBytes;
|
|
104
|
+
existing.accessCount += entry.accessCount;
|
|
105
|
+
entityMap.set(entry.name, existing);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [...entityMap.entries()]
|
|
109
|
+
.map(([name, data]) => {
|
|
110
|
+
const gql = new CacheEntityBreakdownGQL();
|
|
111
|
+
gql.EntityName = name;
|
|
112
|
+
gql.EntryCount = data.count;
|
|
113
|
+
gql.TotalSizeBytes = data.sizeBytes;
|
|
114
|
+
gql.TotalAccessCount = data.accessCount;
|
|
115
|
+
return gql;
|
|
116
|
+
})
|
|
117
|
+
.sort((a, b) => b.EntryCount - a.EntryCount);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Resolver
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
@Resolver()
|
|
125
|
+
export class CacheStatsResolver {
|
|
126
|
+
@RequireSystemUser()
|
|
127
|
+
@Query(() => CacheStatsGQL)
|
|
128
|
+
CacheStats(): CacheStatsGQL {
|
|
129
|
+
const stats = LocalCacheManager.Instance.GetStats();
|
|
130
|
+
return toGQL(stats);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@RequireSystemUser()
|
|
134
|
+
@Query(() => CacheStatsDetailGQL)
|
|
135
|
+
CacheStatsDetail(): CacheStatsDetailGQL {
|
|
136
|
+
const stats = LocalCacheManager.Instance.GetStats();
|
|
137
|
+
const result = new CacheStatsDetailGQL();
|
|
138
|
+
Object.assign(result, toGQL(stats));
|
|
139
|
+
result.EntityBreakdown = buildEntityBreakdown();
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -2,7 +2,6 @@ import { EntitySaveOptions, IRunReportProvider, Metadata, RunReport } from '@mem
|
|
|
2
2
|
import { Arg, Ctx, Field, Int, Mutation, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { MJConversationDetailEntity, MJReportEntity } from '@memberjunction/core-entities';
|
|
5
|
-
import { SkipAPIAnalysisCompleteResponse } from '@memberjunction/skip-types';
|
|
6
5
|
import { DataContext } from '@memberjunction/data-context';
|
|
7
6
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
8
7
|
import { z } from 'zod';
|
|
@@ -104,7 +103,7 @@ export class ReportResolverExtended extends ResolverBase {
|
|
|
104
103
|
const request = new mssql.Request(dataSource);
|
|
105
104
|
const result = await request.query(sql);
|
|
106
105
|
if (!result || !result.recordset || result.recordset.length === 0) throw new Error('Unable to retrieve converation details');
|
|
107
|
-
const skipData =
|
|
106
|
+
const skipData: { title?: string; reportTitle?: string; userExplanation?: string; messages?: unknown[] } = JSON.parse(result.recordset[0].Message);
|
|
108
107
|
|
|
109
108
|
const report = await md.GetEntityObject<MJReportEntity>('MJ: Reports', u);
|
|
110
109
|
report.NewRecord();
|
|
@@ -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
|
-
});
|