@memberjunction/server 5.4.0 → 5.5.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.js +2 -2
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/auth/AuthProviderFactory.d.ts +9 -0
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
- package/dist/auth/AuthProviderFactory.js +27 -1
- package/dist/auth/AuthProviderFactory.js.map +1 -1
- package/dist/auth/index.d.ts +6 -4
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +10 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +52 -15
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +944 -843
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +5269 -6194
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +9 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +3 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +25 -6
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -22
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.js +4 -4
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +5 -4
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +3 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +3 -3
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +4 -3
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +4 -3
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +4 -3
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/package.json +58 -53
- package/src/__tests__/databaseAbstraction.test.ts +816 -0
- package/src/agents/skip-sdk.ts +2 -2
- package/src/auth/AuthProviderFactory.ts +31 -1
- package/src/auth/__tests__/backward-compatibility.test.ts +114 -0
- package/src/auth/index.ts +14 -9
- package/src/config.ts +9 -0
- package/src/context.ts +65 -20
- package/src/generated/generated.ts +5056 -6164
- package/src/generic/ResolverBase.ts +9 -0
- package/src/generic/RunViewResolver.ts +24 -7
- package/src/index.ts +207 -23
- package/src/resolvers/ActionResolver.ts +4 -4
- package/src/resolvers/ComponentRegistryResolver.ts +8 -7
- package/src/resolvers/FileResolver.ts +3 -2
- package/src/resolvers/RunAIAgentResolver.ts +3 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +8 -7
- package/src/resolvers/SyncRolesUsersResolver.ts +4 -3
- package/src/resolvers/UserFavoriteResolver.ts +4 -3
- package/src/rest/OAuthCallbackHandler.ts +4 -3
package/src/agents/skip-sdk.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { gzip as gzipCompress, createGunzip } from 'zlib';
|
|
|
34
34
|
import { configInfo, baseUrl, publicUrl, graphqlPort, graphqlRootPath, apiKey as callbackAPIKey } from '../config.js';
|
|
35
35
|
import { GetAIAPIKey } from '@memberjunction/ai';
|
|
36
36
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
37
|
-
import { CopyScalarsAndArrays } from '@memberjunction/global';
|
|
37
|
+
import { CopyScalarsAndArrays, UUIDsEqual } from '@memberjunction/global';
|
|
38
38
|
import mssql from 'mssql';
|
|
39
39
|
import { registerAccessToken, GetDataAccessToken } from '../resolvers/GetDataResolver.js';
|
|
40
40
|
import { BehaviorSubject } from 'rxjs';
|
|
@@ -493,7 +493,7 @@ export class SkipSDK {
|
|
|
493
493
|
* Recursively build category path for a query
|
|
494
494
|
*/
|
|
495
495
|
private buildQueryCategoryPath(md: Metadata, categoryID: string): string {
|
|
496
|
-
const cat = md.QueryCategories.find((c) => c.ID
|
|
496
|
+
const cat = md.QueryCategories.find((c) => UUIDsEqual(c.ID, categoryID));
|
|
497
497
|
if (!cat) return '';
|
|
498
498
|
if (!cat.ParentID) return cat.Name;
|
|
499
499
|
const parentPath = this.buildQueryCategoryPath(md, cat.ParentID);
|
|
@@ -18,6 +18,7 @@ export class AuthProviderFactory {
|
|
|
18
18
|
private static instance: AuthProviderFactory;
|
|
19
19
|
private providers: Map<string, IAuthProvider> = new Map();
|
|
20
20
|
private issuerCache: Map<string, IAuthProvider> = new Map();
|
|
21
|
+
private issuerMultiCache: Map<string, IAuthProvider[]> = new Map();
|
|
21
22
|
|
|
22
23
|
private constructor() {}
|
|
23
24
|
|
|
@@ -67,8 +68,9 @@ export class AuthProviderFactory {
|
|
|
67
68
|
|
|
68
69
|
this.providers.set(provider.name, provider);
|
|
69
70
|
|
|
70
|
-
// Clear issuer
|
|
71
|
+
// Clear issuer caches when registering new provider
|
|
71
72
|
this.issuerCache.clear();
|
|
73
|
+
this.issuerMultiCache.clear();
|
|
72
74
|
|
|
73
75
|
console.log(`Registered auth provider: ${provider.name} with issuer: ${provider.issuer}`);
|
|
74
76
|
}
|
|
@@ -94,6 +96,33 @@ export class AuthProviderFactory {
|
|
|
94
96
|
return undefined;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Gets all providers matching an issuer URL.
|
|
101
|
+
* Unlike getByIssuer() which returns only the first match, this returns
|
|
102
|
+
* all providers for a given issuer. This is needed when multiple apps
|
|
103
|
+
* (e.g. MJExplorer + MJCentral) share the same Auth0 domain but have
|
|
104
|
+
* different audiences (client IDs).
|
|
105
|
+
*/
|
|
106
|
+
getAllByIssuer(issuer: string): IAuthProvider[] {
|
|
107
|
+
// Check multi-provider cache first
|
|
108
|
+
if (this.issuerMultiCache.has(issuer)) {
|
|
109
|
+
return this.issuerMultiCache.get(issuer)!;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const matches: IAuthProvider[] = [];
|
|
113
|
+
for (const provider of this.providers.values()) {
|
|
114
|
+
if (provider.matchesIssuer(issuer)) {
|
|
115
|
+
matches.push(provider);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (matches.length > 0) {
|
|
120
|
+
this.issuerMultiCache.set(issuer, matches);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return matches;
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
/**
|
|
98
127
|
* Gets a provider by its name
|
|
99
128
|
*/
|
|
@@ -121,6 +150,7 @@ export class AuthProviderFactory {
|
|
|
121
150
|
clear(): void {
|
|
122
151
|
this.providers.clear();
|
|
123
152
|
this.issuerCache.clear();
|
|
153
|
+
this.issuerMultiCache.clear();
|
|
124
154
|
}
|
|
125
155
|
|
|
126
156
|
/**
|
|
@@ -147,6 +147,120 @@ describe('Authentication Provider Backward Compatibility', () => {
|
|
|
147
147
|
});
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
describe('Multi-Audience Same-Issuer Support', () => {
|
|
151
|
+
it('should return all providers matching the same issuer via getAllByIssuer()', () => {
|
|
152
|
+
const sharedIssuer = 'https://auth.example.com/';
|
|
153
|
+
const provider1 = {
|
|
154
|
+
name: 'app1',
|
|
155
|
+
issuer: sharedIssuer,
|
|
156
|
+
audience: 'client-id-app1',
|
|
157
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
158
|
+
validateConfig: () => true,
|
|
159
|
+
getSigningKey: vi.fn(),
|
|
160
|
+
extractUserInfo: vi.fn(),
|
|
161
|
+
matchesIssuer: (iss: string) => iss === sharedIssuer,
|
|
162
|
+
} as IAuthProvider;
|
|
163
|
+
const provider2 = {
|
|
164
|
+
name: 'app2',
|
|
165
|
+
issuer: sharedIssuer,
|
|
166
|
+
audience: 'client-id-app2',
|
|
167
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
168
|
+
validateConfig: () => true,
|
|
169
|
+
getSigningKey: vi.fn(),
|
|
170
|
+
extractUserInfo: vi.fn(),
|
|
171
|
+
matchesIssuer: (iss: string) => iss === sharedIssuer,
|
|
172
|
+
} as IAuthProvider;
|
|
173
|
+
|
|
174
|
+
factory.register(provider1);
|
|
175
|
+
factory.register(provider2);
|
|
176
|
+
|
|
177
|
+
const results = factory.getAllByIssuer(sharedIssuer);
|
|
178
|
+
expect(results).toHaveLength(2);
|
|
179
|
+
expect(results).toContain(provider1);
|
|
180
|
+
expect(results).toContain(provider2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return empty array from getAllByIssuer() for unknown issuer', () => {
|
|
184
|
+
const results = factory.getAllByIssuer('https://unknown.example.com');
|
|
185
|
+
expect(results).toEqual([]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should cache getAllByIssuer() results and invalidate on registration', () => {
|
|
189
|
+
const sharedIssuer = 'https://auth.example.com/';
|
|
190
|
+
const provider1 = {
|
|
191
|
+
name: 'app1',
|
|
192
|
+
issuer: sharedIssuer,
|
|
193
|
+
audience: 'client-id-app1',
|
|
194
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
195
|
+
validateConfig: () => true,
|
|
196
|
+
getSigningKey: vi.fn(),
|
|
197
|
+
extractUserInfo: vi.fn(),
|
|
198
|
+
matchesIssuer: vi.fn((iss: string): boolean => iss === sharedIssuer),
|
|
199
|
+
} as IAuthProvider;
|
|
200
|
+
|
|
201
|
+
factory.register(provider1);
|
|
202
|
+
|
|
203
|
+
// First call populates cache
|
|
204
|
+
const first = factory.getAllByIssuer(sharedIssuer);
|
|
205
|
+
expect(first).toHaveLength(1);
|
|
206
|
+
expect(provider1.matchesIssuer).toHaveBeenCalledTimes(1);
|
|
207
|
+
|
|
208
|
+
// Second call uses cache
|
|
209
|
+
const second = factory.getAllByIssuer(sharedIssuer);
|
|
210
|
+
expect(second).toHaveLength(1);
|
|
211
|
+
expect(provider1.matchesIssuer).toHaveBeenCalledTimes(1);
|
|
212
|
+
|
|
213
|
+
// Registering a new provider invalidates the cache
|
|
214
|
+
const provider2 = {
|
|
215
|
+
name: 'app2',
|
|
216
|
+
issuer: sharedIssuer,
|
|
217
|
+
audience: 'client-id-app2',
|
|
218
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
219
|
+
validateConfig: () => true,
|
|
220
|
+
getSigningKey: vi.fn(),
|
|
221
|
+
extractUserInfo: vi.fn(),
|
|
222
|
+
matchesIssuer: vi.fn((iss: string): boolean => iss === sharedIssuer),
|
|
223
|
+
} as IAuthProvider;
|
|
224
|
+
factory.register(provider2);
|
|
225
|
+
|
|
226
|
+
// After registration, cache is cleared, so matchesIssuer is called again
|
|
227
|
+
const third = factory.getAllByIssuer(sharedIssuer);
|
|
228
|
+
expect(third).toHaveLength(2);
|
|
229
|
+
// provider1.matchesIssuer called once more (from the new lookup)
|
|
230
|
+
expect(provider1.matchesIssuer).toHaveBeenCalledTimes(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should not include providers with different issuers', () => {
|
|
234
|
+
const provider1 = {
|
|
235
|
+
name: 'app1',
|
|
236
|
+
issuer: 'https://issuer-a.com/',
|
|
237
|
+
audience: 'client-a',
|
|
238
|
+
jwksUri: 'https://issuer-a.com/.well-known/jwks.json',
|
|
239
|
+
validateConfig: () => true,
|
|
240
|
+
getSigningKey: vi.fn(),
|
|
241
|
+
extractUserInfo: vi.fn(),
|
|
242
|
+
matchesIssuer: (iss: string) => iss === 'https://issuer-a.com/',
|
|
243
|
+
} as IAuthProvider;
|
|
244
|
+
const provider2 = {
|
|
245
|
+
name: 'app2',
|
|
246
|
+
issuer: 'https://issuer-b.com/',
|
|
247
|
+
audience: 'client-b',
|
|
248
|
+
jwksUri: 'https://issuer-b.com/.well-known/jwks.json',
|
|
249
|
+
validateConfig: () => true,
|
|
250
|
+
getSigningKey: vi.fn(),
|
|
251
|
+
extractUserInfo: vi.fn(),
|
|
252
|
+
matchesIssuer: (iss: string) => iss === 'https://issuer-b.com/',
|
|
253
|
+
} as IAuthProvider;
|
|
254
|
+
|
|
255
|
+
factory.register(provider1);
|
|
256
|
+
factory.register(provider2);
|
|
257
|
+
|
|
258
|
+
const results = factory.getAllByIssuer('https://issuer-a.com/');
|
|
259
|
+
expect(results).toHaveLength(1);
|
|
260
|
+
expect(results[0]).toBe(provider1);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
150
264
|
describe('Error Handling', () => {
|
|
151
265
|
it('should handle missing provider gracefully', () => {
|
|
152
266
|
const unknownIssuer = 'https://unknown.provider.com';
|
package/src/auth/index.ts
CHANGED
|
@@ -47,20 +47,25 @@ const refreshUserCache = async (dataSource?: sql.ConnectionPool) => {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Gets validation options for a specific issuer
|
|
51
|
-
*
|
|
50
|
+
* Gets validation options for a specific issuer.
|
|
51
|
+
* When multiple providers share the same issuer (e.g. two Auth0 apps on
|
|
52
|
+
* the same domain with different audiences/client IDs), all unique audiences
|
|
53
|
+
* are aggregated into an array. jwt.verify() natively accepts string | string[].
|
|
52
54
|
*/
|
|
53
|
-
export const getValidationOptions = (issuer: string): { audience: string; jwksUri: string } | undefined => {
|
|
55
|
+
export const getValidationOptions = (issuer: string): { audience: string | string[]; jwksUri: string } | undefined => {
|
|
54
56
|
const factory = AuthProviderFactory.getInstance();
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
if (
|
|
57
|
+
const providers = factory.getAllByIssuer(issuer);
|
|
58
|
+
|
|
59
|
+
if (providers.length === 0) {
|
|
58
60
|
return undefined;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
// Collect unique audiences from all providers matching this issuer
|
|
64
|
+
const audiences = [...new Set(providers.map(p => p.audience))];
|
|
65
|
+
|
|
61
66
|
return {
|
|
62
|
-
audience:
|
|
63
|
-
jwksUri:
|
|
67
|
+
audience: audiences.length === 1 ? audiences[0] : audiences,
|
|
68
|
+
jwksUri: providers[0].jwksUri // Same issuer = same JWKS endpoint
|
|
64
69
|
};
|
|
65
70
|
};
|
|
66
71
|
|
|
@@ -68,7 +73,7 @@ export const getValidationOptions = (issuer: string): { audience: string; jwksUr
|
|
|
68
73
|
* Backward compatible validationOptions object
|
|
69
74
|
* @deprecated Use getValidationOptions() or AuthProviderRegistry instead
|
|
70
75
|
*/
|
|
71
|
-
export const validationOptions: Record<string, { audience: string; jwksUri: string }> = new Proxy({}, {
|
|
76
|
+
export const validationOptions: Record<string, { audience: string | string[]; jwksUri: string }> = new Proxy({}, {
|
|
72
77
|
get: (target, prop: string) => {
|
|
73
78
|
return getValidationOptions(prop);
|
|
74
79
|
},
|
package/src/config.ts
CHANGED
|
@@ -140,6 +140,13 @@ const scheduledJobsSchema = z.object({
|
|
|
140
140
|
staleLockCleanupInterval: z.number().optional().default(300000), // 5 minutes in ms
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
const queryDialectSchema = z.object({
|
|
144
|
+
/** When true, saving a Query entity auto-generates QuerySQL entries for configured target dialects */
|
|
145
|
+
autoConvertOnSave: zodBooleanWithTransforms().default(false),
|
|
146
|
+
/** List of SQLDialect PlatformKey values to auto-convert to (e.g., ['postgresql']) */
|
|
147
|
+
targetPlatforms: z.array(z.string()).optional().default([]),
|
|
148
|
+
});
|
|
149
|
+
|
|
143
150
|
const telemetrySchema = z.object({
|
|
144
151
|
enabled: zodBooleanWithTransforms().default(
|
|
145
152
|
process.env.MJ_TELEMETRY_ENABLED !== 'false' // Enabled by default unless explicitly disabled
|
|
@@ -158,6 +165,7 @@ const configInfoSchema = z.object({
|
|
|
158
165
|
componentRegistries: z.array(componentRegistrySchema).optional(),
|
|
159
166
|
scheduledJobs: scheduledJobsSchema.optional().default({}),
|
|
160
167
|
telemetry: telemetrySchema.optional().default({}),
|
|
168
|
+
queryDialects: queryDialectSchema.optional().default({}),
|
|
161
169
|
|
|
162
170
|
apiKey: z.string().optional(),
|
|
163
171
|
baseUrl: z.string().default('http://localhost'),
|
|
@@ -201,6 +209,7 @@ export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
|
|
|
201
209
|
export type ComponentRegistryConfig = z.infer<typeof componentRegistrySchema>;
|
|
202
210
|
export type ScheduledJobsConfig = z.infer<typeof scheduledJobsSchema>;
|
|
203
211
|
export type TelemetryConfig = z.infer<typeof telemetrySchema>;
|
|
212
|
+
export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
204
213
|
export type ConfigInfo = z.infer<typeof configInfoSchema>;
|
|
205
214
|
|
|
206
215
|
/**
|
package/src/context.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { DatabaseProviderBase } from '@memberjunction/core';
|
|
|
16
16
|
import { SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
17
17
|
import { AuthProviderFactory } from './auth/AuthProviderFactory.js';
|
|
18
18
|
import { Metadata } from '@memberjunction/core';
|
|
19
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
19
20
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
20
21
|
|
|
21
22
|
const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
|
|
@@ -27,7 +28,14 @@ const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayloa
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
const verifyOptions: jwt.VerifyOptions = {};
|
|
32
|
+
if (Array.isArray(options.audience)) {
|
|
33
|
+
verifyOptions.audience = options.audience as [string, ...string[]];
|
|
34
|
+
} else {
|
|
35
|
+
verifyOptions.audience = options.audience;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
jwt.verify(token, getSigningKeys(issuer), verifyOptions, (err, jwt) => {
|
|
31
39
|
if (jwt && typeof jwt !== 'string' && !err) {
|
|
32
40
|
const payload = jwt.payload ?? jwt;
|
|
33
41
|
|
|
@@ -96,7 +104,7 @@ export const getUserPayload = async (
|
|
|
96
104
|
// Get the user from UserCache to ensure UserRoles is properly populated
|
|
97
105
|
// The validationResult.User from APIKeyEngine doesn't include UserRoles
|
|
98
106
|
const cachedUser = UserCache.Instance.Users.find(
|
|
99
|
-
u => u.ID
|
|
107
|
+
u => UUIDsEqual(u.ID, validationResult.User.ID)
|
|
100
108
|
);
|
|
101
109
|
|
|
102
110
|
// Use cached user if available, otherwise fall back to the validation result
|
|
@@ -247,22 +255,59 @@ export const contextFunction =
|
|
|
247
255
|
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!!!');
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
//
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
258
|
+
// Create per-request provider instance based on database type
|
|
259
|
+
const dbType = process.env.DB_TYPE?.toLowerCase();
|
|
260
|
+
const isPostgres = dbType === 'postgresql' || dbType === 'postgres' || dbType === 'pg';
|
|
261
|
+
|
|
262
|
+
let p: DatabaseProviderBase;
|
|
263
|
+
if (isPostgres) {
|
|
264
|
+
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
265
|
+
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
266
|
+
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
267
|
+
const pgUser = process.env.PG_USERNAME || process.env.DB_USERNAME || 'postgres';
|
|
268
|
+
const pgPass = process.env.PG_PASSWORD || process.env.DB_PASSWORD || '';
|
|
269
|
+
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
270
|
+
|
|
271
|
+
const pgProvider = new PostgreSQLDataProvider();
|
|
272
|
+
const pgConfig = new PostgreSQLProviderConfigData(
|
|
273
|
+
{ Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgUser, Password: pgPass },
|
|
274
|
+
mj_core_schema,
|
|
275
|
+
0,
|
|
276
|
+
undefined,
|
|
277
|
+
undefined,
|
|
278
|
+
false, // use existing metadata from global provider
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Share the connection pool from the primary provider to avoid pool exhaustion.
|
|
282
|
+
// Each per-request provider was creating its own pool, leading to "too many clients" errors.
|
|
283
|
+
const primaryProvider = Metadata.Provider as unknown as { DatabaseConnection?: import('pg').Pool };
|
|
284
|
+
if (primaryProvider?.DatabaseConnection) {
|
|
285
|
+
await pgProvider.ConfigWithSharedPool(pgConfig, primaryProvider.DatabaseConnection);
|
|
286
|
+
} else {
|
|
287
|
+
await pgProvider.Config(pgConfig);
|
|
262
288
|
}
|
|
289
|
+
p = pgProvider;
|
|
290
|
+
} else {
|
|
291
|
+
const config = new SQLServerProviderConfigData(dataSource, mj_core_schema, 0, undefined, undefined, false);
|
|
292
|
+
const sqlProvider = new SQLServerDataProvider();
|
|
293
|
+
await sqlProvider.Config(config);
|
|
294
|
+
p = sqlProvider as unknown as DatabaseProviderBase;
|
|
263
295
|
}
|
|
264
|
-
|
|
265
|
-
|
|
296
|
+
|
|
297
|
+
let rp: DatabaseProviderBase | null = null;
|
|
298
|
+
if (!isPostgres) {
|
|
299
|
+
try {
|
|
300
|
+
const readOnlyDataSource = GetReadOnlyDataSource(dataSources, { allowFallbackToReadWrite: false });
|
|
301
|
+
if (readOnlyDataSource) {
|
|
302
|
+
const roProvider = new SQLServerDataProvider();
|
|
303
|
+
const rConfig = new SQLServerProviderConfigData(readOnlyDataSource, mj_core_schema, 0, undefined, undefined, false);
|
|
304
|
+
await roProvider.Config(rConfig);
|
|
305
|
+
rp = roProvider as unknown as DatabaseProviderBase;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (_err) {
|
|
309
|
+
// no read only data source available, so rp will remain null, this is OK!
|
|
310
|
+
}
|
|
266
311
|
}
|
|
267
312
|
|
|
268
313
|
const providers = [{
|
|
@@ -276,12 +321,12 @@ export const contextFunction =
|
|
|
276
321
|
});
|
|
277
322
|
}
|
|
278
323
|
|
|
279
|
-
const contextResult = {
|
|
280
|
-
dataSource,
|
|
281
|
-
dataSources,
|
|
324
|
+
const contextResult = {
|
|
325
|
+
dataSource,
|
|
326
|
+
dataSources,
|
|
282
327
|
userPayload: userPayload,
|
|
283
328
|
providers,
|
|
284
329
|
};
|
|
285
|
-
|
|
330
|
+
|
|
286
331
|
return contextResult;
|
|
287
332
|
};
|