@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +180 -4
- package/dist/agentSessions/SessionManager.d.ts +19 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +27 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/generated.d.ts +619 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3341 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +28 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +57 -8
- 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 +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +79 -15
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +472 -97
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +48 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
- package/dist/integration/InactiveRowWarnings.js +57 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +62 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +67 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +4 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +20 -6
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts +37 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +3 -6
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/InactiveRowWarnings.test.ts +87 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +85 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +55 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/agentSessions/SessionManager.ts +27 -9
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +60 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +55 -36
- package/src/context.ts +26 -2
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
- package/src/generated/generated.ts +2347 -10551
- package/src/generic/ResolverBase.ts +67 -8
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +165 -16
- package/src/integration/CustomColumnPromoter.ts +520 -98
- package/src/integration/InactiveRowWarnings.ts +85 -0
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +93 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/AdhocQueryResolver.ts +21 -6
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/resolvers/meetingRecordingRegistration.ts +3 -7
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/types.ts +14 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply-time warnings for DECLARED integration rows that an apply silently leaves out.
|
|
3
|
+
*
|
|
4
|
+
* An IntegrationObject / IntegrationObjectField is never deleted — a comprehensive rediscovery
|
|
5
|
+
* that does not observe a declared row sets it `Status='Disabled'` instead (reversible on a later
|
|
6
|
+
* discovery), and a schema-limit breach can do the same. The apply path then rebuilds its source
|
|
7
|
+
* schema from ACTIVE rows only, so those rows simply are not in the schema it materializes: the
|
|
8
|
+
* table appears without the column, or a requested object is not created at all, and nothing in
|
|
9
|
+
* the apply output says why.
|
|
10
|
+
*
|
|
11
|
+
* The decision is kept pure here — the resolver supplies the rows and surfaces the strings — so it
|
|
12
|
+
* is testable without standing up the resolver or the metadata provider, matching how
|
|
13
|
+
* `decideAbsentDeactivations` and `ComputeRemovedDependencyWarnings` are factored.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A row reduced to what the warning needs: its name and its lifecycle status. */
|
|
17
|
+
export type NamedStatusRow = { Name: string; Status: string | null };
|
|
18
|
+
|
|
19
|
+
export type InactiveRowWarningInput = {
|
|
20
|
+
/**
|
|
21
|
+
* The object names this apply asked for, exactly as requested. `null`/empty means "everything
|
|
22
|
+
* active", in which case deactivated OBJECTS are not reported: a large catalog can legitimately
|
|
23
|
+
* carry hundreds of them and announcing those on every apply is noise, not signal. Requested
|
|
24
|
+
* objects are always reported, because the caller named them and got nothing.
|
|
25
|
+
*/
|
|
26
|
+
RequestedNames?: string[] | null;
|
|
27
|
+
/** Every object on the integration, active or not. */
|
|
28
|
+
AllObjects: NamedStatusRow[];
|
|
29
|
+
/**
|
|
30
|
+
* Fields keyed by object name, for the objects this apply actually materializes. Only those
|
|
31
|
+
* objects need their fields: a non-materialized object's fields are reported by the object
|
|
32
|
+
* warning, not one line per field.
|
|
33
|
+
*/
|
|
34
|
+
FieldsByObjectName: Record<string, NamedStatusRow[]>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Renders a name list for an operator-facing warning: every name when the list is short, and a
|
|
39
|
+
* capped sample plus a count when it is not. Apply runs against catalogs with hundreds of objects,
|
|
40
|
+
* so an uncapped list turns a useful warning into a wall of text nobody reads.
|
|
41
|
+
*/
|
|
42
|
+
export function SummarizeNames(names: string[], max = 12): string {
|
|
43
|
+
if (names.length <= max) return names.join(', ');
|
|
44
|
+
return `${names.slice(0, max).join(', ')}, and ${names.length - max} more`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isActive = (row: NamedStatusRow): boolean => row.Status === 'Active';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds the operator-facing warnings for declared rows this apply will not materialize.
|
|
51
|
+
* Returns an empty array when everything declared and in scope is Active.
|
|
52
|
+
*/
|
|
53
|
+
export function ComputeInactiveRowWarnings(input: InactiveRowWarningInput): string[] {
|
|
54
|
+
const warnings: string[] = [];
|
|
55
|
+
const requested = input.RequestedNames && input.RequestedNames.length > 0
|
|
56
|
+
? new Set(input.RequestedNames.map(n => n.toLowerCase()))
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
if (requested) {
|
|
60
|
+
const inactiveRequested = input.AllObjects
|
|
61
|
+
.filter(o => requested.has(o.Name.toLowerCase()) && !isActive(o))
|
|
62
|
+
.map(o => `${o.Name} (${o.Status ?? 'no status'})`);
|
|
63
|
+
if (inactiveRequested.length > 0) {
|
|
64
|
+
warnings.push(
|
|
65
|
+
`${inactiveRequested.length} requested object(s) are not Active and were NOT materialized — ` +
|
|
66
|
+
`${SummarizeNames(inactiveRequested)}. An object is deactivated (never deleted) when an ` +
|
|
67
|
+
`authoritative rediscovery does not observe it, or when it exceeded a schema limit; re-run ` +
|
|
68
|
+
`discovery against a source that exposes it, or re-enable it on MJ: Integration Objects.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const [objectName, fields] of Object.entries(input.FieldsByObjectName)) {
|
|
74
|
+
const dropped = fields.filter(f => !isActive(f)).map(f => `${f.Name} (${f.Status ?? 'no status'})`);
|
|
75
|
+
if (dropped.length === 0) continue;
|
|
76
|
+
warnings.push(
|
|
77
|
+
`${objectName}: ${dropped.length} declared field(s) are not Active and were NOT materialized — ` +
|
|
78
|
+
`${SummarizeNames(dropped)}. A field is deactivated (never deleted) when an authoritative ` +
|
|
79
|
+
`rediscovery does not observe it; re-run discovery against a source that exposes it, or re-enable ` +
|
|
80
|
+
`it on MJ: Integration Object Fields.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return warnings;
|
|
85
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Bridges RSU pipeline lifecycle events onto the integration progress artifact
|
|
3
|
+
* stream, so a Runtime Schema Update is observable the same way a sync or a connector build is.
|
|
4
|
+
*
|
|
5
|
+
* The gap this closes: `IntegrationRunKind` has always included `'RSU'` and
|
|
6
|
+
* `RUN_KIND_TO_TOPIC` has always mapped it to an `'RSU'` subscription channel — but nothing in
|
|
7
|
+
* production ever constructed an emitter with that kind, so the channel carried no traffic and
|
|
8
|
+
* `IntegrationTailRunEvents` had no RSU runs to tail. The only live signal was polling
|
|
9
|
+
* `RuntimeSchemaUpdateStatus`, which reports the CURRENT step and nothing else: no history, no
|
|
10
|
+
* durable record, and nothing at all to read after a mid-run API restart — which the RSU pipeline
|
|
11
|
+
* does to itself, by design, as one of its own steps.
|
|
12
|
+
*
|
|
13
|
+
* Layering: `@memberjunction/schema-engine` must not depend on the progress artifacts package, so
|
|
14
|
+
* it publishes plain {@link RSUObserverEvent}s and this module — one layer up, where both are
|
|
15
|
+
* already dependencies — owns the emitter lifecycle.
|
|
16
|
+
*/
|
|
17
|
+
import { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
|
|
18
|
+
import type { EmitterOptions } from '@memberjunction/integration-progress-artifacts';
|
|
19
|
+
import { RuntimeSchemaManager } from '@memberjunction/schema-engine';
|
|
20
|
+
import type { RSUObserverEvent } from '@memberjunction/schema-engine';
|
|
21
|
+
import { LogError } from '@memberjunction/core';
|
|
22
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
23
|
+
|
|
24
|
+
/** The emitter stage name used for run-level (non-step) events. */
|
|
25
|
+
const RUN_STAGE = 'RSUPipeline';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* How often an in-flight step reports that it is still alive.
|
|
29
|
+
*
|
|
30
|
+
* RSU's expensive steps — CodeGen, TypeScript compile, npm install — run for minutes with no
|
|
31
|
+
* intermediate observer event, so without this the stream goes silent for the entire step and a
|
|
32
|
+
* watcher cannot tell "compiling" from "hung". 30s matches the interval the agent notification
|
|
33
|
+
* conventions ask for on long phases.
|
|
34
|
+
*/
|
|
35
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Owns one emitter per RSU pipeline run and translates observer events into progress events.
|
|
39
|
+
*
|
|
40
|
+
* A {@link BaseSingleton} because the state here is inherently process-wide AND has to be readable
|
|
41
|
+
* from elsewhere. `RuntimeSchemaManager` is itself a singleton and serializes its runs behind the
|
|
42
|
+
* pipeline lock, so at most one RSU run is ever in flight and one current-emitter slot is sufficient
|
|
43
|
+
* — but the reason for the singleton is {@link CurrentRunID}: a resolver that has just triggered an
|
|
44
|
+
* RSU needs to hand its client a run to tail, and it can only do that if it can reach this instance.
|
|
45
|
+
* Constructing the bridge and keeping the only reference inside the observer closure made that
|
|
46
|
+
* accessor unreachable from any caller.
|
|
47
|
+
*
|
|
48
|
+
* NOT a `BaseEngine`: there is no metadata to load and nothing to `Config()`. The distinction is
|
|
49
|
+
* "single shared instance" (this) versus "cached metadata with a load lifecycle" (that).
|
|
50
|
+
*/
|
|
51
|
+
export class RSUProgressBridge extends BaseSingleton<RSUProgressBridge> {
|
|
52
|
+
private emitter: IntegrationProgressEmitter | null = null;
|
|
53
|
+
private currentRunID: string | null = null;
|
|
54
|
+
/** Liveness timer for the step currently in flight — see {@link HEARTBEAT_INTERVAL_MS}. */
|
|
55
|
+
private heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
private emitterOptions: EmitterOptions = {};
|
|
57
|
+
|
|
58
|
+
/** The one bridge for this process. */
|
|
59
|
+
public static get Instance(): RSUProgressBridge {
|
|
60
|
+
return super.getInstance<RSUProgressBridge>();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options passed through to each run's emitter. Set once, before the first run.
|
|
65
|
+
*
|
|
66
|
+
* The only field that matters in practice is `rootDir` — left unset in production so RSU runs
|
|
67
|
+
* land in the same `logs/integration-runs/` tree as every other run kind and
|
|
68
|
+
* `IntegrationTailRunEvents` finds them without configuration. Tests point it at a temp dir.
|
|
69
|
+
*
|
|
70
|
+
* A settable property rather than a constructor argument because `BaseSingleton.getInstance`
|
|
71
|
+
* requires a zero-argument constructor.
|
|
72
|
+
*/
|
|
73
|
+
public Configure(emitterOptions: EmitterOptions): void {
|
|
74
|
+
this.emitterOptions = emitterOptions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The run ID of the in-flight RSU run, or null when idle. Lets a caller that triggered an RSU
|
|
79
|
+
* hand a client something to tail via `IntegrationTailRunEvents` / the `RSU` subscription
|
|
80
|
+
* channel.
|
|
81
|
+
*/
|
|
82
|
+
public get CurrentRunID(): string | null {
|
|
83
|
+
return this.currentRunID;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Abandons any in-flight run state and stops its heartbeat, without emitting a terminal event.
|
|
88
|
+
*
|
|
89
|
+
* Needed because singleton state outlives a single caller: a test (or a server tearing down)
|
|
90
|
+
* must be able to return the bridge to idle without the next run inheriting a stale emitter or
|
|
91
|
+
* leaking an interval that keeps the process alive.
|
|
92
|
+
*/
|
|
93
|
+
public Reset(): void {
|
|
94
|
+
this.stopHeartbeat();
|
|
95
|
+
this.emitter = null;
|
|
96
|
+
this.currentRunID = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The observer to hand to {@link RuntimeSchemaManager.PipelineObserver}. */
|
|
100
|
+
public readonly Observe = (event: RSUObserverEvent): void => {
|
|
101
|
+
switch (event.Kind) {
|
|
102
|
+
case 'run.start':
|
|
103
|
+
return this.onRunStart(event);
|
|
104
|
+
case 'step.start':
|
|
105
|
+
return this.onStepStart(event);
|
|
106
|
+
case 'step.end':
|
|
107
|
+
return this.onStepEnd(event);
|
|
108
|
+
case 'run.end':
|
|
109
|
+
return this.onRunEnd(event);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private onRunStart(event: Extract<RSUObserverEvent, { Kind: 'run.start' }>): void {
|
|
114
|
+
// A retry re-enters RunPipelineBatch, so it legitimately starts a NEW run. If a previous
|
|
115
|
+
// emitter is still open (a throw escaped before run.end), close it rather than leak it.
|
|
116
|
+
if (this.emitter) this.closeCurrent('Superseded by a new RSU run');
|
|
117
|
+
|
|
118
|
+
const runID = IntegrationProgressEmitter.newRunID('rsu');
|
|
119
|
+
this.currentRunID = runID;
|
|
120
|
+
this.emitter = new IntegrationProgressEmitter(
|
|
121
|
+
{
|
|
122
|
+
runID,
|
|
123
|
+
runKind: 'RSU',
|
|
124
|
+
triggerType: 'Pipeline',
|
|
125
|
+
startedAt: new Date().toISOString(),
|
|
126
|
+
context: {
|
|
127
|
+
itemCount: event.ItemCount,
|
|
128
|
+
descriptions: event.Descriptions,
|
|
129
|
+
affectedTables: event.AffectedTables,
|
|
130
|
+
stepTotal: event.StepTotal,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
this.emitterOptions,
|
|
134
|
+
);
|
|
135
|
+
this.emitter.runStart(
|
|
136
|
+
`RSU pipeline started — ${event.ItemCount} migration(s), ${event.StepTotal} expected step(s): ` +
|
|
137
|
+
event.Descriptions.join('; ')
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private onStepStart(event: Extract<RSUObserverEvent, { Kind: 'step.start' }>): void {
|
|
142
|
+
if (!this.emitter) return;
|
|
143
|
+
this.emitter.stageStart(event.Name, this.positionLabel(event.StepIndex, event.StepTotal));
|
|
144
|
+
this.startHeartbeat(event.Name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private onStepEnd(event: Extract<RSUObserverEvent, { Kind: 'step.end' }>): void {
|
|
148
|
+
this.stopHeartbeat();
|
|
149
|
+
if (!this.emitter) return;
|
|
150
|
+
// A failed step is a stage error, not a stage completion — the run may still continue (a
|
|
151
|
+
// per-item migration failure does not abort the batch), so this is deliberately not fatal.
|
|
152
|
+
if (event.Status === 'failed') {
|
|
153
|
+
this.emitter.stageError(event.Name, event.Message, { durationMs: event.DurationMs, code: 'RSU_STEP_FAILED' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Deliberately no `counts` here. Counts roll up into the run aggregate, and the countable
|
|
157
|
+
// unit of an RSU run is MIGRATIONS, not steps — reporting "processed: 12" for a 12-step
|
|
158
|
+
// single-migration run would read as 12 migrations. The run-level quartet is emitted once,
|
|
159
|
+
// in onRunEnd. Step position lives in the stage name + the step.start message.
|
|
160
|
+
this.emitter.stageComplete(event.Name);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private onRunEnd(event: Extract<RSUObserverEvent, { Kind: 'run.end' }>): void {
|
|
164
|
+
this.stopHeartbeat();
|
|
165
|
+
const emitter = this.emitter;
|
|
166
|
+
if (!emitter) return;
|
|
167
|
+
this.emitter = null;
|
|
168
|
+
this.currentRunID = null;
|
|
169
|
+
const summary = `${event.SuccessCount}/${event.TotalCount} migration(s) succeeded`;
|
|
170
|
+
// The one authoritative applied quartet for the run, in migrations — this is what feeds
|
|
171
|
+
// the reader's aggregateCounts.
|
|
172
|
+
emitter.stageComplete(RUN_STAGE, {
|
|
173
|
+
processed: event.TotalCount,
|
|
174
|
+
succeeded: event.SuccessCount,
|
|
175
|
+
failed: event.FailureCount,
|
|
176
|
+
totalKnown: event.TotalCount,
|
|
177
|
+
});
|
|
178
|
+
// Terminal writes are async (they flush the JSONL + write result.json). The observer
|
|
179
|
+
// contract is synchronous, so this is intentionally fire-and-forget with a logged catch —
|
|
180
|
+
// a progress-artifact write must never surface as a pipeline failure.
|
|
181
|
+
const terminal = event.Success
|
|
182
|
+
? emitter.complete(`RSU pipeline complete — ${summary}`)
|
|
183
|
+
: emitter.fail(
|
|
184
|
+
`RSU pipeline failed at '${event.ErrorStep ?? 'unknown step'}' — ${summary}: ` +
|
|
185
|
+
`${event.ErrorMessage ?? 'no error message'}`,
|
|
186
|
+
'rsu-pipeline-failed'
|
|
187
|
+
);
|
|
188
|
+
void terminal.catch(err => LogError(`RSUProgressBridge: terminal write failed — ${err}`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Starts reporting liveness for the step that just opened. Any previous timer is cleared first,
|
|
193
|
+
* so a missed `step.end` can never leave two timers running.
|
|
194
|
+
*/
|
|
195
|
+
private startHeartbeat(stage: string): void {
|
|
196
|
+
this.stopHeartbeat();
|
|
197
|
+
const startedAt = Date.now();
|
|
198
|
+
this.heartbeat = setInterval(() => {
|
|
199
|
+
const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
|
|
200
|
+
this.emitter?.heartbeat(stage, `${stage} still running — ${elapsedSec}s elapsed`);
|
|
201
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
202
|
+
// Never hold the process open on a progress timer.
|
|
203
|
+
this.heartbeat.unref?.();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Stops the liveness timer. Safe to call when none is running. */
|
|
207
|
+
private stopHeartbeat(): void {
|
|
208
|
+
if (!this.heartbeat) return;
|
|
209
|
+
clearInterval(this.heartbeat);
|
|
210
|
+
this.heartbeat = null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Closes an orphaned emitter so a leaked run does not stay in-flight forever. */
|
|
214
|
+
private closeCurrent(reason: string): void {
|
|
215
|
+
this.stopHeartbeat();
|
|
216
|
+
const emitter = this.emitter;
|
|
217
|
+
this.emitter = null;
|
|
218
|
+
this.currentRunID = null;
|
|
219
|
+
void emitter?.fail(reason, 'rsu-run-abandoned')
|
|
220
|
+
.catch(err => LogError(`RSUProgressBridge: abandon write failed — ${err}`));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* "step 4 of 12" — omitted entirely when the determinate counter isn't armed.
|
|
225
|
+
*
|
|
226
|
+
* Explicit null/undefined checks rather than truthiness: a 0 index is falsy, and silently
|
|
227
|
+
* dropping the label for the first step of a 0-based counter would be a bug that only appears
|
|
228
|
+
* if the pipeline's numbering ever changes.
|
|
229
|
+
*/
|
|
230
|
+
private positionLabel(stepIndex?: number, stepTotal?: number): string | undefined {
|
|
231
|
+
if (stepIndex == null || stepTotal == null) return undefined;
|
|
232
|
+
return `step ${stepIndex} of ${stepTotal}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Attaches a fresh {@link RSUProgressBridge} to the RuntimeSchemaManager singleton. Call once at
|
|
238
|
+
* server startup, after the progress publish hook is registered so the very first RSU event also
|
|
239
|
+
* reaches live subscribers.
|
|
240
|
+
*
|
|
241
|
+
* @returns the bridge, so callers can read {@link RSUProgressBridge.CurrentRunID}.
|
|
242
|
+
*/
|
|
243
|
+
export function RegisterRSUProgressBridge(emitterOptions: EmitterOptions = {}): RSUProgressBridge {
|
|
244
|
+
const bridge = RSUProgressBridge.Instance;
|
|
245
|
+
bridge.Configure(emitterOptions);
|
|
246
|
+
RuntimeSchemaManager.Instance.PipelineObserver = bridge.Observe;
|
|
247
|
+
return bridge;
|
|
248
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure reporting helpers for the connector schema-refresh pipeline launch.
|
|
3
|
+
*
|
|
4
|
+
* The refresh is a live vendor introspect and can run for minutes on a large catalog, so
|
|
5
|
+
* IntegrationCreateConnection / IntegrationUpdateConnection can launch it *detached* and hand the
|
|
6
|
+
* caller a tailable run ID instead of blocking. That split creates one easy-to-get-wrong reporting
|
|
7
|
+
* case: a detached summary's counts are placeholder zeros, and describing them as results ("0
|
|
8
|
+
* created, 0 updated") reads to an operator as "the refresh found nothing" — the opposite of "the
|
|
9
|
+
* refresh has not finished yet". This module owns that decision so it can be pinned by a test
|
|
10
|
+
* without standing up a resolver.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The subset of the pipeline summary that message construction reads. Structural so both the
|
|
15
|
+
* GraphQL `@ObjectType` and a test fixture satisfy it without either depending on the other.
|
|
16
|
+
*/
|
|
17
|
+
export interface SchemaRefreshSummaryLike {
|
|
18
|
+
/** Tailable run ID — the only meaningful field while `InProgress` is true. */
|
|
19
|
+
RunID: string;
|
|
20
|
+
/** True ⇒ launched detached and still running; every count below is a placeholder zero. */
|
|
21
|
+
InProgress: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the pipeline itself succeeded. A pipeline that fails at ConnectionTest RETURNS rather
|
|
24
|
+
* than throwing, with every count at zero — so counts alone read identically to a clean
|
|
25
|
+
* no-op refresh. Reporting that as "0 created, 0 updated" tells an operator the vendor had
|
|
26
|
+
* nothing new when in fact the credentials were never accepted.
|
|
27
|
+
*/
|
|
28
|
+
Succeeded: boolean;
|
|
29
|
+
/** The pipeline's own failure reason, when `Succeeded` is false. */
|
|
30
|
+
FailureMessage?: string;
|
|
31
|
+
ObjectsCreated: number;
|
|
32
|
+
ObjectsUpdated: number;
|
|
33
|
+
UnresolvedObjects: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The counts clause, or an explicit failure clause when the pipeline did not succeed. */
|
|
37
|
+
function describeFinishedRefresh(summary: SchemaRefreshSummaryLike): string {
|
|
38
|
+
if (!summary.Succeeded) {
|
|
39
|
+
return `schema refresh FAILED (${summary.FailureMessage ?? 'no reason reported'}) — see run ${summary.RunID}`;
|
|
40
|
+
}
|
|
41
|
+
return `schema refresh: ${summary.ObjectsCreated} created, ` +
|
|
42
|
+
`${summary.ObjectsUpdated} updated, ${summary.UnresolvedObjects.length} PK-unresolved`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the human-facing `Message` for IntegrationCreateConnection.
|
|
47
|
+
*
|
|
48
|
+
* @param testConnection whether the caller asked for (and got) a live connection test
|
|
49
|
+
* @param summary the refresh summary, or undefined when no refresh was requested
|
|
50
|
+
*/
|
|
51
|
+
export function BuildCreateConnectionMessage(
|
|
52
|
+
testConnection: boolean,
|
|
53
|
+
summary: SchemaRefreshSummaryLike | undefined
|
|
54
|
+
): string {
|
|
55
|
+
if (!summary) return 'Connection created and test passed';
|
|
56
|
+
const created = `Connection created${testConnection ? ', test passed' : ''}`;
|
|
57
|
+
// Never report a detached run's placeholder counts as findings — point at the run stream instead.
|
|
58
|
+
if (summary.InProgress) return `${created}, schema refresh running — tail run ${summary.RunID}`;
|
|
59
|
+
return `${created}, ${describeFinishedRefresh(summary)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the human-facing `Message` for IntegrationUpdateConnection's blocking-refresh path.
|
|
64
|
+
* Shares {@link describeFinishedRefresh} with create so a failed refresh can never be reported as
|
|
65
|
+
* a clean zero-count run on one path and honestly on the other.
|
|
66
|
+
*/
|
|
67
|
+
export function BuildUpdateConnectionMessage(summary: SchemaRefreshSummaryLike): string {
|
|
68
|
+
return `Updated, ${describeFinishedRefresh(summary)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Builds the human-facing `Message` for IntegrationUpdateConnection's detached-refresh path.
|
|
73
|
+
* Separate from {@link BuildCreateConnectionMessage} because update has no connection-test clause.
|
|
74
|
+
*/
|
|
75
|
+
export function BuildDetachedRefreshMessage(runID: string): string {
|
|
76
|
+
return `Updated, schema refresh running — tail run ${runID}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Builds the human-facing `Message` for IntegrationReactivateConnection.
|
|
81
|
+
*
|
|
82
|
+
* Reactivation is committed before any refresh starts, so every message here begins by saying so.
|
|
83
|
+
* That ordering is the whole point: whatever the refresh does or fails to do afterwards, the
|
|
84
|
+
* connection IS active, and the operator must not read a refresh problem as "resume didn't work".
|
|
85
|
+
*
|
|
86
|
+
* @param summary the refresh summary, or undefined when no refresh was requested
|
|
87
|
+
*/
|
|
88
|
+
export function BuildReactivateMessage(summary: SchemaRefreshSummaryLike | undefined): string {
|
|
89
|
+
if (!summary) return 'Reactivated';
|
|
90
|
+
// Never report a detached run's placeholder counts as findings — point at the run stream instead.
|
|
91
|
+
if (summary.InProgress) return `Reactivated, schema refresh running — tail run ${summary.RunID}`;
|
|
92
|
+
return `Reactivated, ${describeFinishedRefresh(summary)}`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit-conversion seam between MJServer's configuration and SQLServerDataProvider.
|
|
3
|
+
*
|
|
4
|
+
* MJServer configures `databaseSettings.metadataCacheRefreshInterval` in **milliseconds**
|
|
5
|
+
* (default 180000 = 3 minutes, overridable via the METADATA_CACHE_REFRESH_INTERVAL env var).
|
|
6
|
+
* `SQLServerProviderConfigData`'s third constructor parameter is `checkRefreshIntervalSeconds`
|
|
7
|
+
* — declared in **seconds**.
|
|
8
|
+
*
|
|
9
|
+
* Commit 645c5a5e8 fixed a bug where the raw millisecond value was handed straight to the
|
|
10
|
+
* provider, so the "3 minute" default became a 180000-second (~50 hour) metadata cache
|
|
11
|
+
* refresh cadence. This function is the single place that conversion happens; both provider
|
|
12
|
+
* bootstrap call sites in `index.ts` (the main pool and the CodeGen-credentialed pool) go
|
|
13
|
+
* through it, and `config-units.test.ts` pins the contract so the units can never silently
|
|
14
|
+
* regress again.
|
|
15
|
+
*
|
|
16
|
+
* @param metadataCacheRefreshIntervalMs - the configured refresh interval, in milliseconds
|
|
17
|
+
* @returns the same interval expressed in seconds, the unit `checkRefreshIntervalSeconds` declares
|
|
18
|
+
*/
|
|
19
|
+
export function MetadataCacheRefreshIntervalSeconds(metadataCacheRefreshIntervalMs: number): number {
|
|
20
|
+
return metadataCacheRefreshIntervalMs / 1000;
|
|
21
|
+
}
|
|
@@ -5,6 +5,7 @@ import { RenderPipeline } from '@memberjunction/generic-database-provider';
|
|
|
5
5
|
import { AppContext } from '../types.js';
|
|
6
6
|
import { GetReadOnlyDataSource, GetReadOnlyProvider } from '../util.js';
|
|
7
7
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
8
|
+
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
8
9
|
import { RunQueryResultType } from './QueryResolver.js';
|
|
9
10
|
import { exactTotalFromPage, resolveAdhocTotalRowCount } from './adhoc-query-helpers.js';
|
|
10
11
|
import sql from 'mssql';
|
|
@@ -36,6 +37,10 @@ class AdhocQueryInput {
|
|
|
36
37
|
* - Executes on read-only connection pool only (no fallback to read-write)
|
|
37
38
|
* - Configurable timeout (default 30s)
|
|
38
39
|
* - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
|
|
40
|
+
* - Refuses scope-limited principals (see {@link IsScopeLimitedPrincipal}). Raw SQL bypasses
|
|
41
|
+
* RunView, entity permissions and row-level security, so the RLS scope tokens that confine a
|
|
42
|
+
* magic-link session are never applied on this path — there is no narrower filter to fall back
|
|
43
|
+
* to, only refusal.
|
|
39
44
|
*
|
|
40
45
|
* Auto-discovered by MJServer's dynamic resolver import.
|
|
41
46
|
*/
|
|
@@ -49,14 +54,24 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
49
54
|
const startTime = Date.now();
|
|
50
55
|
|
|
51
56
|
try {
|
|
52
|
-
// 1.
|
|
57
|
+
// 1. SECURITY: refuse principals whose read authority is narrower than their role
|
|
58
|
+
// grant. This resolver runs a raw SELECT on the read-only pool — it does NOT go
|
|
59
|
+
// through RunView, entity permissions or row-level security, so the RLS scope tokens
|
|
60
|
+
// that confine a magic-link session are never substituted here. There is no filter
|
|
61
|
+
// to narrow, so the only correct answer for such a principal is no. Checked before
|
|
62
|
+
// the SQL is even validated: authorization gates the work, it does not race it.
|
|
63
|
+
if (IsScopeLimitedPrincipal(context.userPayload?.userRecord)) {
|
|
64
|
+
return this.buildErrorResult('Ad-hoc SQL execution is not permitted for scope-limited sessions.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Security: validate SQL using SQLExpressionValidator
|
|
53
68
|
const validator = SQLExpressionValidator.Instance;
|
|
54
69
|
const validation = validator.validateFullQuery(input.SQL);
|
|
55
70
|
if (!validation.valid) {
|
|
56
71
|
return this.buildErrorResult(validation.error || 'SQL validation failed');
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
//
|
|
74
|
+
// 3. Get READ-ONLY data source (no fallback to read-write)
|
|
60
75
|
let readOnlyDS: sql.ConnectionPool;
|
|
61
76
|
try {
|
|
62
77
|
readOnlyDS = GetReadOnlyDataSource(context.dataSources, { allowFallbackToReadWrite: false });
|
|
@@ -64,7 +79,7 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
64
79
|
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
65
80
|
}
|
|
66
81
|
|
|
67
|
-
//
|
|
82
|
+
// 4. Resolve platform from the read-only provider for the render pipeline.
|
|
68
83
|
let platform: DatabasePlatform = 'sqlserver';
|
|
69
84
|
try {
|
|
70
85
|
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
@@ -74,7 +89,7 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
74
89
|
}
|
|
75
90
|
const contextUser = context.userPayload?.userRecord;
|
|
76
91
|
|
|
77
|
-
//
|
|
92
|
+
// 5. Route the SQL through RenderPipeline so composition tokens
|
|
78
93
|
// resolve, comments and templates are processed, and the row cap
|
|
79
94
|
// is applied at the database (via TOP / LIMIT / OFFSET-FETCH).
|
|
80
95
|
//
|
|
@@ -106,7 +121,7 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
106
121
|
return this.buildErrorResult(`Ad-hoc query rendering failed: ${renderMsg}`);
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
//
|
|
124
|
+
// 6. Execute the page (and, only when a full page needs it, the count) under
|
|
110
125
|
// a shared wall-clock deadline derived from the request's timeout budget.
|
|
111
126
|
const deadline = startTime + (input.TimeoutSeconds ?? 30) * 1000;
|
|
112
127
|
const { recordset, totalRowCount } = await this.executeDataAndCount(
|
|
@@ -114,7 +129,7 @@ export class AdhocQueryResolver extends ResolverBase {
|
|
|
114
129
|
);
|
|
115
130
|
const executionTimeMs = Date.now() - startTime;
|
|
116
131
|
|
|
117
|
-
//
|
|
132
|
+
// 7. Return as RunQueryResultType
|
|
118
133
|
return {
|
|
119
134
|
QueryID: '',
|
|
120
135
|
QueryName: 'Ad-Hoc Query',
|