@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
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/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
package/src/index.ts
CHANGED
|
@@ -31,6 +31,8 @@ import { WebSocketServer } from 'ws';
|
|
|
31
31
|
import { RealtimeProxyServer } from './realtimeProxy/RealtimeProxyServer.js';
|
|
32
32
|
import buildApolloServer from './apolloServer/index.js';
|
|
33
33
|
import { configInfo, configFilePath, dbDatabase, dbHost, dbPort, dbUsername, graphqlPort, graphqlRootPath, mj_core_schema, websiteRunFromPackage, RESTApiOptions } from './config.js';
|
|
34
|
+
import { TranslateBracketsToPG } from './postgresqlCompat.js';
|
|
35
|
+
import { BuildPostgreSQLConnectionConfig, DescribeReadOnlyLoginOverreach, PostgreSQLReadOnlyPool, ResolvePostgreSQLEndpoint, ResolvePostgreSQLReadOnlyCredentials, ToPGPoolConfig } from './postgresqlPoolSettings.js';
|
|
34
36
|
import { default as jwt } from 'jsonwebtoken';
|
|
35
37
|
import { contextFunction, CreateUnifiedAuthMiddleware, getUserPayload } from './context.js';
|
|
36
38
|
import { UserPayload } from './types.js';
|
|
@@ -67,12 +69,20 @@ import {
|
|
|
67
69
|
SetPushStatusPublishHook,
|
|
68
70
|
ParseReplicatedStatusUpdate,
|
|
69
71
|
} from './generic/PushStatusResolver.js';
|
|
72
|
+
import {
|
|
73
|
+
HANDOFF_OFFER_FANOUT_CHANNEL,
|
|
74
|
+
HANDOFF_OFFER_TOPIC,
|
|
75
|
+
SetHandoffOfferPublishHook,
|
|
76
|
+
ParseReplicatedHandoffOfferUpdate,
|
|
77
|
+
} from './resolvers/HumanHandoffResolver.js';
|
|
78
|
+
import { RoomHandoffEngine } from '@memberjunction/livekit-room-server';
|
|
70
79
|
import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
|
|
71
80
|
import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
|
|
72
81
|
import { RegisterRSUProgressBridge } from './integration/RSUProgressBridge.js';
|
|
73
82
|
import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
|
|
74
83
|
import { SessionJanitor } from './agentSessions/index.js';
|
|
75
84
|
import { StartTaskGraphDispatcher } from './services/StartTaskGraphDispatcher.js';
|
|
85
|
+
import { MJServerWorkQueueProviderSource, StartWorkQueueHost } from './services/WorkQueueHostService.js';
|
|
76
86
|
import { GetAttachmentService } from '@memberjunction/aiengine';
|
|
77
87
|
import { MJStorageBlobStore } from './services/MJStorageBlobStore.js';
|
|
78
88
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData, ConfigureRecordDataBroadcast } from './generic/CacheInvalidationResolver.js';
|
|
@@ -243,6 +253,7 @@ export * from './resolvers/CurrentUserContextResolver.js';
|
|
|
243
253
|
export * from './resolvers/RSUResolver.js';
|
|
244
254
|
export * from './resolvers/AgentSessionResolver.js';
|
|
245
255
|
export * from './resolvers/RealtimeClientSessionResolver.js';
|
|
256
|
+
export * from './resolvers/MeetingResolver.js';
|
|
246
257
|
export * from './resolvers/RemoteBrowserActionResolver.js';
|
|
247
258
|
export * from './agentSessions/index.js';
|
|
248
259
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
@@ -296,7 +307,8 @@ function resolveServerVersion(): string | undefined {
|
|
|
296
307
|
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
297
308
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string };
|
|
298
309
|
return pkg.version;
|
|
299
|
-
} catch {
|
|
310
|
+
} catch (err) {
|
|
311
|
+
LogError('Failed to resolve server version from package.json', undefined, err);
|
|
300
312
|
return undefined;
|
|
301
313
|
}
|
|
302
314
|
}
|
|
@@ -336,8 +348,9 @@ async function wirePushStatusFanOut(redisProvider: RedisLocalStorageProvider, st
|
|
|
336
348
|
message: payload.message,
|
|
337
349
|
SourceServerId: payload.SourceServerId,
|
|
338
350
|
});
|
|
339
|
-
} catch {
|
|
351
|
+
} catch (err) {
|
|
340
352
|
// A malformed message on a shared channel must not take down the subscriber.
|
|
353
|
+
LogError('Error processing push-status fan-out message', undefined, err);
|
|
341
354
|
}
|
|
342
355
|
});
|
|
343
356
|
|
|
@@ -356,6 +369,46 @@ async function wirePushStatusFanOut(redisProvider: RedisLocalStorageProvider, st
|
|
|
356
369
|
}
|
|
357
370
|
}
|
|
358
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Replicate handoff-offer updates across server instances over Redis.
|
|
374
|
+
*
|
|
375
|
+
* Outbound: every locally-published offer change is forwarded on a shared Redis channel.
|
|
376
|
+
* Inbound: a message from another instance is republished onto THIS instance's local GraphQL
|
|
377
|
+
* topic (where the subscription filter scopes to the target user) and delivered to RoomHandoffEngine
|
|
378
|
+
* so the call-hosting instance can transition its local flow (e.g. wait for the user to join the room).
|
|
379
|
+
*/
|
|
380
|
+
async function wireHandoffOfferFanOut(redisProvider: RedisLocalStorageProvider, startupLog: StartupLogger): Promise<void> {
|
|
381
|
+
try {
|
|
382
|
+
await redisProvider.SubscribeToChannel(HANDOFF_OFFER_FANOUT_CHANNEL, (raw: string) => {
|
|
383
|
+
try {
|
|
384
|
+
const payload = ParseReplicatedHandoffOfferUpdate(raw, MJGlobal.Instance.ProcessUUID);
|
|
385
|
+
if (!payload) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
PubSubManager.Instance.Publish(HANDOFF_OFFER_TOPIC, {
|
|
389
|
+
UserID: payload.UserID,
|
|
390
|
+
Kind: payload.Kind,
|
|
391
|
+
Offer: payload.Offer,
|
|
392
|
+
});
|
|
393
|
+
RoomHandoffEngine.Instance.OnRemoteOfferChange(payload);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// A malformed message on a shared channel must not take down the subscriber.
|
|
396
|
+
LogError('Error processing handoff-offer fan-out message', undefined, err);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
SetHandoffOfferPublishHook((payload) => {
|
|
401
|
+
redisProvider.PublishMessage(HANDOFF_OFFER_FANOUT_CHANNEL, JSON.stringify(payload));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
console.log('[MJAPI] Handoff-offer updates: cross-instance fan-out enabled via Redis');
|
|
405
|
+
startupLog.LogIf('verbose', 'Handoff-offer updates: cross-instance fan-out enabled via Redis');
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.warn(`Handoff-offer fan-out unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
359
412
|
// Bind MJStorage as the conversation-attachment blob store. The attachment service itself no longer
|
|
360
413
|
// imports `@memberjunction/storage` — that dependency made it unusable from any browser or React
|
|
361
414
|
// Native client, which is why the same attachment rules had been reimplemented three times.
|
|
@@ -402,25 +455,13 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
402
455
|
startupLog.BeginPhase('Connecting to database');
|
|
403
456
|
startupLog.LogIf('verbose', 'Database type: PostgreSQL');
|
|
404
457
|
const pg = await import('pg');
|
|
405
|
-
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData } = await import('@memberjunction/postgresql-dataprovider');
|
|
458
|
+
const { PostgreSQLDataProvider, PostgreSQLProviderConfigData, MJPostgresTypes } = await import('@memberjunction/postgresql-dataprovider');
|
|
406
459
|
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
const pgPool = new pg.default.Pool({
|
|
414
|
-
host: pgHost,
|
|
415
|
-
port: pgPort,
|
|
416
|
-
user: pgUser,
|
|
417
|
-
password: pgPass,
|
|
418
|
-
database: pgDatabase,
|
|
419
|
-
max: configInfo.databaseSettings.connectionPool?.max ?? 50,
|
|
420
|
-
min: configInfo.databaseSettings.connectionPool?.min ?? 5,
|
|
421
|
-
idleTimeoutMillis: configInfo.databaseSettings.connectionPool?.idleTimeoutMillis ?? 30000,
|
|
422
|
-
connectionTimeoutMillis: configInfo.databaseSettings.connectionPool?.acquireTimeoutMillis ?? 30000,
|
|
423
|
-
});
|
|
460
|
+
const pgEndpoint = ResolvePostgreSQLEndpoint();
|
|
461
|
+
const { Host: pgHost, Port: pgPort, User: pgUser, Database: pgDatabase } = pgEndpoint;
|
|
462
|
+
// Every API pool carries the statement and idle-in-transaction timeouts from connection #1.
|
|
463
|
+
const pgConnectionConfig = BuildPostgreSQLConnectionConfig(pgEndpoint, configInfo.databaseSettings, 'api');
|
|
464
|
+
const pgPool = new pg.default.Pool(ToPGPoolConfig(pgConnectionConfig));
|
|
424
465
|
|
|
425
466
|
// Verify connection
|
|
426
467
|
const testClient = await pgPool.connect();
|
|
@@ -428,6 +469,28 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
428
469
|
testClient.release();
|
|
429
470
|
startupLog.LogIf('verbose', `PostgreSQL pool connected to ${pgHost}:${pgPort}/${pgDatabase}`);
|
|
430
471
|
|
|
472
|
+
// A read-only pool opened with the read-only login, as SQL Server has. Read-only per-request
|
|
473
|
+
// providers (TestQuerySQL and other caller-supplied SQL) share it instead of the primary pool.
|
|
474
|
+
const pgReadOnlyCredentials = ResolvePostgreSQLReadOnlyCredentials(configInfo);
|
|
475
|
+
if (pgReadOnlyCredentials) {
|
|
476
|
+
// A pool a provider runs on must carry the provider's type parsers, so BIGINT and NUMERIC
|
|
477
|
+
// come back as numbers here as they do on the provider's own pool.
|
|
478
|
+
const readOnlyPgPool = new pg.default.Pool({
|
|
479
|
+
...ToPGPoolConfig(BuildPostgreSQLConnectionConfig({ ...pgEndpoint, ...pgReadOnlyCredentials }, configInfo.databaseSettings, 'read-only')),
|
|
480
|
+
types: MJPostgresTypes,
|
|
481
|
+
});
|
|
482
|
+
const readOnlyTestClient = await readOnlyPgPool.connect();
|
|
483
|
+
try {
|
|
484
|
+
for (const warning of await DescribeReadOnlyLoginOverreach(readOnlyTestClient, mj_core_schema)) {
|
|
485
|
+
LogStatus(`WARNING: ${warning}`);
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
readOnlyTestClient.release();
|
|
489
|
+
}
|
|
490
|
+
PostgreSQLReadOnlyPool.Instance.Pool = readOnlyPgPool;
|
|
491
|
+
startupLog.LogIf('verbose', 'Read-only PostgreSQL pool has been initialized.');
|
|
492
|
+
}
|
|
493
|
+
|
|
431
494
|
// Create a DataSourceInfo with a MSSQL-compatible wrapper around pg.Pool
|
|
432
495
|
// This allows existing code (types, util, context) to work without changes
|
|
433
496
|
const mssqlCompatPool = createMSSQLCompatPool(pgPool);
|
|
@@ -441,15 +504,6 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
441
504
|
}));
|
|
442
505
|
|
|
443
506
|
// Set up the PostgreSQL provider
|
|
444
|
-
const pgConnectionConfig = {
|
|
445
|
-
Host: pgHost,
|
|
446
|
-
Port: pgPort,
|
|
447
|
-
Database: pgDatabase,
|
|
448
|
-
User: pgUser,
|
|
449
|
-
Password: pgPass,
|
|
450
|
-
MaxConnections: configInfo.databaseSettings.connectionPool?.max ?? 50,
|
|
451
|
-
MinConnections: configInfo.databaseSettings.connectionPool?.min ?? 5,
|
|
452
|
-
};
|
|
453
507
|
const pgConfigData = new PostgreSQLProviderConfigData(
|
|
454
508
|
pgConnectionConfig,
|
|
455
509
|
mj_core_schema,
|
|
@@ -483,7 +537,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
483
537
|
if (poolAny._pgPool) {
|
|
484
538
|
const thePgPool = poolAny._pgPool as import('pg').Pool;
|
|
485
539
|
// Translate SQL Server bracket syntax to PostgreSQL double-quote syntax
|
|
486
|
-
const pgQuery =
|
|
540
|
+
const pgQuery = TranslateBracketsToPG(query);
|
|
487
541
|
const result = await thePgPool.query(pgQuery);
|
|
488
542
|
return result.rows;
|
|
489
543
|
}
|
|
@@ -502,21 +556,21 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
502
556
|
const pgCodegenPass = process.env.CODEGEN_DB_PASSWORD;
|
|
503
557
|
if (pgCodegenUser && pgCodegenPass) {
|
|
504
558
|
try {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
559
|
+
// CodeGen and DDL work runs long statements across every entity, so this pool gets the
|
|
560
|
+
// long CodeGen timeout rather than the API one, as the SQL Server CodeGen pool does.
|
|
561
|
+
const codegenPgConnectionConfig = BuildPostgreSQLConnectionConfig(
|
|
562
|
+
{ ...pgEndpoint, User: pgCodegenUser, Password: pgCodegenPass },
|
|
563
|
+
configInfo.databaseSettings,
|
|
564
|
+
'codegen',
|
|
565
|
+
);
|
|
566
|
+
const codegenPgPool = new pg.default.Pool(ToPGPoolConfig(codegenPgConnectionConfig));
|
|
513
567
|
const codegenTestClient = await codegenPgPool.connect();
|
|
514
568
|
await codegenTestClient.query('SELECT 1');
|
|
515
569
|
codegenTestClient.release();
|
|
516
570
|
|
|
517
571
|
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
518
572
|
const codegenPgConfigData = new PostgreSQLProviderConfigData(
|
|
519
|
-
|
|
573
|
+
codegenPgConnectionConfig,
|
|
520
574
|
mj_core_schema,
|
|
521
575
|
cacheRefreshInterval / 1000, // ms → seconds
|
|
522
576
|
);
|
|
@@ -792,6 +846,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
792
846
|
// forever for an event that was delivered to nobody. Replicating progress and completion over
|
|
793
847
|
// Redis closes that, and the durable tail query remains the backstop if Redis is down.
|
|
794
848
|
await wirePushStatusFanOut(redisProvider, startupLog);
|
|
849
|
+
await wireHandoffOfferFanOut(redisProvider, startupLog);
|
|
795
850
|
|
|
796
851
|
startupLog.LogIf('verbose', `Redis cache provider connected: ${process.env.REDIS_URL}`);
|
|
797
852
|
}
|
|
@@ -1367,13 +1422,22 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1367
1422
|
// Backwards-compatibility shim: synthesize ServerExtensionConfig entries from legacy configInfo.telephony
|
|
1368
1423
|
const telephonyExtensionConfigs: ServerExtensionConfig[] = [];
|
|
1369
1424
|
if (configInfo.telephony?.enabled) {
|
|
1425
|
+
// Settings every carrier shares (inbound run-as user, call cap, outbound policy). Each carrier's own block is
|
|
1426
|
+
// spread AFTER them, so a carrier can override one explicitly.
|
|
1427
|
+
const sharedTelephonySettings: Record<string, unknown> = {
|
|
1428
|
+
inboundRunAsUserEmail: configInfo.telephony.inboundRunAsUserEmail,
|
|
1429
|
+
maxCallSeconds: configInfo.telephony.maxCallSeconds,
|
|
1430
|
+
maxConcurrentCalls: configInfo.telephony.maxConcurrentCalls,
|
|
1431
|
+
transferTargets: configInfo.telephony.transferTargets,
|
|
1432
|
+
outbound: configInfo.telephony.outbound,
|
|
1433
|
+
};
|
|
1370
1434
|
if (configInfo.telephony.twilio) {
|
|
1371
1435
|
telephonyExtensionConfigs.push({
|
|
1372
1436
|
Enabled: true,
|
|
1373
1437
|
DriverClass: 'TwilioTelephonyExtension',
|
|
1374
1438
|
RootPath: '/telephony/twilio',
|
|
1375
1439
|
Phase: 'pre-auth',
|
|
1376
|
-
Settings: configInfo.telephony.twilio
|
|
1440
|
+
Settings: { ...sharedTelephonySettings, ...configInfo.telephony.twilio },
|
|
1377
1441
|
});
|
|
1378
1442
|
}
|
|
1379
1443
|
if (configInfo.telephony.vonage) {
|
|
@@ -1382,7 +1446,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1382
1446
|
DriverClass: 'VonageTelephonyExtension',
|
|
1383
1447
|
RootPath: '/telephony/vonage',
|
|
1384
1448
|
Phase: 'pre-auth',
|
|
1385
|
-
Settings: configInfo.telephony.vonage
|
|
1449
|
+
Settings: { ...sharedTelephonySettings, ...configInfo.telephony.vonage },
|
|
1386
1450
|
});
|
|
1387
1451
|
}
|
|
1388
1452
|
if (configInfo.telephony.ringcentral) {
|
|
@@ -1391,7 +1455,16 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1391
1455
|
DriverClass: 'RingCentralTelephonyExtension',
|
|
1392
1456
|
RootPath: '/telephony/ringcentral',
|
|
1393
1457
|
Phase: 'pre-auth',
|
|
1394
|
-
Settings: configInfo.telephony.ringcentral
|
|
1458
|
+
Settings: { ...sharedTelephonySettings, ...configInfo.telephony.ringcentral },
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
if (configInfo.telephony.livekitSip) {
|
|
1462
|
+
telephonyExtensionConfigs.push({
|
|
1463
|
+
Enabled: true,
|
|
1464
|
+
DriverClass: 'LiveKitSipExtension',
|
|
1465
|
+
RootPath: '/telephony/livekit-sip',
|
|
1466
|
+
Phase: 'pre-auth',
|
|
1467
|
+
Settings: { ...sharedTelephonySettings, ...configInfo.telephony.livekitSip },
|
|
1395
1468
|
});
|
|
1396
1469
|
}
|
|
1397
1470
|
if (configInfo.telephony.teams?.enabled) {
|
|
@@ -1723,6 +1796,18 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1723
1796
|
.catch(err => console.warn(`[TaskGraphDispatcher] Startup failed: ${err}`));
|
|
1724
1797
|
}
|
|
1725
1798
|
|
|
1799
|
+
// Start the durable work-queue host where enabled. It plans which subscriptions this instance runs,
|
|
1800
|
+
// re-plans on a timer, and self-registers with ShutdownRegistry, so gracefulShutdown's awaited
|
|
1801
|
+
// ShutdownAll() drains it (up to 2 × shutdownDrainMs) before the HTTP server closes.
|
|
1802
|
+
// Not awaited: a slow engine load must not delay readiness, and a failure never stops the API.
|
|
1803
|
+
const workQueueProvider = Metadata.Provider; // global-provider-ok: server startup — the work-queue host runs on the server's own provider
|
|
1804
|
+
if (configInfo.workQueue?.enabled && workQueueProvider instanceof DatabaseProviderBase) {
|
|
1805
|
+
const workQueuePool = dataSources[0]?.dataSource;
|
|
1806
|
+
const providerSource = new MJServerWorkQueueProviderSource(workQueuePool instanceof sql.ConnectionPool ? workQueuePool : null, workQueueProvider);
|
|
1807
|
+
StartWorkQueueHost(configInfo.workQueue, workQueueProvider, providerSource)
|
|
1808
|
+
.catch(error => console.error('❌ Failed to start the work queue host:', error));
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1726
1811
|
|
|
1727
1812
|
// Set up graceful shutdown handlers
|
|
1728
1813
|
const gracefulShutdown = async (signal: string) => {
|
|
@@ -2206,11 +2291,3 @@ function createMSSQLCompatPool(pgPool: import('pg').Pool): sql.ConnectionPool {
|
|
|
2206
2291
|
return wrapper as unknown as sql.ConnectionPool;
|
|
2207
2292
|
}
|
|
2208
2293
|
|
|
2209
|
-
/**
|
|
2210
|
-
* Translates SQL Server bracket-quoted identifiers to PostgreSQL double-quoted identifiers.
|
|
2211
|
-
* Converts [schema].[table] to "schema"."table" and handles common T-SQL patterns.
|
|
2212
|
-
*/
|
|
2213
|
-
function translateBracketsToPG(sql: string): string {
|
|
2214
|
-
// Replace [identifier] with "identifier"
|
|
2215
|
-
return sql.replace(/\[([^\]]+)\]/g, '"$1"');
|
|
2216
|
-
}
|
|
@@ -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
|
+
}
|