@memberjunction/server 3.3.0 → 4.0.0
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 +59 -0
- package/dist/agents/skip-agent.d.ts +65 -0
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +63 -5
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +163 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +143 -12
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/TransactionPlugin.d.ts +4 -0
- package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -0
- package/dist/apolloServer/TransactionPlugin.js +46 -0
- package/dist/apolloServer/TransactionPlugin.js.map +1 -0
- package/dist/apolloServer/index.d.ts +0 -1
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.d.ts +82 -0
- package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.js +78 -0
- package/dist/auth/APIKeyScopeAuth.js.map +1 -1
- package/dist/auth/AuthProviderFactory.d.ts +35 -0
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
- package/dist/auth/AuthProviderFactory.js +51 -4
- package/dist/auth/AuthProviderFactory.js.map +1 -1
- package/dist/auth/BaseAuthProvider.d.ts +22 -0
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +25 -8
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +33 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/auth/__tests__/backward-compatibility.test.d.ts +2 -0
- package/dist/auth/__tests__/backward-compatibility.test.d.ts.map +1 -0
- package/dist/auth/__tests__/backward-compatibility.test.js +135 -0
- package/dist/auth/__tests__/backward-compatibility.test.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.d.ts +5 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +21 -6
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +35 -22
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +3 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +6 -0
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/newUsers.js +11 -2
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts +9 -0
- package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
- package/dist/auth/providers/Auth0Provider.js +10 -0
- package/dist/auth/providers/Auth0Provider.js.map +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts +9 -0
- package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
- package/dist/auth/providers/CognitoProvider.js +10 -0
- package/dist/auth/providers/CognitoProvider.js.map +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts +9 -0
- package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
- package/dist/auth/providers/GoogleProvider.js +11 -1
- package/dist/auth/providers/GoogleProvider.js.map +1 -1
- package/dist/auth/providers/MSALProvider.d.ts +9 -0
- package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
- package/dist/auth/providers/MSALProvider.js +10 -0
- package/dist/auth/providers/MSALProvider.js.map +1 -1
- package/dist/auth/providers/OktaProvider.d.ts +9 -0
- package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
- package/dist/auth/providers/OktaProvider.js +10 -0
- package/dist/auth/providers/OktaProvider.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +44 -10
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +26 -4
- package/dist/context.js.map +1 -1
- package/dist/directives/Public.js +2 -0
- package/dist/directives/Public.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +7 -2
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +26 -8
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +954 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +12183 -14532
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/DeleteOptionsInput.d.ts +3 -0
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -1
- package/dist/generic/DeleteOptionsInput.js +3 -2
- package/dist/generic/DeleteOptionsInput.js.map +1 -1
- package/dist/generic/KeyInputOutputTypes.js +0 -6
- package/dist/generic/KeyInputOutputTypes.js.map +1 -1
- package/dist/generic/KeyValuePairInput.d.ts +4 -0
- package/dist/generic/KeyValuePairInput.d.ts.map +1 -1
- package/dist/generic/KeyValuePairInput.js +4 -2
- package/dist/generic/KeyValuePairInput.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +0 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +59 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +233 -18
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +22 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +42 -108
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -39
- package/dist/index.js.map +1 -1
- package/dist/orm.d.ts.map +1 -1
- package/dist/orm.js +2 -1
- package/dist/orm.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts +76 -1
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
- package/dist/resolvers/APIKeyResolver.js +53 -11
- package/dist/resolvers/APIKeyResolver.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +191 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +156 -22
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ColorResolver.js +0 -5
- package/dist/resolvers/ColorResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts +65 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +118 -40
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +47 -0
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +92 -116
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +5 -4
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +9 -18
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +42 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +5 -37
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +0 -7
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +13 -1
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +16 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +59 -74
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +20 -2
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +27 -12
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts +19 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +35 -35
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.d.ts.map +1 -1
- package/dist/resolvers/InfoResolver.js +4 -7
- package/dist/resolvers/InfoResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +361 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -0
- package/dist/resolvers/MCPResolver.js +1270 -0
- package/dist/resolvers/MCPResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +6 -30
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +0 -3
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +22 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +50 -37
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +5 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +13 -11
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +54 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +118 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +42 -0
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +98 -22
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +10 -6
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts +12 -0
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +35 -21
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts +312 -0
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +295 -45
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +21 -0
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +36 -22
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +14 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +54 -21
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts +13 -0
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +23 -7
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +22 -0
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
- package/dist/resolvers/TelemetryResolver.js +45 -79
- package/dist/resolvers/TelemetryResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +11 -13
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +3 -12
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +14 -0
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.js +4 -0
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts +39 -0
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -0
- package/dist/resolvers/VersionHistoryResolver.js +208 -0
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -0
- package/dist/rest/EntityCRUDHandler.d.ts +19 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +55 -0
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +143 -0
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -0
- package/dist/rest/OAuthCallbackHandler.js +634 -0
- package/dist/rest/OAuthCallbackHandler.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +120 -0
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +213 -24
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +19 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +39 -0
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/index.d.ts +1 -0
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +1 -0
- package/dist/rest/index.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +35 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +15 -1
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts +31 -0
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +38 -4
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +73 -0
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +137 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -13
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +37 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +55 -8
- package/dist/util.js.map +1 -1
- package/package.json +79 -77
- package/src/auth/BaseAuthProvider.ts +3 -0
- package/src/auth/IAuthProvider.ts +5 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -5
- package/src/config.ts +2 -2
- package/src/entitySubclasses/entityPermissions.server.ts +1 -3
- package/src/generated/generated.ts +6245 -2558
- package/src/generic/ResolverBase.ts +89 -3
- package/src/index.ts +71 -64
- package/src/resolvers/APIKeyResolver.ts +8 -1
- package/src/resolvers/ActionResolver.ts +8 -1
- package/src/resolvers/DatasetResolver.ts +11 -4
- package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
- package/src/resolvers/GetDataContextDataResolver.ts +14 -6
- package/src/resolvers/InfoResolver.ts +5 -1
- package/src/resolvers/MCPResolver.ts +1380 -0
- package/src/resolvers/MergeRecordsResolver.ts +5 -1
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +0 -4
- package/src/resolvers/QueryResolver.ts +17 -3
- package/src/resolvers/ReportResolver.ts +8 -1
- package/src/resolvers/RunAIAgentResolver.ts +6 -0
- package/src/resolvers/RunAIPromptResolver.ts +10 -1
- package/src/resolvers/RunTemplateResolver.ts +4 -1
- package/src/resolvers/TaskResolver.ts +3 -0
- package/src/resolvers/UserResolver.ts +15 -3
- package/src/resolvers/VersionHistoryResolver.ts +177 -0
- package/src/rest/OAuthCallbackHandler.ts +766 -0
- package/src/rest/RESTEndpointHandler.ts +58 -35
- package/src/rest/index.ts +2 -1
- package/src/rest/setupRESTEndpoints.ts +13 -12
- package/src/resolvers/AskSkipResolver.ts +0 -3446
- package/src/scheduler/LearningCycleScheduler.ts +0 -320
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, LogDebug, LogError, LogStatus, Metadata, } from '@memberjunction/core';
|
|
2
2
|
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
3
|
+
import { AuthorizationError } from 'type-graphql';
|
|
3
4
|
import { GraphQLError } from 'graphql';
|
|
5
|
+
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
4
6
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
5
7
|
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
6
8
|
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
7
9
|
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
|
|
8
10
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
9
11
|
export class ResolverBase {
|
|
10
|
-
static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
|
|
11
|
-
static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
|
|
12
|
-
static _eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions';
|
|
12
|
+
static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
|
|
13
|
+
static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
|
|
14
|
+
static { this._eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions'; }
|
|
13
15
|
get EventSubscriptions() {
|
|
16
|
+
// here we use the global object store instead of a static member becuase in some cases based on import code paths/bundling/etc, the static member
|
|
17
|
+
// could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
|
|
14
18
|
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
15
19
|
if (!g[ResolverBase._eventSubscriptionKey]) {
|
|
16
20
|
LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
|
|
@@ -18,15 +22,35 @@ export class ResolverBase {
|
|
|
18
22
|
}
|
|
19
23
|
return g[ResolverBase._eventSubscriptionKey];
|
|
20
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
|
|
27
|
+
*
|
|
28
|
+
* For encrypted fields coming from raw SQL queries (not entity objects):
|
|
29
|
+
* - AllowDecryptInAPI=true: Decrypt the value before sending to client
|
|
30
|
+
* - AllowDecryptInAPI=false + SendEncryptedValue=true: Keep encrypted ciphertext
|
|
31
|
+
* - AllowDecryptInAPI=false + SendEncryptedValue=false: Replace with sentinel
|
|
32
|
+
*
|
|
33
|
+
* @param entityName - The entity name
|
|
34
|
+
* @param dataObject - The data object with field values
|
|
35
|
+
* @param contextUser - Optional user context for decryption (required for encrypted fields)
|
|
36
|
+
* @returns The processed data object
|
|
37
|
+
*/
|
|
21
38
|
async MapFieldNamesToCodeNames(entityName, dataObject, contextUser) {
|
|
39
|
+
// for the given entity name provided, check to see if there are any fields
|
|
40
|
+
// where the code name is different from the field name, and for just those
|
|
41
|
+
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
42
|
+
// with the CodeName, because we can't transfer those via GraphQL as they are not
|
|
43
|
+
// valid property names in GraphQL
|
|
22
44
|
if (dataObject) {
|
|
23
45
|
const md = new Metadata();
|
|
24
46
|
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
25
47
|
if (!entityInfo)
|
|
26
48
|
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
49
|
+
// const fields = entityInfo.Fields.filter((f) => f.Name !== f.CodeName || f.Name.startsWith('__mj_'));
|
|
27
50
|
const mapper = new FieldMapper();
|
|
28
51
|
entityInfo.Fields.forEach((f) => {
|
|
29
52
|
if (dataObject.hasOwnProperty(f.Name)) {
|
|
53
|
+
// GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
|
|
30
54
|
const mappedFieldName = mapper.MapFieldName(f.CodeName);
|
|
31
55
|
if (mappedFieldName !== f.Name) {
|
|
32
56
|
dataObject[mappedFieldName] = dataObject[f.Name];
|
|
@@ -34,18 +58,22 @@ export class ResolverBase {
|
|
|
34
58
|
}
|
|
35
59
|
}
|
|
36
60
|
});
|
|
61
|
+
// Handle encrypted fields - data from raw SQL queries is still encrypted
|
|
37
62
|
const encryptedFields = entityInfo.EncryptedFields;
|
|
38
63
|
if (encryptedFields.length > 0) {
|
|
39
64
|
for (const field of encryptedFields) {
|
|
40
65
|
const fieldName = field.CodeName;
|
|
41
66
|
const value = dataObject[fieldName];
|
|
67
|
+
// Skip null/undefined values
|
|
42
68
|
if (value === null || value === undefined)
|
|
43
69
|
continue;
|
|
70
|
+
// Check if value is encrypted (raw SQL returns encrypted values)
|
|
44
71
|
const engine = EncryptionEngine.Instance;
|
|
45
72
|
await engine.Config(false, contextUser);
|
|
46
73
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
47
74
|
if (typeof value === 'string' && IsValueEncrypted(value, keyMarker)) {
|
|
48
75
|
if (field.AllowDecryptInAPI) {
|
|
76
|
+
// Decrypt the value for the client
|
|
49
77
|
if (contextUser) {
|
|
50
78
|
try {
|
|
51
79
|
const decryptedValue = await engine.Decrypt(value, contextUser);
|
|
@@ -53,16 +81,20 @@ export class ResolverBase {
|
|
|
53
81
|
}
|
|
54
82
|
catch (err) {
|
|
55
83
|
LogError(`Failed to decrypt field ${fieldName} for API response: ${err}`);
|
|
84
|
+
// On decryption failure, use sentinel for safety
|
|
56
85
|
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
57
86
|
}
|
|
58
87
|
}
|
|
59
88
|
else {
|
|
89
|
+
// No context user, can't decrypt - use sentinel
|
|
60
90
|
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
93
|
else if (!field.SendEncryptedValue) {
|
|
94
|
+
// AllowDecryptInAPI=false and SendEncryptedValue=false - use sentinel
|
|
64
95
|
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
65
96
|
}
|
|
97
|
+
// else: AllowDecryptInAPI=false and SendEncryptedValue=true - keep encrypted value as-is
|
|
66
98
|
}
|
|
67
99
|
}
|
|
68
100
|
}
|
|
@@ -70,6 +102,7 @@ export class ResolverBase {
|
|
|
70
102
|
return dataObject;
|
|
71
103
|
}
|
|
72
104
|
async ArrayMapFieldNamesToCodeNames(entityName, dataObjectArray, contextUser) {
|
|
105
|
+
// iterate through the array and call MapFieldNamesToCodeNames for each element
|
|
73
106
|
if (dataObjectArray && dataObjectArray.length > 0) {
|
|
74
107
|
for (const element of dataObjectArray) {
|
|
75
108
|
await this.MapFieldNamesToCodeNames(entityName, element, contextUser);
|
|
@@ -77,6 +110,20 @@ export class ResolverBase {
|
|
|
77
110
|
}
|
|
78
111
|
return dataObjectArray;
|
|
79
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Filters encrypted field values before sending to the API client.
|
|
115
|
+
*
|
|
116
|
+
* For each encrypted field in the entity:
|
|
117
|
+
* - If AllowDecryptInAPI is true: value passes through unchanged (already decrypted by data provider)
|
|
118
|
+
* - If AllowDecryptInAPI is false and SendEncryptedValue is true: re-encrypt and send ciphertext
|
|
119
|
+
* - If AllowDecryptInAPI is false and SendEncryptedValue is false: replace with sentinel value
|
|
120
|
+
*
|
|
121
|
+
* @param entityName - Name of the entity
|
|
122
|
+
* @param dataObject - The data object containing field values
|
|
123
|
+
* @param encryptionEngine - Optional encryption engine for re-encryption (lazy loaded if needed)
|
|
124
|
+
* @param contextUser - User context for encryption operations
|
|
125
|
+
* @returns The filtered data object
|
|
126
|
+
*/
|
|
80
127
|
async FilterEncryptedFieldsForAPI(entityName, dataObject, contextUser) {
|
|
81
128
|
if (!dataObject)
|
|
82
129
|
return dataObject;
|
|
@@ -84,40 +131,54 @@ export class ResolverBase {
|
|
|
84
131
|
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
85
132
|
if (!entityInfo)
|
|
86
133
|
return dataObject;
|
|
134
|
+
// Find all encrypted fields that need filtering
|
|
87
135
|
const encryptedFields = entityInfo.EncryptedFields;
|
|
88
136
|
if (encryptedFields.length === 0)
|
|
89
137
|
return dataObject;
|
|
138
|
+
// Process each encrypted field
|
|
90
139
|
for (const field of encryptedFields) {
|
|
91
140
|
const fieldName = field.CodeName;
|
|
92
141
|
const value = dataObject[fieldName];
|
|
142
|
+
// Skip null/undefined values
|
|
93
143
|
if (value === null || value === undefined)
|
|
94
144
|
continue;
|
|
145
|
+
// If AllowDecryptInAPI is true, the decrypted value passes through
|
|
95
146
|
if (field.AllowDecryptInAPI)
|
|
96
147
|
continue;
|
|
148
|
+
// AllowDecryptInAPI is false - we need to filter the value
|
|
97
149
|
const engine = EncryptionEngine.Instance;
|
|
98
150
|
await engine.Config(false, contextUser);
|
|
99
151
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
100
152
|
if (field.SendEncryptedValue) {
|
|
153
|
+
// Re-encrypt the value before sending
|
|
154
|
+
// Only re-encrypt if it's not already encrypted (data provider decrypted it)
|
|
101
155
|
if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
|
|
102
156
|
try {
|
|
103
157
|
const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
|
|
104
158
|
dataObject[fieldName] = encryptedValue;
|
|
105
159
|
}
|
|
106
160
|
catch (err) {
|
|
161
|
+
// If re-encryption fails, use sentinel for safety
|
|
107
162
|
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
108
163
|
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
109
164
|
}
|
|
110
165
|
}
|
|
166
|
+
// If already encrypted (shouldn't happen normally), keep as-is
|
|
111
167
|
}
|
|
112
168
|
else {
|
|
169
|
+
// SendEncryptedValue is false - replace with sentinel
|
|
113
170
|
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
114
171
|
}
|
|
115
172
|
}
|
|
116
173
|
return dataObject;
|
|
117
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Filters encrypted fields for an array of data objects
|
|
177
|
+
*/
|
|
118
178
|
async ArrayFilterEncryptedFieldsForAPI(entityName, dataObjectArray, contextUser) {
|
|
119
179
|
if (!dataObjectArray || dataObjectArray.length === 0)
|
|
120
180
|
return dataObjectArray;
|
|
181
|
+
// Check if entity has any encrypted fields first to avoid unnecessary processing
|
|
121
182
|
const md = new Metadata();
|
|
122
183
|
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
123
184
|
if (!entityInfo)
|
|
@@ -125,27 +186,33 @@ export class ResolverBase {
|
|
|
125
186
|
const encryptedFields = entityInfo.Fields.filter(f => f.Encrypt && !f.AllowDecryptInAPI);
|
|
126
187
|
if (encryptedFields.length === 0)
|
|
127
188
|
return dataObjectArray;
|
|
189
|
+
// Process each element
|
|
128
190
|
for (const element of dataObjectArray) {
|
|
129
191
|
await this.FilterEncryptedFieldsForAPI(entityName, element, contextUser);
|
|
130
192
|
}
|
|
131
193
|
return dataObjectArray;
|
|
132
194
|
}
|
|
133
195
|
async findBy(provider, entity, params, contextUser) {
|
|
196
|
+
// build the SQL query based on the params passed in
|
|
134
197
|
const rv = provider;
|
|
135
198
|
const e = provider.Entities.find((e) => e.Name === entity);
|
|
136
199
|
if (!e)
|
|
137
200
|
throw new Error(`Entity ${entity} not found in metadata`);
|
|
201
|
+
// now build a SQL string using the entityInfo and using the properties in the params object
|
|
138
202
|
let extraFilter = "";
|
|
139
203
|
const keys = Object.keys(params);
|
|
140
204
|
keys.forEach((k, i) => {
|
|
141
205
|
if (i > 0)
|
|
142
206
|
extraFilter += ' AND ';
|
|
207
|
+
// look up the field in the entityInfo to see if it needs quotes
|
|
143
208
|
const field = e.Fields.find((f) => f.Name === k);
|
|
144
209
|
if (!field)
|
|
145
210
|
throw new Error(`Field ${k} not found in entity ${entity}`);
|
|
146
211
|
const quotes = field.NeedsQuotes ? "'" : '';
|
|
147
212
|
extraFilter += `${k} = ${quotes}${params[k]}${quotes}`;
|
|
148
213
|
});
|
|
214
|
+
// ok, now we have a SQL string, run it and return the results
|
|
215
|
+
// use the SQLServerDataProvider
|
|
149
216
|
const result = await rv.RunView({
|
|
150
217
|
EntityName: entity,
|
|
151
218
|
ExtraFilter: extraFilter,
|
|
@@ -159,6 +226,7 @@ export class ResolverBase {
|
|
|
159
226
|
}
|
|
160
227
|
async RunViewByNameGeneric(viewInput, provider, userPayload, pubSub) {
|
|
161
228
|
try {
|
|
229
|
+
// Log aggregate input for debugging
|
|
162
230
|
if (viewInput.Aggregates?.length) {
|
|
163
231
|
LogStatus(`[ResolverBase] RunViewByNameGeneric received aggregates: viewName=${viewInput.ViewName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
164
232
|
}
|
|
@@ -183,6 +251,7 @@ export class ResolverBase {
|
|
|
183
251
|
}
|
|
184
252
|
async RunViewByIDGeneric(viewInput, provider, userPayload, pubSub) {
|
|
185
253
|
try {
|
|
254
|
+
// Log aggregate input for debugging
|
|
186
255
|
if (viewInput.Aggregates?.length) {
|
|
187
256
|
LogStatus(`[ResolverBase] RunViewByIDGeneric received aggregates: viewID=${viewInput.ViewID}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
188
257
|
}
|
|
@@ -198,6 +267,7 @@ export class ResolverBase {
|
|
|
198
267
|
}
|
|
199
268
|
async RunDynamicViewGeneric(viewInput, provider, userPayload, pubSub) {
|
|
200
269
|
try {
|
|
270
|
+
// Log aggregate input for debugging
|
|
201
271
|
if (viewInput.Aggregates?.length) {
|
|
202
272
|
LogStatus(`[ResolverBase] RunDynamicViewGeneric received aggregates: entityName=${viewInput.EntityName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
203
273
|
}
|
|
@@ -210,7 +280,7 @@ export class ResolverBase {
|
|
|
210
280
|
Entity: viewInput.EntityName,
|
|
211
281
|
EntityID: entity.ID,
|
|
212
282
|
EntityBaseView: entity.BaseView,
|
|
213
|
-
};
|
|
283
|
+
}; // only providing a few bits of data here, but it's enough to get the view to run
|
|
214
284
|
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates);
|
|
215
285
|
}
|
|
216
286
|
catch (err) {
|
|
@@ -237,6 +307,7 @@ export class ResolverBase {
|
|
|
237
307
|
if (!entity) {
|
|
238
308
|
throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
239
309
|
}
|
|
310
|
+
// only providing a few bits of data here, but it's enough to get the view to run
|
|
240
311
|
viewInfo = {
|
|
241
312
|
ID: '',
|
|
242
313
|
Entity: viewInput.EntityName,
|
|
@@ -276,7 +347,7 @@ export class ResolverBase {
|
|
|
276
347
|
let results = await this.RunViewsGenericInternal(params);
|
|
277
348
|
return results;
|
|
278
349
|
}
|
|
279
|
-
static _priorEmittedData = [];
|
|
350
|
+
static { this._priorEmittedData = []; }
|
|
280
351
|
async EmitCloudEvent({ component, event, eventCode, args }) {
|
|
281
352
|
if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
|
|
282
353
|
const extendedType = args instanceof BaseEntityEvent ? `.${args.type}` : '';
|
|
@@ -286,20 +357,23 @@ export class ResolverBase {
|
|
|
286
357
|
const data = args?.baseEntity?.GetAll() ?? {};
|
|
287
358
|
const cloudEvent = new CloudEvent({ type, source, subject, data });
|
|
288
359
|
try {
|
|
360
|
+
// check to see if the combination of Entity and pkey was already emitted, if so, Log that condtion next
|
|
289
361
|
const pkey = args.baseEntity.PrimaryKeys;
|
|
290
362
|
const emittedData = { Entity: args.baseEntity.EntityInfo.Name, PKey: pkey };
|
|
291
363
|
if (ResolverBase._priorEmittedData.find((e) => {
|
|
292
364
|
if (e.Entity !== emittedData.Entity)
|
|
293
365
|
return false;
|
|
366
|
+
// if we get here compare the pkeys
|
|
294
367
|
const pkey2 = e.PKey;
|
|
295
368
|
if (pkey.KeyValuePairs.length !== pkey2.KeyValuePairs.length)
|
|
296
369
|
return false;
|
|
297
370
|
for (const kv of pkey.KeyValuePairs) {
|
|
371
|
+
// find the match by field name
|
|
298
372
|
const kv2 = pkey2.KeyValuePairs.find((k) => k.FieldName === kv.FieldName);
|
|
299
373
|
if (!kv2 || kv2.Value !== kv.Value)
|
|
300
374
|
return false;
|
|
301
375
|
}
|
|
302
|
-
return true;
|
|
376
|
+
return true; // if we get here, all the keys matched
|
|
303
377
|
})) {
|
|
304
378
|
console.log(`IMPORTANT: CloudEvent already emitted for ${JSON.stringify(emittedData)}`);
|
|
305
379
|
}
|
|
@@ -321,8 +395,9 @@ export class ResolverBase {
|
|
|
321
395
|
if (!userPayload) {
|
|
322
396
|
throw new Error(`userPayload is null`);
|
|
323
397
|
}
|
|
398
|
+
// first check permissions, the logged in user must have read permissions on the entity to run the view
|
|
324
399
|
if (entityInfo) {
|
|
325
|
-
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
|
|
400
|
+
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim()); // get the user record from MD so we have ROLES attached, don't use the one from payload directly
|
|
326
401
|
if (!userInfo) {
|
|
327
402
|
throw new Error(`User ${userPayload.email} not found in metadata`);
|
|
328
403
|
}
|
|
@@ -335,10 +410,63 @@ export class ResolverBase {
|
|
|
335
410
|
throw new Error(`Entity not found in metadata`);
|
|
336
411
|
}
|
|
337
412
|
}
|
|
413
|
+
/**
|
|
414
|
+
* Checks API key scope authorization. Only performs check if request
|
|
415
|
+
* was authenticated via API key (apiKeyHash present in userPayload).
|
|
416
|
+
* For OAuth/JWT auth, this is a no-op.
|
|
417
|
+
*
|
|
418
|
+
* @param scopePath - The scope path (e.g., 'entity:read', 'agent:execute')
|
|
419
|
+
* @param resource - The resource name (e.g., entity name, agent name)
|
|
420
|
+
* @param userPayload - The user payload from context
|
|
421
|
+
* @throws AuthorizationError if API key lacks required scope
|
|
422
|
+
*/
|
|
423
|
+
async CheckAPIKeyScopeAuthorization(scopePath, resource, userPayload) {
|
|
424
|
+
// Skip scope check for OAuth/JWT auth (no API key)
|
|
425
|
+
if (!userPayload.apiKeyHash) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Get system user for authorization call
|
|
429
|
+
// NOTE: We use system user here because Authorize() needs to run internal
|
|
430
|
+
// database queries (loading scope rules, logging decisions). The system user
|
|
431
|
+
// ensures these queries work regardless of what permissions the API key's
|
|
432
|
+
// user has. The API key's associated user (in userPayload.userRecord) is
|
|
433
|
+
// used later when the actual operation executes - their permissions are
|
|
434
|
+
// the ultimate ceiling that scopes can only narrow, never expand.
|
|
435
|
+
const systemUser = UserCache.Instance.Users.find(u => u.Type === 'System');
|
|
436
|
+
if (!systemUser) {
|
|
437
|
+
throw new Error('System user not found');
|
|
438
|
+
}
|
|
439
|
+
const apiKeyEngine = GetAPIKeyEngine();
|
|
440
|
+
// Check for full_access scope first (god power - bypasses all other checks)
|
|
441
|
+
const fullAccessResult = await apiKeyEngine.Authorize(userPayload.apiKeyHash, 'MJAPI', 'full_access', '*', systemUser, { endpoint: '/graphql', method: 'POST' });
|
|
442
|
+
if (fullAccessResult.Allowed) {
|
|
443
|
+
// full_access granted - skip specific scope check
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
// Check specific scope
|
|
447
|
+
const result = await apiKeyEngine.Authorize(userPayload.apiKeyHash, 'MJAPI', scopePath, resource, systemUser, {
|
|
448
|
+
endpoint: '/graphql',
|
|
449
|
+
method: 'POST'
|
|
450
|
+
});
|
|
451
|
+
if (!result.Allowed) {
|
|
452
|
+
// Provide specific, actionable error message
|
|
453
|
+
throw new AuthorizationError(`Access denied. This API key requires the '${scopePath}' scope ` +
|
|
454
|
+
`for resource '${resource}' to perform this operation. ` +
|
|
455
|
+
`Please update the API key's scopes or use an API key with appropriate permissions. ` +
|
|
456
|
+
`Denial reason: ${result.Reason}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Optimized RunViewGenericInternal implementation with:
|
|
461
|
+
* - Field filtering at source (Fix #7)
|
|
462
|
+
* - Improved error handling (Fix #9)
|
|
463
|
+
*/
|
|
338
464
|
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates) {
|
|
339
465
|
try {
|
|
340
466
|
if (!viewInfo || !userPayload)
|
|
341
467
|
return null;
|
|
468
|
+
// Check API key scope authorization for view operations
|
|
469
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
|
|
342
470
|
const md = provider;
|
|
343
471
|
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
344
472
|
if (!user)
|
|
@@ -347,18 +475,23 @@ export class ResolverBase {
|
|
|
347
475
|
if (!entityInfo)
|
|
348
476
|
throw new Error(`Entity ${viewInfo.Entity} not found in metadata`);
|
|
349
477
|
const rv = md;
|
|
478
|
+
// Determine result type
|
|
350
479
|
let rt = 'simple';
|
|
351
480
|
if (resultType?.trim().toLowerCase() === 'count_only') {
|
|
352
481
|
rt = 'count_only';
|
|
353
482
|
}
|
|
483
|
+
// Fix #7: Implement field filtering - preprocess fields for more efficient field selection
|
|
484
|
+
// This is passed to RunView() which uses it to optimize the SQL query
|
|
354
485
|
let optimizedFields = fields;
|
|
355
486
|
if (fields?.length) {
|
|
487
|
+
// Always ensure primary keys are included for proper record handling
|
|
356
488
|
const primaryKeys = entityInfo.PrimaryKeys.map(pk => pk.Name);
|
|
357
489
|
const missingPrimaryKeys = primaryKeys.filter(pk => !fields.find(f => f.toLowerCase() === pk.toLowerCase()));
|
|
358
490
|
if (missingPrimaryKeys.length) {
|
|
359
491
|
optimizedFields = [...fields, ...missingPrimaryKeys];
|
|
360
492
|
}
|
|
361
493
|
}
|
|
494
|
+
// Log aggregate request for debugging
|
|
362
495
|
if (aggregates?.length) {
|
|
363
496
|
LogStatus(`[ResolverBase] RunViewGenericInternal with aggregates: entityName=${viewInfo.Entity}, viewName=${viewInfo.Name}, aggregateCount=${aggregates.length}, aggregates=${JSON.stringify(aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
364
497
|
}
|
|
@@ -368,7 +501,7 @@ export class ResolverBase {
|
|
|
368
501
|
EntityName: viewInfo.Entity,
|
|
369
502
|
ExtraFilter: extraFilter,
|
|
370
503
|
OrderBy: orderBy,
|
|
371
|
-
Fields: optimizedFields,
|
|
504
|
+
Fields: optimizedFields, // Use optimized fields list
|
|
372
505
|
UserSearchString: userSearchString,
|
|
373
506
|
ExcludeUserViewRunID: excludeUserViewRunID,
|
|
374
507
|
OverrideExcludeFilter: overrideExcludeFilter,
|
|
@@ -382,19 +515,23 @@ export class ResolverBase {
|
|
|
382
515
|
ResultType: rt,
|
|
383
516
|
Aggregates: aggregates,
|
|
384
517
|
}, user);
|
|
518
|
+
// Log aggregate results for debugging
|
|
385
519
|
if (aggregates?.length) {
|
|
386
520
|
LogStatus(`[ResolverBase] RunView result aggregate info: entityName=${viewInfo.Entity}, hasAggregateResults=${!!result?.AggregateResults}, aggregateResultCount=${result?.AggregateResults?.length || 0}, aggregateExecutionTime=${result?.AggregateExecutionTime}, aggregateResults=${JSON.stringify(result?.AggregateResults)}`);
|
|
387
521
|
}
|
|
522
|
+
// Process results for GraphQL transport
|
|
388
523
|
const mapper = new FieldMapper();
|
|
389
524
|
if (result?.Success && result.Results?.length) {
|
|
390
525
|
for (const r of result.Results) {
|
|
391
526
|
mapper.MapFields(r);
|
|
392
527
|
}
|
|
528
|
+
// Filter encrypted fields before sending to API client
|
|
393
529
|
await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user);
|
|
394
530
|
}
|
|
395
531
|
return result;
|
|
396
532
|
}
|
|
397
533
|
catch (err) {
|
|
534
|
+
// Fix #9: Improved error handling with structured logging
|
|
398
535
|
const error = err;
|
|
399
536
|
LogError({
|
|
400
537
|
service: 'RunView',
|
|
@@ -402,18 +539,27 @@ export class ResolverBase {
|
|
|
402
539
|
error: error.message,
|
|
403
540
|
entityName: viewInfo?.Entity,
|
|
404
541
|
errorType: error.constructor.name,
|
|
542
|
+
// Only include stack trace for non-validation errors
|
|
405
543
|
stack: error.message?.includes('not found in metadata') ? undefined : error.stack
|
|
406
544
|
});
|
|
407
545
|
throw err;
|
|
408
546
|
}
|
|
409
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Optimized implementation that:
|
|
550
|
+
* 1. Fetches user info only once (fixes N+1 query)
|
|
551
|
+
* 2. Processes views in parallel for independent operations
|
|
552
|
+
* 3. Implements structured error logging
|
|
553
|
+
*/
|
|
410
554
|
async RunViewsGenericInternal(params) {
|
|
411
555
|
try {
|
|
556
|
+
// Skip processing if no params
|
|
412
557
|
if (!params.length)
|
|
413
558
|
return [];
|
|
414
559
|
let md = null;
|
|
415
560
|
const rv = params[0].provider;
|
|
416
561
|
let runViewParams = [];
|
|
562
|
+
// Fix #1: Get user info only once for all queries
|
|
417
563
|
let contextUser = null;
|
|
418
564
|
if (params[0]?.userPayload?.email) {
|
|
419
565
|
const userEmail = params[0].userPayload.email.toLowerCase().trim();
|
|
@@ -423,10 +569,13 @@ export class ResolverBase {
|
|
|
423
569
|
}
|
|
424
570
|
contextUser = user;
|
|
425
571
|
}
|
|
572
|
+
// Create a map of entities to validate only once per entity
|
|
426
573
|
const validatedEntities = new Set();
|
|
427
574
|
md = new Metadata();
|
|
575
|
+
// Transform parameters
|
|
428
576
|
for (const param of params) {
|
|
429
577
|
if (param.viewInfo) {
|
|
578
|
+
// Validate entity only once per entity type
|
|
430
579
|
const entityName = param.viewInfo.Entity;
|
|
431
580
|
if (!validatedEntities.has(entityName)) {
|
|
432
581
|
const entityInfo = md.Entities.find(e => e.Name === entityName);
|
|
@@ -436,10 +585,12 @@ export class ResolverBase {
|
|
|
436
585
|
validatedEntities.add(entityName);
|
|
437
586
|
}
|
|
438
587
|
}
|
|
588
|
+
// Determine result type
|
|
439
589
|
let rt = 'simple';
|
|
440
590
|
if (param.resultType?.trim().toLowerCase() === 'count_only') {
|
|
441
591
|
rt = 'count_only';
|
|
442
592
|
}
|
|
593
|
+
// Build parameters
|
|
443
594
|
runViewParams.push({
|
|
444
595
|
ViewID: param.viewInfo.ID,
|
|
445
596
|
ViewName: param.viewInfo.Name,
|
|
@@ -461,7 +612,9 @@ export class ResolverBase {
|
|
|
461
612
|
Aggregates: param.aggregates,
|
|
462
613
|
});
|
|
463
614
|
}
|
|
615
|
+
// Fix #4: Run views in a single batch through RunViews
|
|
464
616
|
const runViewResults = await rv.RunViews(runViewParams, contextUser);
|
|
617
|
+
// Process results
|
|
465
618
|
const mapper = new FieldMapper();
|
|
466
619
|
for (let i = 0; i < runViewResults.length; i++) {
|
|
467
620
|
const runViewResult = runViewResults[i];
|
|
@@ -469,6 +622,8 @@ export class ResolverBase {
|
|
|
469
622
|
for (const result of runViewResult.Results) {
|
|
470
623
|
mapper.MapFields(result);
|
|
471
624
|
}
|
|
625
|
+
// Filter encrypted fields before sending to API client
|
|
626
|
+
// Use the corresponding param's entity name
|
|
472
627
|
const entityName = params[i]?.viewInfo?.Entity;
|
|
473
628
|
if (entityName && contextUser) {
|
|
474
629
|
await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser);
|
|
@@ -478,6 +633,7 @@ export class ResolverBase {
|
|
|
478
633
|
return runViewResults;
|
|
479
634
|
}
|
|
480
635
|
catch (err) {
|
|
636
|
+
// Fix #9: Structured error logging with less verbosity
|
|
481
637
|
console.log(err);
|
|
482
638
|
throw err;
|
|
483
639
|
}
|
|
@@ -525,7 +681,7 @@ export class ResolverBase {
|
|
|
525
681
|
throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
526
682
|
if (!auditLogType)
|
|
527
683
|
throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
|
|
528
|
-
const auditLog = await md.GetEntityObject('Audit Logs', userInfo);
|
|
684
|
+
const auditLog = await md.GetEntityObject('Audit Logs', userInfo); // must pass user context on back end as we're not authenticated the same way as the front end
|
|
529
685
|
auditLog.NewRecord();
|
|
530
686
|
auditLog.UserID = userInfo.ID;
|
|
531
687
|
auditLog.AuditLogTypeID = auditLogType.ID;
|
|
@@ -567,7 +723,7 @@ export class ResolverBase {
|
|
|
567
723
|
if (!userPayload)
|
|
568
724
|
return undefined;
|
|
569
725
|
if (userPayload.userRecord)
|
|
570
|
-
return userPayload.userRecord;
|
|
726
|
+
return userPayload.userRecord; // if we have a user record, use that directly
|
|
571
727
|
if (!userPayload.email)
|
|
572
728
|
return undefined;
|
|
573
729
|
return UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim());
|
|
@@ -576,12 +732,18 @@ export class ResolverBase {
|
|
|
576
732
|
return Metadata.Provider.ConfigData.MJCoreSchemaName;
|
|
577
733
|
}
|
|
578
734
|
ListenForEntityMessages(entityObject, pubSub, userPayload) {
|
|
735
|
+
// The unique key is set up for each entity object via it's primary key to ensure that we only have one listener at most for each unique
|
|
736
|
+
// entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
|
|
737
|
+
// cause issues with multiple messages for the same event.
|
|
579
738
|
const uniqueKey = entityObject.EntityInfo.Name;
|
|
580
739
|
if (!this.EventSubscriptions.has(uniqueKey)) {
|
|
740
|
+
// listen for events from the entityObject in case it is a long running task and we can push messages back to the client via pubSub
|
|
581
741
|
LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
|
|
582
742
|
const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
|
|
583
743
|
if (event) {
|
|
584
744
|
const baseEntity = event.args?.baseEntity;
|
|
745
|
+
// Only process events for the entity type this subscription was created for
|
|
746
|
+
// This prevents duplicate CloudEvents when multiple entity types have active subscriptions
|
|
585
747
|
if (baseEntity?.EntityInfo?.Name !== uniqueKey) {
|
|
586
748
|
return;
|
|
587
749
|
}
|
|
@@ -592,6 +754,7 @@ export class ResolverBase {
|
|
|
592
754
|
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
593
755
|
if (event.args && event.args instanceof BaseEntityEvent) {
|
|
594
756
|
const baseEntityEvent = event.args;
|
|
757
|
+
// message from our entity object, relay it to the client
|
|
595
758
|
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
596
759
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
597
760
|
message: JSON.stringify({
|
|
@@ -610,16 +773,23 @@ export class ResolverBase {
|
|
|
610
773
|
}
|
|
611
774
|
}
|
|
612
775
|
async CreateRecord(entityName, input, provider, userPayload, pubSub) {
|
|
776
|
+
// Check API key scope authorization for entity create operations
|
|
777
|
+
await this.CheckAPIKeyScopeAuthorization('entity:create', entityName, userPayload);
|
|
613
778
|
if (await this.BeforeCreate(provider, input)) {
|
|
779
|
+
// fire event and proceed if it wasn't cancelled
|
|
614
780
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
615
781
|
entityObject.NewRecord();
|
|
616
782
|
entityObject.SetMany(input);
|
|
617
783
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
784
|
+
// Pass the transactionScopeId from the user payload to the save operation
|
|
618
785
|
if (await entityObject.Save()) {
|
|
619
|
-
|
|
786
|
+
// save worked, fire the AfterCreate event and then return all the data
|
|
787
|
+
await this.AfterCreate(provider, input); // fire event
|
|
620
788
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
789
|
+
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
621
790
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
622
791
|
}
|
|
792
|
+
// save failed, throw error with message
|
|
623
793
|
else {
|
|
624
794
|
throw new GraphQLError(entityObject.LatestResult?.CompleteMessage ?? 'Unknown error creating record', {
|
|
625
795
|
extensions: { code: 'CREATE_ENTITY_ERROR', entityName },
|
|
@@ -629,12 +799,16 @@ export class ResolverBase {
|
|
|
629
799
|
else
|
|
630
800
|
return null;
|
|
631
801
|
}
|
|
802
|
+
// Before/After CREATE Event Hooks for Sub-Classes to Override
|
|
632
803
|
async BeforeCreate(provider, input) {
|
|
633
804
|
return true;
|
|
634
805
|
}
|
|
635
806
|
async AfterCreate(provider, input) { }
|
|
636
807
|
async UpdateRecord(entityName, input, provider, userPayload, pubSub) {
|
|
808
|
+
// Check API key scope authorization for entity update operations
|
|
809
|
+
await this.CheckAPIKeyScopeAuthorization('entity:update', entityName, userPayload);
|
|
637
810
|
if (await this.BeforeUpdate(provider, input)) {
|
|
811
|
+
// fire event and proceed if it wasn't cancelled
|
|
638
812
|
const userInfo = this.GetUserFromPayload(userPayload);
|
|
639
813
|
const entityObject = await provider.GetEntityObject(entityName, userInfo);
|
|
640
814
|
const entityInfo = entityObject.EntityInfo;
|
|
@@ -643,8 +817,9 @@ export class ResolverBase {
|
|
|
643
817
|
if (key !== 'OldValues___') {
|
|
644
818
|
clientNewValues[key] = input[key];
|
|
645
819
|
}
|
|
646
|
-
});
|
|
820
|
+
}); // grab all the props except for the OldValues property
|
|
647
821
|
if (entityInfo.TrackRecordChanges || !input.OldValues___) {
|
|
822
|
+
// We get here because EITHER the entity tracks record changes OR the client did not provide OldValues, so we need to load the old values from the DB
|
|
648
823
|
const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
|
|
649
824
|
return {
|
|
650
825
|
FieldName: pk.Name,
|
|
@@ -652,28 +827,38 @@ export class ResolverBase {
|
|
|
652
827
|
};
|
|
653
828
|
}));
|
|
654
829
|
if (await entityObject.InnerLoad(cKey)) {
|
|
830
|
+
// load worked, now, only IF we have OldValues, we need to check them against the values in the DB we just loaded.
|
|
655
831
|
if (input.OldValues___) {
|
|
832
|
+
// we DO have OldValues, so we need to do a more in depth analysis
|
|
656
833
|
await this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, userInfo);
|
|
657
834
|
}
|
|
658
835
|
else {
|
|
836
|
+
// no OldValues, so we can just set the new values from input
|
|
659
837
|
entityObject.SetMany(input);
|
|
660
838
|
}
|
|
661
839
|
}
|
|
662
840
|
else {
|
|
841
|
+
// save failed, return null
|
|
663
842
|
throw new GraphQLError(`Record not found for ${entityName} with key ${JSON.stringify(cKey)}`, {
|
|
664
843
|
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
665
844
|
});
|
|
666
845
|
}
|
|
667
846
|
}
|
|
668
847
|
else {
|
|
848
|
+
// we get here if we are NOT tracking changes and we DO have OldValues, so we can load from them
|
|
669
849
|
const oldValues = {};
|
|
850
|
+
// for each item in the oldValues array, add it to the oldValues object
|
|
670
851
|
input.OldValues___?.forEach((item) => (oldValues[item.Key] = item.Value));
|
|
852
|
+
// 1) load the old values, this will be the initial state of the object
|
|
671
853
|
await entityObject.LoadFromData(oldValues);
|
|
854
|
+
// 2) set the new values from the input, not including the OldValues property
|
|
672
855
|
entityObject.SetMany(clientNewValues);
|
|
673
856
|
}
|
|
674
857
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
675
858
|
if (await entityObject.Save()) {
|
|
676
|
-
|
|
859
|
+
// save worked, fire afterevent and return all the data
|
|
860
|
+
await this.AfterUpdate(provider, input); // fire event
|
|
861
|
+
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
677
862
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
|
|
678
863
|
}
|
|
679
864
|
else {
|
|
@@ -687,13 +872,23 @@ export class ResolverBase {
|
|
|
687
872
|
extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
|
|
688
873
|
});
|
|
689
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
|
|
877
|
+
* is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values
|
|
878
|
+
* and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.
|
|
879
|
+
*
|
|
880
|
+
* ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
|
|
881
|
+
*/
|
|
690
882
|
async TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, contextUser) {
|
|
883
|
+
// we have OldValues, so we need to compare them to the values we just loaded from the DB
|
|
691
884
|
const clientOldValues = {};
|
|
885
|
+
// for each item in the oldValues array, add it to the clientOldValues object
|
|
692
886
|
input.OldValues___.forEach((item) => {
|
|
887
|
+
// we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
|
|
693
888
|
const field = entityObject.EntityInfo.Fields.find((f) => f.CodeName === item.Key);
|
|
694
889
|
let val = item.Value;
|
|
695
890
|
if ((val === null || val === undefined) && field.DefaultValue !== null && field.DefaultValue !== undefined && !field.AllowsNull)
|
|
696
|
-
val = field.DefaultValue;
|
|
891
|
+
val = field.DefaultValue; // set default value as the field was never set and it does NOT allow nulls
|
|
697
892
|
if (field) {
|
|
698
893
|
switch (field.TSType) {
|
|
699
894
|
case EntityFieldTSType.Number:
|
|
@@ -726,17 +921,21 @@ export class ResolverBase {
|
|
|
726
921
|
val = val === null || val === undefined || val === 'false' || val === '0' || parseInt(val) === 0 ? false : true;
|
|
727
922
|
break;
|
|
728
923
|
case EntityFieldTSType.Date:
|
|
924
|
+
// first, if val is a string and it is actually a number (milliseconds since epoch), convert it to a number.
|
|
729
925
|
if (val !== null && val !== undefined && val.toString().trim() !== '' && !isNaN(val))
|
|
730
926
|
val = parseInt(val);
|
|
731
927
|
val = val !== null && val !== undefined ? new Date(val) : null;
|
|
732
928
|
break;
|
|
733
929
|
default:
|
|
734
|
-
break;
|
|
930
|
+
break; // already a string
|
|
735
931
|
}
|
|
736
932
|
}
|
|
737
933
|
clientOldValues[item.Key] = val;
|
|
738
934
|
});
|
|
935
|
+
// clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
|
|
936
|
+
// with the DB values
|
|
739
937
|
const dbValues = entityObject.GetAll();
|
|
938
|
+
// now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
|
|
740
939
|
const dbDifferences = [];
|
|
741
940
|
Object.keys(clientOldValues).forEach((key) => {
|
|
742
941
|
const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
|
|
@@ -756,12 +955,13 @@ export class ResolverBase {
|
|
|
756
955
|
different = true;
|
|
757
956
|
}
|
|
758
957
|
else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
958
|
+
// Check if this is a datetime/datetime2 field with only a timezone hour shift
|
|
759
959
|
const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
|
|
760
960
|
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
|
|
761
961
|
console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
|
|
762
962
|
`Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
|
|
763
963
|
`Consider using datetimeoffset to avoid timezone ambiguity.`);
|
|
764
|
-
different = false;
|
|
964
|
+
different = false; // Allow timezone shifts through for non-datetimeoffset fields
|
|
765
965
|
}
|
|
766
966
|
else {
|
|
767
967
|
different = true;
|
|
@@ -777,6 +977,7 @@ export class ResolverBase {
|
|
|
777
977
|
break;
|
|
778
978
|
}
|
|
779
979
|
if (different && f && !f.ReadOnly) {
|
|
980
|
+
// only include updateable fields
|
|
780
981
|
dbDifferences.push({
|
|
781
982
|
FieldName: key,
|
|
782
983
|
ClientOldValue: clientOldValues[key],
|
|
@@ -785,10 +986,13 @@ export class ResolverBase {
|
|
|
785
986
|
}
|
|
786
987
|
});
|
|
787
988
|
if (dbDifferences.length > 0) {
|
|
989
|
+
// now we have an array of any dbDifferences with length > 0, between the clientOldValues and the dbValues, we need to check to see if any of the differences are on fields that the client is trying to update
|
|
990
|
+
// first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
|
|
788
991
|
const clientDifferences = [];
|
|
789
992
|
Object.keys(clientOldValues).forEach((key) => {
|
|
790
993
|
const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
|
|
791
994
|
if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
|
|
995
|
+
// only include updateable fields
|
|
792
996
|
clientDifferences.push({
|
|
793
997
|
FieldName: key,
|
|
794
998
|
ClientOldValue: clientOldValues[key],
|
|
@@ -796,6 +1000,8 @@ export class ResolverBase {
|
|
|
796
1000
|
});
|
|
797
1001
|
}
|
|
798
1002
|
});
|
|
1003
|
+
// now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
|
|
1004
|
+
// if there is ANY overlap in the FIELDS that appear in both arrays, we need to log a warning but allow the save to continue
|
|
799
1005
|
const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
|
|
800
1006
|
if (overlap.length > 0) {
|
|
801
1007
|
const msg = {
|
|
@@ -804,6 +1010,7 @@ export class ResolverBase {
|
|
|
804
1010
|
DBDifferences: dbDifferences,
|
|
805
1011
|
Overlap: overlap,
|
|
806
1012
|
};
|
|
1013
|
+
// Log as warning to console and ErrorLog table instead of throwing error
|
|
807
1014
|
console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
|
|
808
1015
|
LogError({
|
|
809
1016
|
service: 'ResolverBase',
|
|
@@ -816,6 +1023,7 @@ export class ResolverBase {
|
|
|
816
1023
|
overlap: overlap
|
|
817
1024
|
}
|
|
818
1025
|
});
|
|
1026
|
+
// Create ErrorLog record in the database
|
|
819
1027
|
try {
|
|
820
1028
|
const md = new Metadata();
|
|
821
1029
|
const errorLogEntity = await md.GetEntityObject('Error Logs', contextUser);
|
|
@@ -834,16 +1042,21 @@ export class ResolverBase {
|
|
|
834
1042
|
}
|
|
835
1043
|
}
|
|
836
1044
|
}
|
|
1045
|
+
// If we get here that means we've not thrown an exception, so there is
|
|
1046
|
+
// NO OVERLAP, so we can set the new values from the data provided from the client now...
|
|
837
1047
|
entityObject.SetMany(clientNewValues);
|
|
838
1048
|
}
|
|
839
1049
|
async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
|
|
1050
|
+
// Check API key scope authorization for entity delete operations
|
|
1051
|
+
await this.CheckAPIKeyScopeAuthorization('entity:delete', entityName, userPayload);
|
|
840
1052
|
if (await this.BeforeDelete(provider, key)) {
|
|
1053
|
+
// fire event and proceed if it wasn't cancelled
|
|
841
1054
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
842
1055
|
await entityObject.InnerLoad(key);
|
|
843
|
-
const returnValue = entityObject.GetAll();
|
|
1056
|
+
const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
|
|
844
1057
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
845
1058
|
if (await entityObject.Delete(options)) {
|
|
846
|
-
await this.AfterDelete(provider, key);
|
|
1059
|
+
await this.AfterDelete(provider, key); // fire event
|
|
847
1060
|
return returnValue;
|
|
848
1061
|
}
|
|
849
1062
|
else {
|
|
@@ -858,10 +1071,12 @@ export class ResolverBase {
|
|
|
858
1071
|
});
|
|
859
1072
|
}
|
|
860
1073
|
}
|
|
1074
|
+
// Before/After DELETE Event Hooks for Sub-Classes to Override
|
|
861
1075
|
async BeforeDelete(provider, key) {
|
|
862
1076
|
return true;
|
|
863
1077
|
}
|
|
864
1078
|
async AfterDelete(provider, key) { }
|
|
1079
|
+
// Before/After UPDATE Event Hooks for Sub-Classes to Override
|
|
865
1080
|
async BeforeUpdate(provider, input) {
|
|
866
1081
|
return true;
|
|
867
1082
|
}
|