@memberjunction/server 5.24.0 → 5.25.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.
Files changed (52) hide show
  1. package/dist/agents/skip-sdk.d.ts +12 -0
  2. package/dist/agents/skip-sdk.d.ts.map +1 -1
  3. package/dist/agents/skip-sdk.js +70 -1
  4. package/dist/agents/skip-sdk.js.map +1 -1
  5. package/dist/generated/generated.d.ts +492 -0
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +2731 -0
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/resolvers/ArtifactFileResolver.d.ts +15 -0
  14. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -0
  15. package/dist/resolvers/ArtifactFileResolver.js +74 -0
  16. package/dist/resolvers/ArtifactFileResolver.js.map +1 -0
  17. package/dist/resolvers/AutotagPipelineResolver.d.ts +13 -0
  18. package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
  19. package/dist/resolvers/AutotagPipelineResolver.js +103 -3
  20. package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
  21. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  22. package/dist/resolvers/FileResolver.js +12 -32
  23. package/dist/resolvers/FileResolver.js.map +1 -1
  24. package/dist/resolvers/GeoResolver.d.ts +58 -0
  25. package/dist/resolvers/GeoResolver.d.ts.map +1 -0
  26. package/dist/resolvers/GeoResolver.js +302 -0
  27. package/dist/resolvers/GeoResolver.js.map +1 -0
  28. package/dist/resolvers/RunAIAgentResolver.d.ts +13 -1
  29. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  30. package/dist/resolvers/RunAIAgentResolver.js +115 -20
  31. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  32. package/dist/resolvers/SearchKnowledgeResolver.d.ts +21 -80
  33. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  34. package/dist/resolvers/SearchKnowledgeResolver.js +129 -604
  35. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  36. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts +19 -0
  37. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -0
  38. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +149 -0
  39. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -0
  40. package/package.json +63 -63
  41. package/src/__tests__/search-knowledge-tags.test.ts +177 -337
  42. package/src/__tests__/skip-sdk-organic-keys.test.ts +274 -0
  43. package/src/agents/skip-sdk.ts +83 -2
  44. package/src/generated/generated.ts +1884 -1
  45. package/src/index.ts +2 -0
  46. package/src/resolvers/ArtifactFileResolver.ts +71 -0
  47. package/src/resolvers/AutotagPipelineResolver.ts +118 -4
  48. package/src/resolvers/FileResolver.ts +12 -41
  49. package/src/resolvers/GeoResolver.ts +258 -0
  50. package/src/resolvers/RunAIAgentResolver.ts +137 -23
  51. package/src/resolvers/SearchKnowledgeResolver.ts +114 -715
  52. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +138 -0
@@ -0,0 +1,274 @@
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
+ });
@@ -21,6 +21,8 @@ import {
21
21
  SkipEntityFieldInfo,
22
22
  SkipEntityFieldValueInfo,
23
23
  SkipEntityRelationshipInfo,
24
+ SkipEntityOrganicKeyInfo,
25
+ SkipEntityOrganicKeyRelatedEntityInfo,
24
26
  SkipAPIAgentNote,
25
27
  SkipAPIAgentNoteType,
26
28
  SkipAPIArtifact,
@@ -28,7 +30,7 @@ import {
28
30
  SkipAPIArtifactType
29
31
  } from '@memberjunction/skip-types';
30
32
  import { DataContext } from '@memberjunction/data-context';
31
- import { UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityRelationshipInfo } from '@memberjunction/core';
33
+ import { UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityRelationshipInfo, EntityOrganicKeyInfo, EntityOrganicKeyRelatedEntityInfo } from '@memberjunction/core';
32
34
  import { request as httpRequest } from 'http';
33
35
  import { request as httpsRequest } from 'https';
34
36
  import { gzip as gzipCompress, createGunzip } from 'zlib';
@@ -927,7 +929,12 @@ export class SkipSDK {
927
929
 
928
930
  relatedEntities: e.RelatedEntities.map((r) => {
929
931
  return this.packSingleSkipEntityRelationship(r);
930
- })
932
+ }),
933
+
934
+ organicKeys: (e.OrganicKeys ?? [])
935
+ .filter((ok) => ok.Status === 'Active')
936
+ .map((ok) => this.packSingleSkipOrganicKey(ok))
937
+ .filter((ok): ok is SkipEntityOrganicKeyInfo => ok !== null)
931
938
  };
