@memberjunction/server 6.1.4 → 6.1.6
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/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 +129 -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/exampleNewUserSubClass.js +2 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +55 -38
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +20 -52
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +15 -11
- package/dist/auth/newUsers.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/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +3417 -3428
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +95 -11
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +186 -76
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -45
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- 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 +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- 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 +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- 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 +8 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +11 -48
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
- package/dist/resolvers/FetchEntityVectorsResolver.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/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +2 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +83 -22
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +174 -72
- 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 +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +2 -1
- 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/SearchKnowledgeResolver.d.ts +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/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/TaskGraphActionRunner.d.ts +21 -8
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +43 -16
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/package.json +92 -92
- 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 +3 -3
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- 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 +137 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +310 -31
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
- package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SessionManager.test.ts +94 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
- package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLink.test.ts +15 -76
- package/src/__tests__/magicLinkService.consume.test.ts +230 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.test.ts +62 -5
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +23 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/exampleNewUserSubClass.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +61 -39
- package/src/auth/magicLink/magicLinkCore.ts +19 -56
- package/src/auth/newUsers.ts +15 -11
- package/src/context.ts +25 -34
- package/src/generated/graphql-schemas/__mj.ts +3417 -3428
- package/src/generic/CacheInvalidationResolver.ts +74 -19
- package/src/generic/ResolverBase.ts +207 -80
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +49 -46
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +11 -52
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/QuerySystemUserResolver.ts +2 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +196 -77
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +4 -1
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskGraphActionRunner.ts +45 -16
- 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,6 +1,6 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
-
import { AppContext } from '../types.js';
|
|
3
|
-
import { LogError, Metadata } from '@memberjunction/core';
|
|
2
|
+
import { AppContext, UserPayload } from '../types.js';
|
|
3
|
+
import { DatabaseProviderBase, DatasetItemFilterType, DatasetItemResultType, LogError, Metadata, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { GetReadOnlyProvider } from '../util.js';
|
|
5
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
6
6
|
|
|
@@ -34,9 +34,64 @@ export class DatasetItemFilterTypeGQL {
|
|
|
34
34
|
Filter: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Request handling shared by the dataset queries. Every read runs as the session's user, so the
|
|
39
|
+
* provider applies that user's entity read permission and row-level security per item, and every
|
|
40
|
+
* client ItemFilter passes the same AST screen as a RunView ExtraFilter before a provider sees it.
|
|
41
|
+
*/
|
|
42
|
+
export abstract class DatasetResolverBase extends ResolverBase {
|
|
43
|
+
/** The session's user. A dataset read never runs without one: the provider applies no read checks then. */
|
|
44
|
+
protected RequireDatasetUser(userPayload: UserPayload): UserInfo {
|
|
45
|
+
const user = this.GetUserFromPayload(userPayload);
|
|
46
|
+
if (!user) {
|
|
47
|
+
throw new Error('Dataset request refused: the session resolves to no user');
|
|
48
|
+
}
|
|
49
|
+
return user;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Screens every client ItemFilter: one statement, read-only, and subqueries only over entity base
|
|
54
|
+
* views the user can read. Throws on the first refusal. Returns the filters without null entries.
|
|
55
|
+
*/
|
|
56
|
+
protected ScreenItemFilters(
|
|
57
|
+
itemFilters: DatasetItemFilterTypeGQL[] | undefined,
|
|
58
|
+
provider: DatabaseProviderBase,
|
|
59
|
+
user: UserInfo,
|
|
60
|
+
): DatasetItemFilterType[] | undefined {
|
|
61
|
+
if (!itemFilters) return undefined;
|
|
62
|
+
itemFilters.forEach((itemFilter, index) => {
|
|
63
|
+
if (itemFilter) {
|
|
64
|
+
this.assertClientClauseUsesEntityBaseViews(itemFilter.Filter, `ItemFilters[${index}].Filter`, provider, user);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return itemFilters.filter((itemFilter) => itemFilter != null);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Applies the API encryption policy to each item's rows, as every other GraphQL read path does:
|
|
72
|
+
* an encrypted field the entity does not allow decrypted over the API leaves as ciphertext or the
|
|
73
|
+
* sentinel. Works on copies, because the rows can be the server cache's own frozen objects.
|
|
74
|
+
*/
|
|
75
|
+
protected async ProtectEncryptedFields(
|
|
76
|
+
items: DatasetItemResultType[],
|
|
77
|
+
provider: DatabaseProviderBase,
|
|
78
|
+
user: UserInfo,
|
|
79
|
+
): Promise<DatasetItemResultType[]> {
|
|
80
|
+
return Promise.all(
|
|
81
|
+
items.map(async (item) => {
|
|
82
|
+
const entity = provider.EntityByName(item.EntityName);
|
|
83
|
+
if (!item.Results?.length || !entity?.EncryptedFields.some((field) => !field.AllowDecryptInAPI)) {
|
|
84
|
+
return item;
|
|
85
|
+
}
|
|
86
|
+
const rows = item.Results.map((row: Record<string, unknown>) => ({ ...row }));
|
|
87
|
+
return { ...item, Results: await this.ArrayFilterEncryptedFieldsForAPI(item.EntityName, rows, user, provider, 'Name') };
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
37
92
|
|
|
38
93
|
@Resolver(DatasetResultType)
|
|
39
|
-
export class DatasetResolverExtended extends
|
|
94
|
+
export class DatasetResolverExtended extends DatasetResolverBase {
|
|
40
95
|
@Query(() => DatasetResultType)
|
|
41
96
|
async GetDatasetByName(
|
|
42
97
|
@Arg('DatasetName', () => String) DatasetName: string,
|
|
@@ -45,10 +100,12 @@ export class DatasetResolverExtended extends ResolverBase {
|
|
|
45
100
|
) {
|
|
46
101
|
// Check API key scope authorization for dataset read
|
|
47
102
|
await this.CheckAPIKeyScopeAuthorization('dataset:read', DatasetName, userPayload);
|
|
103
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
104
|
+
const user = this.RequireDatasetUser(userPayload);
|
|
105
|
+
const itemFilters = this.ScreenItemFilters(ItemFilters, md, user);
|
|
48
106
|
|
|
49
107
|
try {
|
|
50
|
-
const
|
|
51
|
-
const result = await md.GetDatasetByName(DatasetName, ItemFilters);
|
|
108
|
+
const result = await md.GetDatasetByName(DatasetName, itemFilters, user);
|
|
52
109
|
if (result) {
|
|
53
110
|
return {
|
|
54
111
|
DatasetID: result.DatasetID,
|
|
@@ -56,7 +113,7 @@ export class DatasetResolverExtended extends ResolverBase {
|
|
|
56
113
|
Success: result.Success,
|
|
57
114
|
Status: result.Status,
|
|
58
115
|
LatestUpdateDate: result.LatestUpdateDate,
|
|
59
|
-
Results: JSON.stringify(result.Results),
|
|
116
|
+
Results: JSON.stringify(await this.ProtectEncryptedFields(result.Results, md, user)),
|
|
60
117
|
};
|
|
61
118
|
} else {
|
|
62
119
|
throw new Error('Error retrieving Dataset: ' + DatasetName);
|
|
@@ -90,7 +147,7 @@ export class DatasetStatusResultType {
|
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
@Resolver(DatasetStatusResultType)
|
|
93
|
-
export class DatasetStatusResolver extends
|
|
150
|
+
export class DatasetStatusResolver extends DatasetResolverBase {
|
|
94
151
|
@Query(() => DatasetStatusResultType)
|
|
95
152
|
async GetDatasetStatusByName(
|
|
96
153
|
@Arg('DatasetName', () => String) DatasetName: string,
|
|
@@ -99,10 +156,12 @@ export class DatasetStatusResolver extends ResolverBase {
|
|
|
99
156
|
) {
|
|
100
157
|
// Check API key scope authorization for dataset read
|
|
101
158
|
await this.CheckAPIKeyScopeAuthorization('dataset:read', DatasetName, userPayload);
|
|
159
|
+
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
160
|
+
const user = this.RequireDatasetUser(userPayload);
|
|
161
|
+
const itemFilters = this.ScreenItemFilters(ItemFilters, md, user);
|
|
102
162
|
|
|
103
163
|
try {
|
|
104
|
-
const
|
|
105
|
-
const result = await md.GetDatasetStatusByName(DatasetName, ItemFilters);
|
|
164
|
+
const result = await md.GetDatasetStatusByName(DatasetName, itemFilters, user);
|
|
106
165
|
if (result) {
|
|
107
166
|
return {
|
|
108
167
|
DatasetID: result.DatasetID,
|
|
@@ -131,12 +190,13 @@ export class DatasetStatusResolver extends ResolverBase {
|
|
|
131
190
|
@Ctx() { providers, userPayload }: AppContext,
|
|
132
191
|
): Promise<DatasetStatusResultType[]> {
|
|
133
192
|
const md = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
|
|
193
|
+
const user = this.RequireDatasetUser(userPayload);
|
|
134
194
|
const results: DatasetStatusResultType[] = [];
|
|
135
195
|
|
|
136
196
|
// Execute all status checks in parallel
|
|
137
197
|
const statusPromises = DatasetNames.map(async (name) => {
|
|
138
198
|
await this.CheckAPIKeyScopeAuthorization('dataset:read', name, userPayload);
|
|
139
|
-
return md.GetDatasetStatusByName(name);
|
|
199
|
+
return md.GetDatasetStatusByName(name, undefined, user);
|
|
140
200
|
});
|
|
141
201
|
|
|
142
202
|
const statuses = await Promise.all(statusPromises);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompositeKey, DatabaseProviderBase
|
|
1
|
+
import { CompositeKey, DatabaseProviderBase } from '@memberjunction/core';
|
|
2
2
|
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
3
|
import { AppContext, UserPayload } from '../types.js';
|
|
4
4
|
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
@@ -36,20 +36,14 @@ export class EntityRecordNameResult {
|
|
|
36
36
|
* Resolves a record's display NAME from its primary key — the lookup behind every foreign-key
|
|
37
37
|
* link, breadcrumb and picker label in the UI.
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* form-field FK control falls through to this query precisely WHEN the joined display column is
|
|
44
|
-
* denied, so the one case the fallback exists for was the case that leaked.
|
|
39
|
+
* Entity-level read permission is checked here ({@link ResolverBase.CheckUserReadPermissions}).
|
|
40
|
+
* Field- and row-level security are applied by the provider's lookup, for the acting user passed
|
|
41
|
+
* to it: when any field the name is built from is withheld, it answers with no name, and it reads
|
|
42
|
+
* only rows the user's read row filter allows. Without an acting user there is no lookup.
|
|
45
43
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* A field-level denial answers `Success: false` with no name rather than throwing. Callers
|
|
51
|
-
* already treat "no name" as "show the primary key", which is the intended degradation — and it
|
|
52
|
-
* keeps this query from becoming a probe that distinguishes "restricted" from "missing".
|
|
44
|
+
* A withheld name answers `Success: false` with the same status as a record that does not exist.
|
|
45
|
+
* Callers already treat "no name" as "show the primary key", and the identical answer keeps this
|
|
46
|
+
* query from becoming a probe that distinguishes "restricted" from "missing".
|
|
53
47
|
*/
|
|
54
48
|
@Resolver(EntityRecordNameResult)
|
|
55
49
|
export class EntityRecordNameResolver extends ResolverBase {
|
|
@@ -91,53 +85,18 @@ export class EntityRecordNameResolver extends ResolverBase {
|
|
|
91
85
|
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
92
86
|
if (e) {
|
|
93
87
|
const contextUser = userPayload ? this.GetUserFromPayload(userPayload) : undefined;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`[FieldSecurity] Suppressed GetEntityRecordName for '${EntityName}': ` +
|
|
97
|
-
`the name field is not readable by this user`
|
|
98
|
-
);
|
|
99
|
-
return {
|
|
100
|
-
Success: false,
|
|
101
|
-
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue`,
|
|
102
|
-
CompositeKey: pk,
|
|
103
|
-
EntityName,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
// Pass the acting user through: the provider signature accepts it, and omitting it ran the
|
|
107
|
-
// underlying lookup with no identity at all — so row-level security had nothing to filter on
|
|
108
|
-
// either. The field-level gate above is the column half; this is the row half.
|
|
109
|
-
const recordName = await md.GetEntityRecordName(e.Name, pk, contextUser);
|
|
88
|
+
// The acting user is what field- and row-level security are applied for.
|
|
89
|
+
const recordName = contextUser ? await md.GetEntityRecordName(e.Name, pk, contextUser) : '';
|
|
110
90
|
if (recordName) return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
|
|
111
91
|
else
|
|
112
92
|
return {
|
|
113
93
|
Success: false,
|
|
114
|
-
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue
|
|
94
|
+
Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue`,
|
|
115
95
|
CompositeKey: pk,
|
|
116
96
|
EntityName,
|
|
117
97
|
};
|
|
118
98
|
} else return { Success: false, Status: `Entity ${EntityName} not found`, CompositeKey: pk, EntityName };
|
|
119
99
|
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Whether field security withholds this entity's NAME field from the user.
|
|
123
|
-
*
|
|
124
|
-
* Resolves the name field the same way the provider's own lookup does — the designated
|
|
125
|
-
* `IsNameField`, else a field literally called `Name` — so the check cannot drift from the
|
|
126
|
-
* value it is guarding. Fails OPEN only where field security does not apply at all (flag off,
|
|
127
|
-
* no user, no name field), matching every other FLS gate.
|
|
128
|
-
*/
|
|
129
|
-
protected IsNameFieldDeniedToUser(entity: EntityInfo, contextUser?: UserInfo): boolean {
|
|
130
|
-
if (!entity?.EnableFieldLevelSecurity || !contextUser) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
const nameField =
|
|
134
|
-
entity.Fields?.find(f => f.IsNameField) ??
|
|
135
|
-
entity.Fields?.find(f => f.Name?.trim().toLowerCase() === 'name');
|
|
136
|
-
if (!nameField) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
return entity.GetDeniedReadFields(contextUser).has(nameField.Name.trim().toLowerCase());
|
|
140
|
-
}
|
|
141
100
|
}
|
|
142
101
|
|
|
143
102
|
export default EntityRecordNameResolver;
|
|
@@ -5,7 +5,7 @@ import { MJEntityDocumentEntity, MJVectorIndexEntity, MJVectorDatabaseEntity } f
|
|
|
5
5
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
6
6
|
import { GetAIAPIKey } from '@memberjunction/ai';
|
|
7
7
|
import { VectorDBBase } from '@memberjunction/ai-vectordb';
|
|
8
|
-
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
8
|
+
import { EscapeSQLString, MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
9
9
|
|
|
10
10
|
/* ───── GraphQL types ───── */
|
|
11
11
|
|
|
@@ -144,7 +144,7 @@ export class FetchEntityVectorsResolver extends ResolverBase {
|
|
|
144
144
|
const rv = new RunView();
|
|
145
145
|
const result = await rv.RunView<MJEntityDocumentEntity>({
|
|
146
146
|
EntityName: 'MJ: Entity Documents',
|
|
147
|
-
ExtraFilter: `ID='${entityDocumentID}'`,
|
|
147
|
+
ExtraFilter: `ID='${EscapeSQLString(entityDocumentID)}'`,
|
|
148
148
|
ResultType: 'entity_object',
|
|
149
149
|
}, contextUser);
|
|
150
150
|
|
|
@@ -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
|
|
@@ -6,6 +6,7 @@ import { MJQueryResolver, MJQuery_, MJQueryField_, MJQueryParameter_, MJQueryEnt
|
|
|
6
6
|
import { GetReadWriteProvider } from '../util.js';
|
|
7
7
|
import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
|
|
8
8
|
import { MJQueryEntityServer } from '@memberjunction/core-entities-server';
|
|
9
|
+
import { EscapeSQLString } from '@memberjunction/global';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Minimal shape of a query row returned by RunView lookups (plain object, not entity instance).
|
|
@@ -493,7 +494,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
493
494
|
// now make sure there is NO existing query by the same name in the specified category
|
|
494
495
|
const existingQueryResult = await provider.RunView({
|
|
495
496
|
EntityName: 'MJ: Queries',
|
|
496
|
-
ExtraFilter: `Name='${input.Name}' AND CategoryID='${finalCategoryID}'`
|
|
497
|
+
ExtraFilter: `Name='${EscapeSQLString(input.Name)}' AND CategoryID='${EscapeSQLString(finalCategoryID)}'`
|
|
497
498
|
}, context.userPayload.userRecord);
|
|
498
499
|
if (existingQueryResult.Success && existingQueryResult.Results?.length > 0) {
|
|
499
500
|
// we have a match! Let's return an error
|