@memberjunction/server 5.24.0 → 5.26.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 +12 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +70 -1
- 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 +498 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +15 -0
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -0
- package/dist/resolvers/ArtifactFileResolver.js +74 -0
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts +13 -0
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +103 -3
- package/dist/resolvers/AutotagPipelineResolver.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/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +12 -32
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GeoResolver.d.ts +58 -0
- package/dist/resolvers/GeoResolver.d.ts.map +1 -0
- package/dist/resolvers/GeoResolver.js +302 -0
- package/dist/resolvers/GeoResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +13 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +115 -20
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +21 -80
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +129 -604
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts +19 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +149 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -0
- package/package.json +66 -63
- package/src/__tests__/search-knowledge-tags.test.ts +177 -337
- package/src/__tests__/skip-sdk-organic-keys.test.ts +274 -0
- package/src/agents/skip-sdk.ts +83 -2
- package/src/config.ts +24 -0
- package/src/generated/generated.ts +1902 -1
- package/src/index.ts +18 -2
- package/src/resolvers/ArtifactFileResolver.ts +71 -0
- package/src/resolvers/AutotagPipelineResolver.ts +118 -4
- package/src/resolvers/CacheStatsResolver.ts +142 -0
- package/src/resolvers/FileResolver.ts +12 -41
- package/src/resolvers/GeoResolver.ts +258 -0
- package/src/resolvers/RunAIAgentResolver.ts +137 -23
- package/src/resolvers/SearchKnowledgeResolver.ts +114 -715
- 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
|
+
});
|
package/src/agents/skip-sdk.ts
CHANGED
|
@@ -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
|
package/src/config.ts
CHANGED
|
@@ -192,6 +192,19 @@ const serverExtensionSchema = z.object({
|
|
|
192
192
|
Settings: z.record(z.unknown()).default({})
|
|
193
193
|
}).passthrough();
|
|
194
194
|
|
|
195
|
+
const cacheSettingsSchema = z.object({
|
|
196
|
+
/** Maximum total estimated memory for all cached results in MB. Default: 150. Set to 0 to disable memory-based eviction. */
|
|
197
|
+
maxMemoryMB: z.number().optional().default(150),
|
|
198
|
+
/** Maximum percentage of total cache memory that any single entity can occupy. Default: 50. Set to 0 to disable. */
|
|
199
|
+
maxPercentOfCachePerEntity: z.number().optional().default(50),
|
|
200
|
+
/** Default TTL in seconds. 0 = no TTL, rely on event-based invalidation. Default: 0. */
|
|
201
|
+
defaultTTLSeconds: z.number().optional().default(0),
|
|
202
|
+
/** Interval in seconds for periodic eviction sweep. 0 = disabled. Default: 300 (5 minutes). */
|
|
203
|
+
evictionSweepIntervalSeconds: z.number().optional().default(300),
|
|
204
|
+
/** Enable verbose cache logging (hits, misses, evictions). Default: false. */
|
|
205
|
+
verboseLogging: z.boolean().optional().default(false),
|
|
206
|
+
});
|
|
207
|
+
|
|
195
208
|
const configInfoSchema = z.object({
|
|
196
209
|
userHandling: userHandlingInfoSchema,
|
|
197
210
|
databaseSettings: databaseSettingsInfoSchema,
|
|
@@ -206,6 +219,7 @@ const configInfoSchema = z.object({
|
|
|
206
219
|
queryDialects: queryDialectSchema.optional().default({}),
|
|
207
220
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
208
221
|
serverExtensions: z.array(serverExtensionSchema).optional().default([]),
|
|
222
|
+
cacheSettings: cacheSettingsSchema.optional().default({}),
|
|
209
223
|
|
|
210
224
|
apiKey: z.string().optional(),
|
|
211
225
|
baseUrl: z.string().default('http://localhost'),
|
|
@@ -252,6 +266,7 @@ export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
|
252
266
|
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
253
267
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
254
268
|
export type ServerExtensionConfig = z.infer<typeof serverExtensionSchema>;
|
|
269
|
+
export type CacheSettingsConfig = z.infer<typeof cacheSettingsSchema>;
|
|
255
270
|
export type ConfigInfo = z.infer<typeof configInfoSchema>;
|
|
256
271
|
|
|
257
272
|
/**
|
|
@@ -378,6 +393,15 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
378
393
|
level: 'standard'
|
|
379
394
|
},
|
|
380
395
|
|
|
396
|
+
// Cache settings defaults
|
|
397
|
+
cacheSettings: {
|
|
398
|
+
maxMemoryMB: 150,
|
|
399
|
+
maxPercentOfCachePerEntity: 50,
|
|
400
|
+
defaultTTLSeconds: 0,
|
|
401
|
+
evictionSweepIntervalSeconds: 300,
|
|
402
|
+
verboseLogging: false,
|
|
403
|
+
},
|
|
404
|
+
|
|
381
405
|
// Auth providers (environment-driven)
|
|
382
406
|
authProviders: [
|
|
383
407
|
// Microsoft Azure AD / Entra ID
|