932
939
  return ret;
933
940
  }
@@ -937,6 +944,80 @@ export class SkipSDK {
937
944
  }
938
945
  }
939
946
 
947
+ /**
948
+ * Packs information about a single organic key for Skip.
949
+ * Organic keys express cross-entity relationships via shared business data
950
+ * (email, acronym, etc.) rather than database FK constraints.
951
+ */
952
+ private packSingleSkipOrganicKey(ok: EntityOrganicKeyInfo): SkipEntityOrganicKeyInfo | null {
953
+ try {
954
+ return {
955
+ id: ok.ID,
956
+ name: ok.Name,
957
+ description: ok.Description ?? undefined,
958
+ matchFieldNames: ok.MatchFieldNamesArray,
959
+ normalizationStrategy: ok.NormalizationStrategy,
960
+ customNormalizationExpression: ok.CustomNormalizationExpression ?? undefined,
961
+ sequence: ok.Sequence,
962
+ relatedEntities: ok.RelatedEntities
963
+ .map((re) => this.packSingleSkipOrganicKeyRelatedEntity(re))
964
+ .filter((re): re is SkipEntityOrganicKeyRelatedEntityInfo => re !== null)
965
+ };
966
+ }
967
+ catch (e) {
968
+ LogError(`[SkipSDK] packSingleSkipOrganicKey error: ${e}`);
969
+ return null;
970
+ }
971
+ }
972
+
973
+ /**
974
+ * Packs information about a single organic key related entity for Skip.
975
+ * Looks up schema name and base view from metadata since they are not
976
+ * stored on EntityOrganicKeyRelatedEntityInfo directly.
977
+ */
978
+ private packSingleSkipOrganicKeyRelatedEntity(
979
+ re: EntityOrganicKeyRelatedEntityInfo
980
+ ): SkipEntityOrganicKeyRelatedEntityInfo | null {
981
+ try {
982
+ // Look up the related entity to obtain schema name and base view, which
983
+ // Skip needs in order to generate schema-qualified SQL.
984
+ const relatedEntity = Metadata.Provider.Entities.find(
985
+ (ent) => UUIDsEqual(ent.ID, re.RelatedEntityID)
986
+ );
987
+ if (!relatedEntity) {
988
+ LogError(
989
+ `[SkipSDK] packSingleSkipOrganicKeyRelatedEntity: related entity not found for ID ${re.RelatedEntityID}`
990
+ );
991
+ return null;
992
+ }
993
+
994
+ return {
995
+ id: re.ID,
996
+ relatedEntityID: re.RelatedEntityID,
997
+ relatedEntityName: relatedEntity.Name,
998
+ relatedEntitySchemaName: relatedEntity.SchemaName,
999
+ relatedEntityBaseView: relatedEntity.BaseView,
1000
+ isDirectMatch: re.IsDirectMatch,
1001
+ isTransitiveMatch: re.IsTransitiveMatch,
1002
+ relatedEntityFieldNames: re.IsDirectMatch
1003
+ ? re.RelatedEntityFieldNamesArray
1004
+ : undefined,
1005
+ transitiveObjectName: re.TransitiveObjectName ?? undefined,
1006
+ transitiveObjectMatchFieldNames: re.IsTransitiveMatch
1007
+ ? re.TransitiveObjectMatchFieldNamesArray
1008
+ : undefined,
1009
+ transitiveObjectOutputFieldName: re.TransitiveObjectOutputFieldName ?? undefined,
1010
+ relatedEntityJoinFieldName: re.RelatedEntityJoinFieldName ?? undefined,
1011
+ displayName: re.DisplayName ?? undefined,
1012
+ sequence: re.Sequence
1013
+ };
1014
+ }
1015
+ catch (e) {
1016
+ LogError(`[SkipSDK] packSingleSkipOrganicKeyRelatedEntity error: ${e}`);
1017
+ return null;
1018
+ }
1019
+ }
1020
+
940
1021
  /**
941
1022
  * Packs information about a single entity relationship
942
1023
  * These relationships help Skip understand the data model