@memberjunction/server 5.30.1 → 5.31.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 +17 -1
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +18 -5
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/newUsers.js +2 -2
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.js +3 -3
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +218 -8
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1267 -52
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +5 -5
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +21 -18
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.js +1 -1
- package/dist/multiTenancy/index.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
- package/dist/resolvers/APIKeyResolver.js +5 -3
- package/dist/resolvers/APIKeyResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +3 -3
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +18 -12
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +6 -4
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.js +2 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +1 -2
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/ISAEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/ISAEntityResolver.js +2 -5
- package/dist/resolvers/ISAEntityResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +75 -66
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +4 -4
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +9 -8
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +6 -6
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +22 -18
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.d.ts +43 -0
- package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -0
- package/dist/resolvers/TagGovernanceResolver.js +245 -0
- package/dist/resolvers/TagGovernanceResolver.js.map +1 -0
- package/dist/resolvers/TaskResolver.d.ts +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +4 -2
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +2 -1
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +4 -4
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +9 -9
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +4 -4
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +4 -2
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +16 -12
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +68 -66
- package/src/__tests__/TagGovernanceResolver.test.ts +255 -0
- package/src/agents/skip-sdk.ts +30 -7
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +2 -2
- package/src/auth/newUsers.ts +2 -2
- package/src/context.ts +3 -3
- package/src/generated/generated.ts +872 -41
- package/src/generic/ResolverBase.ts +28 -21
- package/src/index.ts +9 -9
- package/src/multiTenancy/index.ts +1 -1
- package/src/resolvers/APIKeyResolver.ts +7 -4
- package/src/resolvers/AutotagPipelineResolver.ts +20 -11
- package/src/resolvers/ComponentRegistryResolver.ts +8 -5
- package/src/resolvers/FileResolver.ts +2 -2
- package/src/resolvers/GetDataContextDataResolver.ts +1 -2
- package/src/resolvers/ISAEntityResolver.ts +3 -5
- package/src/resolvers/IntegrationDiscoveryResolver.ts +83 -66
- package/src/resolvers/SyncDataResolver.ts +12 -11
- package/src/resolvers/SyncRolesUsersResolver.ts +23 -19
- package/src/resolvers/TagGovernanceResolver.ts +189 -0
- package/src/resolvers/TaskResolver.ts +5 -3
- package/src/resolvers/TransactionGroupResolver.ts +3 -2
- package/src/rest/EntityCRUDHandler.ts +4 -4
- package/src/rest/RESTEndpointHandler.ts +9 -9
- package/src/rest/ViewOperationsHandler.ts +4 -4
- package/src/services/TaskOrchestrator.ts +18 -13
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for TagGovernanceResolver.
|
|
5
|
+
*
|
|
6
|
+
* type-graphql decorators on the resolver class need a registered TypeGraphQL
|
|
7
|
+
* runtime to instantiate, so we don't construct the resolver directly. Instead
|
|
8
|
+
* we verify the resolver wraps the engines correctly by stubbing the engines
|
|
9
|
+
* and asserting the dispatch contract — what gets called, with what args, and
|
|
10
|
+
* how errors are surfaced.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { promoteCalls, rejectCalls, rebuildCalls, healthCalls, behaviorFlags } = vi.hoisted(() => ({
|
|
14
|
+
promoteCalls: [] as Array<{ id: string; strategy: unknown; user: unknown }>,
|
|
15
|
+
rejectCalls: [] as Array<{ id: string; notes: string | null; user: unknown }>,
|
|
16
|
+
rebuildCalls: [] as Array<{ user: unknown }>,
|
|
17
|
+
healthCalls: [] as Array<{ thresholds: unknown; user: unknown }>,
|
|
18
|
+
behaviorFlags: { promoteThrows: false, rejectThrows: false, rebuildThrows: false, healthThrows: false },
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@memberjunction/tag-engine', () => ({
|
|
22
|
+
TagGovernanceEngine: {
|
|
23
|
+
Instance: {
|
|
24
|
+
PromoteSuggestion: async (id: string, strategy: unknown, user: unknown) => {
|
|
25
|
+
promoteCalls.push({ id, strategy, user });
|
|
26
|
+
if (behaviorFlags.promoteThrows) throw new Error('promote-fail');
|
|
27
|
+
return { ID: 'new-tag-id', Name: 'Resolved Tag' };
|
|
28
|
+
},
|
|
29
|
+
RejectSuggestion: async (id: string, notes: string | null, user: unknown) => {
|
|
30
|
+
rejectCalls.push({ id, notes, user });
|
|
31
|
+
if (behaviorFlags.rejectThrows) throw new Error('reject-fail');
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
TagEngine: {
|
|
36
|
+
Instance: {
|
|
37
|
+
RebuildTagEmbeddings: async (user: unknown) => {
|
|
38
|
+
rebuildCalls.push({ user });
|
|
39
|
+
if (behaviorFlags.rebuildThrows) throw new Error('rebuild-fail');
|
|
40
|
+
return { refreshed: 7, total: 100 };
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
TagHealthJob: {
|
|
45
|
+
Instance: {
|
|
46
|
+
Run: async (thresholds: unknown, user: unknown) => {
|
|
47
|
+
healthCalls.push({ thresholds, user });
|
|
48
|
+
if (behaviorFlags.healthThrows) throw new Error('health-fail');
|
|
49
|
+
return { mergeCount: 3, lowUsageCount: 2, wideNodeCount: 1, durationMs: 42 };
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
DEFAULT_TAG_HEALTH_THRESHOLDS: {
|
|
54
|
+
minCoOccurrence: 5,
|
|
55
|
+
minNameSimilarity: 0.5,
|
|
56
|
+
minEmbeddingSimilarity: 0.85,
|
|
57
|
+
maxUsage: 2,
|
|
58
|
+
maxImplicitChildren: 25,
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Minimal type-graphql shim — we only need the decorators to be no-ops so the
|
|
63
|
+
// resolver class can load. Method behavior is what we test.
|
|
64
|
+
vi.mock('type-graphql', () => ({
|
|
65
|
+
Resolver: () => () => undefined,
|
|
66
|
+
Mutation: () => () => undefined,
|
|
67
|
+
Query: () => () => undefined,
|
|
68
|
+
Ctx: () => () => undefined,
|
|
69
|
+
Arg: () => () => undefined,
|
|
70
|
+
ObjectType: () => () => undefined,
|
|
71
|
+
Field: () => () => undefined,
|
|
72
|
+
Int: () => undefined,
|
|
73
|
+
Float: () => undefined,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
vi.mock('@memberjunction/core', () => ({
|
|
77
|
+
LogError: vi.fn(),
|
|
78
|
+
LogStatus: vi.fn(),
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
// Stub ResolverBase — we only need GetUserFromPayload to return a synthetic user.
|
|
82
|
+
vi.mock('../generic/ResolverBase.js', () => ({
|
|
83
|
+
ResolverBase: class {
|
|
84
|
+
GetUserFromPayload(payload: unknown) {
|
|
85
|
+
if (!payload) return null;
|
|
86
|
+
return { ID: 'user-1', Email: 'test@example.com' };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
import { TagGovernanceResolver } from '../resolvers/TagGovernanceResolver.js';
|
|
92
|
+
|
|
93
|
+
describe('TagGovernanceResolver', () => {
|
|
94
|
+
let resolver: TagGovernanceResolver;
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
promoteCalls.length = 0;
|
|
98
|
+
rejectCalls.length = 0;
|
|
99
|
+
rebuildCalls.length = 0;
|
|
100
|
+
healthCalls.length = 0;
|
|
101
|
+
behaviorFlags.promoteThrows = false;
|
|
102
|
+
behaviorFlags.rejectThrows = false;
|
|
103
|
+
behaviorFlags.rebuildThrows = false;
|
|
104
|
+
behaviorFlags.healthThrows = false;
|
|
105
|
+
resolver = new TagGovernanceResolver();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const ctx = (withUser = true) => withUser ? { userPayload: { sub: 'u-1' } } : { userPayload: undefined };
|
|
109
|
+
|
|
110
|
+
// ---- PromoteTagSuggestion ----------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe('PromoteTagSuggestion', () => {
|
|
113
|
+
it('rejects when no current user is resolved from the payload', async () => {
|
|
114
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'create-new', undefined, ctx(false) as never);
|
|
115
|
+
expect(r.Success).toBe(false);
|
|
116
|
+
expect(r.ErrorMessage).toContain('current user');
|
|
117
|
+
expect(promoteCalls).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('rejects unknown strategies', async () => {
|
|
121
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'wat' as never, undefined, ctx() as never);
|
|
122
|
+
expect(r.Success).toBe(false);
|
|
123
|
+
expect(r.ErrorMessage).toContain('Unknown strategy');
|
|
124
|
+
expect(promoteCalls).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('requires targetTagID for merge-into-existing', async () => {
|
|
128
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'merge-into-existing', undefined, ctx() as never);
|
|
129
|
+
expect(r.Success).toBe(false);
|
|
130
|
+
expect(r.ErrorMessage).toContain('targetTagID is required');
|
|
131
|
+
expect(promoteCalls).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('dispatches create-new and returns the resolved tag', async () => {
|
|
135
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'create-new', undefined, ctx() as never);
|
|
136
|
+
expect(r.Success).toBe(true);
|
|
137
|
+
expect(r.ResolvedTagID).toBe('new-tag-id');
|
|
138
|
+
expect(r.ResolvedTagName).toBe('Resolved Tag');
|
|
139
|
+
expect(promoteCalls[0].id).toBe('s-1');
|
|
140
|
+
expect((promoteCalls[0].strategy as { kind: string }).kind).toBe('create-new');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('dispatches merge-into-existing with the target tag ID', async () => {
|
|
144
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'merge-into-existing', 'target-tag', ctx() as never);
|
|
145
|
+
expect(r.Success).toBe(true);
|
|
146
|
+
expect((promoteCalls[0].strategy as { kind: string; targetTagID: string }).targetTagID).toBe('target-tag');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns Success=false with the error message when the engine throws', async () => {
|
|
150
|
+
behaviorFlags.promoteThrows = true;
|
|
151
|
+
const r = await resolver.PromoteTagSuggestion('s-1', 'create-new', undefined, ctx() as never);
|
|
152
|
+
expect(r.Success).toBe(false);
|
|
153
|
+
expect(r.ErrorMessage).toBe('promote-fail');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---- RejectTagSuggestion -----------------------------------------------
|
|
158
|
+
|
|
159
|
+
describe('RejectTagSuggestion', () => {
|
|
160
|
+
it('rejects without a user', async () => {
|
|
161
|
+
const r = await resolver.RejectTagSuggestion('s-1', undefined, ctx(false) as never);
|
|
162
|
+
expect(r.Success).toBe(false);
|
|
163
|
+
expect(rejectCalls).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('passes optional reviewer notes to the engine', async () => {
|
|
167
|
+
const r = await resolver.RejectTagSuggestion('s-1', 'looked spammy', ctx() as never);
|
|
168
|
+
expect(r.Success).toBe(true);
|
|
169
|
+
expect(rejectCalls[0]).toMatchObject({ id: 's-1', notes: 'looked spammy' });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('coerces undefined notes to null', async () => {
|
|
173
|
+
await resolver.RejectTagSuggestion('s-1', undefined, ctx() as never);
|
|
174
|
+
expect(rejectCalls[0].notes).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns the engine error on failure', async () => {
|
|
178
|
+
behaviorFlags.rejectThrows = true;
|
|
179
|
+
const r = await resolver.RejectTagSuggestion('s-1', undefined, ctx() as never);
|
|
180
|
+
expect(r.Success).toBe(false);
|
|
181
|
+
expect(r.ErrorMessage).toBe('reject-fail');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---- RebuildTagEmbeddings ----------------------------------------------
|
|
186
|
+
|
|
187
|
+
describe('RebuildTagEmbeddings', () => {
|
|
188
|
+
it('rejects without a user', async () => {
|
|
189
|
+
const r = await resolver.RebuildTagEmbeddings(ctx(false) as never);
|
|
190
|
+
expect(r.Success).toBe(false);
|
|
191
|
+
expect(r.Refreshed).toBe(0);
|
|
192
|
+
expect(r.Total).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('returns the refresh counts from the engine', async () => {
|
|
196
|
+
const r = await resolver.RebuildTagEmbeddings(ctx() as never);
|
|
197
|
+
expect(r.Success).toBe(true);
|
|
198
|
+
expect(r.Refreshed).toBe(7);
|
|
199
|
+
expect(r.Total).toBe(100);
|
|
200
|
+
expect(rebuildCalls).toHaveLength(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns 0/0 with an error message when the engine throws', async () => {
|
|
204
|
+
behaviorFlags.rebuildThrows = true;
|
|
205
|
+
const r = await resolver.RebuildTagEmbeddings(ctx() as never);
|
|
206
|
+
expect(r.Success).toBe(false);
|
|
207
|
+
expect(r.ErrorMessage).toBe('rebuild-fail');
|
|
208
|
+
expect(r.Refreshed).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ---- RunTagHealth ------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe('RunTagHealth', () => {
|
|
215
|
+
it('rejects without a user', async () => {
|
|
216
|
+
const r = await resolver.RunTagHealth(undefined, undefined, undefined, undefined, undefined, ctx(false) as never);
|
|
217
|
+
expect(r.Success).toBe(false);
|
|
218
|
+
expect(healthCalls).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('falls back to defaults when no thresholds are supplied', async () => {
|
|
222
|
+
const r = await resolver.RunTagHealth(undefined, undefined, undefined, undefined, undefined, ctx() as never);
|
|
223
|
+
expect(r.Success).toBe(true);
|
|
224
|
+
expect(r.MergeCount).toBe(3);
|
|
225
|
+
expect(r.LowUsageCount).toBe(2);
|
|
226
|
+
expect(r.WideNodeCount).toBe(1);
|
|
227
|
+
const t = healthCalls[0].thresholds as Record<string, number>;
|
|
228
|
+
expect(t.minCoOccurrence).toBe(5); // from DEFAULT
|
|
229
|
+
expect(t.maxImplicitChildren).toBe(25);// from DEFAULT
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('overrides only the supplied threshold fields', async () => {
|
|
233
|
+
await resolver.RunTagHealth(20, 0.7, undefined, 1, undefined, ctx() as never);
|
|
234
|
+
const t = healthCalls[0].thresholds as Record<string, number>;
|
|
235
|
+
expect(t.minCoOccurrence).toBe(20);
|
|
236
|
+
expect(t.minNameSimilarity).toBe(0.7);
|
|
237
|
+
expect(t.minEmbeddingSimilarity).toBe(0.85); // from DEFAULT
|
|
238
|
+
expect(t.maxUsage).toBe(1);
|
|
239
|
+
expect(t.maxImplicitChildren).toBe(25); // from DEFAULT
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns the duration alongside the counts', async () => {
|
|
243
|
+
const r = await resolver.RunTagHealth(undefined, undefined, undefined, undefined, undefined, ctx() as never);
|
|
244
|
+
expect(r.DurationMs).toBe(42);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('surfaces engine errors with zeroed counts', async () => {
|
|
248
|
+
behaviorFlags.healthThrows = true;
|
|
249
|
+
const r = await resolver.RunTagHealth(undefined, undefined, undefined, undefined, undefined, ctx() as never);
|
|
250
|
+
expect(r.Success).toBe(false);
|
|
251
|
+
expect(r.MergeCount).toBe(0);
|
|
252
|
+
expect(r.ErrorMessage).toBe('health-fail');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
package/src/agents/skip-sdk.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
SkipAPIArtifactType
|
|
25
25
|
} from '@memberjunction/skip-types';
|
|
26
26
|
import { DataContext } from '@memberjunction/data-context';
|
|
27
|
-
import { UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
|
|
27
|
+
import { IMetadataProvider, UserInfo, LogStatus, LogError, Metadata, RunQuery, EntityInfo, EntityFieldInfo, EntityFieldValueInfo, DatabaseProviderBase } from '@memberjunction/core';
|
|
28
28
|
import { request as httpRequest } from 'http';
|
|
29
29
|
import { request as httpsRequest } from 'https';
|
|
30
30
|
import { gzip as gzipCompress, createGunzip } from 'zlib';
|
|
@@ -61,6 +61,15 @@ export interface SkipSDKConfig {
|
|
|
61
61
|
* Optional organization context information
|
|
62
62
|
*/
|
|
63
63
|
organizationInfo?: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Optional metadata provider this SDK instance binds to. When set, every metadata
|
|
67
|
+
* lookup (entities, queries, schema) and direct SQL call is routed through this
|
|
68
|
+
* provider — multi-tenant servers should pass the per-request provider here.
|
|
69
|
+
* When omitted, the SDK falls back to the global `Metadata.Provider` (single-server
|
|
70
|
+
* mode, legacy callers).
|
|
71
|
+
*/
|
|
72
|
+
provider?: IMetadataProvider;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
/**
|
|
@@ -199,6 +208,19 @@ interface SkipStreamMessage {
|
|
|
199
208
|
export class SkipSDK {
|
|
200
209
|
private config: SkipSDKConfig;
|
|
201
210
|
|
|
211
|
+
/**
|
|
212
|
+
* The metadata provider this SDK instance is bound to. Set via the constructor
|
|
213
|
+
* config or the `Provider` setter. Falls back to the global `Metadata.Provider`
|
|
214
|
+
* when not set — multi-tenant servers should always supply an explicit provider
|
|
215
|
+
* so each request reaches its own database connection.
|
|
216
|
+
*/
|
|
217
|
+
public get Provider(): IMetadataProvider {
|
|
218
|
+
return this.config.provider ?? (new Metadata() as unknown as IMetadataProvider);
|
|
219
|
+
}
|
|
220
|
+
public set Provider(value: IMetadataProvider | null) {
|
|
221
|
+
this.config.provider = value ?? undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
202
224
|
// Static cache for Skip entities (shared across all instances)
|
|
203
225
|
private static __skipEntitiesCache$: BehaviorSubject<Promise<EntityInfo[]> | null> = new BehaviorSubject<Promise<EntityInfo[]> | null>(null);
|
|
204
226
|
private static __lastRefreshTime: number = 0;
|
|
@@ -460,7 +482,7 @@ export class SkipSDK {
|
|
|
460
482
|
* Build saved queries for Skip
|
|
461
483
|
*/
|
|
462
484
|
private buildQueries(status: "Pending" | "In-Review" | "Approved" | "Rejected" | "Obsolete" = 'Approved'): SkipQueryInfo[] {
|
|
463
|
-
const md =
|
|
485
|
+
const md = this.Provider;
|
|
464
486
|
const approvedQueries = md.Queries.filter((q) => q.Status === status);
|
|
465
487
|
|
|
466
488
|
return approvedQueries.map((q) => ({
|
|
@@ -521,7 +543,7 @@ export class SkipSDK {
|
|
|
521
543
|
/**
|
|
522
544
|
* Recursively build category path for a query
|
|
523
545
|
*/
|
|
524
|
-
private buildQueryCategoryPath(md:
|
|
546
|
+
private buildQueryCategoryPath(md: IMetadataProvider, categoryID: string): string {
|
|
525
547
|
const cat = md.QueryCategories.find((c) => UUIDsEqual(c.ID, categoryID));
|
|
526
548
|
if (!cat) return '';
|
|
527
549
|
if (!cat.ParentID) return cat.Name;
|
|
@@ -537,7 +559,7 @@ export class SkipSDK {
|
|
|
537
559
|
* across all queries, not just approved ones.
|
|
538
560
|
*/
|
|
539
561
|
private buildQueryCatalog(): SkipQueryCatalogEntry[] {
|
|
540
|
-
const md =
|
|
562
|
+
const md = this.Provider;
|
|
541
563
|
|
|
542
564
|
return md.Queries.map((q) => ({
|
|
543
565
|
Name: q.Name,
|
|
@@ -856,7 +878,7 @@ export class SkipSDK {
|
|
|
856
878
|
*/
|
|
857
879
|
private async refreshSkipEntities(): Promise<EntityInfo[]> {
|
|
858
880
|
try {
|
|
859
|
-
const md =
|
|
881
|
+
const md = this.Provider;
|
|
860
882
|
|
|
861
883
|
// Diagnostic logging
|
|
862
884
|
LogStatus(`[SkipSDK.refreshSkipEntities] Total entities in metadata: ${md.Entities.length}`);
|
|
@@ -983,8 +1005,9 @@ export class SkipSDK {
|
|
|
983
1005
|
*/
|
|
984
1006
|
private async getFieldDistinctValues(f: EntityFieldInfo): Promise<EntityFieldValueInfo[]> {
|
|
985
1007
|
try {
|
|
986
|
-
//
|
|
987
|
-
|
|
1008
|
+
// Use this SDK instance's bound provider so multi-tenant servers route the SQL
|
|
1009
|
+
// through the right connection. ExecuteSQL works on both SQL Server and PostgreSQL.
|
|
1010
|
+
const provider = this.Provider as unknown as DatabaseProviderBase;
|
|
988
1011
|
const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
|
|
989
1012
|
const rows = await provider.ExecuteSQL<Record<string, unknown>>(sql);
|
|
990
1013
|
if (!rows || rows.length === 0) {
|
|
@@ -16,7 +16,7 @@ import { MJUserEntity } from '@memberjunction/core-entities';
|
|
|
16
16
|
export class ExampleNewUserSubClass extends NewUserBase {
|
|
17
17
|
public override async createNewUser(firstName: string, lastName: string, email: string, linkedRecordType: string = 'None', linkedEntityId?: string, linkedEntityRecordId?: string): Promise<MJUserEntity | null> {
|
|
18
18
|
try {
|
|
19
|
-
const md = new Metadata();
|
|
19
|
+
const md = new Metadata(); // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
|
|
20
20
|
|
|
21
21
|
const contextUser = UserCache.Instance.Users.find(
|
|
22
22
|
(u) => u.Email.trim().toLowerCase() === configInfo?.userHandling?.contextUserForNewUserCreation?.trim().toLowerCase()
|
package/src/auth/index.ts
CHANGED
|
@@ -233,7 +233,7 @@ export const verifyUserRecord = async (
|
|
|
233
233
|
if (newUser) {
|
|
234
234
|
// new user worked! we already have the stuff we need for the cache, so no need to go to the DB now, just create a new UserInfo object and use the return value from the createNewUser method
|
|
235
235
|
// to init it, including passing in the role list for the user.
|
|
236
|
-
const md: Metadata = new Metadata();
|
|
236
|
+
const md: Metadata = new Metadata(); // global-provider-ok: JWT validation + role lookup runs BEFORE AppContext.providers is built — no per-request provider yet
|
|
237
237
|
|
|
238
238
|
const initData: MJUserEntityType & { UserRoles: { UserID: string; RoleName: string; RoleID: string }[] } = newUser.GetAll();
|
|
239
239
|
|
|
@@ -244,7 +244,7 @@ export const verifyUserRecord = async (
|
|
|
244
244
|
return { UserID: initData.ID, RoleName: role, RoleID: roleID };
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
user = new UserInfo(Metadata.Provider, initData);
|
|
247
|
+
user = new UserInfo(Metadata.Provider, initData); // global-provider-ok: same JWT-validation context — no per-request provider yet
|
|
248
248
|
UserCache.Instance.Users.push(user);
|
|
249
249
|
console.warn(` >>> New user ${email} created successfully!`);
|
|
250
250
|
}
|
package/src/auth/newUsers.ts
CHANGED
|
@@ -24,7 +24,7 @@ export class NewUserBase {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const md: Metadata = new Metadata();
|
|
27
|
+
const md: Metadata = new Metadata(); // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
|
|
28
28
|
const user = await md.GetEntityObject<MJUserEntity>('MJ: Users', contextUser) // To-Do - change this to be a different defined user for the user creation process
|
|
29
29
|
user.NewRecord();
|
|
30
30
|
user.Name = email;
|
|
@@ -46,7 +46,7 @@ export class NewUserBase {
|
|
|
46
46
|
// Create the user and all of its role/application/app-entity records atomically.
|
|
47
47
|
// If any Save fails partway through, the whole provisioning rolls back so we never
|
|
48
48
|
// leave a half-created user with partial roles/applications behind.
|
|
49
|
-
const provider = Metadata.Provider as DatabaseProviderBase;
|
|
49
|
+
const provider = Metadata.Provider as DatabaseProviderBase; // global-provider-ok: new-user creation runs in the JWT auth flow, before AppContext.providers is built
|
|
50
50
|
await provider.BeginTransaction();
|
|
51
51
|
try {
|
|
52
52
|
if (!await user.Save()) {
|
package/src/context.ts
CHANGED
|
@@ -333,8 +333,8 @@ export const contextFunction =
|
|
|
333
333
|
throw new AuthenticationError('No user payload — auth middleware may not have run');
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
if (Metadata.Provider.Entities.length === 0 ) {
|
|
337
|
-
console.warn('WARNING: No entities found in global/shared metadata, this can often be due to the use of **global** Metadata/RunView/DB Providers in a multi-user environment. Check your code to make sure you are using the providers passed to you in AppContext by MJServer and not calling new Metadata() new RunView() new RunQuery() and similar patterns as those are unstable at times in multi-user server environments!!!');
|
|
336
|
+
if (Metadata.Provider.Entities.length === 0 ) { // global-provider-ok: diagnostic warning about global provider state
|
|
337
|
+
console.warn('WARNING: No entities found in global/shared metadata, this can often be due to the use of **global** Metadata/RunView/DB Providers in a multi-user environment. Check your code to make sure you are using the providers passed to you in AppContext by MJServer and not calling new Metadata() new RunView() new RunQuery() and similar patterns as those are unstable at times in multi-user server environments!!!'); // global-provider-ok: diagnostic warning text mentions the anti-pattern by name
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
// Create per-request provider instance based on database type
|
|
@@ -406,7 +406,7 @@ async function createPostgresProvider(): Promise<DatabaseProviderBase> {
|
|
|
406
406
|
);
|
|
407
407
|
|
|
408
408
|
// Share the connection pool from the primary provider to avoid pool exhaustion
|
|
409
|
-
const primaryProvider = Metadata.Provider as unknown as { DatabaseConnection?: import('pg').Pool };
|
|
409
|
+
const primaryProvider = Metadata.Provider as unknown as { DatabaseConnection?: import('pg').Pool }; // global-provider-ok: bootstrap (per-connection PG pool sharing)
|
|
410
410
|
if (primaryProvider?.DatabaseConnection) {
|
|
411
411
|
await pgProvider.ConfigWithSharedPool(pgConfig, primaryProvider.DatabaseConnection);
|
|
412
412
|
} else {
|