@memberjunction/server 5.22.0 → 5.24.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 +35 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +610 -4
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +17333 -13889
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +30 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -0
- package/dist/resolvers/AutotagPipelineResolver.js +231 -0
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -0
- package/dist/resolvers/ClientToolRequestResolver.d.ts +43 -0
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -0
- package/dist/resolvers/ClientToolRequestResolver.js +161 -0
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +29 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -0
- package/dist/resolvers/FetchEntityVectorsResolver.js +222 -0
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +21 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +75 -33
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +42 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +239 -13
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/package.json +63 -63
- package/src/__tests__/search-knowledge-tags.test.ts +415 -0
- package/src/config.ts +11 -0
- package/src/generated/generated.ts +2373 -7
- package/src/generic/RunViewResolver.ts +1 -0
- package/src/index.ts +10 -0
- package/src/resolvers/AutotagPipelineResolver.ts +235 -0
- package/src/resolvers/ClientToolRequestResolver.ts +128 -0
- package/src/resolvers/FetchEntityVectorsResolver.ts +238 -0
- package/src/resolvers/RunAIAgentResolver.ts +97 -56
- package/src/resolvers/SearchKnowledgeResolver.ts +270 -13
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for SearchKnowledgeResolver — focusing on tag enrichment,
|
|
5
|
+
* title extraction, and metadata-to-result conversion logic.
|
|
6
|
+
*
|
|
7
|
+
* The resolver has heavy infrastructure dependencies (type-graphql, vector DBs,
|
|
8
|
+
* embeddings), so we test the pure-logic methods by constructing an instance
|
|
9
|
+
* and accessing them via prototype / casting tricks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Mocks ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const mockRunViewResults = new Map<string, { Success: boolean; Results: unknown[] }>();
|
|
15
|
+
|
|
16
|
+
vi.mock('@memberjunction/core', () => {
|
|
17
|
+
class MockRunView {
|
|
18
|
+
async RunView(params: { EntityName: string; ExtraFilter?: string }): Promise<{ Success: boolean; Results: unknown[] }> {
|
|
19
|
+
// Allow tests to set up per-entity results
|
|
20
|
+
for (const [key, val] of mockRunViewResults.entries()) {
|
|
21
|
+
if (params.EntityName === key || (params.ExtraFilter && params.ExtraFilter.includes(key))) {
|
|
22
|
+
return val;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { Success: true, Results: [] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const mockEntities = [
|
|
30
|
+
{
|
|
31
|
+
Name: 'Contacts',
|
|
32
|
+
ID: 'entity-contacts-id',
|
|
33
|
+
Icon: 'fa-solid fa-user',
|
|
34
|
+
NameField: { Name: 'FullName', IsNameField: true, Sequence: 1 },
|
|
35
|
+
Fields: [
|
|
36
|
+
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
37
|
+
{ Name: 'FirstName', IsNameField: true, Sequence: 1, TSType: 'string', IsPrimaryKey: false },
|
|
38
|
+
{ Name: 'LastName', IsNameField: true, Sequence: 2, TSType: 'string', IsPrimaryKey: false },
|
|
39
|
+
{ Name: 'Email', IsNameField: false, Sequence: 3, TSType: 'string', IsPrimaryKey: false },
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
Name: 'Companies',
|
|
44
|
+
ID: 'entity-companies-id',
|
|
45
|
+
Icon: 'fa-solid fa-building',
|
|
46
|
+
NameField: { Name: 'Name', IsNameField: true, Sequence: 1 },
|
|
47
|
+
Fields: [
|
|
48
|
+
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
49
|
+
{ Name: 'Name', IsNameField: true, Sequence: 1, TSType: 'string', IsPrimaryKey: false },
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
Name: 'Events',
|
|
54
|
+
ID: 'entity-events-id',
|
|
55
|
+
Icon: null,
|
|
56
|
+
NameField: null,
|
|
57
|
+
Fields: [
|
|
58
|
+
{ Name: 'ID', IsNameField: false, IsPrimaryKey: true, Sequence: 0, TSType: 'string' },
|
|
59
|
+
{ Name: 'EventCode', IsNameField: false, Sequence: 1, TSType: 'number', IsPrimaryKey: false },
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
class MockMetadata {
|
|
65
|
+
Entities = mockEntities;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
RunView: MockRunView,
|
|
70
|
+
Metadata: MockMetadata,
|
|
71
|
+
LogError: vi.fn(),
|
|
72
|
+
LogStatus: vi.fn(),
|
|
73
|
+
UserInfo: class {},
|
|
74
|
+
ComputeRRF: vi.fn(),
|
|
75
|
+
ScoredCandidate: class {},
|
|
76
|
+
EntityRecordNameInput: class {},
|
|
77
|
+
CompositeKey: class { LoadFromURLSegment = vi.fn(); },
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
vi.mock('@memberjunction/core-entities', () => ({
|
|
82
|
+
MJVectorIndexEntity: class {},
|
|
83
|
+
MJVectorDatabaseEntity: class {},
|
|
84
|
+
KnowledgeHubMetadataEngine: {
|
|
85
|
+
Instance: {
|
|
86
|
+
Config: vi.fn(),
|
|
87
|
+
ContentSourceTypes: [],
|
|
88
|
+
ContentSources: [],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
vi.mock('@memberjunction/ai', () => ({
|
|
94
|
+
GetAIAPIKey: vi.fn(() => 'mock-key'),
|
|
95
|
+
BaseEmbeddings: class {},
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
vi.mock('@memberjunction/ai-vectordb', () => ({
|
|
99
|
+
VectorDBBase: class {},
|
|
100
|
+
BaseResponse: class {},
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
vi.mock('@memberjunction/aiengine', () => ({
|
|
104
|
+
AIEngine: { Instance: { Config: vi.fn(), Models: [] } },
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
vi.mock('@memberjunction/global', () => ({
|
|
108
|
+
MJGlobal: { Instance: { ClassFactory: { CreateInstance: vi.fn() } } },
|
|
109
|
+
UUIDsEqual: (a: string, b: string) => a?.toLowerCase() === b?.toLowerCase(),
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
vi.mock('type-graphql', () => ({
|
|
113
|
+
Resolver: () => (target: unknown) => target,
|
|
114
|
+
Query: () => (target: unknown, key: string, descriptor: PropertyDescriptor) => descriptor,
|
|
115
|
+
Mutation: () => (target: unknown, key: string, descriptor: PropertyDescriptor) => descriptor,
|
|
116
|
+
Arg: () => (target: unknown, key: string, index: number) => {},
|
|
117
|
+
Ctx: () => (target: unknown, key: string, index: number) => {},
|
|
118
|
+
ObjectType: () => (target: unknown) => target,
|
|
119
|
+
Field: () => (target: unknown, key: string) => {},
|
|
120
|
+
Float: Number,
|
|
121
|
+
Int: Number,
|
|
122
|
+
InputType: () => (target: unknown) => target,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
vi.mock('../generic/ResolverBase.js', () => ({
|
|
126
|
+
ResolverBase: class {
|
|
127
|
+
GetUserFromPayload() { return {}; }
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
vi.mock('../types.js', () => ({
|
|
132
|
+
AppContext: class {},
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
// ── Import under test (after mocks) ──────────────────────────────────
|
|
136
|
+
|
|
137
|
+
import { SearchKnowledgeResolver } from '../resolvers/SearchKnowledgeResolver.js';
|
|
138
|
+
|
|
139
|
+
// Helper to access private methods
|
|
140
|
+
function getPrivateMethod<T>(instance: T, method: string): (...args: unknown[]) => unknown {
|
|
141
|
+
return (instance as Record<string, unknown>)[method] as (...args: unknown[]) => unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe('SearchKnowledgeResolver', () => {
|
|
145
|
+
let resolver: SearchKnowledgeResolver;
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
resolver = new SearchKnowledgeResolver();
|
|
149
|
+
mockRunViewResults.clear();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── extractDisplayTitle ──────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe('extractDisplayTitle', () => {
|
|
155
|
+
const extractDisplayTitle = (meta: Record<string, unknown>, fallback: string) =>
|
|
156
|
+
getPrivateMethod(resolver, 'extractDisplayTitle')(meta, fallback) as string;
|
|
157
|
+
|
|
158
|
+
it('should combine multiple IsNameField values for a multi-name entity', () => {
|
|
159
|
+
const meta = { Entity: 'Contacts', FirstName: 'Sarah', LastName: 'Chen' };
|
|
160
|
+
const title = extractDisplayTitle(meta, 'Contacts');
|
|
161
|
+
expect(title).toBe('Sarah Chen');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should use single NameField when IsNameField fields are not populated', () => {
|
|
165
|
+
const meta = { Entity: 'Companies', Name: 'Acme Corp' };
|
|
166
|
+
const title = extractDisplayTitle(meta, 'Companies');
|
|
167
|
+
// Companies has Name as IsNameField, so it should use it
|
|
168
|
+
expect(title).toBe('Acme Corp');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should fall back to heuristic fields when no entity metadata matches', () => {
|
|
172
|
+
const meta = { Entity: 'UnknownEntity', Title: 'My Document' };
|
|
173
|
+
const title = extractDisplayTitle(meta, 'UnknownEntity');
|
|
174
|
+
expect(title).toBe('My Document');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should try Name heuristic field', () => {
|
|
178
|
+
const meta = { Entity: 'UnknownEntity', Name: 'Widget A' };
|
|
179
|
+
const title = extractDisplayTitle(meta, 'UnknownEntity');
|
|
180
|
+
expect(title).toBe('Widget A');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should try Subject heuristic field', () => {
|
|
184
|
+
const meta = { Entity: 'NonExistent', Subject: 'RE: Meeting' };
|
|
185
|
+
const title = extractDisplayTitle(meta, 'NonExistent');
|
|
186
|
+
expect(title).toBe('RE: Meeting');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return fallback when no fields match', () => {
|
|
190
|
+
const meta = { Entity: 'Events', NumericCode: 42 };
|
|
191
|
+
const title = extractDisplayTitle(meta, 'Events');
|
|
192
|
+
expect(title).toBe('Events Record');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should skip empty/whitespace name field values', () => {
|
|
196
|
+
const meta = { Entity: 'Contacts', FirstName: '', LastName: 'Rodriguez' };
|
|
197
|
+
const title = extractDisplayTitle(meta, 'Contacts');
|
|
198
|
+
expect(title).toBe('Rodriguez');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle metadata with no Entity key by using heuristics', () => {
|
|
202
|
+
const meta = { Name: 'Fallback Name' };
|
|
203
|
+
const title = extractDisplayTitle(meta, 'SomeEntity');
|
|
204
|
+
expect(title).toBe('Fallback Name');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return fallback entity record string when all heuristic fields are non-string', () => {
|
|
208
|
+
const meta = { Entity: 'NonExistent', Count: 42, Active: true };
|
|
209
|
+
const title = extractDisplayTitle(meta, 'MyEntity');
|
|
210
|
+
expect(title).toBe('MyEntity Record');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ─── extractDisplaySnippet ────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
describe('extractDisplaySnippet', () => {
|
|
217
|
+
const extractDisplaySnippet = (meta: Record<string, unknown>, indexName: string, score?: number) =>
|
|
218
|
+
getPrivateMethod(resolver, 'extractDisplaySnippet')(meta, indexName, score) as string;
|
|
219
|
+
|
|
220
|
+
it('should return Description field when present', () => {
|
|
221
|
+
const meta = { Description: 'A detailed description of the record' };
|
|
222
|
+
expect(extractDisplaySnippet(meta, 'idx-1', 0.95)).toBe('A detailed description of the record');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should truncate long descriptions to 200 chars', () => {
|
|
226
|
+
const longDesc = 'A'.repeat(250);
|
|
227
|
+
const meta = { Description: longDesc };
|
|
228
|
+
const snippet = extractDisplaySnippet(meta, 'idx-1');
|
|
229
|
+
expect(snippet.length).toBe(203); // 200 + '...'
|
|
230
|
+
expect(snippet.endsWith('...')).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should build snippet from metadata fields when no description fields exist', () => {
|
|
234
|
+
const meta = { Status: 'Active', Type: 'Premium' };
|
|
235
|
+
const snippet = extractDisplaySnippet(meta, 'idx-1');
|
|
236
|
+
expect(snippet).toContain('Status: Active');
|
|
237
|
+
expect(snippet).toContain('Type: Premium');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should return index fallback when no usable metadata', () => {
|
|
241
|
+
const meta = { RecordID: 'abc-123', Entity: 'Contacts' };
|
|
242
|
+
const snippet = extractDisplaySnippet(meta, 'my-index', 0.85);
|
|
243
|
+
expect(snippet).toContain('my-index');
|
|
244
|
+
expect(snippet).toContain('0.8500');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ─── Tag extraction from vector metadata (inline logic) ────────
|
|
249
|
+
|
|
250
|
+
describe('tag extraction from vector metadata', () => {
|
|
251
|
+
it('should extract Tags array when present and is an array', () => {
|
|
252
|
+
const meta: Record<string, unknown> = { Tags: ['AI', 'Machine Learning'] };
|
|
253
|
+
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
254
|
+
expect(tags).toEqual(['AI', 'Machine Learning']);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should return empty array when Tags is not in metadata', () => {
|
|
258
|
+
const meta: Record<string, unknown> = { Entity: 'Companies', RecordID: 'r2' };
|
|
259
|
+
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
260
|
+
expect(tags).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should return empty array when Tags is not an array', () => {
|
|
264
|
+
const meta: Record<string, unknown> = { Tags: 'not-an-array' };
|
|
265
|
+
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
266
|
+
expect(tags).toEqual([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should return empty array when metadata is empty', () => {
|
|
270
|
+
const meta: Record<string, unknown> = {};
|
|
271
|
+
const tags = Array.isArray(meta['Tags']) ? (meta['Tags'] as string[]) : [];
|
|
272
|
+
expect(tags).toEqual([]);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ─── enrichResultsWithTags ────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
describe('enrichResultsWithTags', () => {
|
|
279
|
+
const enrichResultsWithTags = (
|
|
280
|
+
results: Array<{ EntityName: string; RecordID: string; Tags: string[] }>,
|
|
281
|
+
md: { Entities: Array<{ Name: string; ID: string }> },
|
|
282
|
+
contextUser: unknown
|
|
283
|
+
) => getPrivateMethod(resolver, 'enrichResultsWithTags')(results, md, contextUser) as Promise<void>;
|
|
284
|
+
|
|
285
|
+
it('should not modify results when results array is empty', async () => {
|
|
286
|
+
const results: Array<{ EntityName: string; RecordID: string; Tags: string[] }> = [];
|
|
287
|
+
const md = { Entities: [{ Name: 'Contacts', ID: 'eid-1' }] };
|
|
288
|
+
await enrichResultsWithTags(results, md, {});
|
|
289
|
+
expect(results).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should not modify results when entity is not found in metadata', async () => {
|
|
293
|
+
const results = [{ EntityName: 'NonExistent', RecordID: 'r1', Tags: [] as string[] }];
|
|
294
|
+
const md = { Entities: [{ Name: 'Contacts', ID: 'eid-1' }] };
|
|
295
|
+
await enrichResultsWithTags(results, md, {});
|
|
296
|
+
expect(results[0].Tags).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should enrich results with tags from RunView response', async () => {
|
|
300
|
+
const results = [
|
|
301
|
+
{ EntityName: 'Contacts', RecordID: 'rec-1', Tags: [] as string[] },
|
|
302
|
+
{ EntityName: 'Contacts', RecordID: 'rec-2', Tags: [] as string[] },
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// Set up mock RunView to return tagged items
|
|
306
|
+
// loadTaggedItemTags queries 'MJ: Tagged Items' with EntityID+RecordID filter
|
|
307
|
+
mockRunViewResults.set('MJ: Tagged Items', {
|
|
308
|
+
Success: true,
|
|
309
|
+
Results: [
|
|
310
|
+
{ EntityID: 'entity-contacts-id', RecordID: 'rec-1', Tag: 'VIP' },
|
|
311
|
+
{ EntityID: 'entity-contacts-id', RecordID: 'rec-1', Tag: 'Partner' },
|
|
312
|
+
{ EntityID: 'entity-contacts-id', RecordID: 'rec-2', Tag: 'Prospect' },
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Test loadTaggedItemTags directly since enrichResultsWithTags wraps it in try/catch
|
|
317
|
+
const loadTaggedItemTags = getPrivateMethod(resolver, 'loadTaggedItemTags');
|
|
318
|
+
const { Metadata: MetadataCtor } = await import('@memberjunction/core');
|
|
319
|
+
const md = new MetadataCtor();
|
|
320
|
+
await loadTaggedItemTags(results, md, {});
|
|
321
|
+
expect(results[0].Tags).toEqual(['VIP', 'Partner']);
|
|
322
|
+
expect(results[1].Tags).toEqual(['Prospect']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should leave existing tags unchanged when RunView fails', async () => {
|
|
326
|
+
const results = [
|
|
327
|
+
{ EntityName: 'Contacts', RecordID: 'rec-1', Tags: ['Existing'] },
|
|
328
|
+
];
|
|
329
|
+
const md = { Entities: [{ Name: 'Contacts', ID: 'eid-contacts' }] };
|
|
330
|
+
|
|
331
|
+
mockRunViewResults.set('MJ: Tagged Items', {
|
|
332
|
+
Success: false,
|
|
333
|
+
Results: []
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// enrichResultsWithTags replaces Tags; on RunView failure it returns early
|
|
337
|
+
await enrichResultsWithTags(results, md, {});
|
|
338
|
+
// Tags should remain unchanged since RunView failed before tag assignment
|
|
339
|
+
expect(results[0].Tags).toEqual(['Existing']);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ─── deduplicateResults ───────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
describe('deduplicateResults', () => {
|
|
346
|
+
const deduplicateResults = (results: Array<{ EntityName: string; RecordID: string; Score: number }>) =>
|
|
347
|
+
getPrivateMethod(resolver, 'deduplicateResults')(results) as Array<{ EntityName: string; RecordID: string; Score: number }>;
|
|
348
|
+
|
|
349
|
+
it('should keep the highest-scored entry for duplicate entity+recordID', () => {
|
|
350
|
+
const results = [
|
|
351
|
+
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.8 },
|
|
352
|
+
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.95 },
|
|
353
|
+
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.7 },
|
|
354
|
+
];
|
|
355
|
+
const deduped = deduplicateResults(results);
|
|
356
|
+
expect(deduped).toHaveLength(1);
|
|
357
|
+
expect(deduped[0].Score).toBe(0.95);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should keep distinct records from different entities', () => {
|
|
361
|
+
const results = [
|
|
362
|
+
{ EntityName: 'Contacts', RecordID: 'r1', Score: 0.8 },
|
|
363
|
+
{ EntityName: 'Companies', RecordID: 'r1', Score: 0.7 },
|
|
364
|
+
];
|
|
365
|
+
const deduped = deduplicateResults(results);
|
|
366
|
+
expect(deduped).toHaveLength(2);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should sort results by score descending', () => {
|
|
370
|
+
const results = [
|
|
371
|
+
{ EntityName: 'A', RecordID: 'r1', Score: 0.3 },
|
|
372
|
+
{ EntityName: 'B', RecordID: 'r2', Score: 0.9 },
|
|
373
|
+
{ EntityName: 'C', RecordID: 'r3', Score: 0.6 },
|
|
374
|
+
];
|
|
375
|
+
const deduped = deduplicateResults(results);
|
|
376
|
+
expect(deduped[0].Score).toBe(0.9);
|
|
377
|
+
expect(deduped[1].Score).toBe(0.6);
|
|
378
|
+
expect(deduped[2].Score).toBe(0.3);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should return empty array for empty input', () => {
|
|
382
|
+
expect(deduplicateResults([])).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ─── buildPineconeFilter ──────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe('buildPineconeFilter', () => {
|
|
389
|
+
const buildPineconeFilter = (filters?: { EntityNames?: string[]; SourceTypes?: string[]; Tags?: string[] }) =>
|
|
390
|
+
getPrivateMethod(resolver, 'buildPineconeFilter')(filters) as object | undefined;
|
|
391
|
+
|
|
392
|
+
it('should return undefined for no filters', () => {
|
|
393
|
+
expect(buildPineconeFilter(undefined)).toBeUndefined();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should return undefined for empty filters', () => {
|
|
397
|
+
expect(buildPineconeFilter({})).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should return single condition without $and wrapper', () => {
|
|
401
|
+
const result = buildPineconeFilter({ EntityNames: ['Contacts'] });
|
|
402
|
+
expect(result).toEqual({ Entity: { $in: ['Contacts'] } });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should combine multiple filters with $and', () => {
|
|
406
|
+
const result = buildPineconeFilter({
|
|
407
|
+
EntityNames: ['Contacts'],
|
|
408
|
+
Tags: ['VIP']
|
|
409
|
+
}) as Record<string, unknown>;
|
|
410
|
+
expect(result).toHaveProperty('$and');
|
|
411
|
+
const conditions = result['$and'] as object[];
|
|
412
|
+
expect(conditions).toHaveLength(2);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -402,6 +402,17 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
402
402
|
clientSecret: process.env.AUTH0_CLIENT_SECRET,
|
|
403
403
|
domain: process.env.AUTH0_DOMAIN
|
|
404
404
|
} : null,
|
|
405
|
+
// AWS Cognito
|
|
406
|
+
process.env.COGNITO_USER_POOL_ID && process.env.COGNITO_CLIENT_ID && process.env.AWS_REGION ? {
|
|
407
|
+
name: 'cognito',
|
|
408
|
+
type: 'cognito',
|
|
409
|
+
issuer: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`,
|
|
410
|
+
audience: process.env.COGNITO_CLIENT_ID,
|
|
411
|
+
jwksUri: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}/.well-known/jwks.json`,
|
|
412
|
+
clientId: process.env.COGNITO_CLIENT_ID,
|
|
413
|
+
region: process.env.AWS_REGION,
|
|
414
|
+
userPoolId: process.env.COGNITO_USER_POOL_ID
|
|
415
|
+
} : null,
|
|
405
416
|
].filter(Boolean),
|
|
406
417
|
};
|
|
407
418
|
|