@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.
Files changed (74) hide show
  1. package/dist/agents/skip-sdk.js +2 -2
  2. package/dist/agents/skip-sdk.js.map +1 -1
  3. package/dist/auth/AuthProviderFactory.d.ts +9 -0
  4. package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
  5. package/dist/auth/AuthProviderFactory.js +27 -1
  6. package/dist/auth/AuthProviderFactory.js.map +1 -1
  7. package/dist/auth/index.d.ts +6 -4
  8. package/dist/auth/index.d.ts.map +1 -1
  9. package/dist/auth/index.js +10 -6
  10. package/dist/auth/index.js.map +1 -1
  11. package/dist/config.d.ts +37 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +7 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/context.d.ts +2 -2
  16. package/dist/context.d.ts.map +1 -1
  17. package/dist/context.js +52 -15
  18. package/dist/context.js.map +1 -1
  19. package/dist/generated/generated.d.ts +944 -843
  20. package/dist/generated/generated.d.ts.map +1 -1
  21. package/dist/generated/generated.js +5269 -6194
  22. package/dist/generated/generated.js.map +1 -1
  23. package/dist/generic/ResolverBase.d.ts.map +1 -1
  24. package/dist/generic/ResolverBase.js +9 -0
  25. package/dist/generic/ResolverBase.js.map +1 -1
  26. package/dist/generic/RunViewResolver.d.ts +3 -1
  27. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  28. package/dist/generic/RunViewResolver.js +25 -6
  29. package/dist/generic/RunViewResolver.js.map +1 -1
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +181 -22
  33. package/dist/index.js.map +1 -1
  34. package/dist/resolvers/ActionResolver.js +4 -4
  35. package/dist/resolvers/ActionResolver.js.map +1 -1
  36. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  37. package/dist/resolvers/ComponentRegistryResolver.js +5 -4
  38. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  39. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  40. package/dist/resolvers/FileResolver.js +3 -2
  41. package/dist/resolvers/FileResolver.js.map +1 -1
  42. package/dist/resolvers/RunAIAgentResolver.js +3 -3
  43. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  44. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  45. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  46. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  47. package/dist/resolvers/SyncRolesUsersResolver.js +4 -3
  48. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  49. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  50. package/dist/resolvers/UserFavoriteResolver.js +4 -3
  51. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  52. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  53. package/dist/rest/OAuthCallbackHandler.js +4 -3
  54. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  55. package/package.json +58 -53
  56. package/src/__tests__/databaseAbstraction.test.ts +816 -0
  57. package/src/agents/skip-sdk.ts +2 -2
  58. package/src/auth/AuthProviderFactory.ts +31 -1
  59. package/src/auth/__tests__/backward-compatibility.test.ts +114 -0
  60. package/src/auth/index.ts +14 -9
  61. package/src/config.ts +9 -0
  62. package/src/context.ts +65 -20
  63. package/src/generated/generated.ts +5056 -6164
  64. package/src/generic/ResolverBase.ts +9 -0
  65. package/src/generic/RunViewResolver.ts +24 -7
  66. package/src/index.ts +207 -23
  67. package/src/resolvers/ActionResolver.ts +4 -4
  68. package/src/resolvers/ComponentRegistryResolver.ts +8 -7
  69. package/src/resolvers/FileResolver.ts +3 -2
  70. package/src/resolvers/RunAIAgentResolver.ts +3 -3
  71. package/src/resolvers/SqlLoggingConfigResolver.ts +8 -7
  72. package/src/resolvers/SyncRolesUsersResolver.ts +4 -3
  73. package/src/resolvers/UserFavoriteResolver.ts +4 -3
  74. 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
- EntityName: string;
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 === viewInfo.EntityID);
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 === viewInfo.EntityID);
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
- const entity = provider.Entities.find((e) => e.Name === input[index].EntityName);
751
- const returnData: any[] = this.processRawData(data.Results, entity.ID, entity);
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 === viewInfo.EntityID);
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 { setupSQLServerClient, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
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
- await pool.connect();
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
- const dataSources = [new DataSourceInfo({dataSource: pool, type: 'Read-Write', host: dbHost, port: dbPort, database: dbDatabase, userName: dbUsername})];
142
-
143
- // Establish a second read-only connection to the database if dbReadOnlyUsername and dbReadOnlyPassword exist
144
- let readOnlyPool: sql.ConnectionPool | null = null;
145
- if (configInfo.dbReadOnlyUsername && configInfo.dbReadOnlyPassword) {
146
- const readOnlyConfig = {
147
- ...createMSSQLConfig(),
148
- user: configInfo.dbReadOnlyUsername,
149
- password: configInfo.dbReadOnlyPassword,
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
- // since we created a read-only pool, add it to the list of data sources
155
- dataSources.push(new DataSourceInfo({dataSource: readOnlyPool, type: 'Read-Only', host: dbHost, port: dbPort, database: dbDatabase, userName: configInfo.dbReadOnlyUsername}));
156
- console.log('Read-only Connection Pool has been initialized.');
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
- const config = new SQLServerProviderConfigData(pool, mj_core_schema, cacheRefreshInterval);
160
- await setupSQLServerClient(config); // datasource is already initialized, so we can setup the client right away
161
- const md = new Metadata();
162
- console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
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(pool), // default read-write data source
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 === actionID);
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 === actionID);
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 === input.EntityID);
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 === registryId
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 === registry.ID || r.name === registry.Name
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 === registry.ID || r.name === registry.Name
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 === registryId
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.Agents.find((a: MJAIAgentEntityExtended) => a.ID === agentId);
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 === agentRun.AgentID);
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: import('@memberjunction/sqlserver-dataprovider').SqlLoggingOptions): SqlLoggingOptions {
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 === dbUser.ID && ur.RoleID === dbRole.ID)) {
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 === dbUser.ID);
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 === dbUserRole.RoleID)?.Name.trim().toLowerCase());
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 === params.EntityID);
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 === params.EntityID);
105
- const u = UserCache.Users.find((u) => u.ID === userPayload.userRecord.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 === authState.userId);
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 !== contextUser.ID) {
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 !== contextUser.ID) {
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,