@memberjunction/server 5.4.1 → 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
|
@@ -405,9 +405,18 @@ export class ResolverBase {
|
|
|
405
405
|
|
|
406
406
|
if (viewInput.ViewName) {
|
|
407
407
|
viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'MJ: User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
|
|
408
|
+
// Populate EntityName on the input so callers (e.g. RunViews resolver) can
|
|
409
|
+
// look up the entity without re-querying the view
|
|
410
|
+
if (viewInfo && !viewInput.EntityName) {
|
|
411
|
+
viewInput.EntityName = viewInfo.Entity;
|
|
412
|
+
}
|
|
408
413
|
} else if (viewInput.ViewID) {
|
|
409
414
|
viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
|
|
410
415
|
await viewInfo.Load(viewInput.ViewID);
|
|
416
|
+
// Populate EntityName on the input so callers can look up the entity
|
|
417
|
+
if (viewInfo && !viewInput.EntityName) {
|
|
418
|
+
viewInput.EntityName = viewInfo.Entity;
|
|
419
|
+
}
|
|
411
420
|
} else if (viewInput.EntityName) {
|
|
412
421
|
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
413
422
|
if (!entity) {
|
|
@@ -2,6 +2,7 @@ import { Arg, Ctx, Field, InputType, Int, ObjectType, PubSubEngine, Query, Resol
|
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { ResolverBase } from './ResolverBase.js';
|
|
4
4
|
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult } from '@memberjunction/core';
|
|
5
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
5
6
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
6
7
|
import { GetReadOnlyProvider } from '../util.js';
|
|
7
8
|
import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
@@ -355,8 +356,23 @@ export class RunDynamicViewInput {
|
|
|
355
356
|
|
|
356
357
|
@InputType()
|
|
357
358
|
export class RunViewGenericInput {
|
|
358
|
-
@Field(() => String
|
|
359
|
-
|
|
359
|
+
@Field(() => String, {
|
|
360
|
+
nullable: true,
|
|
361
|
+
description: 'The ID of a saved User View to run. Provide either ViewID, ViewName, or EntityName.',
|
|
362
|
+
})
|
|
363
|
+
ViewID?: string;
|
|
364
|
+
|
|
365
|
+
@Field(() => String, {
|
|
366
|
+
nullable: true,
|
|
367
|
+
description: 'The name of a saved User View to run. Provide either ViewID, ViewName, or EntityName.',
|
|
368
|
+
})
|
|
369
|
+
ViewName?: string;
|
|
370
|
+
|
|
371
|
+
@Field(() => String, {
|
|
372
|
+
nullable: true,
|
|
373
|
+
description: 'The entity name for a dynamic view. Provide either ViewID, ViewName, or EntityName.',
|
|
374
|
+
})
|
|
375
|
+
EntityName?: string;
|
|
360
376
|
|
|
361
377
|
@Field(() => String, {
|
|
362
378
|
nullable: true,
|
|
@@ -653,7 +669,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
653
669
|
return null;
|
|
654
670
|
|
|
655
671
|
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
|
|
656
|
-
const entity = provider.Entities.find((e) => e.ID
|
|
672
|
+
const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
|
|
657
673
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
658
674
|
return {
|
|
659
675
|
Results: returnData,
|
|
@@ -684,7 +700,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
684
700
|
return null;
|
|
685
701
|
|
|
686
702
|
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
|
|
687
|
-
const entity = provider.Entities.find((e) => e.ID
|
|
703
|
+
const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
|
|
688
704
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
689
705
|
return {
|
|
690
706
|
Results: returnData,
|
|
@@ -747,8 +763,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
747
763
|
|
|
748
764
|
let results: RunViewGenericResult[] = [];
|
|
749
765
|
for (const [index, data] of rawData.entries()) {
|
|
750
|
-
|
|
751
|
-
const
|
|
766
|
+
// EntityName is backfilled by RunViewsGeneric when ViewID/ViewName was used
|
|
767
|
+
const entity = input[index].EntityName ? provider.Entities.find((e) => e.Name === input[index].EntityName) : null;
|
|
768
|
+
const returnData: any[] = this.processRawData(data.Results, entity ? entity.ID : null, entity);
|
|
752
769
|
|
|
753
770
|
results.push({
|
|
754
771
|
Results: returnData,
|
|
@@ -838,7 +855,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
838
855
|
}
|
|
839
856
|
|
|
840
857
|
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
|
|
841
|
-
const entity = provider.Entities.find((e) => e.ID
|
|
858
|
+
const entity = provider.Entities.find((e) => UUIDsEqual(e.ID, viewInfo.EntityID));
|
|
842
859
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
843
860
|
return {
|
|
844
861
|
Results: returnData,
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,9 @@ dotenv.config({ quiet: true });
|
|
|
4
4
|
|
|
5
5
|
import { expressMiddleware } from '@apollo/server/express4';
|
|
6
6
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
7
|
-
import { Metadata } from '@memberjunction/core';
|
|
8
|
-
import {
|
|
7
|
+
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport } from '@memberjunction/core';
|
|
8
|
+
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
9
|
+
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
9
10
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
10
11
|
import { default as BodyParser } from 'body-parser';
|
|
11
12
|
import compression from 'compression'; // Add compression middleware
|
|
@@ -39,6 +40,18 @@ import { getSystemUser } from './auth/index.js';
|
|
|
39
40
|
|
|
40
41
|
const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Returns the configured database platform type based on the DB_TYPE environment variable.
|
|
45
|
+
* Defaults to 'sqlserver' for backward compatibility.
|
|
46
|
+
*/
|
|
47
|
+
export function getDbType(): DatabasePlatform {
|
|
48
|
+
const dbType = process.env.DB_TYPE?.toLowerCase();
|
|
49
|
+
if (dbType === 'postgresql' || dbType === 'postgres' || dbType === 'pg') {
|
|
50
|
+
return 'postgresql';
|
|
51
|
+
}
|
|
52
|
+
return 'sqlserver';
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
export { MaxLength } from 'class-validator';
|
|
43
56
|
export * from 'type-graphql';
|
|
44
57
|
export { NewUserBase } from './auth/newUsers.js';
|
|
@@ -134,32 +147,145 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
134
147
|
console.log({ combinedResolverPaths, paths, cwd: process.cwd() });
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
const pool = new sql.ConnectionPool(createMSSQLConfig());
|
|
138
150
|
const setupComplete$ = new ReplaySubject(1);
|
|
139
|
-
|
|
151
|
+
const dbType = getDbType();
|
|
152
|
+
const dataSources: DataSourceInfo[] = [];
|
|
153
|
+
|
|
154
|
+
if (dbType === 'postgresql') {
|
|
155
|
+
// ─── PostgreSQL Path ───────────────────────────────────────────
|
|
156
|
+
console.log('Database type: PostgreSQL');
|
|
157
|
+
const pg = await import('pg');
|
|
158
|
+
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
159
|
+
|
|
160
|
+
const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
|
|
161
|
+
const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
|
|
162
|
+
const pgUser = process.env.PG_USERNAME || process.env.DB_USERNAME || 'postgres';
|
|
163
|
+
const pgPass = process.env.PG_PASSWORD || process.env.DB_PASSWORD || '';
|
|
164
|
+
const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
|
|
165
|
+
|
|
166
|
+
const pgPool = new pg.default.Pool({
|
|
167
|
+
host: pgHost,
|
|
168
|
+
port: pgPort,
|
|
169
|
+
user: pgUser,
|
|
170
|
+
password: pgPass,
|
|
171
|
+
database: pgDatabase,
|
|
172
|
+
max: configInfo.databaseSettings.connectionPool?.max ?? 50,
|
|
173
|
+
min: configInfo.databaseSettings.connectionPool?.min ?? 5,
|
|
174
|
+
idleTimeoutMillis: configInfo.databaseSettings.connectionPool?.idleTimeoutMillis ?? 30000,
|
|
175
|
+
connectionTimeoutMillis: configInfo.databaseSettings.connectionPool?.acquireTimeoutMillis ?? 30000,
|
|
176
|
+
});
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
// Verify connection
|
|
179
|
+
const testClient = await pgPool.connect();
|
|
180
|
+
await testClient.query('SELECT 1');
|
|
181
|
+
testClient.release();
|
|
182
|
+
console.log(`PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
|
|
183
|
+
|
|
184
|
+
// Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
|
|
185
|
+
// This allows existing code (types, util, context) to work without changes
|
|
186
|
+
const mssqlCompatPool = createMSSQLCompatPool(pgPool);
|
|
187
|
+
dataSources.push(new DataSourceInfo({
|
|
188
|
+
dataSource: mssqlCompatPool,
|
|
189
|
+
type: 'Read-Write',
|
|
190
|
+
host: pgHost,
|
|
191
|
+
port: pgPort,
|
|
192
|
+
database: pgDatabase,
|
|
193
|
+
userName: pgUser,
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
// Set up the PostgreSQL provider
|
|
197
|
+
const pgConnectionConfig = {
|
|
198
|
+
Host: pgHost,
|
|
199
|
+
Port: pgPort,
|
|
200
|
+
Database: pgDatabase,
|
|
201
|
+
User: pgUser,
|
|
202
|
+
Password: pgPass,
|
|
203
|
+
MaxConnections: configInfo.databaseSettings.connectionPool?.max ?? 50,
|
|
204
|
+
MinConnections: configInfo.databaseSettings.connectionPool?.min ?? 5,
|
|
205
|
+
};
|
|
206
|
+
const pgConfigData = new PostgreSQLProviderConfigData(
|
|
207
|
+
pgConnectionConfig,
|
|
208
|
+
mj_core_schema,
|
|
209
|
+
cacheRefreshInterval / 1000, // convert ms to seconds
|
|
210
|
+
);
|
|
211
|
+
const provider = new PostgreSQLDataProvider();
|
|
212
|
+
await provider.Config(pgConfigData);
|
|
213
|
+
SetProvider(provider);
|
|
214
|
+
|
|
215
|
+
// Refresh user cache using PostgreSQL
|
|
216
|
+
await refreshUserCacheFromPG(pgPool, mj_core_schema);
|
|
217
|
+
|
|
218
|
+
// Run startup actions
|
|
219
|
+
const sysUser = UserCache.Instance.GetSystemUser();
|
|
220
|
+
const backupSysUser = UserCache.Instance.Users.find(u => u.IsActive && u.Type === 'Owner');
|
|
221
|
+
await StartupManagerImport.Instance.Startup(false, sysUser || backupSysUser, provider);
|
|
222
|
+
|
|
223
|
+
// Monkey-patch SQLServerDataProvider.ExecuteSQLWithPool to support PostgreSQL
|
|
224
|
+
// Generated resolvers call this static method with bracket-quoted SQL.
|
|
225
|
+
// When the pool is our PG-compat wrapper, translate and execute via pg.Pool.
|
|
226
|
+
const origExecuteSQLWithPool = SQLServerDataProvider.ExecuteSQLWithPool;
|
|
227
|
+
SQLServerDataProvider.ExecuteSQLWithPool = async function(
|
|
228
|
+
pool: sql.ConnectionPool, query: string, parameters?: unknown[], contextUser?: import('@memberjunction/core').UserInfo
|
|
229
|
+
): Promise<unknown[]> {
|
|
230
|
+
const poolAny = pool as unknown as Record<string, unknown>;
|
|
231
|
+
if (poolAny._pgPool) {
|
|
232
|
+
const thePgPool = poolAny._pgPool as import('pg').Pool;
|
|
233
|
+
// Translate SQL Server bracket syntax to PostgreSQL double-quote syntax
|
|
234
|
+
const pgQuery = translateBracketsToPG(query);
|
|
235
|
+
const result = await thePgPool.query(pgQuery);
|
|
236
|
+
return result.rows;
|
|
237
|
+
}
|
|
238
|
+
return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
|
|
150
239
|
};
|
|
151
|
-
readOnlyPool = new sql.ConnectionPool(readOnlyConfig);
|
|
152
|
-
await readOnlyPool.connect();
|
|
153
240
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
241
|
+
const md = new Metadata();
|
|
242
|
+
console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
|
|
243
|
+
} else {
|
|
244
|
+
// ─── SQL Server Path (existing behavior) ───────────────────────
|
|
245
|
+
console.log('Database type: SQL Server');
|
|
246
|
+
const pool = new sql.ConnectionPool(createMSSQLConfig());
|
|
247
|
+
|
|
248
|
+
// Handle connection-level errors from dead/stale connections in the pool.
|
|
249
|
+
// Without this handler, when Azure drops idle TCP connections, the pool silently
|
|
250
|
+
// hands out dead connections that throw "Final state" errors on next use.
|
|
251
|
+
pool.on('error', (err) => {
|
|
252
|
+
console.error('[ConnectionPool] Pool-level connection error (stale connection evicted):', err.message);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await pool.connect();
|
|
256
|
+
|
|
257
|
+
dataSources.push(new DataSourceInfo({dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername}));
|
|
258
|
+
|
|
259
|
+
// Establish a second read-only connection to the database if dbReadOnlyUsername and dbReadOnlyPassword exist
|
|
260
|
+
if (configInfo.dbReadOnlyUsername && configInfo.dbReadOnlyPassword) {
|
|
261
|
+
const readOnlyConfig = {
|
|
262
|
+
...createMSSQLConfig(),
|
|
263
|
+
user: configInfo.dbReadOnlyUsername,
|
|
264
|
+
password: configInfo.dbReadOnlyPassword,
|
|
265
|
+
};
|
|
266
|
+
const readOnlyPool = new sql.ConnectionPool(readOnlyConfig);
|
|
267
|
+
|
|
268
|
+
readOnlyPool.on('error', (err) => {
|
|
269
|
+
console.error('[ConnectionPool] Read-only pool connection error (stale connection evicted):', err.message);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await readOnlyPool.connect();
|
|
273
|
+
|
|
274
|
+
dataSources.push(new DataSourceInfo({dataSource: readOnlyPool, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername}));
|
|
275
|
+
console.log('Read-only Connection Pool has been initialized.');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
|
|
279
|
+
await setupSQLServerClient(config);
|
|
280
|
+
const md = new Metadata();
|
|
281
|
+
console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
|
|
157
282
|
}
|
|
158
283
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
284
|
+
// Store queryDialects config in GlobalObjectStore so MJQueryEntityServer can
|
|
285
|
+
// read it without a circular dependency on MJServer
|
|
286
|
+
if (configInfo.queryDialects) {
|
|
287
|
+
MJGlobal.Instance.GetGlobalObjectStore()['queryDialects'] = configInfo.queryDialects;
|
|
288
|
+
}
|
|
163
289
|
|
|
164
290
|
// Initialize server telemetry based on config
|
|
165
291
|
const tm = TelemetryManager.Instance;
|
|
@@ -375,7 +501,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
375
501
|
expressMiddleware(apolloServer, {
|
|
376
502
|
context: contextFunction({
|
|
377
503
|
setupComplete$,
|
|
378
|
-
dataSource: extendConnectionPoolWithQuery(
|
|
504
|
+
dataSource: extendConnectionPoolWithQuery(dataSources[0].dataSource), // default read-write data source
|
|
379
505
|
dataSources // all data source
|
|
380
506
|
}),
|
|
381
507
|
}) as unknown as express.RequestHandler
|
|
@@ -440,3 +566,61 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
440
566
|
// This is critical for server stability when downstream dependencies fail
|
|
441
567
|
});
|
|
442
568
|
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Creates a MSSQL ConnectionPool-compatible wrapper around a pg.Pool.
|
|
572
|
+
* This allows existing code that references DataSourceInfo.dataSource (typed as sql.ConnectionPool)
|
|
573
|
+
* to work with PostgreSQL pools. Only the .query() and .connected properties are used.
|
|
574
|
+
*/
|
|
575
|
+
function createMSSQLCompatPool(pgPool: import('pg').Pool): sql.ConnectionPool {
|
|
576
|
+
const wrapper = {
|
|
577
|
+
connected: true,
|
|
578
|
+
query: async (sqlQuery: string): Promise<{ recordset: Record<string, unknown>[] }> => {
|
|
579
|
+
const result = await pgPool.query(sqlQuery);
|
|
580
|
+
return { recordset: result.rows };
|
|
581
|
+
},
|
|
582
|
+
request: (): { query: (sql: string) => Promise<{ recordset: Record<string, unknown>[] }> } => ({
|
|
583
|
+
query: async (sqlQuery: string) => {
|
|
584
|
+
const result = await pgPool.query(sqlQuery);
|
|
585
|
+
return { recordset: result.rows };
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
// pg.Pool reference for consumers that need it
|
|
589
|
+
_pgPool: pgPool,
|
|
590
|
+
};
|
|
591
|
+
return wrapper as unknown as sql.ConnectionPool;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Refreshes the UserCache using PostgreSQL queries instead of MSSQL.
|
|
596
|
+
* This mirrors the logic in UserCache.Refresh() but uses pg.Pool.
|
|
597
|
+
*/
|
|
598
|
+
async function refreshUserCacheFromPG(pgPool: import('pg').Pool, coreSchema: string): Promise<void> {
|
|
599
|
+
const { UserInfo } = await import('@memberjunction/core');
|
|
600
|
+
const uResult = await pgPool.query(`SELECT * FROM ${coreSchema}."vwUsers"`);
|
|
601
|
+
const rResult = await pgPool.query(`SELECT * FROM ${coreSchema}."vwUserRoles"`);
|
|
602
|
+
const users = uResult.rows;
|
|
603
|
+
const roles = rResult.rows;
|
|
604
|
+
|
|
605
|
+
if (users) {
|
|
606
|
+
const userInfos = users.map((user: Record<string, unknown>) => {
|
|
607
|
+
const userWithRoles = {
|
|
608
|
+
...user,
|
|
609
|
+
UserRoles: roles.filter((role: Record<string, unknown>) => UUIDsEqual(role.UserID as string, user.ID as string)),
|
|
610
|
+
};
|
|
611
|
+
return new UserInfo(Metadata.Provider, userWithRoles);
|
|
612
|
+
});
|
|
613
|
+
// Access the UserCache internals to set users
|
|
614
|
+
const cache = UserCache.Instance;
|
|
615
|
+
(cache as unknown as Record<string, unknown>)['_users'] = userInfos;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Translates SQL Server bracket-quoted identifiers to PostgreSQL double-quoted identifiers.
|
|
621
|
+
* Converts [schema].[table] to "schema"."table" and handles common T-SQL patterns.
|
|
622
|
+
*/
|
|
623
|
+
function translateBracketsToPG(sql: string): string {
|
|
624
|
+
// Replace [identifier] with "identifier"
|
|
625
|
+
return sql.replace(/\[([^\]]+)\]/g, '"$1"');
|
|
626
|
+
}
|
|
@@ -6,7 +6,7 @@ import { ActionParam, ActionResult } from "@memberjunction/actions-base";
|
|
|
6
6
|
import { Field, InputType, ObjectType } from "type-graphql";
|
|
7
7
|
import { KeyValuePairInput } from "../generic/KeyValuePairInput.js";
|
|
8
8
|
import { AppContext, ProviderInfo } from "../types.js";
|
|
9
|
-
import { CopyScalarsAndArrays } from "@memberjunction/global";
|
|
9
|
+
import { CopyScalarsAndArrays, UUIDsEqual } from "@memberjunction/global";
|
|
10
10
|
import { GetReadOnlyProvider } from "../util.js";
|
|
11
11
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
12
12
|
|
|
@@ -221,7 +221,7 @@ export class ActionResolver extends ResolverBase {
|
|
|
221
221
|
* @private
|
|
222
222
|
*/
|
|
223
223
|
private findActionById(actionID: string): any {
|
|
224
|
-
const action = ActionEngineServer.Instance.Actions.find(a => a.ID
|
|
224
|
+
const action = ActionEngineServer.Instance.Actions.find(a => UUIDsEqual(a.ID, actionID));
|
|
225
225
|
if (!action) {
|
|
226
226
|
throw new Error(`Action with ID ${actionID} not found`);
|
|
227
227
|
}
|
|
@@ -377,7 +377,7 @@ export class ActionResolver extends ResolverBase {
|
|
|
377
377
|
* @private
|
|
378
378
|
*/
|
|
379
379
|
private getEntityAction(actionID: string): any {
|
|
380
|
-
const entityAction = EntityActionEngineServer.Instance.EntityActions.find(ea => ea.ID
|
|
380
|
+
const entityAction = EntityActionEngineServer.Instance.EntityActions.find(ea => UUIDsEqual(ea.ID, actionID));
|
|
381
381
|
if (!entityAction) {
|
|
382
382
|
throw new Error(`EntityAction with ID ${actionID} not found`);
|
|
383
383
|
}
|
|
@@ -419,7 +419,7 @@ export class ActionResolver extends ResolverBase {
|
|
|
419
419
|
throw new Error(`Entity with name ${input.EntityName} not found`);
|
|
420
420
|
}
|
|
421
421
|
} else if (input.EntityID) {
|
|
422
|
-
entity = md.Entities.find(e => e.ID
|
|
422
|
+
entity = md.Entities.find(e => UUIDsEqual(e.ID, input.EntityID));
|
|
423
423
|
if (!entity) {
|
|
424
424
|
throw new Error(`Entity with ID ${input.EntityID} not found`);
|
|
425
425
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Mutation, Resolver } from 'type-graphql';
|
|
2
2
|
import { UserInfo, Metadata, LogError, LogStatus } from '@memberjunction/core';
|
|
3
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
3
4
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
4
5
|
import { MJComponentEntity, MJComponentRegistryEntity, ComponentMetadataEngine } from '@memberjunction/core-entities';
|
|
5
6
|
import { ComponentSpec } from '@memberjunction/interactive-component-types';
|
|
@@ -398,7 +399,7 @@ export class ComponentRegistryExtendedResolver {
|
|
|
398
399
|
await this.componentEngine.Config(false, userInfo);
|
|
399
400
|
|
|
400
401
|
const registry = this.componentEngine.ComponentRegistries?.find(
|
|
401
|
-
r => r.ID
|
|
402
|
+
r => UUIDsEqual(r.ID, registryId)
|
|
402
403
|
);
|
|
403
404
|
|
|
404
405
|
return registry || null;
|
|
@@ -457,10 +458,10 @@ export class ComponentRegistryExtendedResolver {
|
|
|
457
458
|
*/
|
|
458
459
|
private createClientForRegistry(registry: MJComponentRegistryEntity): ComponentRegistryClient {
|
|
459
460
|
// Check if there's a configuration for this registry
|
|
460
|
-
const config = configInfo.componentRegistries?.find(r =>
|
|
461
|
-
r.id
|
|
461
|
+
const config = configInfo.componentRegistries?.find(r =>
|
|
462
|
+
UUIDsEqual(r.id, registry.ID) || r.name === registry.Name
|
|
462
463
|
);
|
|
463
|
-
|
|
464
|
+
|
|
464
465
|
// Get API key from environment or config
|
|
465
466
|
const apiKey = process.env[`REGISTRY_API_KEY_${registry.ID.replace(/-/g, '_').toUpperCase()}`] ||
|
|
466
467
|
process.env[`REGISTRY_API_KEY_${registry.Name?.replace(/-/g, '_').toUpperCase()}`] ||
|
|
@@ -492,8 +493,8 @@ export class ComponentRegistryExtendedResolver {
|
|
|
492
493
|
*/
|
|
493
494
|
private shouldCache(registry: MJComponentRegistryEntity): boolean {
|
|
494
495
|
// Check config for caching settings
|
|
495
|
-
const config = configInfo.componentRegistries?.find(r =>
|
|
496
|
-
r.id
|
|
496
|
+
const config = configInfo.componentRegistries?.find(r =>
|
|
497
|
+
UUIDsEqual(r.id, registry.ID) || r.name === registry.Name
|
|
497
498
|
);
|
|
498
499
|
return config?.cache !== false; // Cache by default
|
|
499
500
|
}
|
|
@@ -515,7 +516,7 @@ export class ComponentRegistryExtendedResolver {
|
|
|
515
516
|
const existingComponent = this.componentEngine.Components?.find(
|
|
516
517
|
c => c.Name === component.name &&
|
|
517
518
|
c.Namespace === component.namespace &&
|
|
518
|
-
c.SourceRegistryID
|
|
519
|
+
UUIDsEqual(c.SourceRegistryID, registryId)
|
|
519
520
|
);
|
|
520
521
|
|
|
521
522
|
if (existingComponent) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EntityPermissionType, Metadata, FieldValueCollection, EntitySaveOptions, RunView } from '@memberjunction/core';
|
|
2
|
+
import { NormalizeUUID } from '@memberjunction/global';
|
|
2
3
|
import { MJFileEntity, MJFileStorageProviderEntity, MJFileStorageAccountEntity } from '@memberjunction/core-entities';
|
|
3
4
|
import {
|
|
4
5
|
AppContext,
|
|
@@ -748,8 +749,8 @@ export class FileResolver extends FileResolverBase {
|
|
|
748
749
|
|
|
749
750
|
// Log any accounts that weren't found
|
|
750
751
|
if (accountEntities.length < input.AccountIDs.length) {
|
|
751
|
-
const foundIDs = new Set(accountEntities.map((a) => a.ID));
|
|
752
|
-
const missingIDs = input.AccountIDs.filter((id) => !foundIDs.has(id));
|
|
752
|
+
const foundIDs = new Set(accountEntities.map((a) => NormalizeUUID(a.ID)));
|
|
753
|
+
const missingIDs = input.AccountIDs.filter((id) => !foundIDs.has(NormalizeUUID(id)));
|
|
753
754
|
console.warn(`[FileResolver] Accounts not found: ${missingIDs.join(', ')}`);
|
|
754
755
|
}
|
|
755
756
|
|
|
@@ -10,7 +10,7 @@ import { ResolverBase } from '../generic/ResolverBase.js';
|
|
|
10
10
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
11
11
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
12
12
|
import { GetReadWriteProvider } from '../util.js';
|
|
13
|
-
import { SafeJSONParse } from '@memberjunction/global';
|
|
13
|
+
import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
|
|
14
14
|
import { getAttachmentService } from '@memberjunction/aiengine';
|
|
15
15
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
16
16
|
|
|
@@ -212,7 +212,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
212
212
|
await AIEngine.Instance.Config(false, currentUser);
|
|
213
213
|
|
|
214
214
|
// Find agent in cached collection
|
|
215
|
-
const agentEntity = AIEngine.Instance.
|
|
215
|
+
const agentEntity = AIEngine.Instance.GetAgentByID(agentId);
|
|
216
216
|
|
|
217
217
|
if (!agentEntity) {
|
|
218
218
|
throw new Error(`AI Agent with ID ${agentId} not found`);
|
|
@@ -676,7 +676,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
676
676
|
|
|
677
677
|
// Get agent info for notification message
|
|
678
678
|
await AIEngine.Instance.Config(false, contextUser);
|
|
679
|
-
const agent = AIEngine.Instance.Agents.find(a => a.ID
|
|
679
|
+
const agent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, agentRun.AgentID));
|
|
680
680
|
const agentName = agent?.Name || 'Agent';
|
|
681
681
|
|
|
682
682
|
// Load conversation detail to get conversation info
|
|
@@ -9,6 +9,7 @@ import * as fs from 'fs/promises';
|
|
|
9
9
|
import { loadConfig } from '../config.js';
|
|
10
10
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
11
11
|
import { GetReadOnlyProvider } from '../util.js';
|
|
12
|
+
import { SqlLoggingOptions as ProviderSqlLoggingOptions } from '@memberjunction/generic-database-provider';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Configuration options for SQL logging sessions.
|
|
@@ -315,7 +316,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
315
316
|
async sqlLoggingConfig(@Ctx() context: AppContext): Promise<SqlLoggingConfig> {
|
|
316
317
|
await this.checkOwnerAccess(context);
|
|
317
318
|
const config = await loadConfig();
|
|
318
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
319
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
319
320
|
const activeSessions = provider.GetActiveSqlLoggingSessions();
|
|
320
321
|
|
|
321
322
|
return {
|
|
@@ -370,7 +371,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
370
371
|
@Query(() => [SqlLoggingSession])
|
|
371
372
|
async activeSqlLoggingSessions(@Ctx() context: AppContext): Promise<SqlLoggingSession[]> {
|
|
372
373
|
await this.checkOwnerAccess(context);
|
|
373
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
374
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
374
375
|
const sessions = provider.GetActiveSqlLoggingSessions();
|
|
375
376
|
|
|
376
377
|
return sessions.map(session => ({
|
|
@@ -435,7 +436,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
435
436
|
}
|
|
436
437
|
|
|
437
438
|
// Check max active sessions
|
|
438
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
439
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
439
440
|
const activeSessions = provider.GetActiveSqlLoggingSessions();
|
|
440
441
|
if (activeSessions.length >= (config.sqlLogging.maxActiveSessions ?? 5)) {
|
|
441
442
|
throw new Error(`Maximum number of active SQL logging sessions (${config.sqlLogging.maxActiveSessions}) reached`);
|
|
@@ -521,7 +522,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
521
522
|
@Ctx() context: AppContext
|
|
522
523
|
): Promise<boolean> {
|
|
523
524
|
await this.checkOwnerAccess(context);
|
|
524
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
525
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
525
526
|
|
|
526
527
|
// Use the public method to get and dispose the session
|
|
527
528
|
const session = provider.GetSqlLoggingSessionById(sessionId);
|
|
@@ -564,7 +565,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
564
565
|
@Mutation(() => Boolean)
|
|
565
566
|
async stopAllSqlLogging(@Ctx() context: AppContext): Promise<boolean> {
|
|
566
567
|
await this.checkOwnerAccess(context);
|
|
567
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
568
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
568
569
|
await provider.DisposeAllSqlLoggingSessions();
|
|
569
570
|
return true;
|
|
570
571
|
}
|
|
@@ -654,7 +655,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
654
655
|
}
|
|
655
656
|
|
|
656
657
|
// Find the session
|
|
657
|
-
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as SQLServerDataProvider;
|
|
658
|
+
const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true}) as unknown as SQLServerDataProvider;
|
|
658
659
|
const sessions = provider.GetActiveSqlLoggingSessions();
|
|
659
660
|
const session = sessions.find((s) => s.id === sessionId);
|
|
660
661
|
|
|
@@ -744,7 +745,7 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
744
745
|
* @returns GraphQL-compatible SqlLoggingOptions
|
|
745
746
|
* @private
|
|
746
747
|
*/
|
|
747
|
-
private convertOptionsToGraphQL(options:
|
|
748
|
+
private convertOptionsToGraphQL(options: ProviderSqlLoggingOptions): SqlLoggingOptions {
|
|
748
749
|
return {
|
|
749
750
|
formatAsMigration: options.formatAsMigration,
|
|
750
751
|
description: options.description,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { EntityDeleteOptions, EntitySaveOptions, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
4
5
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
6
|
import { MJRoleEntity, MJUserEntity, MJUserRoleEntity } from '@memberjunction/core-entities';
|
|
6
7
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
@@ -382,7 +383,7 @@ export class SyncRolesAndUsersResolver {
|
|
|
382
383
|
const dbRole = dbRoles.find(r => r.Name.trim().toLowerCase() === role.Name.trim().toLowerCase());
|
|
383
384
|
if (dbRole) {
|
|
384
385
|
// now we need to make sure there is a user role that matches this user and role
|
|
385
|
-
if (!dbUserRoles.find(ur => ur.UserID
|
|
386
|
+
if (!dbUserRoles.find(ur => UUIDsEqual(ur.UserID, dbUser.ID) && UUIDsEqual(ur.RoleID, dbRole.ID))) {
|
|
386
387
|
// we need to add a user role
|
|
387
388
|
const ur = await md.GetEntityObject<MJUserRoleEntity>("MJ: User Roles", u);
|
|
388
389
|
ur.UserID = dbUser.ID;
|
|
@@ -392,9 +393,9 @@ export class SyncRolesAndUsersResolver {
|
|
|
392
393
|
}
|
|
393
394
|
}
|
|
394
395
|
// now, we check for DB user roles that are NOT in the user.Roles property as they are no longer part of the user's roles
|
|
395
|
-
const thisUserDBRoles = dbUserRoles.filter(ur => ur.UserID
|
|
396
|
+
const thisUserDBRoles = dbUserRoles.filter(ur => UUIDsEqual(ur.UserID, dbUser.ID));
|
|
396
397
|
for (const dbUserRole of thisUserDBRoles) {
|
|
397
|
-
const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRoles.find(rr => rr.ID
|
|
398
|
+
const role = user.Roles.find(r => r.Name.trim().toLowerCase() === dbRoles.find(rr => UUIDsEqual(rr.ID, dbUserRole.RoleID))?.Name.trim().toLowerCase());
|
|
398
399
|
if (!role && !this.IsStandardRole(dbUserRole.Role)) {
|
|
399
400
|
// this user role is no longer in the user's roles, we need to remove it
|
|
400
401
|
//dbUserRole.TransactionGroup = tg;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Metadata, KeyValuePair, CompositeKey, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
2
3
|
import {
|
|
3
4
|
AppContext,
|
|
4
5
|
Arg,
|
|
@@ -85,7 +86,7 @@ export class UserFavoriteResolver extends MJUserFavoriteResolverBase {
|
|
|
85
86
|
const p = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
86
87
|
const pk = new CompositeKey(params.CompositeKey.KeyValuePairs);
|
|
87
88
|
|
|
88
|
-
const e = p.Entities.find((e) => e.ID
|
|
89
|
+
const e = p.Entities.find((e) => UUIDsEqual(e.ID, params.EntityID));
|
|
89
90
|
if (e)
|
|
90
91
|
return {
|
|
91
92
|
EntityID: params.EntityID,
|
|
@@ -101,8 +102,8 @@ export class UserFavoriteResolver extends MJUserFavoriteResolverBase {
|
|
|
101
102
|
async SetRecordFavoriteStatus(@Arg('params', () => UserFavoriteSetParams) params: UserFavoriteSetParams, @Ctx() { userPayload, providers }: AppContext) {
|
|
102
103
|
const p = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
103
104
|
const pk = new CompositeKey(params.CompositeKey.KeyValuePairs);
|
|
104
|
-
const e = p.Entities.find((e) => e.ID
|
|
105
|
-
const u = UserCache.Users.find((u) => u.ID
|
|
105
|
+
const e = p.Entities.find((e) => UUIDsEqual(e.ID, params.EntityID));
|
|
106
|
+
const u = UserCache.Users.find((u) => UUIDsEqual(u.ID, userPayload.userRecord.ID));
|
|
106
107
|
if (e) {
|
|
107
108
|
await p.SetRecordFavoriteStatus(params.UserID, e.Name, pk, params.IsFavorite, u);
|
|
108
109
|
return {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import express from 'express';
|
|
16
16
|
import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
|
|
17
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
17
18
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
18
19
|
import { OAuthManager, MCPClientManager } from '@memberjunction/ai-mcp-client';
|
|
19
20
|
import type { MCPServerOAuthConfig } from '@memberjunction/ai-mcp-client';
|
|
@@ -201,7 +202,7 @@ export class OAuthCallbackHandler {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
// Get the actual user from cache
|
|
204
|
-
const contextUser = UserCache.Users.find(u => u.ID
|
|
205
|
+
const contextUser = UserCache.Users.find(u => UUIDsEqual(u.ID, authState.userId));
|
|
205
206
|
if (!contextUser) {
|
|
206
207
|
LogError(`[OAuth Callback] User ${authState.userId} not found in cache`);
|
|
207
208
|
this.redirectToError(res, 'server_error', 'User context not found', authState.frontendReturnUrl);
|
|
@@ -289,7 +290,7 @@ export class OAuthCallbackHandler {
|
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
// Verify user owns this state
|
|
292
|
-
if (authState.userId
|
|
293
|
+
if (!UUIDsEqual(authState.userId, contextUser.ID)) {
|
|
293
294
|
res.status(403).json({
|
|
294
295
|
success: false,
|
|
295
296
|
errorCode: 'forbidden',
|
|
@@ -491,7 +492,7 @@ export class OAuthCallbackHandler {
|
|
|
491
492
|
}
|
|
492
493
|
|
|
493
494
|
// Verify the authenticated user owns this authorization state
|
|
494
|
-
if (authState.userId
|
|
495
|
+
if (!UUIDsEqual(authState.userId, contextUser.ID)) {
|
|
495
496
|
LogError(`[OAuth Exchange] User ${contextUser.ID} attempted to exchange code for state owned by ${authState.userId}`);
|
|
496
497
|
res.status(403).json({
|
|
497
498
|
success: false,
|