@memberjunction/server 6.2.0-edge.1 → 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 +2143 -205
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +24441 -14433
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +16 -2
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +38 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +84 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +56 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +191 -74
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -50
- 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/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- 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/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.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/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- 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 +45 -89
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +23 -153
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.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 -92
- 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__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- 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__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +24183 -17182
- package/src/generic/FireAndForgetHeartbeat.ts +16 -2
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +97 -1
- package/src/generic/ResolverBase.ts +220 -76
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +228 -51
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
- package/src/resolvers/RunAIAgentResolver.ts +28 -174
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- 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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Arg, Ctx, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { LogError } from '@memberjunction/core';
|
|
3
|
+
import type { DatabasePlatform, IMetadataProvider, RunQueryResult, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { RenderPipeline } from '@memberjunction/generic-database-provider';
|
|
5
5
|
import { AppContext } from '../types.js';
|
|
6
|
-
import {
|
|
6
|
+
import { configInfo } from '../config.js';
|
|
7
|
+
import { GetReadOnlyProvider } from '../util.js';
|
|
7
8
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
8
9
|
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
9
10
|
import { RunQueryResultType } from './QueryResolver.js';
|
|
10
|
-
import {
|
|
11
|
-
import sql from 'mssql';
|
|
11
|
+
import { ClampAdhocTimeoutSeconds } from './adhocTimeout.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Input type for executing ad-hoc SQL queries directly.
|
|
@@ -32,10 +32,19 @@ class AdhocQueryInput {
|
|
|
32
32
|
/**
|
|
33
33
|
* Resolver for executing ad-hoc (unsaved) SQL queries.
|
|
34
34
|
*
|
|
35
|
+
* The query runs through the read-only provider's own ad-hoc path (`RunQuery` with `SQL`), the
|
|
36
|
+
* same one in-process callers use, so it behaves identically on every database platform: the SQL
|
|
37
|
+
* is validated and rendered there, paged in the database with a count, run under the caller's
|
|
38
|
+
* timeout, and on PostgreSQL inside a read-only transaction that is rolled back.
|
|
39
|
+
*
|
|
35
40
|
* Security:
|
|
36
|
-
* - SQL validated
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
41
|
+
* - SQL validated by the provider (SQLExpressionValidator, single read query) — blocks mutations
|
|
42
|
+
* - Authorizes the RENDERED SQL: every table reference must be an entity base view the caller
|
|
43
|
+
* reads unscoped (CanRead, no row-level filter, no denied fields). Entities with row/field
|
|
44
|
+
* policy for this user are refused — raw SQL cannot apply that policy, so those reads belong
|
|
45
|
+
* on RunView.
|
|
46
|
+
* - Read-only provider only (no fallback to read-write)
|
|
47
|
+
* - Configurable timeout (default 30s), enforced by the database
|
|
39
48
|
* - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
|
|
40
49
|
* - Refuses scope-limited principals (see {@link IsScopeLimitedPrincipal}). Raw SQL bypasses
|
|
41
50
|
* RunView, entity permissions and row-level security, so the RLS scope tokens that confine a
|
|
@@ -52,201 +61,92 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
52
61
|
@Ctx() context: AppContext
|
|
53
62
|
): Promise<RunQueryResultType> {
|
|
54
63
|
const startTime = Date.now();
|
|
55
|
-
|
|
64
|
+
// SECURITY: refuse principals whose read authority is narrower than their role grant.
|
|
65
|
+
// Ad-hoc SQL does not go through RunView, entity permissions or row-level security, so the
|
|
66
|
+
// RLS scope tokens that confine a magic-link session are never substituted. There is no
|
|
67
|
+
// filter to narrow, so the only correct answer for such a principal is no.
|
|
68
|
+
if (IsScopeLimitedPrincipal(context.userPayload?.userRecord)) {
|
|
69
|
+
return this.buildErrorResult('Ad-hoc SQL execution is not permitted for scope-limited sessions.');
|
|
70
|
+
}
|
|
71
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
72
|
+
if (!provider) {
|
|
73
|
+
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
74
|
+
}
|
|
75
|
+
const contextUser = context.userPayload?.userRecord;
|
|
76
|
+
const refusal = this.authorizeRenderedSQL(input.SQL, provider.PlatformKey, context, contextUser);
|
|
77
|
+
if (refusal) {
|
|
78
|
+
return this.buildErrorResult(refusal, Date.now() - startTime);
|
|
79
|
+
}
|
|
80
|
+
const timeoutSeconds = ClampAdhocTimeoutSeconds(input.TimeoutSeconds, configInfo.databaseSettings.requestTimeout);
|
|
81
|
+
// A negative offset must not reach paging, where it would turn paging off.
|
|
82
|
+
const startRow = Math.max(0, Number.isInteger(input.StartRow) ? input.StartRow! : 0);
|
|
56
83
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return this.buildErrorResult('Ad-hoc SQL execution is not permitted for scope-limited sessions.');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 2. Security: validate SQL using SQLExpressionValidator
|
|
68
|
-
const validator = SQLExpressionValidator.Instance;
|
|
69
|
-
const validation = validator.validateFullQuery(input.SQL);
|
|
70
|
-
if (!validation.valid) {
|
|
71
|
-
return this.buildErrorResult(validation.error || 'SQL validation failed');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 3. Get READ-ONLY data source (no fallback to read-write)
|
|
75
|
-
let readOnlyDS: sql.ConnectionPool;
|
|
76
|
-
try {
|
|
77
|
-
readOnlyDS = GetReadOnlyDataSource(context.dataSources, { allowFallbackToReadWrite: false });
|
|
78
|
-
} catch {
|
|
79
|
-
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 4. Resolve platform from the read-only provider for the render pipeline.
|
|
83
|
-
let platform: DatabasePlatform = 'sqlserver';
|
|
84
|
-
try {
|
|
85
|
-
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
86
|
-
if (provider?.PlatformKey) platform = provider.PlatformKey;
|
|
87
|
-
} catch {
|
|
88
|
-
// Provider not configured — keep the default platform.
|
|
89
|
-
}
|
|
90
|
-
const contextUser = context.userPayload?.userRecord;
|
|
91
|
-
|
|
92
|
-
// 5. Route the SQL through RenderPipeline so composition tokens
|
|
93
|
-
// resolve, comments and templates are processed, and the row cap
|
|
94
|
-
// is applied at the database (via TOP / LIMIT / OFFSET-FETCH).
|
|
95
|
-
//
|
|
96
|
-
// Page whenever a positive MaxRows is requested — INCLUDING the first
|
|
97
|
-
// page (StartRow 0). This yields OFFSET/FETCH data SQL *and* a
|
|
98
|
-
// COUNT(*) query so the response reports the true total row count
|
|
99
|
-
// rather than just the returned page size. It mirrors the saved-query
|
|
100
|
-
// path (GenericDatabaseProvider.InternalRunQuery → WrapWithPaging).
|
|
101
|
-
// (Previously paging was gated on StartRow > 0, so page 1 fell back to
|
|
102
|
-
// a TOP-N cap with TotalRowCount = page size — hiding the pager.)
|
|
103
|
-
// Clamp StartRow to a non-negative integer. A negative offset must not slip
|
|
104
|
-
// past the paging gate (which would then run the query with no row cap at all).
|
|
105
|
-
const startRow = Math.max(0, Number.isInteger(input.StartRow) ? input.StartRow! : 0);
|
|
106
|
-
const maxRows = input.MaxRows;
|
|
107
|
-
const usePaging = maxRows != null && Number.isInteger(maxRows) && maxRows > 0;
|
|
108
|
-
|
|
109
|
-
let dataSQL: string;
|
|
110
|
-
let countSQL: string | null = null;
|
|
111
|
-
try {
|
|
112
|
-
const rendered = RenderPipeline.Run(input.SQL, {
|
|
113
|
-
Platform: platform,
|
|
114
|
-
ContextUser: contextUser,
|
|
115
|
-
...(usePaging ? { Paging: { StartRow: startRow, MaxRows: maxRows! } } : {}),
|
|
116
|
-
});
|
|
117
|
-
dataSQL = rendered.FinalSQL;
|
|
118
|
-
countSQL = rendered.PagingResult?.CountSQL ?? null;
|
|
119
|
-
} catch (renderErr) {
|
|
120
|
-
const renderMsg = renderErr instanceof Error ? renderErr.message : String(renderErr);
|
|
121
|
-
return this.buildErrorResult(`Ad-hoc query rendering failed: ${renderMsg}`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 6. Execute the page (and, only when a full page needs it, the count) under
|
|
125
|
-
// a shared wall-clock deadline derived from the request's timeout budget.
|
|
126
|
-
const deadline = startTime + (input.TimeoutSeconds ?? 30) * 1000;
|
|
127
|
-
const { recordset, totalRowCount } = await this.executeDataAndCount(
|
|
128
|
-
readOnlyDS, dataSQL, countSQL, startRow, usePaging ? maxRows! : null, deadline
|
|
129
|
-
);
|
|
130
|
-
const executionTimeMs = Date.now() - startTime;
|
|
131
|
-
|
|
132
|
-
// 7. Return as RunQueryResultType
|
|
133
|
-
return {
|
|
134
|
-
QueryID: '',
|
|
135
|
-
QueryName: 'Ad-Hoc Query',
|
|
136
|
-
Success: true,
|
|
137
|
-
Results: JSON.stringify(recordset),
|
|
138
|
-
RowCount: recordset.length,
|
|
139
|
-
TotalRowCount: totalRowCount,
|
|
140
|
-
PageNumber: usePaging ? Math.floor(startRow / maxRows!) + 1 : undefined,
|
|
141
|
-
PageSize: usePaging ? maxRows! : undefined,
|
|
142
|
-
ExecutionTime: executionTimeMs,
|
|
143
|
-
ErrorMessage: ''
|
|
144
|
-
};
|
|
84
|
+
const result = await provider.RunQuery({
|
|
85
|
+
SQL: input.SQL,
|
|
86
|
+
MaxRows: input.MaxRows,
|
|
87
|
+
StartRow: startRow,
|
|
88
|
+
TimeoutSeconds: timeoutSeconds,
|
|
89
|
+
}, contextUser);
|
|
90
|
+
return this.toResultType(result, startRow, input.MaxRows, timeoutSeconds, Date.now() - startTime);
|
|
145
91
|
} catch (err: unknown) {
|
|
146
|
-
const executionTimeMs = Date.now() - startTime;
|
|
147
92
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
148
|
-
|
|
149
|
-
// Handle timeout
|
|
150
|
-
if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
|
|
151
|
-
return {
|
|
152
|
-
QueryID: '',
|
|
153
|
-
QueryName: 'Ad-Hoc Query',
|
|
154
|
-
Success: false,
|
|
155
|
-
Results: '[]',
|
|
156
|
-
RowCount: 0,
|
|
157
|
-
TotalRowCount: 0,
|
|
158
|
-
PageNumber: undefined,
|
|
159
|
-
PageSize: undefined,
|
|
160
|
-
ExecutionTime: executionTimeMs,
|
|
161
|
-
ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
93
|
LogError(`Ad-hoc query execution failed: ${errorMessage}`);
|
|
166
|
-
return {
|
|
167
|
-
QueryID: '',
|
|
168
|
-
QueryName: 'Ad-Hoc Query',
|
|
169
|
-
Success: false,
|
|
170
|
-
Results: '[]',
|
|
171
|
-
RowCount: 0,
|
|
172
|
-
TotalRowCount: 0,
|
|
173
|
-
PageNumber: undefined,
|
|
174
|
-
PageSize: undefined,
|
|
175
|
-
ExecutionTime: executionTimeMs,
|
|
176
|
-
ErrorMessage: `Query execution failed: ${errorMessage}`
|
|
177
|
-
};
|
|
94
|
+
return this.buildErrorResult(`Query execution failed: ${errorMessage}`, Date.now() - startTime);
|
|
178
95
|
}
|
|
179
96
|
}
|
|
180
97
|
|
|
181
98
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
99
|
+
* SECURITY: authorizes the READ itself, against the SQL that will actually run. The provider
|
|
100
|
+
* validates that the SQL is a single read statement but places no restriction on WHAT is read.
|
|
101
|
+
* Rendering comes first because composition (`{{query:"..."}}`) and templates can introduce
|
|
102
|
+
* table references the raw input never named. Every table reference must be an entity base
|
|
103
|
+
* view the caller can read UNSCOPED — CanRead with no row-level filter and no denied fields —
|
|
104
|
+
* because raw SQL applies neither (see assertFullQueryUsesReadableEntityViews). The provider
|
|
105
|
+
* renders the same input the same way and only wraps it in server-built paging and a count,
|
|
106
|
+
* so the rendered SQL is the full set of reads. Returns the refusal message, or null.
|
|
190
107
|
*/
|
|
191
|
-
private
|
|
192
|
-
ds: sql.ConnectionPool,
|
|
193
|
-
dataSQL: string,
|
|
194
|
-
countSQL: string | null,
|
|
195
|
-
startRow: number,
|
|
196
|
-
maxRows: number | null,
|
|
197
|
-
deadline: number,
|
|
198
|
-
): Promise<{ recordset: Record<string, unknown>[]; totalRowCount: number }> {
|
|
199
|
-
const dataResult = await this.runSqlWithDeadline<Record<string, unknown>>(ds, dataSQL, deadline);
|
|
200
|
-
const recordset = (dataResult.recordset ?? []) as Record<string, unknown>[];
|
|
201
|
-
|
|
202
|
-
// Total already known from the page alone (unpaged, or a short page)? Skip the count.
|
|
203
|
-
const exact = ExactTotalFromPage(startRow, recordset.length, maxRows);
|
|
204
|
-
if (exact != null || !countSQL) {
|
|
205
|
-
return { recordset, totalRowCount: exact ?? recordset.length };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Full page — a COUNT(*) is required to know the true total.
|
|
209
|
-
const lowerBound = startRow + recordset.length;
|
|
108
|
+
private authorizeRenderedSQL(sqlText: string, platform: DatabasePlatform, context: AppContext, contextUser: UserInfo | undefined): string | null {
|
|
210
109
|
try {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return
|
|
110
|
+
const rendered = RenderPipeline.Run(sqlText, { Platform: platform, ContextUser: contextUser, RequireReadStatement: true });
|
|
111
|
+
const mdProvider: IMetadataProvider | undefined = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true }) ?? undefined;
|
|
112
|
+
this.assertFullQueryUsesReadableEntityViews(rendered.Trace.AfterTemplates, mdProvider, contextUser, 'ad-hoc SQL');
|
|
113
|
+
return null;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return err instanceof Error ? err.message : String(err);
|
|
217
116
|
}
|
|
218
117
|
}
|
|
219
118
|
|
|
220
|
-
/**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
235
|
-
try {
|
|
236
|
-
return await Promise.race([
|
|
237
|
-
new sql.Request(ds).query<T>(sqlText),
|
|
238
|
-
new Promise<never>((_, reject) => {
|
|
239
|
-
timer = setTimeout(() => reject(new Error('Query timeout exceeded')), remaining);
|
|
240
|
-
}),
|
|
241
|
-
]);
|
|
242
|
-
} finally {
|
|
243
|
-
if (timer) {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
}
|
|
119
|
+
/** Maps the provider's result to the GraphQL shape, naming a timeout plainly. */
|
|
120
|
+
private toResultType(
|
|
121
|
+
result: RunQueryResult,
|
|
122
|
+
startRow: number,
|
|
123
|
+
maxRows: number | undefined,
|
|
124
|
+
timeoutSeconds: number,
|
|
125
|
+
executionTimeMs: number,
|
|
126
|
+
): RunQueryResultType {
|
|
127
|
+
if (!result.Success) {
|
|
128
|
+
const message = result.ErrorMessage ?? 'Ad-hoc query failed';
|
|
129
|
+
return this.buildErrorResult(
|
|
130
|
+
/timeout/i.test(message) ? `Query execution exceeded ${timeoutSeconds} second timeout` : message,
|
|
131
|
+
executionTimeMs,
|
|
132
|
+
);
|
|
246
133
|
}
|
|
134
|
+
const paged = maxRows != null && Number.isInteger(maxRows) && maxRows > 0;
|
|
135
|
+
return {
|
|
136
|
+
QueryID: '',
|
|
137
|
+
QueryName: 'Ad-Hoc Query',
|
|
138
|
+
Success: true,
|
|
139
|
+
Results: JSON.stringify(result.Results),
|
|
140
|
+
RowCount: result.RowCount,
|
|
141
|
+
TotalRowCount: result.TotalRowCount,
|
|
142
|
+
PageNumber: paged ? Math.floor(startRow / maxRows!) + 1 : undefined,
|
|
143
|
+
PageSize: paged ? maxRows : undefined,
|
|
144
|
+
ExecutionTime: executionTimeMs,
|
|
145
|
+
ErrorMessage: ''
|
|
146
|
+
};
|
|
247
147
|
}
|
|
248
148
|
|
|
249
|
-
private buildErrorResult(errorMessage: string): RunQueryResultType {
|
|
149
|
+
private buildErrorResult(errorMessage: string, executionTimeMs = 0): RunQueryResultType {
|
|
250
150
|
return {
|
|
251
151
|
QueryID: '',
|
|
252
152
|
QueryName: 'Ad-Hoc Query',
|
|
@@ -256,7 +156,7 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
256
156
|
TotalRowCount: 0,
|
|
257
157
|
PageNumber: undefined,
|
|
258
158
|
PageSize: undefined,
|
|
259
|
-
ExecutionTime:
|
|
159
|
+
ExecutionTime: executionTimeMs,
|
|
260
160
|
ErrorMessage: errorMessage
|
|
261
161
|
};
|
|
262
162
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishes one agent run's live status on the caller's session.
|
|
3
|
+
*
|
|
4
|
+
* `RunAIAgentResolver` uses this for every status it sends. A host whose
|
|
5
|
+
* `AgentTurnHandler` answers with an In-Progress reply uses the same publisher,
|
|
6
|
+
* on that caller's session, so the chat follows the turn: significant-step
|
|
7
|
+
* progress, streamed text, the partial result, and the completion. When the
|
|
8
|
+
* turn throws instead of returning a result, call `PublishFailure` with the
|
|
9
|
+
* reply row's id and the error.
|
|
10
|
+
*/
|
|
11
|
+
import type { PubSubEngine } from 'type-graphql';
|
|
12
|
+
import type {
|
|
13
|
+
AgentExecutionProgressCallback,
|
|
14
|
+
AgentExecutionStreamingCallback,
|
|
15
|
+
ExecuteAgentResult,
|
|
16
|
+
} from '@memberjunction/ai-core-plus';
|
|
17
|
+
import type { UserPayload } from '../types.js';
|
|
18
|
+
import { PublishStatusUpdate } from '../generic/PushStatusResolver.js';
|
|
19
|
+
|
|
20
|
+
const SIGNIFICANT_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
21
|
+
|
|
22
|
+
/** The run fields the published messages read. Entities and test fakes both satisfy this. */
|
|
23
|
+
interface StatusRun {
|
|
24
|
+
ID?: string;
|
|
25
|
+
ConversationDetailID?: string;
|
|
26
|
+
ErrorMessage?: string;
|
|
27
|
+
Steps?: Array<{ StepName?: string }>;
|
|
28
|
+
GetAll?: () => unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AgentRunStatusPublisher {
|
|
32
|
+
private run: StatusRun | null = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param sessionId Session stamped on the message body. Defaults to the payload's session.
|
|
36
|
+
* The push itself is always addressed to `userPayload.sessionId`, matching the resolver.
|
|
37
|
+
*/
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly pubSub: PubSubEngine,
|
|
40
|
+
private readonly userPayload: UserPayload,
|
|
41
|
+
private readonly sessionId: string = userPayload.sessionId,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
/** Latest run observed from a progress event, including steps that are not published. */
|
|
45
|
+
public get AgentRun(): StatusRun | null {
|
|
46
|
+
return this.run;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** AgentRunner's progress callback. Noise steps are kept for {@link AgentRun} and not published. */
|
|
50
|
+
public get OnProgress(): AgentExecutionProgressCallback {
|
|
51
|
+
return (progress) => {
|
|
52
|
+
const fromEvent = progress.metadata?.agentRun as StatusRun | undefined;
|
|
53
|
+
if (fromEvent) {
|
|
54
|
+
this.run = fromEvent;
|
|
55
|
+
}
|
|
56
|
+
if (!SIGNIFICANT_STEPS.includes(progress.step)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const agentRun = fromEvent || this.run;
|
|
60
|
+
if (!agentRun?.ID) {
|
|
61
|
+
// A host that streams before the first progress event otherwise sees nothing and no reason.
|
|
62
|
+
console.error('❌ No agent run available for progress callback');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.publish('ExecutionProgress', {
|
|
66
|
+
sessionId: this.sessionId,
|
|
67
|
+
agentRunId: agentRun.ID,
|
|
68
|
+
type: 'progress',
|
|
69
|
+
agentRun: agentRun.GetAll ? agentRun.GetAll() : agentRun,
|
|
70
|
+
progress: {
|
|
71
|
+
currentStep: progress.step,
|
|
72
|
+
percentage: progress.percentage,
|
|
73
|
+
message: progress.message,
|
|
74
|
+
agentName: progress.metadata?.agentName,
|
|
75
|
+
agentType: progress.metadata?.agentType,
|
|
76
|
+
stepCount: progress.metadata?.stepCount,
|
|
77
|
+
hierarchicalStep: progress.metadata?.hierarchicalStep,
|
|
78
|
+
},
|
|
79
|
+
timestamp: new Date(),
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** AgentRunner's streaming callback. Publishes nothing until a progress event has carried the run. */
|
|
85
|
+
public get OnStreaming(): AgentExecutionStreamingCallback {
|
|
86
|
+
return (chunk) => {
|
|
87
|
+
const agentRun = this.run;
|
|
88
|
+
if (!agentRun?.ID) {
|
|
89
|
+
console.error('❌ No agent run available for streaming callback');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.publish('StreamingContent', {
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
agentRunId: agentRun.ID,
|
|
95
|
+
type: 'streaming',
|
|
96
|
+
agentRun: agentRun.GetAll ? agentRun.GetAll() : agentRun,
|
|
97
|
+
streaming: {
|
|
98
|
+
content: chunk.content,
|
|
99
|
+
isPartial: !chunk.isComplete,
|
|
100
|
+
stepName: chunk.stepType,
|
|
101
|
+
agentName: chunk.modelName,
|
|
102
|
+
kind: chunk.kind,
|
|
103
|
+
},
|
|
104
|
+
timestamp: new Date(),
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The partial result, then the completion. `conversationDetailId` is the reply row the
|
|
111
|
+
* chat should update. When omitted, the completion uses the run's own detail id.
|
|
112
|
+
*/
|
|
113
|
+
public PublishFinal(result: ExecuteAgentResult, conversationDetailId?: string, resultJson?: string): void {
|
|
114
|
+
const agentRun = result.agentRun as StatusRun | undefined;
|
|
115
|
+
if (agentRun?.ID) {
|
|
116
|
+
let lastStep = 'Completed';
|
|
117
|
+
const steps = agentRun.Steps;
|
|
118
|
+
if (steps && steps.length > 0) {
|
|
119
|
+
lastStep = steps[steps.length - 1]?.StepName || 'Completed';
|
|
120
|
+
}
|
|
121
|
+
this.publish('StreamingContent', {
|
|
122
|
+
sessionId: this.sessionId,
|
|
123
|
+
agentRunId: agentRun.ID,
|
|
124
|
+
type: 'partial_result',
|
|
125
|
+
partialResult: {
|
|
126
|
+
currentStep: lastStep,
|
|
127
|
+
partialOutput: result.payload || undefined,
|
|
128
|
+
},
|
|
129
|
+
timestamp: new Date(),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
this.publish('StreamingContent', {
|
|
133
|
+
sessionId: this.sessionId,
|
|
134
|
+
agentRunId: agentRun?.ID || 'unknown',
|
|
135
|
+
type: 'complete',
|
|
136
|
+
timestamp: new Date(),
|
|
137
|
+
conversationDetailId: conversationDetailId || agentRun?.ConversationDetailID,
|
|
138
|
+
success: result.success,
|
|
139
|
+
errorMessage: agentRun?.ErrorMessage || undefined,
|
|
140
|
+
result: resultJson || undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Completion for a failure that has no run. The fire-and-forget path uses this when
|
|
146
|
+
* `executeAIAgent` rejects. A host uses it the same way: when the turn throws
|
|
147
|
+
* instead of returning a result, call `PublishFailure` with the reply row's id
|
|
148
|
+
* and the error. That completion is what tells the chat the reply finished.
|
|
149
|
+
*/
|
|
150
|
+
public PublishFailure(conversationDetailId: string | undefined, errorMessage: string): void {
|
|
151
|
+
this.publish('StreamingContent', {
|
|
152
|
+
sessionId: this.sessionId,
|
|
153
|
+
agentRunId: 'unknown',
|
|
154
|
+
type: 'complete',
|
|
155
|
+
timestamp: new Date(),
|
|
156
|
+
conversationDetailId,
|
|
157
|
+
success: false,
|
|
158
|
+
errorMessage,
|
|
159
|
+
result: JSON.stringify({ success: false, errorMessage }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private publish(type: 'ExecutionProgress' | 'StreamingContent', data: unknown): void {
|
|
164
|
+
PublishStatusUpdate(this.pubSub, {
|
|
165
|
+
sessionId: this.userPayload.sessionId,
|
|
166
|
+
ownerUserId: this.userPayload?.userRecord?.ID ?? '',
|
|
167
|
+
message: JSON.stringify({
|
|
168
|
+
resolver: 'RunAIAgentResolver',
|
|
169
|
+
type,
|
|
170
|
+
status: 'ok',
|
|
171
|
+
data,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|