@memberjunction/server 5.51.3 → 5.51.4

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 (211) hide show
  1. package/README.md +34 -1
  2. package/dist/agentSessions/SessionManager.d.ts +5 -2
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +17 -8
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.js +136 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  10. package/dist/agentSessions/index.d.ts +1 -0
  11. package/dist/agentSessions/index.d.ts.map +1 -1
  12. package/dist/agentSessions/index.js +1 -0
  13. package/dist/agentSessions/index.js.map +1 -1
  14. package/dist/auth/actionAuthorization.d.ts +40 -0
  15. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  16. package/dist/auth/actionAuthorization.js +94 -0
  17. package/dist/auth/actionAuthorization.js.map +1 -0
  18. package/dist/auth/index.d.ts +1 -0
  19. package/dist/auth/index.d.ts.map +1 -1
  20. package/dist/auth/index.js +1 -0
  21. package/dist/auth/index.js.map +1 -1
  22. package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
  23. package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
  24. package/dist/auth/scopeLimitedPrincipal.js +49 -0
  25. package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
  26. package/dist/config.d.ts +53 -0
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +34 -1
  29. package/dist/config.js.map +1 -1
  30. package/dist/context.d.ts +6 -0
  31. package/dist/context.d.ts.map +1 -1
  32. package/dist/context.js +24 -34
  33. package/dist/context.js.map +1 -1
  34. package/dist/generated/generated.d.ts.map +1 -1
  35. package/dist/generated/generated.js +3262 -3273
  36. package/dist/generated/generated.js.map +1 -1
  37. package/dist/generic/CacheInvalidationResolver.d.ts +124 -4
  38. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  39. package/dist/generic/CacheInvalidationResolver.js +170 -15
  40. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  41. package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
  42. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  43. package/dist/generic/FireAndForgetHeartbeat.js +5 -4
  44. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  45. package/dist/generic/PushStatusResolver.d.ts +58 -0
  46. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  47. package/dist/generic/PushStatusResolver.js +42 -3
  48. package/dist/generic/PushStatusResolver.js.map +1 -1
  49. package/dist/generic/ResolverBase.d.ts +148 -3
  50. package/dist/generic/ResolverBase.d.ts.map +1 -1
  51. package/dist/generic/ResolverBase.js +287 -30
  52. package/dist/generic/ResolverBase.js.map +1 -1
  53. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  54. package/dist/generic/RunViewResolver.js +12 -0
  55. package/dist/generic/RunViewResolver.js.map +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +56 -48
  58. package/dist/index.js.map +1 -1
  59. package/dist/multiTenancy/index.d.ts.map +1 -1
  60. package/dist/multiTenancy/index.js +22 -2
  61. package/dist/multiTenancy/index.js.map +1 -1
  62. package/dist/postgresqlCompat.d.ts +8 -0
  63. package/dist/postgresqlCompat.d.ts.map +1 -0
  64. package/dist/postgresqlCompat.js +19 -0
  65. package/dist/postgresqlCompat.js.map +1 -0
  66. package/dist/postgresqlPoolSettings.d.ts +108 -0
  67. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  68. package/dist/postgresqlPoolSettings.js +134 -0
  69. package/dist/postgresqlPoolSettings.js.map +1 -0
  70. package/dist/realtimeWidget/widgetGuestElevation.d.ts +2 -2
  71. package/dist/realtimeWidget/widgetGuestElevation.js +2 -2
  72. package/dist/resolvers/ActionResolver.d.ts +31 -5
  73. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  74. package/dist/resolvers/ActionResolver.js +75 -14
  75. package/dist/resolvers/ActionResolver.js.map +1 -1
  76. package/dist/resolvers/AdhocQueryResolver.d.ts +39 -17
  77. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  78. package/dist/resolvers/AdhocQueryResolver.js +97 -157
  79. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  80. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  81. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  82. package/dist/resolvers/DatasetResolver.js +57 -8
  83. package/dist/resolvers/DatasetResolver.js.map +1 -1
  84. package/dist/resolvers/EntityRecordNameResolver.d.ts +14 -4
  85. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  86. package/dist/resolvers/EntityRecordNameResolver.js +18 -6
  87. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  88. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  89. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  90. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  91. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  92. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  93. package/dist/resolvers/GetDataResolver.js +13 -1
  94. package/dist/resolvers/GetDataResolver.js.map +1 -1
  95. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  96. package/dist/resolvers/MCPResolver.js +13 -16
  97. package/dist/resolvers/MCPResolver.js.map +1 -1
  98. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +26 -14
  99. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  100. package/dist/resolvers/RealtimeClientSessionResolver.js +63 -46
  101. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  102. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  103. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  104. package/dist/resolvers/RemoteBrowserActionResolver.js +24 -27
  105. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  106. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  107. package/dist/resolvers/RunAIAgentResolver.js +30 -40
  108. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  109. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  110. package/dist/resolvers/RunTemplateResolver.js +12 -11
  111. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  112. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  113. package/dist/resolvers/RunTestResolver.js +90 -112
  114. package/dist/resolvers/RunTestResolver.js.map +1 -1
  115. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  116. package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
  117. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  118. package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
  119. package/dist/resolvers/VersionHistoryResolver.js +8 -12
  120. package/dist/resolvers/VersionHistoryResolver.js.map +1 -1
  121. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  122. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  123. package/dist/resolvers/adhocTimeout.js +12 -0
  124. package/dist/resolvers/adhocTimeout.js.map +1 -0
  125. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  126. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  127. package/dist/rest/ViewOperationsHandler.js +29 -17
  128. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  129. package/dist/services/TaskOrchestrator.d.ts.map +1 -1
  130. package/dist/services/TaskOrchestrator.js +17 -15
  131. package/dist/services/TaskOrchestrator.js.map +1 -1
  132. package/package.json +90 -89
  133. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  134. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  135. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  136. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
  137. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  138. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  139. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  140. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  141. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +136 -0
  142. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +318 -0
  143. package/src/__tests__/RealtimeClientSessionResolver.test.ts +66 -27
  144. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +134 -0
  145. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +221 -0
  146. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +173 -0
  147. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  148. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +116 -0
  149. package/src/__tests__/ResolverBase.filterEscaping.test.ts +351 -0
  150. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  151. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +164 -0
  152. package/src/__tests__/SessionManager.test.ts +88 -13
  153. package/src/__tests__/SessionRunIDVerifier.test.ts +255 -0
  154. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  155. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +155 -0
  156. package/src/__tests__/actionAuthorization.test.ts +130 -0
  157. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  158. package/src/__tests__/cacheInvalidationRecordData.test.ts +241 -0
  159. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +180 -0
  160. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  161. package/src/__tests__/multiTenancy.test.ts +1 -1
  162. package/src/__tests__/newUserRolesDefault.test.ts +81 -0
  163. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  164. package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
  165. package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
  166. package/src/__tests__/resolverBase.rls.test.ts +2 -0
  167. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  168. package/src/agentSessions/SessionManager.ts +24 -25
  169. package/src/agentSessions/SessionRunIDVerifier.ts +179 -0
  170. package/src/agentSessions/index.ts +1 -0
  171. package/src/auth/actionAuthorization.ts +119 -0
  172. package/src/auth/index.ts +1 -0
  173. package/src/auth/scopeLimitedPrincipal.ts +50 -0
  174. package/src/config.ts +34 -1
  175. package/src/context.ts +25 -34
  176. package/src/generated/generated.ts +3262 -3273
  177. package/src/generic/CacheInvalidationResolver.ts +206 -15
  178. package/src/generic/FireAndForgetHeartbeat.ts +10 -4
  179. package/src/generic/PushStatusResolver.ts +79 -4
  180. package/src/generic/ResolverBase.ts +339 -31
  181. package/src/generic/RunViewResolver.ts +17 -0
  182. package/src/index.ts +63 -50
  183. package/src/multiTenancy/index.ts +24 -2
  184. package/src/postgresqlCompat.ts +21 -0
  185. package/src/postgresqlPoolSettings.ts +213 -0
  186. package/src/realtimeWidget/widgetGuestElevation.ts +2 -2
  187. package/src/resolvers/ActionResolver.ts +85 -22
  188. package/src/resolvers/AdhocQueryResolver.ts +110 -177
  189. package/src/resolvers/DatasetResolver.ts +70 -10
  190. package/src/resolvers/EntityRecordNameResolver.ts +24 -7
  191. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  192. package/src/resolvers/GetDataResolver.ts +14 -1
  193. package/src/resolvers/MCPResolver.ts +13 -15
  194. package/src/resolvers/RealtimeClientSessionResolver.ts +82 -50
  195. package/src/resolvers/RemoteBrowserActionResolver.ts +30 -26
  196. package/src/resolvers/RunAIAgentResolver.ts +30 -40
  197. package/src/resolvers/RunTemplateResolver.ts +13 -12
  198. package/src/resolvers/RunTestResolver.ts +90 -112
  199. package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
  200. package/src/resolvers/VersionHistoryResolver.ts +10 -13
  201. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  202. package/src/resolvers/__tests__/ReportResolver.test.ts +2 -0
  203. package/src/resolvers/adhocTimeout.ts +12 -0
  204. package/src/rest/ViewOperationsHandler.ts +37 -18
  205. package/src/services/TaskOrchestrator.ts +17 -17
  206. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
  207. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  208. package/dist/resolvers/adhoc-query-helpers.js +0 -42
  209. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  210. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  211. package/src/resolvers/adhoc-query-helpers.ts +0 -50
