@memberjunction/server 5.51.2 → 5.51.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/README.md +34 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +17 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +136 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +1 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.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 +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3262 -3273
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +124 -4
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +170 -15
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +148 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +287 -30
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -50
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/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/realtimeWidget/widgetGuestElevation.d.ts +2 -2
- package/dist/realtimeWidget/widgetGuestElevation.js +2 -2
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +39 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +97 -157
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +14 -4
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +18 -6
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +13 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +26 -14
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +63 -46
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +24 -27
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.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/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +29 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +90 -89
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- 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 +84 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +136 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +318 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +66 -27
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +134 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +221 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +173 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +116 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +351 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +164 -0
- package/src/__tests__/SessionManager.test.ts +88 -13
- package/src/__tests__/SessionRunIDVerifier.test.ts +255 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +155 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +241 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +180 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/resolverBase.rls.test.ts +2 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +24 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +179 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +34 -1
- package/src/context.ts +25 -34
- package/src/generated/generated.ts +3262 -3273
- package/src/generic/CacheInvalidationResolver.ts +206 -15
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +339 -31
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +65 -52
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +2 -2
- package/src/resolvers/ActionResolver.ts +85 -22
- package/src/resolvers/AdhocQueryResolver.ts +110 -177
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +24 -7
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +82 -50
- package/src/resolvers/RemoteBrowserActionResolver.ts +30 -26
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/__tests__/ReportResolver.test.ts +2 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskOrchestrator.ts +17 -17
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -42
- 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 -50
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Metadata, CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
|
|
2
2
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
|
-
import { AppContext } from '../types.js';
|
|
3
|
+
import { AppContext, UserPayload } from '../types.js';
|
|
4
4
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
5
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
5
6
|
import { GetReadOnlyProvider } from '../util.js';
|
|
6
7
|
|
|
7
8
|
@InputType()
|
|
@@ -31,8 +32,17 @@ export class EntityRecordNameResult {
|
|
|
31
32
|
RecordName?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Resolves a record's display NAME from its primary key — the lookup behind every foreign-key
|
|
37
|
+
* link, breadcrumb and picker label in the UI.
|
|
38
|
+
*
|
|
39
|
+
* Row-level security is applied by the provider's lookup, for the acting user passed to it: it
|
|
40
|
+
* reads only rows the user's read row filter allows, binds the key values as parameters, and
|
|
41
|
+
* refuses a key that does not name the entity's primary key fields. Without an acting user there
|
|
42
|
+
* is no lookup.
|
|
43
|
+
*/
|
|
34
44
|
@Resolver(EntityRecordNameResult)
|
|
35
|
-
export class EntityRecordNameResolver {
|
|
45
|
+
export class EntityRecordNameResolver extends ResolverBase {
|
|
36
46
|
@Query(() => EntityRecordNameResult)
|
|
37
47
|
async GetEntityRecordName(
|
|
38
48
|
@Arg('EntityName', () => String) EntityName: string,
|
|
@@ -41,27 +51,34 @@ export class EntityRecordNameResolver {
|
|
|
41
51
|
): Promise<EntityRecordNameResult> {
|
|
42
52
|
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
43
53
|
|
|
44
|
-
return await this.InnerGetEntityRecordName(md, EntityName, primaryKey);
|
|
54
|
+
return await this.InnerGetEntityRecordName(md, EntityName, primaryKey, userPayload);
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
@Query(() => [EntityRecordNameResult])
|
|
48
58
|
async GetEntityRecordNames(
|
|
49
59
|
@Arg('info', () => [EntityRecordNameInput]) info: EntityRecordNameInput[],
|
|
50
|
-
@Ctx() {providers}: AppContext
|
|
60
|
+
@Ctx() {providers, userPayload}: AppContext
|
|
51
61
|
): Promise<EntityRecordNameResult[]> {
|
|
52
62
|
const result: EntityRecordNameResult[] = [];
|
|
53
63
|
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
54
64
|
for (const i of info) {
|
|
55
|
-
result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.CompositeKey));
|
|
65
|
+
result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.CompositeKey, userPayload));
|
|
56
66
|
}
|
|
57
67
|
return result;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
async InnerGetEntityRecordName(
|
|
70
|
+
async InnerGetEntityRecordName(
|
|
71
|
+
md: DatabaseProviderBase,
|
|
72
|
+
EntityName: string,
|
|
73
|
+
primaryKey: CompositeKeyInputType,
|
|
74
|
+
userPayload?: UserPayload
|
|
75
|
+
): Promise<EntityRecordNameResult> {
|
|
61
76
|
const pk = new CompositeKey(primaryKey.KeyValuePairs);
|
|
62
77
|
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
63
78
|
if (e) {
|
|
64
|
-
const
|
|
79
|
+
const contextUser = userPayload ? this.GetUserFromPayload(userPayload) : undefined;
|
|
80
|
+
// The acting user is what row-level security is applied for.
|
|
81
|
+
const recordName = contextUser ? await md.GetEntityRecordName(e.Name, pk, contextUser) : '';
|
|
65
82
|
if (recordName) return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
|
|
66
83
|
else
|
|
67
84
|
return {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, ObjectType, Query, Resolver } from "type-graphql";
|
|
2
|
+
import type { DatabaseProviderBase, UserInfo } from "@memberjunction/core";
|
|
3
|
+
import { IsValidUUID, UUIDsEqual } from "@memberjunction/global";
|
|
4
|
+
import { DataContext, DataContextItem } from "@memberjunction/data-context";
|
|
5
|
+
import { MJDataContextEntity, MJDataContextItemEntity } from "@memberjunction/core-entities";
|
|
6
|
+
import { RenderPipeline } from "@memberjunction/generic-database-provider";
|
|
2
7
|
import { AppContext } from "../types.js";
|
|
3
|
-
import {
|
|
4
|
-
import { GetReadOnlyDataSource, GetReadOnlyProvider } from "../util.js";
|
|
5
|
-
import { MJDataContextItemEntity } from "@memberjunction/core-entities";
|
|
8
|
+
import { GetReadOnlyProvider } from "../util.js";
|
|
6
9
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
10
|
+
import { IsScopeLimitedPrincipal } from "../auth/scopeLimitedPrincipal.js";
|
|
7
11
|
|
|
8
12
|
@ObjectType()
|
|
9
13
|
export class GetDataContextItemDataOutputType {
|
|
@@ -39,6 +43,28 @@ export class GetDataContextDataOutputType {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
/** The outcome of loading one data context item: its data as JSON, or why it has none. */
|
|
47
|
+
interface ItemOutcome {
|
|
48
|
+
Result: string | null;
|
|
49
|
+
Error: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Which `sql` items of a data context may run: the read-only provider, and each refused item's reason. */
|
|
53
|
+
interface SQLItemGate {
|
|
54
|
+
Provider: DatabaseProviderBase | null;
|
|
55
|
+
Refusals: Map<DataContextItem, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Loads the data of data contexts and their items.
|
|
60
|
+
*
|
|
61
|
+
* Items of type `sql` hold raw SQL, which is held to the rules ExecuteAdhocQuery applies to
|
|
62
|
+
* caller-supplied SQL. It runs only for the data context's owner or an administrator (Owner-type
|
|
63
|
+
* user), never for a scope-limited session, only on the read-only provider, and only as a single read
|
|
64
|
+
* statement over entity base views the caller may read in full. A SQL item that may not run is
|
|
65
|
+
* refused on its own, and the other items still load. Items of the other types load through entity
|
|
66
|
+
* permissions for the calling user.
|
|
67
|
+
*/
|
|
42
68
|
@Resolver()
|
|
43
69
|
export class GetDataContextDataResolver extends ResolverBase {
|
|
44
70
|
/**
|
|
@@ -52,30 +78,25 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
52
78
|
) {
|
|
53
79
|
// Check API key scope authorization for data context read
|
|
54
80
|
await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextItemID, appCtx.userPayload);
|
|
81
|
+
if (!IsValidUUID(DataContextItemID)) {
|
|
82
|
+
return { Success: false, ErrorMessage: 'DataContextItemID must be a UUID', Result: null };
|
|
83
|
+
}
|
|
55
84
|
|
|
56
85
|
try {
|
|
57
|
-
const
|
|
58
|
-
allowFallbackToReadWrite: true,
|
|
59
|
-
})
|
|
86
|
+
const user: UserInfo = appCtx.userPayload.userRecord;
|
|
60
87
|
const md = GetReadOnlyProvider(appCtx.providers, {allowFallbackToReadWrite: true});
|
|
61
|
-
const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items",
|
|
88
|
+
const dciData = await md.GetEntityObject<MJDataContextItemEntity>("MJ: Data Context Items", user);
|
|
62
89
|
if (await dciData.Load(DataContextItemID)) {
|
|
63
90
|
const dci = DataContext.CreateDataContextItem(); // use class factory to get whatever lowest level sub-class is registered
|
|
64
|
-
await dci.LoadMetadataFromEntityRecord(dciData, md,
|
|
91
|
+
await dci.LoadMetadataFromEntityRecord(dciData, md, user);
|
|
92
|
+
const ownerID = dci.Type === 'sql' ? await this.loadDataContextOwnerID(dciData.DataContextID, md, user) : null;
|
|
93
|
+
const gate = this.authorizeSQLItems([dci], ownerID, appCtx);
|
|
65
94
|
// now the metadata is loaded so we can call the regular load function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
return {
|
|
75
|
-
Success: false,
|
|
76
|
-
ErrorMessage: 'Error loading data context item data',
|
|
77
|
-
Result: null,
|
|
78
|
-
}
|
|
95
|
+
const outcome = await this.loadItem(dci, gate, user, false);
|
|
96
|
+
return {
|
|
97
|
+
Success: outcome.Error === null,
|
|
98
|
+
ErrorMessage: outcome.Error,
|
|
99
|
+
Result: outcome.Result,
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
else {
|
|
@@ -89,14 +110,15 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
89
110
|
catch (e) {
|
|
90
111
|
return {
|
|
91
112
|
Success: false,
|
|
92
|
-
ErrorMessage: e,
|
|
113
|
+
ErrorMessage: e instanceof Error ? e.message : String(e),
|
|
93
114
|
Result: null,
|
|
94
115
|
}
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/**
|
|
99
|
-
* Returns data for a given data context.
|
|
120
|
+
* Returns data for a given data context. `Results` and `ErrorMessages` hold one entry per item, in
|
|
121
|
+
* item order. `Success` is true, and `ErrorMessages` null, only when every item loaded.
|
|
100
122
|
* @param DataContextID
|
|
101
123
|
*/
|
|
102
124
|
@Query(() => GetDataContextDataOutputType)
|
|
@@ -106,38 +128,114 @@ export class GetDataContextDataResolver extends ResolverBase {
|
|
|
106
128
|
) {
|
|
107
129
|
// Check API key scope authorization for data context read
|
|
108
130
|
await this.CheckAPIKeyScopeAuthorization('datacontext:read', DataContextID, appCtx.userPayload);
|
|
131
|
+
// DataContext.LoadMetadata places this ID in the item filter, so it must be a plain UUID.
|
|
132
|
+
if (!IsValidUUID(DataContextID)) {
|
|
133
|
+
return { Success: false, ErrorMessages: ['DataContextID must be a UUID'], Results: null };
|
|
134
|
+
}
|
|
109
135
|
|
|
110
136
|
try {
|
|
111
137
|
// our job here is to load the entire data context, so we do that with the Data Context object
|
|
138
|
+
const user: UserInfo = appCtx.userPayload.userRecord;
|
|
139
|
+
const md = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: true });
|
|
112
140
|
const dc = new DataContext();
|
|
113
|
-
|
|
114
|
-
allowFallbackToReadWrite: true,
|
|
115
|
-
})
|
|
116
|
-
const success = await dc.Load(DataContextID, ds, true, false, 0, appCtx.userPayload.userRecord);
|
|
117
|
-
if (success) {
|
|
118
|
-
const retVal = {
|
|
119
|
-
Success: true,
|
|
120
|
-
ErrorMessages: null,
|
|
121
|
-
Results: dc.Items.map((item) => {
|
|
122
|
-
return JSON.stringify(item.Data);
|
|
123
|
-
}),
|
|
124
|
-
}
|
|
125
|
-
return retVal;
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
141
|
+
if (!(await dc.LoadMetadata(DataContextID, user, md))) {
|
|
128
142
|
return {
|
|
129
143
|
Success: false,
|
|
130
144
|
ErrorMessages: ['Error loading data context'],
|
|
131
|
-
Results: null,
|
|
145
|
+
Results: null,
|
|
132
146
|
}
|
|
133
147
|
}
|
|
148
|
+
const gate = this.authorizeSQLItems(dc.Items, dc.MJDataContextEntity.UserID, appCtx);
|
|
149
|
+
const outcomes = await Promise.all(dc.Items.map((item) => this.loadItem(item, gate, user, true)));
|
|
150
|
+
const allLoaded = outcomes.every((outcome) => outcome.Error === null);
|
|
151
|
+
return {
|
|
152
|
+
Success: allLoaded,
|
|
153
|
+
ErrorMessages: allLoaded ? null : outcomes.map((outcome) => outcome.Error),
|
|
154
|
+
Results: outcomes.map((outcome) => outcome.Result),
|
|
155
|
+
}
|
|
134
156
|
}
|
|
135
157
|
catch (e) {
|
|
136
158
|
return {
|
|
137
159
|
Success: false,
|
|
138
|
-
ErrorMessages: [e],
|
|
160
|
+
ErrorMessages: [e instanceof Error ? e.message : String(e)],
|
|
139
161
|
Results: null,
|
|
140
162
|
}
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
|
-
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Decides which `sql` items among `items` may run. Returns the read-only provider they run on, and
|
|
168
|
+
* the reason for each SQL item that may not run.
|
|
169
|
+
*/
|
|
170
|
+
private authorizeSQLItems(items: DataContextItem[], ownerID: string | null, appCtx: AppContext): SQLItemGate {
|
|
171
|
+
const refusals = new Map<DataContextItem, string>();
|
|
172
|
+
const sqlItems = items.filter((item) => item.Type === 'sql');
|
|
173
|
+
if (sqlItems.length === 0) {
|
|
174
|
+
return { Provider: null, Refusals: refusals };
|
|
175
|
+
}
|
|
176
|
+
const user: UserInfo | undefined = appCtx.userPayload?.userRecord;
|
|
177
|
+
const provider = GetReadOnlyProvider(appCtx.providers, { allowFallbackToReadWrite: false });
|
|
178
|
+
for (const item of sqlItems) {
|
|
179
|
+
const refusal = this.sqlItemRefusal(item, ownerID, user, provider);
|
|
180
|
+
if (refusal !== null) {
|
|
181
|
+
refusals.set(item, refusal);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return { Provider: provider, Refusals: refusals };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Returns why `item` may not run for `user`, or null when it may. */
|
|
188
|
+
private sqlItemRefusal(item: DataContextItem, ownerID: string | null, user: UserInfo | undefined, provider: DatabaseProviderBase | null): string | null {
|
|
189
|
+
if (!user || IsScopeLimitedPrincipal(user)) {
|
|
190
|
+
return 'SQL data context items cannot be run by a scope-limited or unidentified session.';
|
|
191
|
+
}
|
|
192
|
+
if (!this.mayRunDataContextSQL(ownerID, user)) {
|
|
193
|
+
return 'Only the owner of this data context, or an administrator, may run its SQL items.';
|
|
194
|
+
}
|
|
195
|
+
if (!provider) {
|
|
196
|
+
return 'No read-only data source is configured, so SQL data context items cannot run.';
|
|
197
|
+
}
|
|
198
|
+
return this.screenItemSQL(item.SQL ?? '', provider, user);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** True when `user` owns the data context or is an administrator (Owner-type user). */
|
|
202
|
+
private mayRunDataContextSQL(ownerID: string | null, user: UserInfo): boolean {
|
|
203
|
+
const isOwner = !!ownerID && UUIDsEqual(ownerID, user.ID);
|
|
204
|
+
return isOwner || user.Type?.trim().toLowerCase() === 'owner';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Returns why `sqlText` may not run, or null when it is a single read statement whose every table
|
|
209
|
+
* reference is an entity base view `user` may read without row-level or field-level narrowing. It is
|
|
210
|
+
* the check ExecuteAdhocQuery makes on the rendered SQL.
|
|
211
|
+
*/
|
|
212
|
+
private screenItemSQL(sqlText: string, provider: DatabaseProviderBase, user: UserInfo): string | null {
|
|
213
|
+
try {
|
|
214
|
+
const rendered = RenderPipeline.Run(sqlText, { Platform: provider.PlatformKey, ContextUser: user, RequireReadStatement: true });
|
|
215
|
+
this.assertFullQueryUsesReadableEntityViews(rendered.Trace.AfterTemplates, provider, user, 'data context SQL');
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
220
|
+
return reason || 'The SQL of this item did not pass the read-only check.';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Loads one item's data unless `gate` refused it. Returns the data as JSON, or why there is none. */
|
|
225
|
+
private async loadItem(item: DataContextItem, gate: SQLItemGate, user: UserInfo, forceRefresh: boolean): Promise<ItemOutcome> {
|
|
226
|
+
const refusal = gate.Refusals.get(item);
|
|
227
|
+
if (refusal !== undefined) {
|
|
228
|
+
return { Result: null, Error: refusal };
|
|
229
|
+
}
|
|
230
|
+
if (await item.LoadData(gate.Provider, forceRefresh, false, 0, user)) {
|
|
231
|
+
return { Result: JSON.stringify(item.Data), Error: null };
|
|
232
|
+
}
|
|
233
|
+
return { Result: null, Error: item.DataLoadingError || 'Error loading data context item data' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Returns the owner (`UserID`) of a data context, or null when the caller cannot load it. */
|
|
237
|
+
private async loadDataContextOwnerID(dataContextID: string, provider: DatabaseProviderBase, user: UserInfo): Promise<string | null> {
|
|
238
|
+
const dataContext = await provider.GetEntityObject<MJDataContextEntity>('MJ: Data Contexts', user);
|
|
239
|
+
return (await dataContext.Load(dataContextID)) ? dataContext.UserID : null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, ObjectType, Query } from 'type-graphql';
|
|
2
|
+
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
2
3
|
import { AppContext } from '../types.js';
|
|
3
4
|
import { LogError, LogStatus, LogStatusEx, Metadata } from '@memberjunction/core';
|
|
4
5
|
import { QueryCompositionEngine } from '@memberjunction/generic-database-provider';
|
|
@@ -154,8 +155,20 @@ export class GetDataResolver {
|
|
|
154
155
|
resolvedSQL = compositionResult.ResolvedSQL;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Defense-in-depth: even system-token holders get SELECT-only SQL here.
|
|
159
|
+
// The read-only pool is the primary control, but if it is ever
|
|
160
|
+
// misconfigured to a read-write login this validator still refuses DML/DDL
|
|
161
|
+
// (mirrors AdhocQueryResolver). The validator reads exactly the text that
|
|
162
|
+
// runs: it is trimmed here as validate() trims it, and literal \n sequences
|
|
163
|
+
// are not rewritten as ValidateFullQuery would.
|
|
164
|
+
const sqlToRun = resolvedSQL.trim();
|
|
165
|
+
const validation = SQLExpressionValidator.Instance.validate(sqlToRun, { context: 'full_query', Dialect: platform });
|
|
166
|
+
if (!validation.valid) {
|
|
167
|
+
return { result: null, error: validation.error || 'SQL validation failed' };
|
|
168
|
+
}
|
|
169
|
+
|
|
157
170
|
const request = new sql.Request(readOnlyDataSource);
|
|
158
|
-
const result = await request.query(
|
|
171
|
+
const result = await request.query(sqlToRun);
|
|
159
172
|
return { result: result.recordset, error: null };
|
|
160
173
|
} catch (err) {
|
|
161
174
|
// Extract clean SQL error message
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from '@memberjunction/ai-mcp-client';
|
|
20
20
|
import { AppContext } from '../types.js';
|
|
21
21
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
22
|
-
import {
|
|
22
|
+
import { publishStatusUpdate } from '../generic/PushStatusResolver.js';
|
|
23
23
|
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
24
24
|
import { configInfo } from '../config.js';
|
|
25
25
|
|
|
@@ -583,7 +583,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
583
583
|
await manager.initialize(user, { publicUrl });
|
|
584
584
|
|
|
585
585
|
// Publish initial progress
|
|
586
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'connecting', 'Connecting to MCP server...');
|
|
586
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'connecting', 'Connecting to MCP server...');
|
|
587
587
|
|
|
588
588
|
// Connect if not already connected
|
|
589
589
|
const isConnected = manager.isConnected(ConnectionID);
|
|
@@ -595,18 +595,18 @@ export class MCPResolver extends ResolverBase {
|
|
|
595
595
|
// Check for OAuth authorization required
|
|
596
596
|
if (connectError instanceof OAuthAuthorizationRequiredError) {
|
|
597
597
|
const authError = connectError as OAuthAuthorizationRequiredError;
|
|
598
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
|
|
598
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error',
|
|
599
599
|
`OAuth authorization required. Please authorize at: ${authError.authorizationUrl}`);
|
|
600
600
|
return this.createOAuthRequiredResult(authError.authorizationUrl, authError.stateParameter);
|
|
601
601
|
}
|
|
602
602
|
if (connectError instanceof OAuthReauthorizationRequiredError) {
|
|
603
603
|
const reAuthError = connectError as OAuthReauthorizationRequiredError;
|
|
604
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error',
|
|
604
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error',
|
|
605
605
|
`OAuth re-authorization required: ${reAuthError.reason}`);
|
|
606
606
|
return this.createOAuthReauthorizationResult(reAuthError.reason, reAuthError.authorizationUrl, reAuthError.stateParameter);
|
|
607
607
|
}
|
|
608
608
|
const connectErrorMsg = connectError instanceof Error ? connectError.message : String(connectError);
|
|
609
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
|
|
609
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Connection failed: ${connectErrorMsg}`);
|
|
610
610
|
return this.createErrorResult(`Failed to connect to MCP server: ${connectErrorMsg}`);
|
|
611
611
|
}
|
|
612
612
|
}
|
|
@@ -617,7 +617,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
617
617
|
const connectionName = connectionInfo?.connectionName || 'Unknown Connection';
|
|
618
618
|
|
|
619
619
|
// Publish listing progress
|
|
620
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'listing', 'Discovering tools from MCP server...');
|
|
620
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'listing', 'Discovering tools from MCP server...');
|
|
621
621
|
|
|
622
622
|
// Perform the sync with event listening for granular progress
|
|
623
623
|
LogStatus(`MCPResolver: Starting tool sync for connection ${ConnectionID}`);
|
|
@@ -626,7 +626,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
626
626
|
const eventHandler = (event: { type: string; data?: Record<string, unknown> }) => {
|
|
627
627
|
if (event.type === 'toolsSynced') {
|
|
628
628
|
const data = event.data as { added: number; updated: number; deprecated: number; total: number } | undefined;
|
|
629
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'complete', 'Tool sync complete', {
|
|
629
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'complete', 'Tool sync complete', {
|
|
630
630
|
added: data?.added || 0,
|
|
631
631
|
updated: data?.updated || 0,
|
|
632
632
|
deprecated: data?.deprecated || 0,
|
|
@@ -637,7 +637,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
637
637
|
manager.addEventListener('toolsSynced', eventHandler);
|
|
638
638
|
|
|
639
639
|
// Publish syncing progress
|
|
640
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'syncing', 'Synchronizing tools to database...');
|
|
640
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'syncing', 'Synchronizing tools to database...');
|
|
641
641
|
|
|
642
642
|
// Perform the sync
|
|
643
643
|
const syncResult: MCPSyncToolsResult = await manager.syncTools(ConnectionID, { contextUser: user });
|
|
@@ -646,12 +646,12 @@ export class MCPResolver extends ResolverBase {
|
|
|
646
646
|
manager.removeEventListener('toolsSynced', eventHandler);
|
|
647
647
|
|
|
648
648
|
if (!syncResult.success) {
|
|
649
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
|
|
649
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Sync failed: ${syncResult.error}`);
|
|
650
650
|
return this.createErrorResult(syncResult.error || 'Tool sync failed');
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
// Publish final completion
|
|
654
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'complete',
|
|
654
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'complete',
|
|
655
655
|
`Sync complete: ${syncResult.added} added, ${syncResult.updated} updated, ${syncResult.deprecated} deprecated`,
|
|
656
656
|
{
|
|
657
657
|
added: syncResult.added,
|
|
@@ -675,7 +675,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
675
675
|
} catch (error) {
|
|
676
676
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
677
677
|
LogError(`MCPResolver: Error syncing tools for ${ConnectionID}: ${errorMsg}`);
|
|
678
|
-
this.publishProgress(pubSub, sessionId, ConnectionID, 'error', `Error: ${errorMsg}`);
|
|
678
|
+
this.publishProgress(pubSub, sessionId, user.ID, ConnectionID, 'error', `Error: ${errorMsg}`);
|
|
679
679
|
return this.createErrorResult(errorMsg);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
@@ -1345,6 +1345,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
1345
1345
|
private publishProgress(
|
|
1346
1346
|
pubSub: PubSubEngine,
|
|
1347
1347
|
sessionId: string,
|
|
1348
|
+
ownerUserId: string,
|
|
1348
1349
|
connectionId: string,
|
|
1349
1350
|
phase: SyncProgressMessage['phase'],
|
|
1350
1351
|
message: string,
|
|
@@ -1360,10 +1361,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
1360
1361
|
result
|
|
1361
1362
|
};
|
|
1362
1363
|
|
|
1363
|
-
pubSub.
|
|
1364
|
-
message: JSON.stringify(progressMessage),
|
|
1365
|
-
sessionId
|
|
1366
|
-
});
|
|
1364
|
+
publishStatusUpdate(pubSub, { sessionId, ownerUserId, message: JSON.stringify(progressMessage) });
|
|
1367
1365
|
}
|
|
1368
1366
|
|
|
1369
1367
|
/**
|