@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -14,10 +14,19 @@ declare class AdhocQueryInput {
|
|
|
14
14
|
/**
|
|
15
15
|
* Resolver for executing ad-hoc (unsaved) SQL queries.
|
|
16
16
|
*
|
|
17
|
+
* The query runs through the read-only provider's own ad-hoc path (`RunQuery` with `SQL`), the
|
|
18
|
+
* same one in-process callers use, so it behaves identically on every database platform: the SQL
|
|
19
|
+
* is validated and rendered there, paged in the database with a count, run under the caller's
|
|
20
|
+
* timeout, and on PostgreSQL inside a read-only transaction that is rolled back.
|
|
21
|
+
*
|
|
17
22
|
* Security:
|
|
18
|
-
* - SQL validated
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
23
|
+
* - SQL validated by the provider (SQLExpressionValidator, single read query) — blocks mutations
|
|
24
|
+
* - Authorizes the RENDERED SQL: every table reference must be an entity base view the caller
|
|
25
|
+
* reads unscoped (CanRead, no row-level filter, no denied fields). Entities with row/field
|
|
26
|
+
* policy for this user are refused — raw SQL cannot apply that policy, so those reads belong
|
|
27
|
+
* on RunView.
|
|
28
|
+
* - Read-only provider only (no fallback to read-write)
|
|
29
|
+
* - Configurable timeout (default 30s), enforced by the database
|
|
21
30
|
* - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
|
|
22
31
|
* - Refuses scope-limited principals (see {@link IsScopeLimitedPrincipal}). Raw SQL bypasses
|
|
23
32
|
* RunView, entity permissions and row-level security, so the RLS scope tokens that confine a
|
|
@@ -29,22 +38,18 @@ declare class AdhocQueryInput {
|
|
|
29
38
|
export declare class AdhocQueryResolver extends ResolverBase {
|
|
30
39
|
ExecuteAdhocQuery(input: AdhocQueryInput, context: AppContext): Promise<RunQueryResultType>;
|
|
31
40
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*/
|
|
41
|
-
private executeDataAndCount;
|
|
42
|
-
/**
|
|
43
|
-
* Executes one SQL statement on the read-only pool, racing it against the shared
|
|
44
|
-
* wall-clock `deadline`. The timer is always cleared on completion so a settled
|
|
45
|
-
* query never leaves a dangling timeout armed.
|
|
41
|
+
* SECURITY: authorizes the READ itself, against the SQL that will actually run. The provider
|
|
42
|
+
* validates that the SQL is a single read statement but places no restriction on WHAT is read.
|
|
43
|
+
* Rendering comes first because composition (`{{query:"..."}}`) and templates can introduce
|
|
44
|
+
* table references the raw input never named. Every table reference must be an entity base
|
|
45
|
+
* view the caller can read UNSCOPED — CanRead with no row-level filter and no denied fields —
|
|
46
|
+
* because raw SQL applies neither (see assertFullQueryUsesReadableEntityViews). The provider
|
|
47
|
+
* renders the same input the same way and only wraps it in server-built paging and a count,
|
|
48
|
+
* so the rendered SQL is the full set of reads. Returns the refusal message, or null.
|
|
46
49
|
*/
|
|
47
|
-
private
|
|
50
|
+
private authorizeRenderedSQL;
|
|
51
|
+
/** Maps the provider's result to the GraphQL shape, naming a timeout plainly. */
|
|
52
|
+
private toResultType;
|
|
48
53
|
private buildErrorResult;
|
|
49
54
|
}
|
|
50
55
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdhocQueryResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/AdhocQueryResolver.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"AdhocQueryResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/AdhocQueryResolver.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;;GAGG;AACH,cACM,eAAe;IAEjB,GAAG,EAAE,MAAM,CAAC;IAGZ,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBACa,kBAAmB,SAAQ,YAAY;IAE1C,iBAAiB,CACkB,KAAK,EAAE,eAAe,EACpD,OAAO,EAAE,UAAU,GAC3B,OAAO,CAAC,kBAAkB,CAAC;IAoC9B;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAW5B,iFAAiF;IACjF,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,gBAAgB;CAc3B"}
|
|
@@ -12,14 +12,13 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
import { Arg, Ctx, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
14
14
|
import { LogError } from '@memberjunction/core';
|
|
15
|
-
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
16
15
|
import { RenderPipeline } from '@memberjunction/generic-database-provider';
|
|
17
|
-
import {
|
|
16
|
+
import { configInfo } from '../config.js';
|
|
17
|
+
import { GetReadOnlyProvider } from '../util.js';
|
|
18
18
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
19
19
|
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
20
20
|
import { RunQueryResultType } from './QueryResolver.js';
|
|
21
|
-
import {
|
|
22
|
-
import sql from 'mssql';
|
|
21
|
+
import { ClampAdhocTimeoutSeconds } from './adhocTimeout.js';
|
|
23
22
|
/**
|
|
24
23
|
* Input type for executing ad-hoc SQL queries directly.
|
|
25
24
|
* The SQL is validated server-side to ensure it's a safe SELECT/WITH statement.
|
|
@@ -48,10 +47,19 @@ AdhocQueryInput = __decorate([
|
|
|
48
47
|
/**
|
|
49
48
|
* Resolver for executing ad-hoc (unsaved) SQL queries.
|
|
50
49
|
*
|
|
50
|
+
* The query runs through the read-only provider's own ad-hoc path (`RunQuery` with `SQL`), the
|
|
51
|
+
* same one in-process callers use, so it behaves identically on every database platform: the SQL
|
|
52
|
+
* is validated and rendered there, paged in the database with a count, run under the caller's
|
|
53
|
+
* timeout, and on PostgreSQL inside a read-only transaction that is rolled back.
|
|
54
|
+
*
|
|
51
55
|
* Security:
|
|
52
|
-
* - SQL validated
|
|
53
|
-
* -
|
|
54
|
-
* -
|
|
56
|
+
* - SQL validated by the provider (SQLExpressionValidator, single read query) — blocks mutations
|
|
57
|
+
* - Authorizes the RENDERED SQL: every table reference must be an entity base view the caller
|
|
58
|
+
* reads unscoped (CanRead, no row-level filter, no denied fields). Entities with row/field
|
|
59
|
+
* policy for this user are refused — raw SQL cannot apply that policy, so those reads belong
|
|
60
|
+
* on RunView.
|
|
61
|
+
* - Read-only provider only (no fallback to read-write)
|
|
62
|
+
* - Configurable timeout (default 30s), enforced by the database
|
|
55
63
|
* - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
|
|
56
64
|
* - Refuses scope-limited principals (see {@link IsScopeLimitedPrincipal}). Raw SQL bypasses
|
|
57
65
|
* RunView, entity permissions and row-level security, so the RLS scope tokens that confine a
|
|
@@ -63,180 +71,82 @@ AdhocQueryInput = __decorate([
|
|
|
63
71
|
let AdhocQueryResolver = class AdhocQueryResolver extends ResolverBase {
|
|
64
72
|
async ExecuteAdhocQuery(input, context) {
|
|
65
73
|
const startTime = Date.now();
|
|
74
|
+
// SECURITY: refuse principals whose read authority is narrower than their role grant.
|
|
75
|
+
// Ad-hoc SQL does not go through RunView, entity permissions or row-level security, so the
|
|
76
|
+
// RLS scope tokens that confine a magic-link session are never substituted. There is no
|
|
77
|
+
// filter to narrow, so the only correct answer for such a principal is no.
|
|
78
|
+
if (IsScopeLimitedPrincipal(context.userPayload?.userRecord)) {
|
|
79
|
+
return this.buildErrorResult('Ad-hoc SQL execution is not permitted for scope-limited sessions.');
|
|
80
|
+
}
|
|
81
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
82
|
+
if (!provider) {
|
|
83
|
+
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
84
|
+
}
|
|
85
|
+
const contextUser = context.userPayload?.userRecord;
|
|
86
|
+
const refusal = this.authorizeRenderedSQL(input.SQL, provider.PlatformKey, context, contextUser);
|
|
87
|
+
if (refusal) {
|
|
88
|
+
return this.buildErrorResult(refusal, Date.now() - startTime);
|
|
89
|
+
}
|
|
90
|
+
const timeoutSeconds = ClampAdhocTimeoutSeconds(input.TimeoutSeconds, configInfo.databaseSettings.requestTimeout);
|
|
91
|
+
// A negative offset must not reach paging, where it would turn paging off.
|
|
92
|
+
const startRow = Math.max(0, Number.isInteger(input.StartRow) ? input.StartRow : 0);
|
|
66
93
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return this.buildErrorResult('Ad-hoc SQL execution is not permitted for scope-limited sessions.');
|
|
75
|
-
}
|
|
76
|
-
// 2. Security: validate SQL using SQLExpressionValidator
|
|
77
|
-
const validator = SQLExpressionValidator.Instance;
|
|
78
|
-
const validation = validator.validateFullQuery(input.SQL);
|
|
79
|
-
if (!validation.valid) {
|
|
80
|
-
return this.buildErrorResult(validation.error || 'SQL validation failed');
|
|
81
|
-
}
|
|
82
|
-
// 3. Get READ-ONLY data source (no fallback to read-write)
|
|
83
|
-
let readOnlyDS;
|
|
84
|
-
try {
|
|
85
|
-
readOnlyDS = GetReadOnlyDataSource(context.dataSources, { allowFallbackToReadWrite: false });
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
89
|
-
}
|
|
90
|
-
// 4. Resolve platform from the read-only provider for the render pipeline.
|
|
91
|
-
let platform = 'sqlserver';
|
|
92
|
-
try {
|
|
93
|
-
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: false });
|
|
94
|
-
if (provider?.PlatformKey)
|
|
95
|
-
platform = provider.PlatformKey;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// Provider not configured — keep the default platform.
|
|
99
|
-
}
|
|
100
|
-
const contextUser = context.userPayload?.userRecord;
|
|
101
|
-
// 5. Route the SQL through RenderPipeline so composition tokens
|
|
102
|
-
// resolve, comments and templates are processed, and the row cap
|
|
103
|
-
// is applied at the database (via TOP / LIMIT / OFFSET-FETCH).
|
|
104
|
-
//
|
|
105
|
-
// Page whenever a positive MaxRows is requested — INCLUDING the first
|
|
106
|
-
// page (StartRow 0). This yields OFFSET/FETCH data SQL *and* a
|
|
107
|
-
// COUNT(*) query so the response reports the true total row count
|
|
108
|
-
// rather than just the returned page size. It mirrors the saved-query
|
|
109
|
-
// path (GenericDatabaseProvider.InternalRunQuery → WrapWithPaging).
|
|
110
|
-
// (Previously paging was gated on StartRow > 0, so page 1 fell back to
|
|
111
|
-
// a TOP-N cap with TotalRowCount = page size — hiding the pager.)
|
|
112
|
-
// Clamp StartRow to a non-negative integer. A negative offset must not slip
|
|
113
|
-
// past the paging gate (which would then run the query with no row cap at all).
|
|
114
|
-
const startRow = Math.max(0, Number.isInteger(input.StartRow) ? input.StartRow : 0);
|
|
115
|
-
const maxRows = input.MaxRows;
|
|
116
|
-
const usePaging = maxRows != null && Number.isInteger(maxRows) && maxRows > 0;
|
|
117
|
-
let dataSQL;
|
|
118
|
-
let countSQL = null;
|
|
119
|
-
try {
|
|
120
|
-
const rendered = RenderPipeline.Run(input.SQL, {
|
|
121
|
-
Platform: platform,
|
|
122
|
-
ContextUser: contextUser,
|
|
123
|
-
...(usePaging ? { Paging: { StartRow: startRow, MaxRows: maxRows } } : {}),
|
|
124
|
-
});
|
|
125
|
-
dataSQL = rendered.FinalSQL;
|
|
126
|
-
countSQL = rendered.PagingResult?.CountSQL ?? null;
|
|
127
|
-
}
|
|
128
|
-
catch (renderErr) {
|
|
129
|
-
const renderMsg = renderErr instanceof Error ? renderErr.message : String(renderErr);
|
|
130
|
-
return this.buildErrorResult(`Ad-hoc query rendering failed: ${renderMsg}`);
|
|
131
|
-
}
|
|
132
|
-
// 6. Execute the page (and, only when a full page needs it, the count) under
|
|
133
|
-
// a shared wall-clock deadline derived from the request's timeout budget.
|
|
134
|
-
const deadline = startTime + (input.TimeoutSeconds ?? 30) * 1000;
|
|
135
|
-
const { recordset, totalRowCount } = await this.executeDataAndCount(readOnlyDS, dataSQL, countSQL, startRow, usePaging ? maxRows : null, deadline);
|
|
136
|
-
const executionTimeMs = Date.now() - startTime;
|
|
137
|
-
// 7. Return as RunQueryResultType
|
|
138
|
-
return {
|
|
139
|
-
QueryID: '',
|
|
140
|
-
QueryName: 'Ad-Hoc Query',
|
|
141
|
-
Success: true,
|
|
142
|
-
Results: JSON.stringify(recordset),
|
|
143
|
-
RowCount: recordset.length,
|
|
144
|
-
TotalRowCount: totalRowCount,
|
|
145
|
-
PageNumber: usePaging ? Math.floor(startRow / maxRows) + 1 : undefined,
|
|
146
|
-
PageSize: usePaging ? maxRows : undefined,
|
|
147
|
-
ExecutionTime: executionTimeMs,
|
|
148
|
-
ErrorMessage: ''
|
|
149
|
-
};
|
|
94
|
+
const result = await provider.RunQuery({
|
|
95
|
+
SQL: input.SQL,
|
|
96
|
+
MaxRows: input.MaxRows,
|
|
97
|
+
StartRow: startRow,
|
|
98
|
+
TimeoutSeconds: timeoutSeconds,
|
|
99
|
+
}, contextUser);
|
|
100
|
+
return this.toResultType(result, startRow, input.MaxRows, timeoutSeconds, Date.now() - startTime);
|
|
150
101
|
}
|
|
151
102
|
catch (err) {
|
|
152
|
-
const executionTimeMs = Date.now() - startTime;
|
|
153
103
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
154
|
-
// Handle timeout
|
|
155
|
-
if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
|
|
156
|
-
return {
|
|
157
|
-
QueryID: '',
|
|
158
|
-
QueryName: 'Ad-Hoc Query',
|
|
159
|
-
Success: false,
|
|
160
|
-
Results: '[]',
|
|
161
|
-
RowCount: 0,
|
|
162
|
-
TotalRowCount: 0,
|
|
163
|
-
PageNumber: undefined,
|
|
164
|
-
PageSize: undefined,
|
|
165
|
-
ExecutionTime: executionTimeMs,
|
|
166
|
-
ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
104
|
LogError(`Ad-hoc query execution failed: ${errorMessage}`);
|
|
170
|
-
return {
|
|
171
|
-
QueryID: '',
|
|
172
|
-
QueryName: 'Ad-Hoc Query',
|
|
173
|
-
Success: false,
|
|
174
|
-
Results: '[]',
|
|
175
|
-
RowCount: 0,
|
|
176
|
-
TotalRowCount: 0,
|
|
177
|
-
PageNumber: undefined,
|
|
178
|
-
PageSize: undefined,
|
|
179
|
-
ExecutionTime: executionTimeMs,
|
|
180
|
-
ErrorMessage: `Query execution failed: ${errorMessage}`
|
|
181
|
-
};
|
|
105
|
+
return this.buildErrorResult(`Query execution failed: ${errorMessage}`, Date.now() - startTime);
|
|
182
106
|
}
|
|
183
107
|
}
|
|
184
108
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
109
|
+
* SECURITY: authorizes the READ itself, against the SQL that will actually run. The provider
|
|
110
|
+
* validates that the SQL is a single read statement but places no restriction on WHAT is read.
|
|
111
|
+
* Rendering comes first because composition (`{{query:"..."}}`) and templates can introduce
|
|
112
|
+
* table references the raw input never named. Every table reference must be an entity base
|
|
113
|
+
* view the caller can read UNSCOPED — CanRead with no row-level filter and no denied fields —
|
|
114
|
+
* because raw SQL applies neither (see assertFullQueryUsesReadableEntityViews). The provider
|
|
115
|
+
* renders the same input the same way and only wraps it in server-built paging and a count,
|
|
116
|
+
* so the rendered SQL is the full set of reads. Returns the refusal message, or null.
|
|
193
117
|
*/
|
|
194
|
-
|
|
195
|
-
const dataResult = await this.runSqlWithDeadline(ds, dataSQL, deadline);
|
|
196
|
-
const recordset = (dataResult.recordset ?? []);
|
|
197
|
-
// Total already known from the page alone (unpaged, or a short page)? Skip the count.
|
|
198
|
-
const exact = ExactTotalFromPage(startRow, recordset.length, maxRows);
|
|
199
|
-
if (exact != null || !countSQL) {
|
|
200
|
-
return { recordset, totalRowCount: exact ?? recordset.length };
|
|
201
|
-
}
|
|
202
|
-
// Full page — a COUNT(*) is required to know the true total.
|
|
203
|
-
const lowerBound = startRow + recordset.length;
|
|
118
|
+
authorizeRenderedSQL(sqlText, platform, context, contextUser) {
|
|
204
119
|
try {
|
|
205
|
-
const
|
|
206
|
-
|
|
120
|
+
const rendered = RenderPipeline.Run(sqlText, { Platform: platform, ContextUser: contextUser, RequireReadStatement: true });
|
|
121
|
+
const mdProvider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true }) ?? undefined;
|
|
122
|
+
this.assertFullQueryUsesReadableEntityViews(rendered.Trace.AfterTemplates, mdProvider, contextUser, 'ad-hoc SQL');
|
|
123
|
+
return null;
|
|
207
124
|
}
|
|
208
|
-
catch (
|
|
209
|
-
|
|
210
|
-
LogError(`Ad-hoc query row-count failed; reporting a lower-bound total (${lowerBound}). ${msg}`);
|
|
211
|
-
return { recordset, totalRowCount: lowerBound };
|
|
125
|
+
catch (err) {
|
|
126
|
+
return err instanceof Error ? err.message : String(err);
|
|
212
127
|
}
|
|
213
128
|
}
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
async runSqlWithDeadline(ds, sqlText, deadline) {
|
|
220
|
-
const remaining = deadline - Date.now();
|
|
221
|
-
if (remaining <= 0) {
|
|
222
|
-
throw new Error('Query timeout exceeded');
|
|
223
|
-
}
|
|
224
|
-
let timer;
|
|
225
|
-
try {
|
|
226
|
-
return await Promise.race([
|
|
227
|
-
new sql.Request(ds).query(sqlText),
|
|
228
|
-
new Promise((_, reject) => {
|
|
229
|
-
timer = setTimeout(() => reject(new Error('Query timeout exceeded')), remaining);
|
|
230
|
-
}),
|
|
231
|
-
]);
|
|
232
|
-
}
|
|
233
|
-
finally {
|
|
234
|
-
if (timer) {
|
|
235
|
-
clearTimeout(timer);
|
|
236
|
-
}
|
|
129
|
+
/** Maps the provider's result to the GraphQL shape, naming a timeout plainly. */
|
|
130
|
+
toResultType(result, startRow, maxRows, timeoutSeconds, executionTimeMs) {
|
|
131
|
+
if (!result.Success) {
|
|
132
|
+
const message = result.ErrorMessage ?? 'Ad-hoc query failed';
|
|
133
|
+
return this.buildErrorResult(/timeout/i.test(message) ? `Query execution exceeded ${timeoutSeconds} second timeout` : message, executionTimeMs);
|
|
237
134
|
}
|
|
135
|
+
const paged = maxRows != null && Number.isInteger(maxRows) && maxRows > 0;
|
|
136
|
+
return {
|
|
137
|
+
QueryID: '',
|
|
138
|
+
QueryName: 'Ad-Hoc Query',
|
|
139
|
+
Success: true,
|
|
140
|
+
Results: JSON.stringify(result.Results),
|
|
141
|
+
RowCount: result.RowCount,
|
|
142
|
+
TotalRowCount: result.TotalRowCount,
|
|
143
|
+
PageNumber: paged ? Math.floor(startRow / maxRows) + 1 : undefined,
|
|
144
|
+
PageSize: paged ? maxRows : undefined,
|
|
145
|
+
ExecutionTime: executionTimeMs,
|
|
146
|
+
ErrorMessage: ''
|
|
147
|
+
};
|
|
238
148
|
}
|
|
239
|
-
buildErrorResult(errorMessage) {
|
|
149
|
+
buildErrorResult(errorMessage, executionTimeMs = 0) {
|
|
240
150
|
return {
|
|
241
151
|
QueryID: '',
|
|
242
152
|
QueryName: 'Ad-Hoc Query',
|
|
@@ -246,7 +156,7 @@ let AdhocQueryResolver = class AdhocQueryResolver extends ResolverBase {
|
|
|
246
156
|
TotalRowCount: 0,
|
|
247
157
|
PageNumber: undefined,
|
|
248
158
|
PageSize: undefined,
|
|
249
|
-
ExecutionTime:
|
|
159
|
+
ExecutionTime: executionTimeMs,
|
|
250
160
|
ErrorMessage: errorMessage
|
|
251
161
|
};
|
|
252
162
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdhocQueryResolver.js","sourceRoot":"","sources":["../../src/resolvers/AdhocQueryResolver.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,
|
|
1
|
+
{"version":3,"file":"AdhocQueryResolver.js","sourceRoot":"","sources":["../../src/resolvers/AdhocQueryResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompositeKey, DatabaseProviderBase
|
|
1
|
+
import { CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
|
|
2
2
|
import { AppContext, UserPayload } from '../types.js';
|
|
3
3
|
import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
|
|
4
4
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -17,34 +17,18 @@ export declare class EntityRecordNameResult {
|
|
|
17
17
|
* Resolves a record's display NAME from its primary key — the lookup behind every foreign-key
|
|
18
18
|
* link, breadcrumb and picker label in the UI.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* strips denied fields. Field-level security turned that latent gap into a live bypass — the
|
|
24
|
-
* form-field FK control falls through to this query precisely WHEN the joined display column is
|
|
25
|
-
* denied, so the one case the fallback exists for was the case that leaked.
|
|
20
|
+
* Entity-level read permission is checked here ({@link ResolverBase.CheckUserReadPermissions}).
|
|
21
|
+
* Field- and row-level security are applied by the provider's lookup, for the acting user passed
|
|
22
|
+
* to it: when any field the name is built from is withheld, it answers with no name.
|
|
26
23
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* A field-level denial answers `Success: false` with no name rather than throwing. Callers
|
|
32
|
-
* already treat "no name" as "show the primary key", which is the intended degradation — and it
|
|
33
|
-
* keeps this query from becoming a probe that distinguishes "restricted" from "missing".
|
|
24
|
+
* A withheld name answers `Success: false` with the same status as a record that does not exist.
|
|
25
|
+
* Callers already treat "no name" as "show the primary key", and the identical answer keeps this
|
|
26
|
+
* query from becoming a probe that distinguishes "restricted" from "missing".
|
|
34
27
|
*/
|
|
35
28
|
export declare class EntityRecordNameResolver extends ResolverBase {
|
|
36
29
|
GetEntityRecordName(EntityName: string, primaryKey: CompositeKey, { providers, userPayload }: AppContext): Promise<EntityRecordNameResult>;
|
|
37
30
|
GetEntityRecordNames(info: EntityRecordNameInput[], { providers, userPayload }: AppContext): Promise<EntityRecordNameResult[]>;
|
|
38
31
|
InnerGetEntityRecordName(md: DatabaseProviderBase, EntityName: string, primaryKey: CompositeKeyInputType, userPayload?: UserPayload): Promise<EntityRecordNameResult>;
|
|
39
|
-
/**
|
|
40
|
-
* Whether field security withholds this entity's NAME field from the user.
|
|
41
|
-
*
|
|
42
|
-
* Resolves the name field the same way the provider's own lookup does — the designated
|
|
43
|
-
* `IsNameField`, else a field literally called `Name` — so the check cannot drift from the
|
|
44
|
-
* value it is guarding. Fails OPEN only where field security does not apply at all (flag off,
|
|
45
|
-
* no user, no name field), matching every other FLS gate.
|
|
46
|
-
*/
|
|
47
|
-
protected IsNameFieldDeniedToUser(entity: EntityInfo, contextUser?: UserInfo): boolean;
|
|
48
32
|
}
|
|
49
33
|
export default EntityRecordNameResolver;
|
|
50
34
|
//# sourceMappingURL=EntityRecordNameResolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityRecordNameResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/EntityRecordNameResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,
|
|
1
|
+
{"version":3,"file":"EntityRecordNameResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/EntityRecordNameResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,qBAAqB,EAA0B,MAAM,mCAAmC,CAAC;AAClG,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,qBACa,qBAAqB;IAEhC,UAAU,EAAE,MAAM,CAAC;IAGnB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,qBACa,sBAAsB;IAEjC,OAAO,EAAE,OAAO,CAAC;IAGjB,MAAM,EAAE,MAAM,CAAC;IAGf,YAAY,EAAE,YAAY,CAAC;IAG3B,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,qBACa,wBAAyB,SAAQ,YAAY;IAElD,mBAAmB,CACU,UAAU,EAAE,MAAM,EACD,UAAU,EAAE,YAAY,EACnE,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,UAAU,GAC5C,OAAO,CAAC,sBAAsB,CAAC;IAQ5B,oBAAoB,CACoB,IAAI,EAAE,qBAAqB,EAAE,EAClE,EAAC,SAAS,EAAE,WAAW,EAAC,EAAE,UAAU,GAC1C,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAY9B,wBAAwB,CAC5B,EAAE,EAAE,oBAAoB,EACxB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,qBAAqB,EACjC,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,sBAAsB,CAAC;CAiBnC;AAED,eAAe,wBAAwB,CAAC"}
|
|
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
-
import { CompositeKey
|
|
13
|
+
import { CompositeKey } from '@memberjunction/core';
|
|
14
14
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
15
15
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
16
16
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -59,20 +59,13 @@ export { EntityRecordNameResult };
|
|
|
59
59
|
* Resolves a record's display NAME from its primary key — the lookup behind every foreign-key
|
|
60
60
|
* link, breadcrumb and picker label in the UI.
|
|
61
61
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* strips denied fields. Field-level security turned that latent gap into a live bypass — the
|
|
66
|
-
* form-field FK control falls through to this query precisely WHEN the joined display column is
|
|
67
|
-
* denied, so the one case the fallback exists for was the case that leaked.
|
|
62
|
+
* Entity-level read permission is checked here ({@link ResolverBase.CheckUserReadPermissions}).
|
|
63
|
+
* Field- and row-level security are applied by the provider's lookup, for the acting user passed
|
|
64
|
+
* to it: when any field the name is built from is withheld, it answers with no name.
|
|
68
65
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* A field-level denial answers `Success: false` with no name rather than throwing. Callers
|
|
74
|
-
* already treat "no name" as "show the primary key", which is the intended degradation — and it
|
|
75
|
-
* keeps this query from becoming a probe that distinguishes "restricted" from "missing".
|
|
66
|
+
* A withheld name answers `Success: false` with the same status as a record that does not exist.
|
|
67
|
+
* Callers already treat "no name" as "show the primary key", and the identical answer keeps this
|
|
68
|
+
* query from becoming a probe that distinguishes "restricted" from "missing".
|
|
76
69
|
*/
|
|
77
70
|
let EntityRecordNameResolver = class EntityRecordNameResolver extends ResolverBase {
|
|
78
71
|
async GetEntityRecordName(EntityName, primaryKey, { providers, userPayload }) {
|
|
@@ -96,26 +89,14 @@ let EntityRecordNameResolver = class EntityRecordNameResolver extends ResolverBa
|
|
|
96
89
|
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
97
90
|
if (e) {
|
|
98
91
|
const contextUser = userPayload ? this.GetUserFromPayload(userPayload) : undefined;
|
|
99
|
-
|
|
100
|
-
LogDebug(`[FieldSecurity] Suppressed GetEntityRecordName for '${EntityName}': ` +
|
|
101
|
-
`the name field is not readable by this user`);
|
|
102
|
-
return {
|
|
103
|
-
Success: false,
|
|
104
|
-
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue`,
|
|
105
|
-
CompositeKey: pk,
|
|
106
|
-
EntityName,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
// Pass the acting user through: the provider signature accepts it, and omitting it ran the
|
|
110
|
-
// underlying lookup with no identity at all — so row-level security had nothing to filter on
|
|
111
|
-
// either. The field-level gate above is the column half; this is the row half.
|
|
92
|
+
// The acting user is what field- and row-level security are applied for.
|
|
112
93
|
const recordName = await md.GetEntityRecordName(e.Name, pk, contextUser);
|
|
113
94
|
if (recordName)
|
|
114
95
|
return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
|
|
115
96
|
else
|
|
116
97
|
return {
|
|
117
98
|
Success: false,
|
|
118
|
-
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue
|
|
99
|
+
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue`,
|
|
119
100
|
CompositeKey: pk,
|
|
120
101
|
EntityName,
|
|
121
102
|
};
|
|
@@ -123,25 +104,6 @@ let EntityRecordNameResolver = class EntityRecordNameResolver extends ResolverBa
|
|
|
123
104
|
else
|
|
124
105
|
return { Success: false, Status: `Entity ${EntityName} not found`, CompositeKey: pk, EntityName };
|
|
125
106
|
}
|
|
126
|
-
/**
|
|
127
|
-
* Whether field security withholds this entity's NAME field from the user.
|
|
128
|
-
*
|
|
129
|
-
* Resolves the name field the same way the provider's own lookup does — the designated
|
|
130
|
-
* `IsNameField`, else a field literally called `Name` — so the check cannot drift from the
|
|
131
|
-
* value it is guarding. Fails OPEN only where field security does not apply at all (flag off,
|
|
132
|
-
* no user, no name field), matching every other FLS gate.
|
|
133
|
-
*/
|
|
134
|
-
IsNameFieldDeniedToUser(entity, contextUser) {
|
|
135
|
-
if (!entity?.EnableFieldLevelSecurity || !contextUser) {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
const nameField = entity.Fields?.find(f => f.IsNameField) ??
|
|
139
|
-
entity.Fields?.find(f => f.Name?.trim().toLowerCase() === 'name');
|
|
140
|
-
if (!nameField) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
return entity.GetDeniedReadFields(contextUser).has(nameField.Name.trim().toLowerCase());
|
|
144
|
-
}
|
|
145
107
|
};
|
|
146
108
|
__decorate([
|
|
147
109
|
Query(() => EntityRecordNameResult),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityRecordNameResolver.js","sourceRoot":"","sources":["../../src/resolvers/EntityRecordNameResolver.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"EntityRecordNameResolver.js","sourceRoot":"","sources":["../../src/resolvers/EntityRecordNameResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GetDataResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/GetDataResolver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GetDataResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/GetDataResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import { Arg, Ctx, Field, InputType, ObjectType, Query } from 'type-graphql';
|
|
14
|
+
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
14
15
|
import { LogError, LogStatus, LogStatusEx } from '@memberjunction/core';
|
|
15
16
|
import { QueryCompositionEngine } from '@memberjunction/generic-database-provider';
|
|
16
17
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
@@ -190,6 +191,14 @@ export class GetDataResolver {
|
|
|
190
191
|
const compositionResult = compositionEngine.ResolveComposition(query, platform, systemUser);
|
|
191
192
|
resolvedSQL = compositionResult.ResolvedSQL;
|
|
192
193
|
}
|
|
194
|
+
// Defense-in-depth: even system-token holders get SELECT-only SQL here.
|
|
195
|
+
// The read-only pool is the primary control, but if it is ever
|
|
196
|
+
// misconfigured to a read-write login this validator still refuses DML/DDL
|
|
197
|
+
// (mirrors AdhocQueryResolver).
|
|
198
|
+
const validation = SQLExpressionValidator.Instance.validateFullQuery(resolvedSQL);
|
|
199
|
+
if (!validation.valid) {
|
|
200
|
+
return { result: null, error: validation.error || 'SQL validation failed' };
|
|
201
|
+
}
|
|
193
202
|
const request = new sql.Request(readOnlyDataSource);
|
|
194
203
|
const result = await request.query(resolvedSQL);
|
|
195
204
|
return { result: result.recordset, error: null };
|