package/src/index.ts CHANGED
@@ -30,8 +30,10 @@ import { WebSocketServer } from 'ws';
30
30
  import { RealtimeProxyServer } from './realtimeProxy/RealtimeProxyServer.js';
31
31
  import buildApolloServer from './apolloServer/index.js';
32
32
  import { configInfo, configFilePath, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage, RESTApiOptions } from './config.js';
33
+ import { TranslateBracketsToPG } from './postgresqlCompat.js';
34
+ import { BuildPostgreSQLConnectionConfig, DescribeReadOnlyLoginOverreach, PostgreSQLReadOnlyPool, ResolvePostgreSQLEndpoint, ResolvePostgreSQLReadOnlyCredentials, ToPGPoolConfig } from './postgresqlPoolSettings.js';
33
35
  import { default as jwt } from 'jsonwebtoken';
34
- import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
36
+ import { contextFunction, CreateReadOnlyProvider, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
35
37
  import { UserPayload } from './types.js';
36
38
  import { requireSystemUserDirective, publicDirective } from './directives/index.js';
37
39
  import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
@@ -59,6 +61,8 @@ import { ScheduledJobsService } from './services/ScheduledJobsService.js';
59
61
  import { LocalCacheManager, StartupManager, TelemetryManager, TelemetryLevel, LogStatus, SetVerboseLogging } from '@memberjunction/core';
60
62
  import { getSystemUser } from './auth/index.js';
61
63
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
64
+ import { ActionEngineServer } from '@memberjunction/actions';
65
+ import { CreateAdhocSQLAuthorizer } from './resolvers/AdhocQueryResolver.js';
62
66
  import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
63
67
  import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
64
68
  import { PubSubManager } from './generic/PubSubManager.js';
@@ -66,7 +70,7 @@ import { IntegrationProgressEmitter } from '@memberjunction/integration-progress
66
70
  import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
67
71
  import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
68
72
  import { SessionJanitor } from './agentSessions/index.js';
69
- import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
73
+ import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData, ConfigureRecordDataBroadcast } from './generic/CacheInvalidationResolver.js';
70
74
  import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
