@memberjunction/server 5.38.0 → 5.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/apolloServer/index.d.ts +0 -8
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +61 -0
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/config.d.ts +68 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -21
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +75 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +314 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
- package/dist/generic/FireAndForgetHeartbeat.js +44 -0
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/NoLog.d.ts +50 -0
- package/dist/logging/NoLog.d.ts.map +1 -0
- package/dist/logging/NoLog.js +80 -0
- package/dist/logging/NoLog.js.map +1 -0
- package/dist/logging/bootAudit.d.ts +43 -0
- package/dist/logging/bootAudit.d.ts.map +1 -0
- package/dist/logging/bootAudit.js +83 -0
- package/dist/logging/bootAudit.js.map +1 -0
- package/dist/logging/boundaryLogPayload.d.ts +18 -0
- package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
- package/dist/logging/boundaryLogPayload.js +18 -0
- package/dist/logging/boundaryLogPayload.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +23 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -0
- package/dist/logging/secretRedactor.js +53 -0
- package/dist/logging/secretRedactor.js.map +1 -0
- package/dist/logging/shortenForLog.d.ts +8 -0
- package/dist/logging/shortenForLog.d.ts.map +1 -0
- package/dist/logging/shortenForLog.js +21 -0
- package/dist/logging/shortenForLog.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.js +127 -0
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +8 -4
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
- package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
- package/dist/resolvers/IntegrationProgressResolver.js +196 -0
- package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +19 -14
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +26 -5
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +12 -2
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
- package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchEntitiesResolver.js +216 -0
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -1
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +14 -2
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +75 -74
- package/src/__tests__/NoLog.test.ts +76 -0
- package/src/__tests__/bootAudit.test.ts +188 -0
- package/src/__tests__/boundaryLogPayload.test.ts +31 -0
- package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
- package/src/__tests__/secretRedactor.test.ts +163 -0
- package/src/__tests__/subscriptionRedaction.test.ts +217 -0
- package/src/apolloServer/index.ts +58 -0
- package/src/config.ts +27 -0
- package/src/context.ts +2 -19
- package/src/generated/generated.ts +239 -1
- package/src/generic/FireAndForgetHeartbeat.ts +85 -0
- package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
- package/src/index.ts +137 -2
- package/src/logging/NoLog.ts +88 -0
- package/src/logging/bootAudit.ts +117 -0
- package/src/logging/boundaryLogPayload.ts +19 -0
- package/src/logging/secretRedactor.ts +82 -0
- package/src/logging/shortenForLog.ts +17 -0
- package/src/logging/variablesLoggingMiddleware.ts +191 -0
- package/src/resolvers/GetDataResolver.ts +9 -5
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
- package/src/resolvers/IntegrationProgressResolver.ts +220 -0
- package/src/resolvers/MCPResolver.ts +18 -14
- package/src/resolvers/RunAIAgentResolver.ts +28 -5
- package/src/resolvers/RunTestResolver.ts +14 -2
- package/src/resolvers/SearchEntitiesResolver.ts +173 -0
- package/src/resolvers/VectorizeEntityResolver.ts +14 -1
- package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
- package/src/services/ScheduledJobsService.ts +15 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { PubSubEngine } from 'type-graphql';
|
|
4
|
+
import {
|
|
5
|
+
startLivenessPulse,
|
|
6
|
+
DEFAULT_PULSE_INTERVAL_MS,
|
|
7
|
+
HEARTBEAT_MESSAGE_TYPE,
|
|
8
|
+
} from '../FireAndForgetHeartbeat';
|
|
9
|
+
import { PUSH_STATUS_UPDATES_TOPIC } from '../PushStatusResolver';
|
|
10
|
+
|
|
11
|
+
/** Minimal PubSubEngine stub capturing publish calls. */
|
|
12
|
+
function makePubSub() {
|
|
13
|
+
const publish = vi.fn().mockResolvedValue(undefined);
|
|
14
|
+
return { publish } as unknown as PubSubEngine & { publish: ReturnType<typeof vi.fn> };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseLastMessage(pubSub: { publish: ReturnType<typeof vi.fn> }) {
|
|
18
|
+
const [topic, payload] = pubSub.publish.mock.calls[pubSub.publish.mock.calls.length - 1];
|
|
19
|
+
return { topic, payload, parsed: JSON.parse(payload.message) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('startLivenessPulse', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.useFakeTimers();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.useRealTimers();
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not publish before the first interval elapses', () => {
|
|
33
|
+
const pubSub = makePubSub();
|
|
34
|
+
startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver' });
|
|
35
|
+
vi.advanceTimersByTime(DEFAULT_PULSE_INTERVAL_MS - 1);
|
|
36
|
+
expect(pubSub.publish).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('publishes a heartbeat on the push-status topic each interval', () => {
|
|
40
|
+
const pubSub = makePubSub();
|
|
41
|
+
startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver', intervalMs: 1000 });
|
|
42
|
+
|
|
43
|
+
vi.advanceTimersByTime(3000);
|
|
44
|
+
|
|
45
|
+
expect(pubSub.publish).toHaveBeenCalledTimes(3);
|
|
46
|
+
const { topic, payload, parsed } = parseLastMessage(pubSub);
|
|
47
|
+
expect(topic).toBe(PUSH_STATUS_UPDATES_TOPIC);
|
|
48
|
+
expect(payload.sessionId).toBe('s1');
|
|
49
|
+
expect(parsed.resolver).toBe('TestResolver');
|
|
50
|
+
expect(parsed.type).toBe(HEARTBEAT_MESSAGE_TYPE);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('includes the enriched status snapshot from readStatus', () => {
|
|
54
|
+
const pubSub = makePubSub();
|
|
55
|
+
startLivenessPulse({
|
|
56
|
+
pubSub,
|
|
57
|
+
sessionId: 's1',
|
|
58
|
+
resolver: 'RunAIAgentResolver',
|
|
59
|
+
intervalMs: 1000,
|
|
60
|
+
readStatus: () => ({ runId: 'run-123', status: 'Running', currentStep: 'Prompt' }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
vi.advanceTimersByTime(1000);
|
|
64
|
+
|
|
65
|
+
const { parsed } = parseLastMessage(pubSub);
|
|
66
|
+
expect(parsed.data).toEqual({ runId: 'run-123', status: 'Running', currentStep: 'Prompt' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('publishes an empty data object when readStatus throws (loop survives)', () => {
|
|
70
|
+
const pubSub = makePubSub();
|
|
71
|
+
startLivenessPulse({
|
|
72
|
+
pubSub,
|
|
73
|
+
sessionId: 's1',
|
|
74
|
+
resolver: 'RunAIAgentResolver',
|
|
75
|
+
intervalMs: 1000,
|
|
76
|
+
readStatus: () => {
|
|
77
|
+
throw new Error('transient read failure');
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
vi.advanceTimersByTime(2000);
|
|
82
|
+
|
|
83
|
+
expect(pubSub.publish).toHaveBeenCalledTimes(2);
|
|
84
|
+
const { parsed } = parseLastMessage(pubSub);
|
|
85
|
+
expect(parsed.data).toEqual({});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('stops publishing after stop() is called', () => {
|
|
89
|
+
const pubSub = makePubSub();
|
|
90
|
+
const handle = startLivenessPulse({ pubSub, sessionId: 's1', resolver: 'TestResolver', intervalMs: 1000 });
|
|
91
|
+
|
|
92
|
+
vi.advanceTimersByTime(2000);
|
|
93
|
+
expect(pubSub.publish).toHaveBeenCalledTimes(2);
|
|
94
|
+
|
|
95
|
+
handle.stop();
|
|
96
|
+
vi.advanceTimersByTime(5000);
|
|
97
|
+
expect(pubSub.publish).toHaveBeenCalledTimes(2);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ dotenv.config({ quiet: true });
|
|
|
4
4
|
|
|
5
5
|
import { expressMiddleware } from '@as-integrations/express5';
|
|
6
6
|
import { mergeSchemas } from '@graphql-tools/schema';
|
|
7
|
-
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView } from '@memberjunction/core';
|
|
7
|
+
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase } from '@memberjunction/core';
|
|
8
8
|
import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
9
9
|
import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
|
|
10
10
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
@@ -30,6 +30,8 @@ import { default as jwt } from 'jsonwebtoken';
|
|
|
30
30
|
import { contextFunction, createUnifiedAuthMiddleware, getUserPayload } from './context.js';
|
|
31
31
|
import { UserPayload } from './types.js';
|
|
32
32
|
import { requireSystemUserDirective, publicDirective } from './directives/index.js';
|
|
33
|
+
import { variablesLoggingMiddleware } from './logging/variablesLoggingMiddleware.js';
|
|
34
|
+
import { auditResolversForUndecoratedArgs } from './logging/bootAudit.js';
|
|
33
35
|
import createMSSQLConfig from './orm.js';
|
|
34
36
|
import { setupRESTEndpoints } from './rest/setupRESTEndpoints.js';
|
|
35
37
|
import { createOAuthCallbackHandler } from './rest/OAuthCallbackHandler.js';
|
|
@@ -45,7 +47,9 @@ import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
|
45
47
|
import { RedisLocalStorageProvider } from '@memberjunction/redis-provider';
|
|
46
48
|
import { GenericDatabaseProvider } from '@memberjunction/generic-database-provider';
|
|
47
49
|
import { PubSubManager } from './generic/PubSubManager.js';
|
|
48
|
-
import {
|
|
50
|
+
import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
|
|
51
|
+
import { PublishIntegrationProgress } from './resolvers/IntegrationProgressResolver.js';
|
|
52
|
+
import { ClientToolRequestManager, AgentRunWatchdog } from '@memberjunction/ai-agents';
|
|
49
53
|
import { CACHE_INVALIDATION_TOPIC } from './generic/CacheInvalidationResolver.js';
|
|
50
54
|
import { ConnectorFactory, IntegrationEngine, IntegrationSyncOptions } from '@memberjunction/integration-engine';
|
|
51
55
|
import { CronExpressionHelper } from '@memberjunction/scheduling-engine';
|
|
@@ -83,6 +87,7 @@ export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
|
83
87
|
export { ServerExtensionLoader, BaseServerExtension } from '@memberjunction/server-extensions-core';
|
|
84
88
|
export type { ServerExtensionConfig, ExtensionInitResult, ExtensionHealthResult } from '@memberjunction/server-extensions-core';
|
|
85
89
|
export * from './directives/index.js';
|
|
90
|
+
export { NoLog, hasNoLogParameter, getNoLogFields } from './logging/NoLog.js';
|
|
86
91
|
export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
|
|
87
92
|
export * from './types.js';
|
|
88
93
|
export {
|
|
@@ -107,6 +112,7 @@ export * from './resolvers/SearchKnowledgeStreamResolver.js';
|
|
|
107
112
|
export * from './resolvers/AvailableSearchProvidersResolver.js';
|
|
108
113
|
export * from './resolvers/FetchEntityVectorsResolver.js';
|
|
109
114
|
export * from './resolvers/PipelineProgressResolver.js';
|
|
115
|
+
export * from './resolvers/IntegrationProgressResolver.js';
|
|
110
116
|
export * from './resolvers/ClientToolRequestResolver.js';
|
|
111
117
|
export * from './resolvers/AutotagPipelineResolver.js';
|
|
112
118
|
export * from './resolvers/TagGovernanceResolver.js';
|
|
@@ -150,6 +156,7 @@ export * from './resolvers/FileResolver.js';
|
|
|
150
156
|
export * from './resolvers/InfoResolver.js';
|
|
151
157
|
export * from './resolvers/PotentialDuplicateRecordResolver.js';
|
|
152
158
|
export * from './resolvers/RunTestResolver.js';
|
|
159
|
+
export * from './resolvers/SearchEntitiesResolver.js';
|
|
153
160
|
export * from './resolvers/UserFavoriteResolver.js';
|
|
154
161
|
export * from './resolvers/UserResolver.js';
|
|
155
162
|
export * from './resolvers/UserViewResolver.js';
|
|
@@ -294,6 +301,86 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
294
301
|
return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
|
|
295
302
|
};
|
|
296
303
|
|
|
304
|
+
// Set up CodeGen-credentialed provider + in-process CodeGen runner for RSU (PostgreSQL).
|
|
305
|
+
// Without this, RuntimeSchemaManager has no injected runner and falls back to spawning a
|
|
306
|
+
// child-process CodeGen. On Postgres that child path is slow, its output is buffered (so
|
|
307
|
+
// progress is unobservable), and a non-responding advancedGen AI pass hangs it indefinitely
|
|
308
|
+
// — which leaves association/junction CRUD functions un-regenerated and blocks the sync.
|
|
309
|
+
// Running in-process keeps CodeGen inside the MJAPI event loop where it is observable and
|
|
310
|
+
// bounded by advanced_generation's per-call timeout + circuit breaker. Mirrors the SQL
|
|
311
|
+
// Server path below (the runner is required for BOTH platforms).
|
|
312
|
+
const pgCodegenUser = process.env.CODEGEN_DB_USERNAME;
|
|
313
|
+
const pgCodegenPass = process.env.CODEGEN_DB_PASSWORD;
|
|
314
|
+
if (pgCodegenUser && pgCodegenPass) {
|
|
315
|
+
try {
|
|
316
|
+
const codegenPgPool = new pg.default.Pool({
|
|
317
|
+
host: pgHost,
|
|
318
|
+
port: pgPort,
|
|
319
|
+
user: pgCodegenUser,
|
|
320
|
+
password: pgCodegenPass,
|
|
321
|
+
database: pgDatabase,
|
|
322
|
+
max: 10,
|
|
323
|
+
});
|
|
324
|
+
const codegenTestClient = await codegenPgPool.connect();
|
|
325
|
+
await codegenTestClient.query('SELECT 1');
|
|
326
|
+
codegenTestClient.release();
|
|
327
|
+
|
|
328
|
+
const { RuntimeSchemaManager } = await import('@memberjunction/schema-engine');
|
|
329
|
+
const codegenPgConfigData = new PostgreSQLProviderConfigData(
|
|
330
|
+
{ Host: pgHost, Port: pgPort, Database: pgDatabase, User: pgCodegenUser, Password: pgCodegenPass },
|
|
331
|
+
mj_core_schema,
|
|
332
|
+
cacheRefreshInterval / 1000, // ms → seconds
|
|
333
|
+
);
|
|
334
|
+
const codegenPgProvider = new PostgreSQLDataProvider();
|
|
335
|
+
await codegenPgProvider.Config(codegenPgConfigData); // separate pool (per-instance manager); does NOT touch the global API provider
|
|
336
|
+
RuntimeSchemaManager.Instance.SetDDLProvider(codegenPgProvider);
|
|
337
|
+
console.log('RSU DDL provider initialized with CodeGen credentials (PostgreSQL).');
|
|
338
|
+
|
|
339
|
+
// Set up in-process CodeGen runner for RSU
|
|
340
|
+
try {
|
|
341
|
+
const { RunCodeGenBase } = await import('@memberjunction/codegen-lib');
|
|
342
|
+
const { PostgreSQLCodeGenConnection } = await import('@memberjunction/codegen-lib/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js');
|
|
343
|
+
|
|
344
|
+
const codegenConnection = new PostgreSQLCodeGenConnection(codegenPgPool);
|
|
345
|
+
const codegenCurrentUser = UserCache.Instance.Users.find(u => u.Type?.trim().toLowerCase() === 'owner') ?? UserCache.Instance.Users[0];
|
|
346
|
+
|
|
347
|
+
const codegenDataSource = {
|
|
348
|
+
provider: codegenPgProvider,
|
|
349
|
+
connection: codegenConnection,
|
|
350
|
+
currentUser: codegenCurrentUser,
|
|
351
|
+
connectionInfo: `${pgHost}:${pgPort}/${pgDatabase} (CodeGen)`,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const runObject = MJGlobal.Instance.ClassFactory.CreateInstance(RunCodeGenBase) as InstanceType<typeof RunCodeGenBase>;
|
|
355
|
+
|
|
356
|
+
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
357
|
+
RuntimeSchemaManager.Instance.SetCodeGenRunner({
|
|
358
|
+
RunInProcess: (skipDB) => runObject.RunInProcess(codegenDataSource, skipDB, rsuWorkDir),
|
|
359
|
+
});
|
|
360
|
+
console.log('RSU in-process CodeGen runner initialized (PostgreSQL).');
|
|
361
|
+
|
|
362
|
+
// Inject CodeGen output paths for targeted git staging
|
|
363
|
+
const { initializeConfig } = await import('@memberjunction/codegen-lib');
|
|
364
|
+
const cgConfig = initializeConfig(rsuWorkDir);
|
|
365
|
+
const outputPaths = (cgConfig.output ?? []).map((o: { directory: string }) => o.directory);
|
|
366
|
+
RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
|
|
367
|
+
console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
|
|
368
|
+
|
|
369
|
+
// Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
|
|
370
|
+
// `additionalSchemaInfo`), or RSU writes soft PKs to its own default path while
|
|
371
|
+
// CodeGen reads a different one and skips integration tables with "No primary key found".
|
|
372
|
+
if (cgConfig.additionalSchemaInfo) {
|
|
373
|
+
RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(cgConfig.additionalSchemaInfo);
|
|
374
|
+
console.log(`RSU additionalSchemaInfo path: ${cgConfig.additionalSchemaInfo}`);
|
|
375
|
+
}
|
|
376
|
+
} catch (codegenErr) {
|
|
377
|
+
console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
console.warn(`RSU DDL provider setup failed (PostgreSQL; RSU will fall back to default provider): ${(err as Error).message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
297
384
|
const md = new Metadata(); // global-provider-ok: bootstrap
|
|
298
385
|
console.log(`Data Source has been initialized. ${md?.Entities ? md.Entities.length : 0} entities loaded.`);
|
|
299
386
|
} else {
|
|
@@ -348,6 +435,13 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
348
435
|
...createMSSQLConfig(),
|
|
349
436
|
user: codegenUser,
|
|
350
437
|
password: codegenPass,
|
|
438
|
+
// CodeGen's metadata management ("manage entity fields", schema refresh) runs
|
|
439
|
+
// long-running queries across ALL entities. The default API requestTimeout (30s)
|
|
440
|
+
// is far too short at scale — e.g. a large integration Create-Tables pushing the
|
|
441
|
+
// schema to 400+ entities times out mid-refresh and leaves entity-field metadata
|
|
442
|
+
// only partially applied. This pool is used ONLY for CodeGen/DDL (never to serve
|
|
443
|
+
// API requests), so a generous timeout is safe. Mirrors MJCLI's baseline connection.
|
|
444
|
+
requestTimeout: 600000,
|
|
351
445
|
});
|
|
352
446
|
codegenPool.on('error', (err) => {
|
|
353
447
|
console.error('[ConnectionPool] CodeGen pool connection error:', err.message);
|
|
@@ -390,6 +484,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
390
484
|
const outputPaths = (codegenConfig.output ?? []).map((o: { directory: string }) => o.directory);
|
|
391
485
|
RuntimeSchemaManager.Instance.SetCodeGenOutputPaths(outputPaths);
|
|
392
486
|
console.log(`RSU CodeGen output paths: ${outputPaths.length} directories configured.`);
|
|
487
|
+
|
|
488
|
+
// Point RSU's soft PK/FK writer at the SAME file CodeGen reads (mj.config.cjs
|
|
489
|
+
// `additionalSchemaInfo`). Without this, RSU writes soft PKs to its own default
|
|
490
|
+
// path while CodeGen reads a different one and skips every integration table
|
|
491
|
+
// with "No primary key found".
|
|
492
|
+
if (codegenConfig.additionalSchemaInfo) {
|
|
493
|
+
RuntimeSchemaManager.Instance.SetAdditionalSchemaInfoPath(codegenConfig.additionalSchemaInfo);
|
|
494
|
+
console.log(`RSU additionalSchemaInfo path: ${codegenConfig.additionalSchemaInfo}`);
|
|
495
|
+
}
|
|
393
496
|
} catch (codegenErr) {
|
|
394
497
|
console.warn(`RSU in-process CodeGen runner setup failed (will fall back to child process): ${(codegenErr as Error).message}`);
|
|
395
498
|
}
|
|
@@ -620,6 +723,24 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
620
723
|
(topic: string, payload: Record<string, unknown>) => PubSubManager.Instance.Publish(topic, payload)
|
|
621
724
|
);
|
|
622
725
|
|
|
726
|
+
// §11: fan every integration progress event onto the live GraphQL subscription topic. The emitter
|
|
727
|
+
// (in @memberjunction/integration-progress-artifacts) stays server-agnostic; we inject the publish
|
|
728
|
+
// here so integration runs/refreshes/syncs are subscribable in addition to the durable JSONL
|
|
729
|
+
// artifact + the pollable IntegrationTailRunEvents query.
|
|
730
|
+
IntegrationProgressEmitter.SetPublishHook((manifest, event) =>
|
|
731
|
+
PublishIntegrationProgress({
|
|
732
|
+
RunID: manifest.runID,
|
|
733
|
+
Kind: manifest.runKind,
|
|
734
|
+
CompanyIntegrationID: manifest.companyIntegrationID,
|
|
735
|
+
EventType: event.eventType,
|
|
736
|
+
Seq: event.seq,
|
|
737
|
+
Message: event.message,
|
|
738
|
+
Stage: event.stage,
|
|
739
|
+
Level: event.level,
|
|
740
|
+
Data: event.data,
|
|
741
|
+
})
|
|
742
|
+
);
|
|
743
|
+
|
|
623
744
|
// Global listener: broadcast CACHE_INVALIDATION to all browser clients whenever
|
|
624
745
|
// ANY BaseEntity save/delete occurs on this server — regardless of whether it
|
|
625
746
|
// originated from a GraphQL mutation or internal server-side code (agents, actions,
|
|
@@ -652,10 +773,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
652
773
|
scalarsMap: [{ type: Date, scalar: GraphQLTimestamp }],
|
|
653
774
|
emitSchemaFile: websiteRunFromPackage !== 1,
|
|
654
775
|
pubSub,
|
|
776
|
+
globalMiddlewares: [variablesLoggingMiddleware],
|
|
655
777
|
}),
|
|
656
778
|
],
|
|
657
779
|
typeDefs: [requireSystemUserDirective.typeDefs, publicDirective.typeDefs],
|
|
658
780
|
});
|
|
781
|
+
|
|
782
|
+
// Verbose-mode-only diagnostic: name custom-resolver args that aren't metadata-bound
|
|
783
|
+
// and aren't @NoLog-marked. No-op in default config (logVariables=false).
|
|
784
|
+
auditResolversForUndecoratedArgs();
|
|
659
785
|
schema = requireSystemUserDirective.transformer(schema);
|
|
660
786
|
schema = publicDirective.transformer(schema);
|
|
661
787
|
|
|
@@ -929,6 +1055,15 @@ export const serve = async (resolverPaths: Array<string>, app: Application = cre
|
|
|
929
1055
|
.catch(err => console.warn(`[IntegrationEngine] Orphaned sync resume failed: ${err}`));
|
|
930
1056
|
}
|
|
931
1057
|
|
|
1058
|
+
// Force-fail any agent runs left 'Running' by a process that died (restart/crash/OOM) or whose
|
|
1059
|
+
// terminal-state write never landed. Staleness-based, so it never touches runs another healthy
|
|
1060
|
+
// instance is still heart-beating. The watchdog also self-registers for graceful-shutdown
|
|
1061
|
+
// cancellation (via ShutdownRegistry) once it begins tracking this process's first live run.
|
|
1062
|
+
if (resumeUser && Metadata.Provider instanceof DatabaseProviderBase) { // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
|
|
1063
|
+
AgentRunWatchdog.SweepOrphanedRuns(Metadata.Provider, resumeUser) // global-provider-ok: server startup recovery — one-shot orphaned-run sweep at boot
|
|
1064
|
+
.catch(err => console.warn(`[AgentRunWatchdog] Startup sweep failed: ${err}`));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
932
1067
|
// Set up graceful shutdown handlers
|
|
933
1068
|
const gracefulShutdown = async (signal: string) => {
|
|
934
1069
|
console.log(`\n${signal} received, shutting down gracefully...`);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reflect-metadata key for `@NoLog` marks. A single symbol covers both parameter-level
|
|
5
|
+
* marks (stored against `(target, propertyKey)` pairs) and field-level marks (stored
|
|
6
|
+
* against the input class prototype).
|
|
7
|
+
*/
|
|
8
|
+
const NO_LOG_PARAM_KEY = Symbol('mj:NoLog:param');
|
|
9
|
+
const NO_LOG_FIELD_KEY = Symbol('mj:NoLog:field');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `@NoLog` — marks a resolver argument or input-type field as never-loggable.
|
|
13
|
+
*
|
|
14
|
+
* Two modes, distinguished by decorator arity at runtime:
|
|
15
|
+
*
|
|
16
|
+
* **Parameter** — applied to an `@Arg(...)` parameter on a resolver method:
|
|
17
|
+
* ```ts
|
|
18
|
+
* @Mutation(() => Boolean)
|
|
19
|
+
* async VoiceTestHubSpotCredential(
|
|
20
|
+
* @Arg('accessToken') @NoLog accessToken: string,
|
|
21
|
+
* @Ctx() ctx: AppContext,
|
|
22
|
+
* ): Promise<boolean> { ... }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* **Property** — applied to a `@Field()` on an `@InputType` class:
|
|
26
|
+
* ```ts
|
|
27
|
+
* @InputType()
|
|
28
|
+
* export class GetDataInputType {
|
|
29
|
+
* @Field(() => String) @NoLog Token: string;
|
|
30
|
+
* @Field(() => [String]) Queries: string[];
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The variables-logging middleware reads these marks at runtime via
|
|
35
|
+
* `hasNoLogParameter` / `getNoLogFields` and replaces the marked value (or whole arg)
|
|
36
|
+
* with `"<redacted>"` before emitting the variables block. Metadata-covered fields
|
|
37
|
+
* (entity columns with `EntityFieldInfo.Encrypt=true`) do not need `@NoLog` — applying
|
|
38
|
+
* it is harmless but redundant. Use `@NoLog` for arguments that the redactor cannot
|
|
39
|
+
* identify via metadata: custom-resolver parameters, MCP tool args, fields on input
|
|
40
|
+
* types that don't map to a known entity.
|
|
41
|
+
*/
|
|
42
|
+
export function NoLog(target: object, propertyKey?: string | symbol, parameterIndex?: number): void {
|
|
43
|
+
// Parameter-decorator path: (target, propertyKey, parameterIndex)
|
|
44
|
+
if (typeof parameterIndex === 'number' && propertyKey !== undefined) {
|
|
45
|
+
const existing = (Reflect.getOwnMetadata(NO_LOG_PARAM_KEY, target, propertyKey) as Set<number> | undefined) ?? new Set<number>();
|
|
46
|
+
existing.add(parameterIndex);
|
|
47
|
+
Reflect.defineMetadata(NO_LOG_PARAM_KEY, existing, target, propertyKey);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Property-decorator path: (target, propertyKey)
|
|
52
|
+
if (propertyKey !== undefined) {
|
|
53
|
+
const existing = (Reflect.getOwnMetadata(NO_LOG_FIELD_KEY, target) as Set<string> | undefined) ?? new Set<string>();
|
|
54
|
+
existing.add(String(propertyKey));
|
|
55
|
+
Reflect.defineMetadata(NO_LOG_FIELD_KEY, existing, target);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns true if `@NoLog` was applied to `methodName`'s argument at `parameterIndex`
|
|
62
|
+
* on the given resolver class prototype. False otherwise.
|
|
63
|
+
*
|
|
64
|
+
* Pass the resolver class itself (e.g. `MyResolver`) — the function reads from the
|
|
65
|
+
* prototype internally to match how type-graphql stores resolver metadata.
|
|
66
|
+
*/
|
|
67
|
+
export function hasNoLogParameter(
|
|
68
|
+
resolverClass: Function,
|
|
69
|
+
methodName: string,
|
|
70
|
+
parameterIndex: number,
|
|
71
|
+
): boolean {
|
|
72
|
+
const target = resolverClass.prototype as object;
|
|
73
|
+
const marks = Reflect.getMetadata(NO_LOG_PARAM_KEY, target, methodName) as Set<number> | undefined;
|
|
74
|
+
return marks !== undefined && marks.has(parameterIndex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns the set of field names decorated `@NoLog` on the given input class. Empty set
|
|
79
|
+
* if none were marked.
|
|
80
|
+
*
|
|
81
|
+
* Pass the input class itself (e.g. `GetDataInputType`) — the function reads from the
|
|
82
|
+
* prototype internally.
|
|
83
|
+
*/
|
|
84
|
+
export function getNoLogFields(inputTypeClass: Function): ReadonlySet<string> {
|
|
85
|
+
const target = inputTypeClass.prototype as object;
|
|
86
|
+
const marks = Reflect.getMetadata(NO_LOG_FIELD_KEY, target) as Set<string> | undefined;
|
|
87
|
+
return marks ?? new Set<string>();
|
|
88
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getMetadataStorage } from 'type-graphql';
|
|
2
|
+
import { configInfo } from '../config.js';
|
|
3
|
+
import { hasNoLogParameter } from './NoLog.js';
|
|
4
|
+
|
|
5
|
+
// Delete is included alongside Create/Update so codegen DeleteMJ*Input resolvers count as
|
|
6
|
+
// metadata-bound and don't flood the audit with false positives (their args are PK + Options
|
|
7
|
+
// only — no encrypted values by construction). Must stay in sync with the redactor's
|
|
8
|
+
// INPUT_TYPE_REGEX in secretRedactor.ts. See docs/adr/0001-graphql-variables-logging-tiered-by-verbose.md.
|
|
9
|
+
const INPUT_TYPE_REGEX = /^(Create|Update|Delete).+Input$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structural shape of an `@Arg` parameter as seen from outside type-graphql's internals.
|
|
13
|
+
* Used by the audit walker. Mirrors the subset of `ArgParamMetadata` we need.
|
|
14
|
+
*/
|
|
15
|
+
export type AuditArgParam = {
|
|
16
|
+
kind: 'arg';
|
|
17
|
+
index: number;
|
|
18
|
+
name: string;
|
|
19
|
+
target: Function;
|
|
20
|
+
methodName: string;
|
|
21
|
+
getType: () => unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Structural shape of a resolver metadata entry from outside type-graphql's internals.
|
|
26
|
+
* Includes `@Query`, `@Mutation`, `@Subscription` entries.
|
|
27
|
+
*/
|
|
28
|
+
export type AuditResolver = {
|
|
29
|
+
target: Function;
|
|
30
|
+
methodName: string;
|
|
31
|
+
params?: ReadonlyArray<{ kind: string; index: number } & Partial<AuditArgParam>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scans every `@Query` / `@Mutation` / `@Subscription` for `@Arg`s that are neither
|
|
36
|
+
* metadata-bound (input type matching `Create<X>Input` / `Update<X>Input`) nor `@NoLog`-marked.
|
|
37
|
+
* Emits one warning per such arg.
|
|
38
|
+
*
|
|
39
|
+
* Gated on `loggingSettings.graphql.logVariables=true` — runs zero work in default config.
|
|
40
|
+
* In verbose mode, the audit is a diagnostic for the operator who chose to turn on
|
|
41
|
+
* variables logging, naming gaps that will leak plaintext while the flag is active.
|
|
42
|
+
*
|
|
43
|
+
* Never throws. Boot does not fail on a missed `@NoLog`.
|
|
44
|
+
*/
|
|
45
|
+
export function auditResolversForUndecoratedArgs(): void {
|
|
46
|
+
if (!configInfo.loggingSettings.graphql.logVariables) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const storage = getMetadataStorage() as unknown as {
|
|
51
|
+
queries: AuditResolver[];
|
|
52
|
+
mutations: AuditResolver[];
|
|
53
|
+
subscriptions: AuditResolver[];
|
|
54
|
+
};
|
|
55
|
+
auditResolverList([
|
|
56
|
+
...storage.queries,
|
|
57
|
+
...storage.mutations,
|
|
58
|
+
...storage.subscriptions,
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Internal helper exported for test injection. Takes a list of resolver metadata directly,
|
|
64
|
+
* skipping the global `getMetadataStorage()` lookup. The boot-time scan uses this after
|
|
65
|
+
* pulling from the global storage; tests pass in a synthetic fixture.
|
|
66
|
+
*/
|
|
67
|
+
export function auditResolverList(resolvers: ReadonlyArray<AuditResolver>): void {
|
|
68
|
+
for (const resolver of resolvers) {
|
|
69
|
+
auditResolver(resolver);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function auditResolver(resolver: AuditResolver): void {
|
|
74
|
+
for (const param of resolver.params ?? []) {
|
|
75
|
+
if (param.kind !== 'arg') {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
auditArg(resolver, param as AuditArgParam);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function auditArg(resolver: AuditResolver, argParam: AuditArgParam): void {
|
|
83
|
+
const typeName = readArgTypeName(argParam);
|
|
84
|
+
if (typeName && INPUT_TYPE_REGEX.test(typeName)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (hasNoLogParameter(resolver.target, resolver.methodName, argParam.index)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const resolverName = `${resolver.target.name}.${resolver.methodName}`;
|
|
91
|
+
console.warn(
|
|
92
|
+
`[mj:NoLog audit] Custom resolver ${resolverName} takes argument '${argParam.name}' ` +
|
|
93
|
+
`(type ${typeName ?? 'unknown'}) which is not marked @NoLog. Verify this argument does ` +
|
|
94
|
+
`not carry sensitive material; if it does, apply @NoLog. Variables for this resolver will ` +
|
|
95
|
+
`be logged in plaintext while logVariables=true is active.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reads the declared input type name from an `@Arg` parameter's `getType()` thunk.
|
|
101
|
+
* Returns the class/scalar name when the thunk resolves to a class or named scalar; otherwise
|
|
102
|
+
* returns `undefined` (e.g. for array literals or complex generics we don't unwrap).
|
|
103
|
+
*/
|
|
104
|
+
function readArgTypeName(argParam: AuditArgParam): string | undefined {
|
|
105
|
+
try {
|
|
106
|
+
const type = argParam.getType();
|
|
107
|
+
if (typeof type === 'function') {
|
|
108
|
+
return (type as { name?: string }).name;
|
|
109
|
+
}
|
|
110
|
+
if (type && typeof type === 'object' && 'name' in type && typeof (type as { name: unknown }).name === 'string') {
|
|
111
|
+
return (type as { name: string }).name;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the always-on GraphQL boundary log payload: operation name ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Variable VALUES are never emitted here, in any configuration — this is the load-bearing
|
|
5
|
+
* fix for the #2638 secret leak. Verbose variables logging (MJ_LOG_GRAPHQL_VARIABLES=true)
|
|
6
|
+
* is the job of `variablesLoggingMiddleware`, which emits per-root-resolver, fully-emitted
|
|
7
|
+
* values with `Encrypt=true` fields masked.
|
|
8
|
+
*
|
|
9
|
+
* Pure function, no MJ-server import surface, so the "no `variables` key in the boundary
|
|
10
|
+
* line" property is testable in isolation without booting the request-context dependency
|
|
11
|
+
* chain.
|
|
12
|
+
*
|
|
13
|
+
* See docs/adr/0001-graphql-variables-logging-tiered-by-verbose.md.
|
|
14
|
+
*/
|
|
15
|
+
export function buildBoundaryLogPayload(
|
|
16
|
+
operationName: string | undefined,
|
|
17
|
+
): { operationName: string | undefined } {
|
|
18
|
+
return { operationName };
|
|
19
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { IMetadataProvider } from '@memberjunction/core';
|
|
2
|
+
import { shortenForLog } from './shortenForLog.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input to the redactor. Built per `@Arg` per resolver call by the variables-logging middleware.
|
|
6
|
+
*
|
|
7
|
+
* - `inputTypeName` is the GraphQL input type name from the schema (e.g. `"CreateMJCredentialInput"`),
|
|
8
|
+
* derived from `info.parentType.getFields()[info.fieldName].args[i].type` — NOT from any
|
|
9
|
+
* `args[i].constructor.name`, because type-graphql v2.0.0-beta.3 passes raw plain-object args
|
|
10
|
+
* to middleware (conversion to typed instances happens after the middleware chain unwinds).
|
|
11
|
+
* - `provider` is the per-request metadata provider (`context.providers[0].provider`).
|
|
12
|
+
* - `noLogParameter` indicates the `@Arg` itself was decorated `@NoLog` at the parameter level.
|
|
13
|
+
* - `noLogFields` lists field names that were decorated `@NoLog` at the `@Field()` level on the
|
|
14
|
+
* input class. The redactor honors both `EntityFieldInfo.Encrypt=true` (metadata-driven) and
|
|
15
|
+
* this set (decorator-driven) when walking top-level keys.
|
|
16
|
+
*/
|
|
17
|
+
export type RedactionContext = {
|
|
18
|
+
inputTypeName: string;
|
|
19
|
+
rawValue: unknown;
|
|
20
|
+
provider: IMetadataProvider;
|
|
21
|
+
noLogParameter: boolean;
|
|
22
|
+
noLogFields: ReadonlySet<string>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Delete is included so DeleteMJ*Input resolvers resolve to their entity (yielding an empty
|
|
26
|
+
// or value-free encrypted-field walk) instead of falling to the fail-open shortenForLog path —
|
|
27
|
+
// keeps the boot audit quiet on them. Security is identical either way: Delete inputs carry
|
|
28
|
+
// PK + Options only, no encrypted values. Must stay in sync with bootAudit.ts's INPUT_TYPE_REGEX.
|
|
29
|
+
const INPUT_TYPE_REGEX = /^(Create|Update|Delete)(?<name>.+)Input$/;
|
|
30
|
+
|
|
31
|
+
// Assumes EntityFieldInfo.Name === GraphQLFieldName for input-type fields.
|
|
32
|
+
// True today for all codegen output (323 Create*Input + 323 Update*Input).
|
|
33
|
+
// type-graphql allows @Field({ name: 'overrideName' }) to rename fields at the
|
|
34
|
+
// GraphQL layer; MJ codegen does not use this. A future maintainer renaming an
|
|
35
|
+
// encrypted field at the GraphQL layer would silently miss redaction.
|
|
36
|
+
export function redactArg(ctx: RedactionContext): unknown {
|
|
37
|
+
if (ctx.noLogParameter) {
|
|
38
|
+
return '<redacted>';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (ctx.provider.Entities.length === 0) {
|
|
42
|
+
return '<metadata-not-ready>';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Determine the encrypted-field set from entity metadata IF this input maps to a known entity.
|
|
46
|
+
// Custom / non-CRUD inputs (e.g. GetDataInput) have no entity binding — that's fine; field-level
|
|
47
|
+
// @NoLog still applies below. The entity path only adds metadata-driven encrypted-field names.
|
|
48
|
+
let encryptedFieldNames: ReadonlySet<string> = EMPTY_SET;
|
|
49
|
+
const match = INPUT_TYPE_REGEX.exec(ctx.inputTypeName);
|
|
50
|
+
if (match?.groups?.name) {
|
|
51
|
+
const entity = ctx.provider.Entities.find((e) => e.ClassName === match.groups!.name);
|
|
52
|
+
if (entity) {
|
|
53
|
+
encryptedFieldNames = new Set(entity.EncryptedFields.map((f) => f.Name));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Walk top-level keys whenever we have BOTH a redaction source (encrypted fields or @NoLog fields)
|
|
58
|
+
// AND a plain-object value. This honors field-level @NoLog even on non-entity-bound inputs — the
|
|
59
|
+
// exact case @NoLog exists for (custom resolvers, e.g. GetDataInputType.Token). Without a redaction
|
|
60
|
+
// source, or for non-object values, fall through to shortenForLog as before.
|
|
61
|
+
const canWalk =
|
|
62
|
+
(encryptedFieldNames.size > 0 || ctx.noLogFields.size > 0) &&
|
|
63
|
+
ctx.rawValue !== null &&
|
|
64
|
+
typeof ctx.rawValue === 'object' &&
|
|
65
|
+
!Array.isArray(ctx.rawValue);
|
|
66
|
+
|
|
67
|
+
if (!canWalk) {
|
|
68
|
+
return shortenForLog(ctx.rawValue);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result: Record<string, unknown> = {};
|
|
72
|
+
for (const [key, value] of Object.entries(ctx.rawValue as Record<string, unknown>)) {
|
|
73
|
+
if (encryptedFieldNames.has(key) || ctx.noLogFields.has(key)) {
|
|
74
|
+
result[key] = '<redacted>';
|
|
75
|
+
} else {
|
|
76
|
+
result[key] = shortenForLog(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const EMPTY_SET: ReadonlySet<string> = new Set<string>();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a value for one-line console logging without Node's `[Object]` truncation.
|
|
3
|
+
* Arrays keep their structure; non-array objects collapse to JSON, truncated at `maxLen`.
|
|
4
|
+
* Objects whose JSON exceeds `maxLen` and contain nested structure are recursed into so
|
|
5
|
+
* outer keys remain visible.
|
|
6
|
+
*/
|
|
7
|
+
export function shortenForLog(value: unknown, maxLen = 300): unknown {
|
|
8
|
+
if (value === null || typeof value !== 'object') return value;
|
|
9
|
+
if (Array.isArray(value)) return value.map((v) => shortenForLog(v, maxLen));
|
|
10
|
+
const json = JSON.stringify(value);
|
|
11
|
+
if (json.length <= maxLen) return json;
|
|
12
|
+
const result: Record<string, unknown> = {};
|
|
13
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
14
|
+
result[k] = shortenForLog(v, maxLen);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|