@memberjunction/server 6.2.0-edge.7 → 6.2.0-edge.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +2 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +6 -8
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +839 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +14652 -10253
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +8 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/globalCors.d.ts.map +1 -1
- package/dist/globalCors.js +1 -0
- package/dist/globalCors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -8
- package/dist/index.js.map +1 -1
- package/dist/logging/StartupLogger.d.ts +50 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +15 -22
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +21 -3
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +4 -2
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +10 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +18 -2
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +59 -15
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.js +1 -1
- package/dist/resolvers/InfoResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +2 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +12 -6
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +7 -8
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.d.ts +12 -0
- package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.js +84 -0
- package/dist/resolvers/TagGovernanceResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +13 -8
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
- package/dist/resolvers/TelemetryResolver.js +40 -9
- package/dist/resolvers/TelemetryResolver.js.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +34 -4
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +55 -2
- package/dist/util.js.map +1 -1
- package/package.json +107 -100
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +4 -0
- package/src/__tests__/ResolverBase.diagnosticSenderRegistrations.test.ts +26 -0
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +28 -4
- package/src/__tests__/TagGovernanceAndAutotagResolvers.test.ts +261 -0
- package/src/__tests__/TagGovernanceResolver.test.ts +60 -2
- package/src/__tests__/globalCors.test.ts +12 -0
- package/src/__tests__/review-capture-valid-token-log.test.ts +98 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +5 -5
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +2 -1
- package/src/context.ts +7 -9
- package/src/generated/graphql-schemas/__mj.ts +9449 -6383
- package/src/generic/ResolverBase.ts +8 -0
- package/src/globalCors.ts +1 -0
- package/src/index.ts +45 -9
- package/src/logging/StartupLogger.ts +24 -0
- package/src/middleware/BaseServerMiddleware.ts +20 -3
- package/src/resolvers/AgentRunStatusPublisher.ts +27 -2
- package/src/resolvers/AutotagPipelineResolver.ts +57 -13
- package/src/resolvers/InfoResolver.ts +1 -1
- package/src/resolvers/QuerySystemUserResolver.ts +19 -7
- package/src/resolvers/SqlLoggingConfigResolver.ts +7 -9
- package/src/resolvers/TagGovernanceResolver.ts +65 -0
- package/src/resolvers/TelemetryResolver.ts +44 -9
- package/src/resolvers/meetings/meetingMutations.ts +37 -7
- package/src/util.ts +55 -3
|
@@ -67,6 +67,12 @@ import { Subscription } from 'rxjs';
|
|
|
67
67
|
* `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
|
|
68
68
|
* (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
|
|
69
69
|
* that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
|
|
70
|
+
*
|
|
71
|
+
* `mj: projects` (conversation folders) is refused a change to `OwnerUserID` or `ParentID` by anyone but the
|
|
72
|
+
* stored owner, a rule that reads the stored owner, so it must hold even if a host stops tracking its changes.
|
|
73
|
+
*
|
|
74
|
+
* `mj: diagnostic sender registrations` is refused a change to its guarded fields by a rule that decides on
|
|
75
|
+
* `Dirty`, so the baseline must be the stored row.
|
|
70
76
|
*/
|
|
71
77
|
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
72
78
|
'mj: record changes',
|
|
@@ -75,6 +81,8 @@ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
|
75
81
|
'mj: conversations',
|
|
76
82
|
'mj: ai agent sessions',
|
|
77
83
|
'mj: ai agent runs',
|
|
84
|
+
'mj: projects',
|
|
85
|
+
'mj: diagnostic sender registrations',
|
|
78
86
|
]);
|
|
79
87
|
|
|
80
88
|
/** One field's values in a concurrency-conflict diagnostic. */
|
package/src/globalCors.ts
CHANGED
|
@@ -44,6 +44,7 @@ export function CreateGlobalCorsMiddleware(
|
|
|
44
44
|
},
|
|
45
45
|
credentials: corsWildcard ? false : (settings?.allowCredentials ?? true),
|
|
46
46
|
maxAge: settings?.maxAge ?? 86400,
|
|
47
|
+
exposedHeaders: ['X-MJ-Trace-ID', 'X-MJ-Diagnostic-Trigger'],
|
|
47
48
|
}) as RequestHandler;
|
|
48
49
|
return SkipForServerExtensionRoots(globalCors, extensionOwnedRoots);
|
|
49
50
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ 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, DatabaseProviderBase, ProviderBase, ResolveStartupMode, UserInfo } from '@memberjunction/core';
|
|
8
|
-
import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
7
|
+
import { Metadata, DatabasePlatform, SetProvider, StartupManager as StartupManagerImport, BaseEntity, BaseEntityEvent, RunView, DatabaseProviderBase, ProviderBase, ResolveStartupMode, UserInfo, GetDiagnosticContext, IsDiagnosticCaptureSuppressed } from '@memberjunction/core';
|
|
8
|
+
import { UserCache, resolveDbPlatformFromEnv, GenericDatabaseProvider, SummarizeSqlParameters } from '@memberjunction/generic-database-provider';
|
|
9
9
|
import { MJGlobal, MJEventType, UUIDsEqual, ShutdownRegistry } from '@memberjunction/global';
|
|
10
10
|
import { setupSQLServerClient, SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
11
11
|
import { extendConnectionPoolWithQuery } from './util.js';
|
|
@@ -299,10 +299,11 @@ export * from './multiTenancy/index.js';
|
|
|
299
299
|
export * from './middleware/index.js';
|
|
300
300
|
|
|
301
301
|
import { RegisterDataHook } from '@memberjunction/core';
|
|
302
|
+
import { EnsureAnchoredConversationReadHookRegistered } from '@memberjunction/core-entities-server';
|
|
302
303
|
import type { RequestHandler, ErrorRequestHandler } from 'express';
|
|
303
304
|
import type { ApolloServerPlugin } from '@apollo/server';
|
|
304
305
|
import type { GraphQLSchema } from 'graphql';
|
|
305
|
-
import { BaseServerMiddleware } from './middleware/BaseServerMiddleware.js';
|
|
306
|
+
import { BaseServerMiddleware, ServerMiddlewareContext } from './middleware/BaseServerMiddleware.js';
|
|
306
307
|
import { SuppressTaskGraphSubmission } from '@memberjunction/ai-core-plus';
|
|
307
308
|
|
|
308
309
|
export type MJServerOptions = {
|
|
@@ -499,7 +500,8 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
499
500
|
await WirePushStatusFanOut(sharedCache);
|
|
500
501
|
await wireHandoffOfferFanOut(sharedCache, startupLog);
|
|
501
502
|
await wireRealtimeSessionEventFanOut(sharedCache);
|
|
502
|
-
|
|
503
|
+
const safeRedisUrl = (process.env.REDIS_URL ?? '').replace(/:\/\/[^@]+@/, '://***@');
|
|
504
|
+
startupLog.LogIf('verbose', `Redis cache provider connected: ${safeRedisUrl}`);
|
|
503
505
|
}
|
|
504
506
|
const cacheManagerConfig = CacheManagerConfigFromSettings(configInfo.cacheSettings);
|
|
505
507
|
const warmupLeaseMs = WarmupLeaseMsFromSettings(configInfo.cacheSettings);
|
|
@@ -596,8 +598,38 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
596
598
|
const thePgPool = poolAny._pgPool as import('pg').Pool;
|
|
597
599
|
// Translate SQL Server bracket syntax to PostgreSQL double-quote syntax
|
|
598
600
|
const pgQuery = TranslateBracketsToPG(query);
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
+
const startTime = performance.now();
|
|
602
|
+
try {
|
|
603
|
+
const result = await thePgPool.query(pgQuery);
|
|
604
|
+
if (!IsDiagnosticCaptureSuppressed() && GenericDatabaseProvider.HasSqlObservers()) {
|
|
605
|
+
GenericDatabaseProvider.NotifySqlObservers({
|
|
606
|
+
ts: Date.now(),
|
|
607
|
+
sql: pgQuery,
|
|
608
|
+
parameterSummaries: SummarizeSqlParameters(parameters),
|
|
609
|
+
durationMs: performance.now() - startTime,
|
|
610
|
+
rowCount: result.rows?.length ?? 0,
|
|
611
|
+
isMutation: false,
|
|
612
|
+
description: 'ExecuteSQLWithPool(PG)',
|
|
613
|
+
diagnosticContext: GetDiagnosticContext(),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return result.rows;
|
|
617
|
+
} catch (err) {
|
|
618
|
+
if (!IsDiagnosticCaptureSuppressed() && GenericDatabaseProvider.HasSqlObservers()) {
|
|
619
|
+
GenericDatabaseProvider.NotifySqlObservers({
|
|
620
|
+
ts: Date.now(),
|
|
621
|
+
sql: pgQuery,
|
|
622
|
+
parameterSummaries: SummarizeSqlParameters(parameters),
|
|
623
|
+
durationMs: performance.now() - startTime,
|
|
624
|
+
rowCount: 0,
|
|
625
|
+
isMutation: false,
|
|
626
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
627
|
+
description: 'ExecuteSQLWithPool(PG)',
|
|
628
|
+
diagnosticContext: GetDiagnosticContext(),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
throw err;
|
|
632
|
+
}
|
|
601
633
|
}
|
|
602
634
|
return origExecuteSQLWithPool.call(this, pool, query, parameters, contextUser);
|
|
603
635
|
};
|
|
@@ -982,8 +1014,9 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
982
1014
|
}
|
|
983
1015
|
|
|
984
1016
|
// Initialize all middleware
|
|
1017
|
+
const middlewareContext: ServerMiddlewareContext = { ApiRoute: (subpath, options) => ApiRoute(subpath, options) };
|
|
985
1018
|
for (const mw of middlewares) {
|
|
986
|
-
await mw.Initialize();
|
|
1019
|
+
await mw.Initialize(middlewareContext);
|
|
987
1020
|
startupLog.LogIf('verbose', ` [Middleware] ${mw.Label}`);
|
|
988
1021
|
}
|
|
989
1022
|
|
|
@@ -1005,7 +1038,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1005
1038
|
|
|
1006
1039
|
// Express app configuration escape hatch
|
|
1007
1040
|
if (mw.ConfigureExpressApp) {
|
|
1008
|
-
await mw.ConfigureExpressApp(app);
|
|
1041
|
+
await mw.ConfigureExpressApp(app, middlewareContext);
|
|
1009
1042
|
}
|
|
1010
1043
|
|
|
1011
1044
|
// Extract hook methods and register in the global hook store
|
|
@@ -1014,6 +1047,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1014
1047
|
RegisterDataHook('PostRunView', mw.PostRunView.bind(mw));
|
|
1015
1048
|
RegisterDataHook('PreSave', mw.PreSave.bind(mw));
|
|
1016
1049
|
}
|
|
1050
|
+
EnsureAnchoredConversationReadHookRegistered();
|
|
1017
1051
|
|
|
1018
1052
|
// ─── Resolve middleware-contributed resolver paths and merge into resolvers ───
|
|
1019
1053
|
let allResolvers = resolvers;
|
|
@@ -1606,7 +1640,7 @@ const setupComplete$ = new ReplaySubject(1);
|
|
|
1606
1640
|
app.use(
|
|
1607
1641
|
graphqlRootPath,
|
|
1608
1642
|
cors<cors.CorsRequest>({
|
|
1609
|
-
exposedHeaders: ['X-MJ-Cache-Seq', 'X-MJ-Server-ID'],
|
|
1643
|
+
exposedHeaders: ['X-MJ-Cache-Seq', 'X-MJ-Server-ID', 'X-MJ-Trace-ID', 'X-MJ-Diagnostic-Trigger'],
|
|
1610
1644
|
}),
|
|
1611
1645
|
BodyParser.json({ limit: '50mb' }),
|
|
1612
1646
|
// Express 5 leaves req.body as undefined for non-JSON or empty bodies;
|
|
@@ -2447,3 +2481,5 @@ export type { MagicLinkJWTClaims } from './auth/magicLink/index.js';
|
|
|
2447
2481
|
export { VerifyHostAssertion } from './realtimeWidget/host-identity.js';
|
|
2448
2482
|
export type { HostAssertedIdentity, HostAssertionResult } from './realtimeWidget/host-identity.js';
|
|
2449
2483
|
export { IsScopeLimitedPrincipal } from './auth/scopeLimitedPrincipal.js';
|
|
2484
|
+
export const GetServerStartupSummary = () => StartupLogger.GetLastStartupSnapshot();
|
|
2485
|
+
export type { StartupSummarySnapshot } from './logging/StartupLogger.js';
|
|
@@ -82,7 +82,25 @@ type StartupSummaryData = {
|
|
|
82
82
|
* logger.PrintSummary();
|
|
83
83
|
* ```
|
|
84
84
|
*/
|
|
85
|
+
export type StartupSummarySnapshot = {
|
|
86
|
+
Level: ServerLogLevel;
|
|
87
|
+
Summary: Readonly<StartupSummaryData>;
|
|
88
|
+
PhaseTimings: ReadonlyArray<PhaseTiming>;
|
|
89
|
+
TotalStartupMs: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
85
92
|
export class StartupLogger {
|
|
93
|
+
private static _lastSnapshot: StartupSummarySnapshot | null = null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns a read-only snapshot of the most recent server startup summary,
|
|
97
|
+
* used by diagnostic health snapshots to report startup phase durations without
|
|
98
|
+
* mutating or re-running startup logic.
|
|
99
|
+
*/
|
|
100
|
+
public static GetLastStartupSnapshot(): StartupSummarySnapshot | null {
|
|
101
|
+
return StartupLogger._lastSnapshot;
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
private readonly resolvedLevel: ServerLogLevel;
|
|
87
105
|
private readonly phaseTimings: PhaseTiming[] = [];
|
|
88
106
|
private readonly summary: StartupSummaryData = { AuthProviders: [] };
|
|
@@ -302,6 +320,12 @@ export class StartupLogger {
|
|
|
302
320
|
// Clear the transient spinner before any block output so the line it
|
|
303
321
|
// occupied is replaced cleanly by the summary.
|
|
304
322
|
this.StopBoot();
|
|
323
|
+
StartupLogger._lastSnapshot = {
|
|
324
|
+
Level: this.resolvedLevel,
|
|
325
|
+
Summary: { ...this.summary, AuthProviders: [...this.summary.AuthProviders] },
|
|
326
|
+
PhaseTimings: this.phaseTimings.map((p) => ({ ...p })),
|
|
327
|
+
TotalStartupMs: performance.now(),
|
|
328
|
+
};
|
|
305
329
|
if (this.resolvedLevel === 'minimal') {
|
|
306
330
|
this.printReadyLineOnly();
|
|
307
331
|
return;
|
|
@@ -18,6 +18,20 @@ import type { ApolloServerPlugin } from '@apollo/server';
|
|
|
18
18
|
import type { GraphQLSchema } from 'graphql';
|
|
19
19
|
import type { RunViewParams, UserInfo, BaseEntity, RunViewResult } from '@memberjunction/core';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* What the server hands each middleware at startup.
|
|
23
|
+
*/
|
|
24
|
+
export interface ServerMiddlewareContext {
|
|
25
|
+
/**
|
|
26
|
+
* Builds the path of a route under MJAPI's base path (`MJAPI_BASE_PATH`).
|
|
27
|
+
* `subpath` must start with `/`. Use it for every route a middleware registers or compares against.
|
|
28
|
+
* With `{ absolute: true }` it returns the public URL of the route (`MJAPI_PUBLIC_URL` + subpath).
|
|
29
|
+
* The options type is written out here, not imported from `apiRoot.ts`: packages that read this
|
|
30
|
+
* file's types from source must not compile `config.ts`.
|
|
31
|
+
*/
|
|
32
|
+
ApiRoute: (subpath: string, options?: { absolute?: boolean }) => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export abstract class BaseServerMiddleware {
|
|
22
36
|
// --- Identity ---
|
|
23
37
|
|
|
@@ -37,9 +51,11 @@ export abstract class BaseServerMiddleware {
|
|
|
37
51
|
|
|
38
52
|
/**
|
|
39
53
|
* Optional async initialization (read config, warm caches, etc.).
|
|
40
|
-
* Called by serve() after instantiation, before middleware/hooks are extracted
|
|
54
|
+
* Called by serve() after instantiation, before middleware/hooks are extracted,
|
|
55
|
+
* so a context stored here is available to every other hook.
|
|
56
|
+
* @param context - The server's route builder. serve() always passes it.
|
|
41
57
|
*/
|
|
42
|
-
async Initialize(): Promise<void> { /* no-op by default */ }
|
|
58
|
+
async Initialize(context?: ServerMiddlewareContext): Promise<void> { /* no-op by default */ }
|
|
43
59
|
|
|
44
60
|
// --- Express Middleware ---
|
|
45
61
|
// Each method corresponds to a named slot in the request pipeline.
|
|
@@ -73,8 +89,9 @@ export abstract class BaseServerMiddleware {
|
|
|
73
89
|
* Called once during server setup with the Express app instance.
|
|
74
90
|
* Use for: trust proxy settings, custom CORS, body parser config.
|
|
75
91
|
* Return undefined to skip (default).
|
|
92
|
+
* @param context - The server's route builder. serve() always passes it.
|
|
76
93
|
*/
|
|
77
|
-
ConfigureExpressApp?(app: Application): void | Promise<void>;
|
|
94
|
+
ConfigureExpressApp?(app: Application, context?: ServerMiddlewareContext): void | Promise<void>;
|
|
78
95
|
|
|
79
96
|
// --- Apollo / GraphQL Extensions ---
|
|
80
97
|
|
|
@@ -17,13 +17,18 @@ import type {
|
|
|
17
17
|
import type { EntityInfo } from '@memberjunction/core';
|
|
18
18
|
import type { UserPayload } from '../types.js';
|
|
19
19
|
import { PublishStatusUpdate, StatusUpdateOwnerID } from '../generic/PushStatusResolver.js';
|
|
20
|
-
import { ProjectEntityObjectForCaller } from '../generic/fieldSecurityTransport.js';
|
|
20
|
+
import { ProjectEntityObjectForCaller, ProjectValuesForCaller } from '../generic/fieldSecurityTransport.js';
|
|
21
21
|
|
|
22
22
|
const SIGNIFICANT_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
23
23
|
|
|
24
24
|
/** The run fields the published messages read. Entities and test fakes both satisfy this. */
|
|
25
25
|
interface StatusRun {
|
|
26
26
|
ID?: string;
|
|
27
|
+
AgentID?: string;
|
|
28
|
+
Agent?: string | null;
|
|
29
|
+
ParentRunID?: string | null;
|
|
30
|
+
Status?: string;
|
|
31
|
+
ConversationID?: string | null;
|
|
27
32
|
ConversationDetailID?: string;
|
|
28
33
|
ErrorMessage?: string;
|
|
29
34
|
Steps?: Array<{ StepName?: string }>;
|
|
@@ -31,6 +36,9 @@ interface StatusRun {
|
|
|
31
36
|
GetAll?: () => Record<string, unknown>;
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
/** The run fields a streamed chunk carries. */
|
|
40
|
+
type RunSummary = Pick<StatusRun, 'ID' | 'AgentID' | 'Agent' | 'ParentRunID' | 'Status' | 'ConversationID' | 'ConversationDetailID'>;
|
|
41
|
+
|
|
34
42
|
export class AgentRunStatusPublisher {
|
|
35
43
|
private run: StatusRun | null = null;
|
|
36
44
|
|
|
@@ -102,7 +110,7 @@ export class AgentRunStatusPublisher {
|
|
|
102
110
|
agentRunId: agentRun.ID,
|
|
103
111
|
type: 'streaming',
|
|
104
112
|
...this.replyRow(),
|
|
105
|
-
agentRun: this.
|
|
113
|
+
agentRun: this.runSummary(agentRun),
|
|
106
114
|
streaming: {
|
|
107
115
|
content: chunk.content,
|
|
108
116
|
isPartial: !chunk.isComplete,
|
|
@@ -188,6 +196,23 @@ export class AgentRunStatusPublisher {
|
|
|
188
196
|
return ProjectEntityObjectForCaller({ EntityInfo: agentRun.EntityInfo, GetAll: () => getAll.call(agentRun) }, this.userPayload?.userRecord);
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The run's identifying fields as the recipient may read them. A streamed chunk carries only
|
|
201
|
+
* these: the run's `GetAll()` holds every step and prompt run, and a run streams many chunks.
|
|
202
|
+
*/
|
|
203
|
+
private runSummary(agentRun: StatusRun): RunSummary {
|
|
204
|
+
const summary: RunSummary = {
|
|
205
|
+
ID: agentRun.ID,
|
|
206
|
+
AgentID: agentRun.AgentID,
|
|
207
|
+
Agent: agentRun.Agent,
|
|
208
|
+
ParentRunID: agentRun.ParentRunID,
|
|
209
|
+
Status: agentRun.Status,
|
|
210
|
+
ConversationID: agentRun.ConversationID,
|
|
211
|
+
ConversationDetailID: agentRun.ConversationDetailID,
|
|
212
|
+
};
|
|
213
|
+
return ProjectValuesForCaller(agentRun.EntityInfo, summary, this.userPayload?.userRecord);
|
|
214
|
+
}
|
|
215
|
+
|
|
191
216
|
private publish(type: 'ExecutionProgress' | 'StreamingContent', data: unknown): void {
|
|
192
217
|
PublishStatusUpdate(this.pubSub, {
|
|
193
218
|
sessionId: this.userPayload.sessionId,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Resolver, Mutation, Ctx, Arg, ObjectType, Field } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { IMetadataProvider, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
+
import { IsValidUUID, EscapeSQLString } from '@memberjunction/global';
|
|
4
5
|
import { MJContentProcessRunEntity } from '@memberjunction/core-entities';
|
|
5
6
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
6
7
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
@@ -32,6 +33,7 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
32
33
|
async RunAutotagPipeline(
|
|
33
34
|
@Arg('contentSourceIDs', () => [String], { nullable: true }) contentSourceIDs: string[] | undefined,
|
|
34
35
|
@Arg('forceReprocess', { nullable: true }) forceReprocess: boolean | undefined,
|
|
36
|
+
@Arg('pipelineRunID', { nullable: true }) clientPipelineRunID: string | undefined,
|
|
35
37
|
@Ctx() { userPayload, providers }: AppContext = {} as AppContext
|
|
36
38
|
): Promise<AutotagPipelineResult> {
|
|
37
39
|
try {
|
|
@@ -40,14 +42,37 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
40
42
|
return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const pipelineRunID = uuidv4();
|
|
44
|
-
LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
|
|
45
|
-
|
|
46
45
|
// Capture the per-request provider snapshot before returning so the fire-and-forget
|
|
47
46
|
// background job binds to the same connection the caller used. Without this, the
|
|
48
47
|
// background job would silently use the global default — wrong for multi-tenant.
|
|
49
|
-
const provider =
|
|
50
|
-
|
|
48
|
+
const provider: IMetadataProvider = GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) ?? Metadata.Provider;
|
|
49
|
+
|
|
50
|
+
let pipelineRunID: string;
|
|
51
|
+
if (clientPipelineRunID != null) {
|
|
52
|
+
if (!IsValidUUID(clientPipelineRunID)) {
|
|
53
|
+
return { Success: false, Status: 'Error', ErrorMessage: 'Invalid pipelineRunID format' };
|
|
54
|
+
}
|
|
55
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
56
|
+
const existing = await rv.RunView<{ ID: string }>({
|
|
57
|
+
EntityName: 'MJ: Content Process Runs',
|
|
58
|
+
ExtraFilter: `ID='${EscapeSQLString(clientPipelineRunID)}'`,
|
|
59
|
+
Fields: ['ID'],
|
|
60
|
+
ResultType: 'simple',
|
|
61
|
+
MaxRows: 1,
|
|
62
|
+
SkipTotalRowCount: true,
|
|
63
|
+
}, currentUser);
|
|
64
|
+
if (!existing.Success) {
|
|
65
|
+
return { Success: false, Status: 'Error', ErrorMessage: existing.ErrorMessage || 'Failed to validate pipelineRunID' };
|
|
66
|
+
}
|
|
67
|
+
if (existing.Results.length > 0) {
|
|
68
|
+
return { Success: false, Status: 'Error', ErrorMessage: `Pipeline run ${clientPipelineRunID} already exists` };
|
|
69
|
+
}
|
|
70
|
+
pipelineRunID = clientPipelineRunID;
|
|
71
|
+
} else {
|
|
72
|
+
pipelineRunID = uuidv4();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
LogStatus(`RunAutotagPipeline: starting pipeline ${pipelineRunID}`);
|
|
51
76
|
|
|
52
77
|
// Fire-and-forget: start the pipeline in the background and return immediately
|
|
53
78
|
this.runPipelineInBackground(pipelineRunID, currentUser, provider, contentSourceIDs, forceReprocess);
|
|
@@ -101,9 +126,19 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
101
126
|
// and keeps the process run record in sync
|
|
102
127
|
this.publishProgress(pipelineRunID, 'autotag', 0, 0, startTime, 'Running autotaggers...');
|
|
103
128
|
|
|
129
|
+
let lastTotal = 0;
|
|
130
|
+
let lastProcessed = 0;
|
|
131
|
+
let lastLabel = 'Scanning & extracting content from sources...';
|
|
132
|
+
|
|
104
133
|
const progressCallback = (processed: number, total: number, currentItem?: string) => {
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
lastProcessed = processed;
|
|
135
|
+
lastTotal = total;
|
|
136
|
+
if (currentItem) {
|
|
137
|
+
lastLabel = currentItem;
|
|
138
|
+
} else if (total > 0) {
|
|
139
|
+
lastLabel = `${processed}/${total} items`;
|
|
140
|
+
}
|
|
141
|
+
this.publishProgress(pipelineRunID, 'autotag', total, processed, startTime, lastLabel);
|
|
107
142
|
this.updateProcessRunProgress(processRun, processed, total);
|
|
108
143
|
};
|
|
109
144
|
|
|
@@ -121,12 +156,21 @@ export class AutotagPipelineResolver extends ResolverBase {
|
|
|
121
156
|
actionParams.push({ Name: 'ForceReprocess', Value: 1, Type: 'Input' });
|
|
122
157
|
}
|
|
123
158
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
const heartbeatTimer = setInterval(() => {
|
|
160
|
+
this.publishProgress(pipelineRunID, 'autotag', lastTotal, lastProcessed, startTime, lastLabel);
|
|
161
|
+
}, 3000);
|
|
162
|
+
|
|
163
|
+
let result;
|
|
164
|
+
try {
|
|
165
|
+
result = await ActionEngineServer.Instance.RunAction({
|
|
166
|
+
Action: action,
|
|
167
|
+
ContextUser: currentUser,
|
|
168
|
+
Filters: [],
|
|
169
|
+
Params: actionParams
|
|
170
|
+
});
|
|
171
|
+
} finally {
|
|
172
|
+
clearInterval(heartbeatTimer);
|
|
173
|
+
}
|
|
130
174
|
|
|
131
175
|
// Stage: vectorize complete
|
|
132
176
|
this.publishProgress(pipelineRunID, 'vectorize', 100, 90, startTime, 'Vectorizing content...');
|
|
@@ -42,7 +42,7 @@ export class InfoResolver {
|
|
|
42
42
|
Version: packageJson.version,
|
|
43
43
|
IsSystemUser: Boolean(context.userPayload.isSystemUser),
|
|
44
44
|
Platform: os.platform(),
|
|
45
|
-
Arch: os.
|
|
45
|
+
Arch: os.arch(),
|
|
46
46
|
CpuModel: os.cpus()?.[0].model,
|
|
47
47
|
Hostname: os.hostname(),
|
|
48
48
|
};
|
|
@@ -382,14 +382,15 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
382
382
|
/** After a successful create: the AfterCreate event, the permissions, and the result. */
|
|
383
383
|
private async completeCreateAsync(provider: DatabaseProviderBase, input: CreateQuerySystemUserInput, record: MJQueryEntityServer, user: UserInfo): Promise<QueryMutationResultType> {
|
|
384
384
|
await this.AfterCreate(provider, input); // fire event
|
|
385
|
+
let createdPermissions: MJQueryPermissionEntity[] | undefined;
|
|
385
386
|
if (input.Permissions && input.Permissions.length > 0) {
|
|
386
387
|
try {
|
|
387
|
-
await this.createPermissions(provider, input.Permissions, record.ID, user);
|
|
388
|
+
createdPermissions = await this.createPermissions(provider, input.Permissions, record.ID, user);
|
|
388
389
|
} catch (e) {
|
|
389
390
|
return { Success: false, ErrorMessage: `Query '${record.Name}' (ID ${record.ID}) was created, but its permissions were not: ${e instanceof Error ? e.message : String(e)}` };
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
|
-
return this.buildSuccessResult(record, user, provider);
|
|
393
|
+
return this.buildSuccessResult(record, user, provider, undefined, createdPermissions);
|
|
393
394
|
}
|
|
394
395
|
|
|
395
396
|
/** The create-or-get result: the query another request created with this name. */
|
|
@@ -415,7 +416,14 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
415
416
|
*
|
|
416
417
|
* @param contextUser The caller the result is for. Field security projects the record for them.
|
|
417
418
|
*/
|
|
418
|
-
private async buildSuccessResult(
|
|
419
|
+
private async buildSuccessResult(
|
|
420
|
+
entity: MJQueryEntityServer,
|
|
421
|
+
contextUser: UserInfo,
|
|
422
|
+
provider: DatabaseProviderBase,
|
|
423
|
+
children?: StoredQueryChildren,
|
|
424
|
+
createdPermissions?: MJQueryPermissionEntity[]
|
|
425
|
+
): Promise<QueryMutationResultType> {
|
|
426
|
+
const permissionsToMap = children?.Permissions ?? createdPermissions ?? entity.QueryPermissions;
|
|
419
427
|
return {
|
|
420
428
|
Success: true,
|
|
421
429
|
// Mapped for the caller, so the field-security projection applies: the record was just
|
|
@@ -425,7 +433,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
425
433
|
Fields: this.mapFields(children?.Fields ?? entity.QueryFields),
|
|
426
434
|
Parameters: this.mapParameters(children?.Parameters ?? entity.QueryParameters),
|
|
427
435
|
Entities: this.mapEntities(children?.Entities ?? entity.QueryEntities),
|
|
428
|
-
Permissions: this.mapPermissions(
|
|
436
|
+
Permissions: this.mapPermissions(permissionsToMap),
|
|
429
437
|
};
|
|
430
438
|
}
|
|
431
439
|
|
|
@@ -532,7 +540,8 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
532
540
|
*
|
|
533
541
|
* @throws Error naming the role and the save's own message when a row is not saved
|
|
534
542
|
*/
|
|
535
|
-
protected async createPermissions(p: DatabaseProviderBase, permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<
|
|
543
|
+
protected async createPermissions(p: DatabaseProviderBase, permissions: QueryPermissionInputType[], queryID: string, contextUser: UserInfo): Promise<MJQueryPermissionEntity[]> {
|
|
544
|
+
const created: MJQueryPermissionEntity[] = [];
|
|
536
545
|
for (const perm of permissions) {
|
|
537
546
|
const permissionEntity = await p.GetEntityObject<MJQueryPermissionEntity>('MJ: Query Permissions', contextUser);
|
|
538
547
|
if (permissionEntity) {
|
|
@@ -541,8 +550,10 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
541
550
|
if (!await permissionEntity.Save()) {
|
|
542
551
|
throw new Error(MJQueryResolverExtended.permissionWriteError('save', perm.RoleID, permissionEntity));
|
|
543
552
|
}
|
|
553
|
+
created.push(permissionEntity);
|
|
544
554
|
}
|
|
545
555
|
}
|
|
556
|
+
return created;
|
|
546
557
|
}
|
|
547
558
|
|
|
548
559
|
/**
|
|
@@ -648,17 +659,18 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
648
659
|
const queryID = queryEntity.ID;
|
|
649
660
|
|
|
650
661
|
// Handle permissions update if provided
|
|
662
|
+
let createdPermissions: MJQueryPermissionEntity[] | undefined;
|
|
651
663
|
if (input.Permissions !== undefined) {
|
|
652
664
|
// Replace the existing permissions (read from the QueryEngine cache); a failed write stops the update with its reason
|
|
653
665
|
try {
|
|
654
666
|
await this.deletePermissions(queryID);
|
|
655
|
-
await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
667
|
+
createdPermissions = await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
656
668
|
} catch (e) {
|
|
657
669
|
return { Success: false, ErrorMessage: `Failed to update query permissions: ${e instanceof Error ? e.message : String(e)}` };
|
|
658
670
|
}
|
|
659
671
|
}
|
|
660
672
|
|
|
661
|
-
return await this.buildSuccessResult(queryEntity, context.userPayload.userRecord, provider);
|
|
673
|
+
return await this.buildSuccessResult(queryEntity, context.userPayload.userRecord, provider, undefined, createdPermissions);
|
|
662
674
|
|
|
663
675
|
} catch (err) {
|
|
664
676
|
LogError(err);
|
|
@@ -9,6 +9,7 @@ import { LoadConfig } from '../config.js';
|
|
|
9
9
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
10
10
|
import { GetReadOnlyProvider } from '../util.js';
|
|
11
11
|
import { UserCache, SqlLoggingOptions as ProviderSqlLoggingOptions } from '@memberjunction/generic-database-provider';
|
|
12
|
+
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Configuration options for SQL logging sessions.
|
|
@@ -252,6 +253,9 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
252
253
|
* @private
|
|
253
254
|
*/
|
|
254
255
|
private async checkOwnerAccess(context: AppContext): Promise<UserInfo> {
|
|
256
|
+
if (context?.userPayload?.userRecord && IsScopeLimitedPrincipal(context.userPayload.userRecord)) {
|
|
257
|
+
throw new Error('Access denied. Scope-limited principals cannot access SQL logging.');
|
|
258
|
+
}
|
|
255
259
|
const userEmail = context.userPayload?.email;
|
|
256
260
|
if (!userEmail) {
|
|
257
261
|
throw new Error('User not authenticated');
|
|
@@ -263,15 +267,9 @@ export class SqlLoggingConfigResolver extends ResolverBase {
|
|
|
263
267
|
if (!user) {
|
|
264
268
|
throw new Error('User not found');
|
|
265
269
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
email: user.Email,
|
|
270
|
-
type: user.Type,
|
|
271
|
-
typeLength: user.Type?.length,
|
|
272
|
-
typeTrimmed: user.Type?.trim(),
|
|
273
|
-
isOwner: user.Type?.trim().toLowerCase() === 'owner'
|
|
274
|
-
});
|
|
270
|
+
if (IsScopeLimitedPrincipal(user)) {
|
|
271
|
+
throw new Error('Access denied. Scope-limited principals cannot access SQL logging.');
|
|
272
|
+
}
|
|
275
273
|
|
|
276
274
|
// Check if user has Type = 'Owner' (trim and case-insensitive for nchar fields)
|
|
277
275
|
if (user.Type?.trim().toLowerCase() !== 'owner') {
|
|
@@ -3,6 +3,7 @@ import { AppContext } from '../types.js';
|
|
|
3
3
|
import { LogError, LogStatus } from '@memberjunction/core';
|
|
4
4
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
5
|
import { TagGovernanceEngine, TagHealthJob, DEFAULT_TAG_HEALTH_THRESHOLDS, TagEngine } from '@memberjunction/tag-engine';
|
|
6
|
+
import { TagCoOccurrenceEngine } from '@memberjunction/tag-engine-base';
|
|
6
7
|
|
|
7
8
|
@ObjectType()
|
|
8
9
|
export class PromoteSuggestionResult {
|
|
@@ -28,6 +29,30 @@ export class RejectSuggestionResult {
|
|
|
28
29
|
ErrorMessage?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
@ObjectType()
|
|
33
|
+
export class MergeTagsResult {
|
|
34
|
+
@Field()
|
|
35
|
+
Success: boolean;
|
|
36
|
+
|
|
37
|
+
@Field(() => Int)
|
|
38
|
+
ItemsMoved: number;
|
|
39
|
+
|
|
40
|
+
@Field(() => Int)
|
|
41
|
+
TaggedItemsMoved: number;
|
|
42
|
+
|
|
43
|
+
@Field({ nullable: true })
|
|
44
|
+
ErrorMessage?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@ObjectType()
|
|
48
|
+
export class RecomputeTagCoOccurrenceResult {
|
|
49
|
+
@Field(() => Int)
|
|
50
|
+
PairsUpdated: number;
|
|
51
|
+
|
|
52
|
+
@Field(() => Int)
|
|
53
|
+
PairsDeleted: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
@ObjectType()
|
|
32
57
|
export class RebuildTagEmbeddingsResult {
|
|
33
58
|
@Field()
|
|
@@ -186,4 +211,44 @@ export class TagGovernanceResolver extends ResolverBase {
|
|
|
186
211
|
return { Success: false, MergeCount: 0, LowUsageCount: 0, WideNodeCount: 0, DurationMs: 0, ErrorMessage: msg };
|
|
187
212
|
}
|
|
188
213
|
}
|
|
214
|
+
|
|
215
|
+
@Mutation(() => MergeTagsResult)
|
|
216
|
+
async MergeTags(
|
|
217
|
+
@Arg('sourceTagIDs', () => [String]) sourceTagIDs: string[],
|
|
218
|
+
@Arg('survivingTagID') survivingTagID: string,
|
|
219
|
+
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
220
|
+
): Promise<MergeTagsResult> {
|
|
221
|
+
try {
|
|
222
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
223
|
+
if (!currentUser) {
|
|
224
|
+
return { Success: false, ItemsMoved: 0, TaggedItemsMoved: 0, ErrorMessage: 'Unable to determine current user' };
|
|
225
|
+
}
|
|
226
|
+
await TagEngine.Instance.Config(false, currentUser);
|
|
227
|
+
const result = await TagGovernanceEngine.Instance.MergeTags(sourceTagIDs, survivingTagID, currentUser);
|
|
228
|
+
return {
|
|
229
|
+
Success: true,
|
|
230
|
+
ItemsMoved: result.ItemsMoved,
|
|
231
|
+
TaggedItemsMoved: result.TaggedItemsMoved,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
235
|
+
LogError(`MergeTags failed for survivingTagID ${survivingTagID}: ${msg}`);
|
|
236
|
+
return { Success: false, ItemsMoved: 0, TaggedItemsMoved: 0, ErrorMessage: msg };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Mutation(() => RecomputeTagCoOccurrenceResult)
|
|
241
|
+
async RecomputeTagCoOccurrence(
|
|
242
|
+
@Ctx() { userPayload }: AppContext = {} as AppContext
|
|
243
|
+
): Promise<RecomputeTagCoOccurrenceResult> {
|
|
244
|
+
const currentUser = this.GetUserFromPayload(userPayload);
|
|
245
|
+
if (!currentUser) {
|
|
246
|
+
throw new Error('Unable to determine current user');
|
|
247
|
+
}
|
|
248
|
+
const result = await TagCoOccurrenceEngine.Instance.RecomputeCoOccurrence(currentUser);
|
|
249
|
+
return {
|
|
250
|
+
PairsUpdated: result.PairsUpdated,
|
|
251
|
+
PairsDeleted: result.PairsDeleted,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
189
254
|
}
|