71
75
  import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
72
76
  import {
@@ -265,25 +269,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
265
269
  startupLog.BeginPhase('Connecting to database');
266
270
  startupLog.LogIf('verbose', 'Database type: PostgreSQL');
267
271
  const pg = await import('pg');
268
- const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
272
+ const { PostgreSQLDataProvider, PostgreSQLProviderConfigData, MJPostgresTypes } = await import('@memberjunction/postgresql-dataprovider');
269
273
 
270
- const pgHost = process.env.PG_HOST || process.env.DB_HOST || 'localhost';
271
- const pgPort = parseInt(process.env.PG_PORT || process.env.DB_PORT || '5432', 10);
272
- const pgUser = process.env.PG_USERNAME || process.env.DB_USERNAME || 'postgres';
273
- const pgPass = process.env.PG_PASSWORD || process.env.DB_PASSWORD || '';
274
- const pgDatabase = process.env.PG_DATABASE || process.env.DB_DATABASE || '';
275
-
276
- const pgPool = new pg.default.Pool({
277
- host: pgHost,
278
- port: pgPort,
279
- user: pgUser,
280
- password: pgPass,
281
- database: pgDatabase,
282
- max: configInfo.databaseSettings.connectionPool?.max ?? 50,
283
- min: configInfo.databaseSettings.connectionPool?.min ?? 5,
284
- idleTimeoutMillis: configInfo.databaseSettings.connectionPool?.idleTimeoutMillis ?? 30000,
285
- connectionTimeoutMillis: configInfo.databaseSettings.connectionPool?.acquireTimeoutMillis ?? 30000,
286
- });
274
+ const pgEndpoint = ResolvePostgreSQLEndpoint();
275
+ const { Host: pgHost, Port: pgPort, User: pgUser, Database: pgDatabase } = pgEndpoint;
276
+ // Every API pool carries the statement and idle-in-transaction timeouts from connection #1.
277
+ const pgConnectionConfig = BuildPostgreSQLConnectionConfig(pgEndpoint, configInfo.databaseSettings, 'api');
278
+ const pgPool = new pg.default.Pool(ToPGPoolConfig(pgConnectionConfig));
287
279
 
288
280
  // Verify connection
289
281
  const testClient = await pgPool.connect();
@@ -291,6 +283,28 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
291
283
  testClient.release();
292
284
  startupLog.LogIf('verbose', `PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
293
285
 
286
+ // A read-only pool opened with the read-only login, as SQL Server has. Read-only per-request
287
+ // providers (TestQuerySQL and other caller-supplied SQL) share it instead of the primary pool.
288
+ const pgReadOnlyCredentials = ResolvePostgreSQLReadOnlyCredentials(configInfo);
289
+ if (pgReadOnlyCredentials) {
290
+ // A pool a provider runs on must carry the provider's type parsers, so BIGINT and NUMERIC
291
+ // come back as numbers here as they do on the provider's own pool.
292
+ const readOnlyPgPool = new pg.default.Pool({
293
+ ...ToPGPoolConfig(BuildPostgreSQLConnectionConfig({ ...pgEndpoint, ...pgReadOnlyCredentials }, configInfo.databaseSettings, 'read-only')),
294
+ types: MJPostgresTypes,
295
+ });
296
+ const readOnlyTestClient = await readOnlyPgPool.connect();
297
+ try {
298
+ for (const warning of await DescribeReadOnlyLoginOverreach(readOnlyTestClient, mj_core_schema)) {
299
+ LogStatus(`WARNING: ${warning}`);
300
+ }
301
+ } finally {
302
+ readOnlyTestClient.release();
303
+ }
304
+ PostgreSQLReadOnlyPool.Instance.Pool = readOnlyPgPool;
305
+ startupLog.LogIf('verbose', 'Read-only PostgreSQL pool has been initialized.');
306
+ }
307
+
294
308
  // Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
295
309
  // This allows existing code (types, util, context) to work without changes
296
310
  const mssqlCompatPool = createMSSQLCompatPool(pgPool);
@@ -304,15 +318,6 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
304
318
  }));
305
319
 
306
320
  // Set up the PostgreSQL provider
307
- const pgConnectionConfig = {
308
- Host: pgHost,
309
- Port: pgPort,
310
- Database: pgDatabase,
311
- User: pgUser,
312
- Password: pgPass,
313
- MaxConnections: configInfo.databaseSettings.connectionPool?.max ?? 50,
314
- MinConnections: configInfo.databaseSettings.connectionPool?.min ?? 5,
315
- };
316
321
  const pgConfigData = new PostgreSQLProviderConfigData(
317
322
  pgConnectionConfig,
318
323
  mj_core_schema,
@@ -342,7 +347,7 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
342
347
  if (poolAny._pgPool) {
343
348
  const thePgPool = poolAny._pgPool as import('pg').Pool;
344
349
  // Translate SQL Server bracket syntax to PostgreSQL double-quote syntax
345
- const pgQuery = translateBracketsToPG(query);
350
+ const pgQuery = TranslateBracketsToPG(query);
346
351
  const result = await thePgPool.query(pgQuery);
347
352
  return result.rows;
348
353
  }
@@ -361,21 +366,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
361
366
  const pgCodegenPass = process.env.CODEGEN_DB_PASSWORD;
362
367
  if (pgCodegenUser && pgCodegenPass) {
363
368
  try {
364
- const codegenPgPool = new pg.default.Pool({
365
- host: pgHost,
366
- port: pgPort,
367
- user: pgCodegenUser,
368
- password: pgCodegenPass,
369
- database: pgDatabase,
370
- max: 10,
371
- });
369
+ // CodeGen and DDL work runs long statements across every entity, so this pool gets the
370
+ // long CodeGen timeout rather than the API one, as the SQL Server CodeGen pool does.
371
+ const codegenPgConnectionConfig = BuildPostgreSQLConnectionConfig(
372
+ { ...pgEndpoint, User: pgCodegenUser, Password: pgCodegenPass },
373
+ configInfo.databaseSettings,
374
+ 'codegen',
375
+ );
376
+ const codegenPgPool = new pg.default.Pool(ToPGPoolConfig(codegenPgConnectionConfig));
372
377
  const codegenTestClient = await codegenPgPool.connect();
373
378
  await codegenTestClient.query('SELECT 1');
374
379
  codegenTestClient.release();
375
380
 
376
381
  const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
377
382
  const codegenPgConfigData = new PostgreSQLProviderConfigData(
378
- { Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgCodegenUser, Password: pgCodegenPass },
383
+ codegenPgConnectionConfig,
379
384
  mj_core_schema,
380
385
  cacheRefreshInterval / 1000, // ms → seconds
381
386
  );
@@ -673,6 +678,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
673
678
  // This must happen before any request handler calls GetAPIKeyEngine()
674
679
  GetAPIKeyEngine();
675
680
 
681
+ // Actions that run caller-supplied SQL (Run Ad-hoc Query) run only on the read-only login, with
682
+ // ExecuteAdhocQuery's table check and timeout limit.
683
+ ActionEngineServer.Instance.SetReadOnlyProviderFactory(() => CreateReadOnlyProvider(dataSources));
684
+ ActionEngineServer.Instance.SetAdhocSQLAuthorizer(CreateAdhocSQLAuthorizer(configInfo.databaseSettings.requestTimeout));
685
+
676
686
  setupComplete$.next(true);
677
687
  raiseEvent('setupComplete', dataSources, null, this);
678
688
 
@@ -827,6 +837,11 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
827
837
  })
828
838
  );
829
839
 
840
+ // Hand the resolver its allowlist before anything can publish. It cannot read configInfo itself:
841
+ // config.ts loads and validates at module scope, so importing it there would pull full config
842
+ // validation into every import chain that touches the resolver, unit tests included.
843
+ ConfigureRecordDataBroadcast(configInfo.cacheSettings?.recordDataBroadcastEntities);
844
+
830
845
  // Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
831
846
  // ANY BaseEntity save/delete occurs on this server — regardless of whether it
832
847
  // originated from a GraphQL mutation or internal server-side code (agents, actions,
@@ -836,14 +851,21 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
836
851
  if (event.event === MJEventType.ComponentEvent && event.eventCode === BaseEntity.BaseEventCode) {
837
852
  const beEvent = event.args as BaseEntityEvent;
838
853
  if (beEvent.type === 'save' || beEvent.type === 'delete') {
854
+ const entityName = beEvent.baseEntity.EntityInfo.Name;
839
855
  PubSubManager.Instance.Publish(CACHE_INVALIDATION_TOPIC, {
840
- entityName: beEvent.baseEntity.EntityInfo.Name,
856
+ entityName,
841
857
  primaryKeyValues: JSON.stringify(beEvent.baseEntity.PrimaryKey.KeyValuePairs),
842
858
  action: beEvent.type,
843
859
  sourceServerId: MJGlobal.Instance.ProcessUUID,
844
860
  timestamp: new Date(),
845
861
  originSessionId: null,
846
- recordData: beEvent.type === 'save' ? JSON.stringify(beEvent.baseEntity.GetAll()) : undefined,
862
+ // Opt-in only: this event reaches every connected client unfiltered, and this listener
863
+ // fires for server-internal saves too (agents, actions, orchestrator), which are exactly
864
+ // the ones no browser session asked for.
865
+ recordData:
866
+ beEvent.type === 'save' && MayBroadcastRecordData(entityName)
867
+ ? JSON.stringify(beEvent.baseEntity.GetAll())
868
+ : undefined,
847
869
  });
848
870
  }
849
871
  }
@@ -1754,12 +1776,3 @@ async function refreshUserCacheFromPG(pgPool: import('pg').Pool, coreSchema: str
1754
1776
  (cache as unknown as Record<string, unknown>)['_users'] = userInfos;
1755
1777
  }
1756
1778
  }
1757
-
1758
- /**
1759
- * Translates SQL Server bracket-quoted identifiers to PostgreSQL double-quoted identifiers.
1760
- * Converts [schema].[table] to "schema"."table" and handles common T-SQL patterns.
1761
- */
1762
- function translateBracketsToPG(sql: string): string {
1763
- // Replace [identifier] with "identifier"
1764
- return sql.replace(/\[([^\]]+)\]/g, '"$1"');
1765
- }
@@ -9,7 +9,9 @@
9
9
 
10
10
  import type { RequestHandler } from 'express';
11
11
  import type { PreRunViewHook, PreSaveHook } from '@memberjunction/core';
12
- import { Metadata, type UserInfo, type TenantContext } from '@memberjunction/core';
12
+ import { DatabaseProviderBase, Metadata, type UserInfo, type TenantContext } from '@memberjunction/core';
13
+ import { PostgreSQLDialect, SQLServerDialect, type SQLParserDialect } from '@memberjunction/sql-dialect';
14
+ import { IsBalancedSQLFragment } from '@memberjunction/sql-parser';
13
15
  import type { MultiTenancyConfig } from '../config.js';
14
16
  import type { UserPayload } from '../types.js';
15
17
 
@@ -99,6 +101,25 @@ function isAdminUser(user: UserInfo, adminRoles: string[]): boolean {
99
101
  );
100
102
  }
101
103
 
104
+ /** Dialects to read an ExtraFilter with when the active provider's dialect cannot be resolved. */
105
+ const ALL_FILTER_DIALECTS: SQLParserDialect[] = [new SQLServerDialect(), new PostgreSQLDialect()];
106
+
107
+ /**
108
+ * Throws unless the caller's ExtraFilter stays inside the parentheses the tenant predicate is
109
+ * ANDed after. A filter such as `1=1) OR (1=1` would close them early and leave the tenant
110
+ * predicate on one side of an OR the caller controls.
111
+ */
112
+ function assertExtraFilterBalanced(extraFilter: string): void {
113
+ const provider = Metadata.Provider; // global-provider-ok: same rationale as isEntityScoped above — hooks carry no per-request provider
114
+ const dialects = provider instanceof DatabaseProviderBase ? [provider.Dialect] : ALL_FILTER_DIALECTS;
115
+ for (const dialect of dialects) {
116
+ const check = IsBalancedSQLFragment(extraFilter, dialect);
117
+ if (!check.IsBalanced) {
118
+ throw new Error(`[MultiTenancy] Invalid ExtraFilter: ${check.Reason}`);
119
+ }
120
+ }
121
+ }
122
+
102
123
  /**
103
124
  * Creates a PreRunViewHook that auto-injects tenant WHERE clauses
104
125
  * into RunView queries for scoped entities.
@@ -124,7 +145,8 @@ export function createTenantPreRunViewHook(config: MultiTenancyConfig): PreRunVi
124
145
 
125
146
  // Inject the tenant filter
126
147
  if (params.ExtraFilter && typeof params.ExtraFilter === 'string' && params.ExtraFilter.trim().length > 0) {
127
- params.ExtraFilter = `(${params.ExtraFilter}) AND ${tenantFilter}`;
148
+ assertExtraFilterBalanced(params.ExtraFilter);
149
+ params.ExtraFilter = `(${params.ExtraFilter}) AND (${tenantFilter})`;
128
150
  } else {
129
151
  params.ExtraFilter = tenantFilter;
130
152
  }
@@ -0,0 +1,21 @@
1
+ import { LexSQL } from '@memberjunction/sql-parser';
2
+ import { SQLServerDialect } from '@memberjunction/sql-dialect';
3
+
4
+ const sqlServer = new SQLServerDialect();
5
+
6
+ /**
7
+ * Translates SQL Server bracket-quoted identifiers (`[Name]`) to PostgreSQL double-quoted ones
8
+ * (`"Name"`), for SQL written in SQL Server syntax that runs on PostgreSQL. Only identifiers
9
+ * change: brackets inside string literals and comments are left as written. A `]]` in a name
10
+ * becomes `]`, and a `"` in a name is doubled.
11
+ */
12
+ export function TranslateBracketsToPG(sql: string): string {
13
+ return LexSQL(sql, sqlServer)
14
+ .map(t => t.Kind === 'identifier' && t.Text.startsWith('[') ? toDoubleQuoted(t.Text) : t.Text)
15
+ .join('');
16
+ }
17
+
18
+ function toDoubleQuoted(bracketed: string): string {
19
+ const name = bracketed.slice(1, bracketed.endsWith(']') ? -1 : undefined).replace(/]]/g, ']');
20
+ return `"${name.replace(/"/g, '""')}"`;
21
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Connection settings for MJAPI's PostgreSQL pools, derived from the same `databaseSettings`
3
+ * values SQL Server reads, with the same defaults.
4
+ *
5
+ * | Setting | SQL Server | PostgreSQL |
6
+ * |--------------------------------------|----------------------------------|----------------------------------------------|
7
+ * | `requestTimeout` | driver request timeout | `statement_timeout` on every backend |
8
+ * | `connectionPool.max` / `min` | pool size | pool size |
9
+ * | `connectionPool.idleTimeoutMillis` | idle connection eviction | idle connection eviction |
10
+ * | `connectionPool.acquireTimeoutMillis`| wait for a pooled connection | `connectionTimeoutMillis` (see below) |
11
+ * | `connectionTimeout` | time to open a connection | not used |
12
+ *
13
+ * Where the two cannot match exactly:
14
+ * - The `pg` pool has one timeout covering both waiting for a free slot and opening a connection.
15
+ * It is fed from `acquireTimeoutMillis`; `connectionTimeout` is not used on PostgreSQL.
16
+ * - SQL Server's `requestTimeout` is a client-side timer over the whole request, including reading
17
+ * rows. PostgreSQL's `statement_timeout` is server-side execution time per statement, so a query
18
+ * that executes quickly and returns its rows slowly is not cut off on PostgreSQL.
19
+ * - `idle_in_transaction_session_timeout` has no SQL Server counterpart. It is derived as twice the
20
+ * statement timeout, so a transaction left open by an abandoned request is ended and its locks
21
+ * released.
22
+ * - MJAPI opens a raw compatibility pool and a provider pool, each up to `connectionPool.max`, plus
23
+ * a CodeGen pool of 10 and, when a read-only login is configured, a read-only pool of at most 10,
24
+ * where SQL Server opens one pool (and a read-only one). PostgreSQL's default `max_connections`
25
+ * is 100, so the read-only and CodeGen pools are kept small.
26
+ */
27
+ import type { PGConnectionConfig } from '@memberjunction/postgresql-dataprovider';
28
+ import type { Pool, PoolConfig } from 'pg';
29
+ import { BaseSingleton } from '@memberjunction/global';
30
+ import type { DatabaseSettingsInfo } from './config.js';
31
+
32
+ /** Statement timeout for the CodeGen/DDL pool, matching the SQL Server CodeGen pool. */
33
+ export const CODEGEN_STATEMENT_TIMEOUT_MS = 600000;
34
+
35
+ /** Pool size for the CodeGen/DDL pool. */
36
+ const CODEGEN_MAX_CONNECTIONS = 10;
37
+
38
+ /** Largest pool size for the read-only pool, which serves only caller-supplied SQL. */
39
+ const READ_ONLY_MAX_CONNECTIONS = 10;
40
+
41
+ /**
42
+ * Which pool the settings are for: API traffic, the read-only pool for caller-supplied SQL, or
43
+ * CodeGen and DDL work.
44
+ */
45
+ export type PostgreSQLPoolPurpose = 'api' | 'read-only' | 'codegen';
46
+
47
+ /** Where a PostgreSQL pool connects and as whom. */
48
+ export interface PostgreSQLEndpoint {
49
+ Host: string;
50
+ Port: number;
51
+ Database: string;
52
+ User: string;
53
+ Password: string;
54
+ }
55
+
56
+ /** Reads the PostgreSQL endpoint from `PG_*` environment variables, then `DB_*` ones. */
57
+ export function ResolvePostgreSQLEndpoint(env: NodeJS.ProcessEnv = process.env): PostgreSQLEndpoint {
58
+ return {
59
+ Host: env.PG_HOST || env.DB_HOST || 'localhost',
60
+ Port: parseInt(env.PG_PORT || env.DB_PORT || '5432', 10),
61
+ Database: env.PG_DATABASE || env.DB_DATABASE || '',
62
+ User: env.PG_USERNAME || env.DB_USERNAME || 'postgres',
63
+ Password: env.PG_PASSWORD || env.DB_PASSWORD || ''
64
+ };
65
+ }
66
+
67
+ /** The idle-in-transaction limit for a given statement timeout; 0 means none. */
68
+ export function IdleInTransactionTimeoutFor(statementTimeoutMs: number): number {
69
+ return statementTimeoutMs > 0 ? statementTimeoutMs * 2 : 0;
70
+ }
71
+
72
+ /** Builds the provider connection config for one of MJAPI's PostgreSQL pools. */
73
+ export function BuildPostgreSQLConnectionConfig(
74
+ endpoint: PostgreSQLEndpoint,
75
+ settings: DatabaseSettingsInfo,
76
+ purpose: PostgreSQLPoolPurpose
77
+ ): PGConnectionConfig {
78
+ const pool = settings.connectionPool;
79
+ if (purpose === 'codegen') {
80
+ return {
81
+ ...endpoint,
82
+ MaxConnections: CODEGEN_MAX_CONNECTIONS,
83
+ StatementTimeoutMs: CODEGEN_STATEMENT_TIMEOUT_MS,
84
+ IdleInTransactionSessionTimeoutMs: 0
85
+ };
86
+ }
87
+ const statementTimeoutMs = Math.max(0, settings.requestTimeout);
88
+ const apiMax = pool?.max ?? 50;
89
+ const apiMin = pool?.min ?? 5;
90
+ const readOnly = purpose === 'read-only';
91
+ return {
92
+ ...endpoint,
93
+ MaxConnections: readOnly ? Math.min(READ_ONLY_MAX_CONNECTIONS, apiMax) : apiMax,
94
+ MinConnections: readOnly ? 0 : apiMin,
95
+ IdleTimeoutMillis: pool?.idleTimeoutMillis ?? 30000,
96
+ ConnectionTimeoutMillis: pool?.acquireTimeoutMillis ?? 30000,
97
+ StatementTimeoutMs: statementTimeoutMs,
98
+ IdleInTransactionSessionTimeoutMs: IdleInTransactionTimeoutFor(statementTimeoutMs)
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Maps a provider connection config onto `pg.Pool` options, for the pools MJAPI creates directly
104
+ * rather than through the provider. Timeouts that are unset or 0 are left out.
105
+ */
106
+ export function ToPGPoolConfig(config: PGConnectionConfig): PoolConfig {
107
+ const pool: PoolConfig = {
108
+ host: config.Host,
109
+ port: config.Port,
110
+ database: config.Database,
111
+ user: config.User,
112
+ password: config.Password,
113
+ max: config.MaxConnections
114
+ };
115
+ if (config.MinConnections !== undefined) pool.min = config.MinConnections;
116
+ if (config.IdleTimeoutMillis !== undefined) pool.idleTimeoutMillis = config.IdleTimeoutMillis;
117
+ if (config.ConnectionTimeoutMillis !== undefined) pool.connectionTimeoutMillis = config.ConnectionTimeoutMillis;
118
+ if (config.StatementTimeoutMs) pool.statement_timeout = config.StatementTimeoutMs;
119
+ if (config.IdleInTransactionSessionTimeoutMs) pool.idle_in_transaction_session_timeout = config.IdleInTransactionSessionTimeoutMs;
120
+ return pool;
121
+ }
122
+
123
+ /** Credentials for a read-only database login. */
124
+ export interface ReadOnlyCredentials {
125
+ User: string;
126
+ Password: string;
127
+ }
128
+
129
+ /**
130
+ * The read-only login for PostgreSQL: the `dbReadOnlyUsername` / `dbReadOnlyPassword` settings
131
+ * SQL Server uses (which default from `DB_READ_ONLY_USERNAME` / `DB_READ_ONLY_PASSWORD`), then
132
+ * `PG_READ_ONLY_USERNAME` / `PG_READ_ONLY_PASSWORD`. `null` when neither pair is complete.
133
+ */
134
+ export function ResolvePostgreSQLReadOnlyCredentials(
135
+ configured: { dbReadOnlyUsername?: string; dbReadOnlyPassword?: string },
136
+ env: NodeJS.ProcessEnv = process.env
137
+ ): ReadOnlyCredentials | null {
138
+ if (configured.dbReadOnlyUsername && configured.dbReadOnlyPassword) {
139
+ return { User: configured.dbReadOnlyUsername, Password: configured.dbReadOnlyPassword };
140
+ }
141
+ if (env.PG_READ_ONLY_USERNAME && env.PG_READ_ONLY_PASSWORD) {
142
+ return { User: env.PG_READ_ONLY_USERNAME, Password: env.PG_READ_ONLY_PASSWORD };
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Holds MJAPI's PostgreSQL read-only pool, opened once at startup with the read-only login.
149
+ * Per-request read-only providers share it, so they connect as the read-only user rather than
150
+ * through the read-write pool.
151
+ */
152
+ export class PostgreSQLReadOnlyPool extends BaseSingleton<PostgreSQLReadOnlyPool> {
153
+ private _pool: Pool | null = null;
154
+
155
+ /** Use {@link PostgreSQLReadOnlyPool.Instance}. */
156
+ public constructor() {
157
+ super();
158
+ }
159
+
160
+ public static get Instance(): PostgreSQLReadOnlyPool {
161
+ return PostgreSQLReadOnlyPool.getInstance<PostgreSQLReadOnlyPool>();
162
+ }
163
+
164
+ /** The read-only pool, or `null` when no read-only login is configured. */
165
+ public get Pool(): Pool | null {
166
+ return this._pool;
167
+ }
168
+
169
+ public set Pool(pool: Pool | null) {
170
+ this._pool = pool;
171
+ }
172
+ }
173
+
174
+ /** What {@link DescribeReadOnlyLoginOverreach} reads about the read-only login. */
175
+ export interface ReadOnlyLoginGrants {
176
+ ReadsServerFiles: boolean;
177
+ ReadableBaseTables: number;
178
+ }
179
+
180
+ /** The one method {@link DescribeReadOnlyLoginOverreach} needs from a connection; a `pg` client has it. */
181
+ export interface ReadOnlyLoginCheckClient {
182
+ query(sql: string, params: unknown[]): Promise<{ rows: ReadOnlyLoginGrants[] }>; // case-violation-ok-legacy-back-compat: mirrors the pg client's query method, which a pg client must satisfy as-is
183
+ }
184
+
185
+ /**
186
+ * What the PostgreSQL read-only login can do beyond reading entity base views, as warnings to log at
187
+ * startup. Ad-hoc SQL and `TestQuerySQL` run as this login, and SQL can reach data through functions
188
+ * that no check of its table references sees (`query_to_xml`, file readers), so the login's own
189
+ * grants are the only complete limit. It should hold `SELECT` on the entity base views and nothing
190
+ * else: not on the base tables, and not membership of `pg_read_server_files`. Empty when the login
191
+ * is that narrow.
192
+ *
193
+ * @param client - A connection opened as the read-only login.
194
+ * @param coreSchema - The MJ core schema, whose base tables the login should not read.
195
+ */
196
+ export async function DescribeReadOnlyLoginOverreach(client: ReadOnlyLoginCheckClient, coreSchema: string): Promise<string[]> {
197
+ const result = await client.query(
198
+ `SELECT pg_has_role(current_user, 'pg_read_server_files', 'MEMBER') AS "ReadsServerFiles",
199
+ (SELECT count(*)::int FROM pg_catalog.pg_tables t
200
+ WHERE t.schemaname = $1
201
+ AND has_table_privilege(current_user, quote_ident(t.schemaname) || '.' || quote_ident(t.tablename), 'SELECT')) AS "ReadableBaseTables"`,
202
+ [coreSchema],
203
+ );
204
+ const row = result.rows[0];
205
+ const warnings: string[] = [];
206
+ if (row?.ReadableBaseTables > 0) {
207
+ warnings.push(`The PostgreSQL read-only login can read ${row.ReadableBaseTables} base table(s) in schema "${coreSchema}". Ad-hoc SQL and TestQuerySQL run as this login; grant it SELECT on the entity base views only.`);
208
+ }
209
+ if (row?.ReadsServerFiles) {
210
+ warnings.push('The PostgreSQL read-only login is a member of pg_read_server_files, so SQL run as it can read files on the database server. Revoke that membership.');
211
+ }
212
+ return warnings;
213
+ }
@@ -85,8 +85,8 @@ export async function resolveWidgetGuestRunContext(
85
85
  * A scoped anonymous session (`IsMagicLinkAnonymous` + `MagicLinkScope.ResourceID`) holds only the
86
86
  * narrow relay grants its invite's role carries — deliberately NOT the AI run entities, whose rows
87
87
  * leak the rendered system prompt. Unlike {@link resolveWidgetGuestRunContext} no widget instance is
88
- * required: on the realtime path the agent authority is already server-side (the session config's
89
- * `targetAgentID`, `CanRun`-gated at session start), so there is no client-supplied agent id to pin.
88
+ * required: the realtime target is the session config's `targetAgentID`, which the owner can edit, so
89
+ * the relay re-checks `CanRun` on it for the CALLER before this elevation.
90
90
  *
91
91
  * PUBLIC WEB-WIDGET guests are deliberately EXCLUDED (returned unchanged): their seeded role writes
92
92
  * run rows under the guest principal, which the `Widget Guest: Own Agent Runs` RLS read filter