@memberjunction/server 6.2.0-edge.5 → 6.2.0-edge.7
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 +96 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +8 -3
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -1
- package/dist/agentSessions/BridgeHostInstance.js +14 -4
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -1
- package/dist/agentSessions/HandoffOfferExpiry.d.ts +10 -0
- package/dist/agentSessions/HandoffOfferExpiry.d.ts.map +1 -0
- package/dist/agentSessions/HandoffOfferExpiry.js +18 -0
- package/dist/agentSessions/HandoffOfferExpiry.js.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +34 -11
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +88 -44
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +29 -9
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +62 -16
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/conversationBrowsers.d.ts +61 -0
- package/dist/agentSessions/conversationBrowsers.d.ts.map +1 -0
- package/dist/agentSessions/conversationBrowsers.js +118 -0
- package/dist/agentSessions/conversationBrowsers.js.map +1 -0
- package/dist/agentSessions/sessionDeadline.d.ts +43 -0
- package/dist/agentSessions/sessionDeadline.d.ts.map +1 -0
- package/dist/agentSessions/sessionDeadline.js +68 -0
- package/dist/agentSessions/sessionDeadline.js.map +1 -0
- package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.js +5 -4
- package/dist/auth/APIKeyScopeAuth.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +2 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +51 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +10 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +37 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/localDevAuth.d.ts +79 -0
- package/dist/auth/localDevAuth.d.ts.map +1 -0
- package/dist/auth/localDevAuth.js +439 -0
- package/dist/auth/localDevAuth.js.map +1 -0
- package/dist/auth/loginEmailResolution.d.ts +41 -0
- package/dist/auth/loginEmailResolution.d.ts.map +1 -0
- package/dist/auth/loginEmailResolution.js +47 -0
- package/dist/auth/loginEmailResolution.js.map +1 -0
- package/dist/auth/magicLink/MagicLinkKeys.d.ts +51 -3
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +130 -13
- package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +3 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +23 -5
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +39 -5
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +221 -9
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/redeemLanding.d.ts +2 -2
- package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -1
- package/dist/auth/magicLink/redeemLanding.js +7 -4
- package/dist/auth/magicLink/redeemLanding.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +39 -3
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/auth/magicLink/types.js +0 -4
- package/dist/auth/magicLink/types.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +1 -0
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/sessionUserClone.d.ts +1 -1
- package/dist/auth/sessionUserClone.d.ts.map +1 -1
- package/dist/auth/sessionUserClone.js +3 -2
- package/dist/auth/sessionUserClone.js.map +1 -1
- package/dist/backgroundSweepConfigUnits.d.ts +231 -0
- package/dist/backgroundSweepConfigUnits.d.ts.map +1 -0
- package/dist/backgroundSweepConfigUnits.js +180 -0
- package/dist/backgroundSweepConfigUnits.js.map +1 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
- package/dist/broadcast/index.d.ts +6 -0
- package/dist/broadcast/index.d.ts.map +1 -0
- package/dist/broadcast/index.js +6 -0
- package/dist/broadcast/index.js.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
- package/dist/cache.d.ts +3 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +1402 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +120 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +5 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -12
- package/dist/context.js.map +1 -1
- package/dist/coreRoutes.d.ts +3 -1
- package/dist/coreRoutes.d.ts.map +1 -1
- package/dist/coreRoutes.js +20 -4
- package/dist/coreRoutes.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +863 -14
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +4167 -62
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +154 -79
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +11 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +29 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PubSubManager.d.ts +10 -0
- package/dist/generic/PubSubManager.d.ts.map +1 -1
- package/dist/generic/PubSubManager.js +44 -1
- package/dist/generic/PubSubManager.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +39 -6
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +93 -11
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +114 -29
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +509 -220
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +15 -3
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +126 -20
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
- package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
- package/dist/generic/actionOutputFieldSecurity.js +47 -0
- package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
- package/dist/generic/fieldSecurityTransport.d.ts +30 -0
- package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
- package/dist/generic/fieldSecurityTransport.js +30 -0
- package/dist/generic/fieldSecurityTransport.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +262 -84
- package/dist/index.js.map +1 -1
- package/dist/integration/ConnectionPause.d.ts +121 -0
- package/dist/integration/ConnectionPause.d.ts.map +1 -0
- package/dist/integration/ConnectionPause.js +183 -0
- package/dist/integration/ConnectionPause.js.map +1 -0
- package/dist/integration/CustomColumnPromoter.d.ts +72 -6
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +93 -47
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/EntityMapLifecycle.d.ts +78 -4
- package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
- package/dist/integration/EntityMapLifecycle.js +199 -5
- package/dist/integration/EntityMapLifecycle.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +31 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -1
- package/dist/integration/InactiveRowWarnings.js +29 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -1
- package/dist/integration/ObjectNamesByID.d.ts +28 -0
- package/dist/integration/ObjectNamesByID.d.ts.map +1 -0
- package/dist/integration/ObjectNamesByID.js +87 -0
- package/dist/integration/ObjectNamesByID.js.map +1 -0
- package/dist/integration/PostRestartIntrospect.d.ts +26 -0
- package/dist/integration/PostRestartIntrospect.d.ts.map +1 -0
- package/dist/integration/PostRestartIntrospect.js +27 -0
- package/dist/integration/PostRestartIntrospect.js.map +1 -0
- package/dist/integration/RSUPostRestartProgress.d.ts +176 -0
- package/dist/integration/RSUPostRestartProgress.d.ts.map +1 -0
- package/dist/integration/RSUPostRestartProgress.js +323 -0
- package/dist/integration/RSUPostRestartProgress.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +41 -2
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -1
- package/dist/integration/RSUProgressBridge.js +84 -1
- package/dist/integration/RSUProgressBridge.js.map +1 -1
- package/dist/integration/RunArtifactAuthorization.d.ts +39 -0
- package/dist/integration/RunArtifactAuthorization.d.ts.map +1 -0
- package/dist/integration/RunArtifactAuthorization.js +45 -0
- package/dist/integration/RunArtifactAuthorization.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +9 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -1
- package/dist/integration/SchemaRefreshLaunch.js +3 -2
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -1
- package/dist/integration/ScopedFieldsForBuild.d.ts +46 -0
- package/dist/integration/ScopedFieldsForBuild.d.ts.map +1 -0
- package/dist/integration/ScopedFieldsForBuild.js +91 -0
- package/dist/integration/ScopedFieldsForBuild.js.map +1 -0
- package/dist/integration/TargetConfigGuard.d.ts +40 -0
- package/dist/integration/TargetConfigGuard.d.ts.map +1 -0
- package/dist/integration/TargetConfigGuard.js +40 -0
- package/dist/integration/TargetConfigGuard.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +50 -1
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.js +14 -6
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
- package/dist/realtimeSessions/sessionAccess.d.ts +103 -11
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +147 -22
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +19 -3
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +203 -19
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/host-identity.d.ts +1 -1
- package/dist/realtimeWidget/host-identity.js +1 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.js +1 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
- package/dist/realtimeWidget/widgetCore.d.ts +20 -1
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetCore.js +21 -8
- package/dist/realtimeWidget/widgetCore.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +3 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/requestOrigin.d.ts +22 -0
- package/dist/requestOrigin.d.ts.map +1 -0
- package/dist/requestOrigin.js +60 -0
- package/dist/requestOrigin.js.map +1 -0
- package/dist/resolvers/ActionResolver.d.ts +19 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +63 -12
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +1 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/AgentSessionResolver.js +44 -17
- package/dist/resolvers/AgentSessionResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +2 -1
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +6 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +30 -7
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -0
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +5 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +13 -4
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +34 -13
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +401 -11
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +2713 -1290
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -2
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +31 -3
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
- package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.js +5 -3
- package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +20 -6
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +38 -0
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +184 -88
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts.map +1 -1
- package/dist/resolvers/RSUResolver.js +12 -0
- package/dist/resolvers/RSUResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +22 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +118 -33
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +62 -10
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +224 -39
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +80 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +388 -125
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +5 -3
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +19 -18
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +7 -5
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +3 -1
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +4 -0
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +1 -0
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +60 -15
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +2 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +2 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +6 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +19 -5
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +6 -2
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +11 -5
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +8 -5
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.js +6 -8
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +6 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +24 -6
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +3 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +11 -2
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverRoleConfigUnits.d.ts +29 -0
- package/dist/serverRoleConfigUnits.d.ts.map +1 -0
- package/dist/serverRoleConfigUnits.js +33 -0
- package/dist/serverRoleConfigUnits.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -1
- package/dist/services/DurableEntityActionTaskSubmitter.js +6 -1
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +2 -26
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +6 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +38 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +43 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +2 -1
- package/dist/services/WorkQueueHostService.d.ts.map +1 -1
- package/dist/services/WorkQueueHostService.js +19 -4
- package/dist/services/WorkQueueHostService.js.map +1 -1
- package/dist/services/workQueueConfig.d.ts +2 -2
- package/dist/services/workQueueConfig.js +5 -5
- package/dist/services/workQueueConfig.js.map +1 -1
- package/dist/sharedCache.js +1 -1
- package/dist/sharedCache.js.map +1 -1
- package/dist/types.d.ts +40 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +98 -98
- package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
- package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
- package/src/__tests__/ActiveOperations.test.ts +68 -0
- package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
- package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
- package/src/__tests__/AgentSessionResolver.ownership.test.ts +150 -0
- package/src/__tests__/AssertTargetConfigs.test.ts +75 -0
- package/src/__tests__/BatchApplyParity.test.ts +109 -0
- package/src/__tests__/BridgeHostInstance.test.ts +42 -0
- package/src/__tests__/CollectInactiveRowWarnings.test.ts +115 -0
- package/src/__tests__/ConnectionPause.test.ts +192 -0
- package/src/__tests__/ConnectionPauseResolver.test.ts +166 -0
- package/src/__tests__/ConnectionPauseSave.test.ts +143 -0
- package/src/__tests__/CustomColumnPromoterCompletion.test.ts +142 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
- package/src/__tests__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
- package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
- package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
- package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
- package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
- package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
- package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
- package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
- package/src/__tests__/HandoffOfferExpiry.test.ts +40 -0
- package/src/__tests__/IntegrationRunArtifactAuthorization.test.ts +126 -0
- package/src/__tests__/MagicLinkKeys.test.ts +102 -0
- package/src/__tests__/MediaAccessKeys.test.ts +57 -10
- package/src/__tests__/NewArrivalsDefaultDisabled.test.ts +50 -0
- package/src/__tests__/ObjectNamesByID.test.ts +176 -0
- package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
- package/src/__tests__/PostRestartMapsInCatalogScope.test.ts +146 -0
- package/src/__tests__/QuerySystemUserResolver.childRows.test.ts +80 -0
- package/src/__tests__/QuerySystemUserResolver.collision.test.ts +200 -0
- package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
- package/src/__tests__/QuerySystemUserResolver.permissions.test.ts +132 -0
- package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
- package/src/__tests__/RSUPostRestartProgress.test.ts +343 -0
- package/src/__tests__/RSUProgressBridge.test.ts +188 -5
- package/src/__tests__/RSUStartupSweepWiring.test.ts +30 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +10 -3
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +346 -46
- package/src/__tests__/{RefreshAdoptsNewColumns.test.ts → RefreshReportsNewColumns.test.ts} +25 -16
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +287 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +3 -0
- package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +100 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +344 -14
- package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
- package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
- package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +227 -0
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +83 -1
- package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
- package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +72 -13
- package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
- package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
- package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
- package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +19 -27
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +10 -2
- package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +91 -13
- package/src/__tests__/RunTemplateResolver.test.ts +35 -2
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
- package/src/__tests__/SchemaEvolutionCustomColumns.test.ts +119 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +24 -2
- package/src/__tests__/ScopedFieldsForBuild.test.ts +192 -0
- package/src/__tests__/SessionJanitor.test.ts +195 -4
- package/src/__tests__/SessionManager.test.ts +164 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
- package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
- package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
- package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +9 -7
- package/src/__tests__/WidgetSessionService.test.ts +583 -4
- package/src/__tests__/WorkQueueHostService.test.ts +25 -7
- package/src/__tests__/apiRootBoot.test.ts +1 -1
- package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
- package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
- package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
- package/src/__tests__/contextRequestOrigin.test.ts +105 -0
- package/src/__tests__/conversationBrowsers.test.ts +62 -0
- package/src/__tests__/fieldSecurityFixtures.ts +91 -0
- package/src/__tests__/globalCors.test.ts +3 -3
- package/src/__tests__/localDevAuth.test.ts +196 -0
- package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
- package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
- package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
- package/src/__tests__/realtimeRecordingHandler.test.ts +24 -2
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +224 -7
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
- package/src/__tests__/requestOrigin.test.ts +104 -0
- package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
- package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
- package/src/__tests__/resolverBase.fls.test.ts +146 -97
- package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
- package/src/__tests__/runParamsTimeZone.test.ts +179 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
- package/src/__tests__/runViewProperties.test.ts +114 -0
- package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
- package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
- package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
- package/src/__tests__/sessionDeadline.test.ts +51 -0
- package/src/__tests__/sessionUserClone.test.ts +5 -1
- package/src/__tests__/sharedCache.test.ts +11 -0
- package/src/__tests__/startupSweepOrder.test.ts +40 -0
- package/src/__tests__/unifiedAuth.test.ts +101 -1
- package/src/__tests__/widgetGuestElevation.test.ts +2 -0
- package/src/agentSessions/BridgeHostInstance.ts +20 -5
- package/src/agentSessions/HandoffOfferExpiry.ts +18 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +1 -0
- package/src/agentSessions/SessionJanitor.ts +95 -45
- package/src/agentSessions/SessionManager.ts +73 -20
- package/src/agentSessions/conversationBrowsers.ts +143 -0
- package/src/agentSessions/sessionDeadline.ts +81 -0
- package/src/auth/APIKeyScopeAuth.ts +5 -4
- package/src/auth/AuthProviderCatalogRouter.ts +3 -0
- package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
- package/src/auth/index.ts +56 -12
- package/src/auth/initializeProviders.ts +43 -1
- package/src/auth/localDevAuth.ts +528 -0
- package/src/auth/loginEmailResolution.ts +67 -0
- package/src/auth/magicLink/MagicLinkKeys.ts +147 -15
- package/src/auth/magicLink/MagicLinkRouter.ts +33 -6
- package/src/auth/magicLink/MagicLinkService.ts +250 -10
- package/src/auth/magicLink/index.ts +1 -1
- package/src/auth/magicLink/redeemLanding.ts +7 -4
- package/src/auth/magicLink/types.ts +38 -3
- package/src/auth/newUsers.ts +1 -0
- package/src/auth/sessionUserClone.ts +3 -2
- package/src/backgroundSweepConfigUnits.ts +213 -0
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
- package/src/broadcast/index.ts +5 -0
- package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
- package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
- package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
- package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
- package/src/cache.ts +2 -1
- package/src/config.ts +138 -0
- package/src/context.ts +71 -14
- package/src/coreRoutes.ts +29 -7
- package/src/generated/graphql-schemas/__mj.ts +4145 -1193
- package/src/generic/CacheInvalidationResolver.ts +135 -78
- package/src/generic/OrphanedConversationDetailReconciler.ts +36 -3
- package/src/generic/PubSubManager.ts +54 -1
- package/src/generic/PushStatusResolver.ts +108 -11
- package/src/generic/ResolverBase.ts +635 -234
- package/src/generic/RunViewResolver.ts +176 -64
- package/src/generic/actionOutputFieldSecurity.ts +75 -0
- package/src/generic/fieldSecurityTransport.ts +62 -0
- package/src/index.ts +310 -85
- package/src/integration/ConnectionPause.ts +222 -0
- package/src/integration/CustomColumnPromoter.ts +129 -54
- package/src/integration/EntityMapLifecycle.ts +229 -6
- package/src/integration/InactiveRowWarnings.ts +50 -0
- package/src/integration/ObjectNamesByID.ts +97 -0
- package/src/integration/PostRestartIntrospect.ts +49 -0
- package/src/integration/RSUPostRestartProgress.ts +359 -0
- package/src/integration/RSUProgressBridge.ts +85 -2
- package/src/integration/RunArtifactAuthorization.ts +65 -0
- package/src/integration/SchemaRefreshLaunch.ts +12 -2
- package/src/integration/ScopedFieldsForBuild.ts +113 -0
- package/src/integration/TargetConfigGuard.ts +71 -0
- package/src/logging/variablesLoggingMiddleware.ts +59 -3
- package/src/realtimeSessions/README.md +7 -3
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +16 -6
- package/src/realtimeSessions/sessionAccess.ts +211 -25
- package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
- package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
- package/src/realtimeSessions/verificationWorkflow.ts +3 -1
- package/src/realtimeWidget/WidgetRouter.ts +22 -4
- package/src/realtimeWidget/WidgetSessionService.ts +254 -17
- package/src/realtimeWidget/host-identity.ts +1 -1
- package/src/realtimeWidget/visitorIdentity.ts +1 -0
- package/src/realtimeWidget/widgetCore.ts +34 -9
- package/src/realtimeWidget/widgetGuestElevation.ts +3 -1
- package/src/requestOrigin.ts +64 -0
- package/src/resolvers/ActionResolver.ts +78 -15
- package/src/resolvers/AdhocQueryResolver.ts +1 -0
- package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
- package/src/resolvers/AgentSessionResolver.ts +50 -18
- package/src/resolvers/AutotagPipelineResolver.ts +2 -1
- package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
- package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
- package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
- package/src/resolvers/FileCategoryResolver.ts +1 -0
- package/src/resolvers/FileResolver.ts +14 -4
- package/src/resolvers/GetDataResolver.ts +35 -13
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1570 -209
- package/src/resolvers/MCPResolver.ts +15 -2
- package/src/resolvers/MergeRecordsResolver.ts +23 -2
- package/src/resolvers/PipelineProgressResolver.ts +12 -3
- package/src/resolvers/QueryResolver.ts +20 -6
- package/src/resolvers/QuerySystemUserResolver.ts +207 -99
- package/src/resolvers/RSUResolver.ts +12 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +144 -40
- package/src/resolvers/RemoteBrowserActionResolver.ts +252 -37
- package/src/resolvers/RunAIAgentResolver.ts +518 -163
- package/src/resolvers/RunAIPromptResolver.ts +18 -18
- package/src/resolvers/RunTemplateResolver.ts +9 -5
- package/src/resolvers/RunTestResolver.ts +3 -1
- package/src/resolvers/SyncDataResolver.ts +4 -0
- package/src/resolvers/TestQuerySQLResolver.ts +1 -0
- package/src/resolvers/TransactionGroupResolver.ts +66 -15
- package/src/resolvers/UserViewResolver.ts +2 -1
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
- package/src/resolvers/meetingRecordingRegistration.ts +2 -0
- package/src/rest/EntityCRUDHandler.ts +22 -5
- package/src/rest/MediaAccessKeys.ts +6 -2
- package/src/rest/OAuthCallbackHandler.ts +13 -5
- package/src/rest/RESTEndpointHandler.ts +8 -5
- package/src/rest/RealtimeRecordingHandler.ts +9 -10
- package/src/rest/ViewOperationsHandler.ts +26 -7
- package/src/rest/setupRESTEndpoints.ts +11 -2
- package/src/serverRoleConfigUnits.ts +48 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +10 -2
- package/src/services/TaskGraphActionRunner.ts +2 -23
- package/src/services/TaskGraphAgentRunner.ts +43 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +53 -20
- package/src/services/WorkQueueHostService.ts +26 -4
- package/src/services/workQueueConfig.ts +5 -5
- package/src/sharedCache.ts +1 -1
- package/src/types.ts +41 -1
- package/src/services/TaskOrchestration-Integration.md +0 -188
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
|
|
1
|
+
import { AssertClientInputWritableForUser, BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogErrorEx, LogStatus, Metadata, ProjectEntityValuesForUser, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
|
|
2
|
+
import { UserViewEngine } from '@memberjunction/core-entities';
|
|
2
3
|
import { ClientClauseScreen, UserCache } from '@memberjunction/generic-database-provider';
|
|
3
4
|
import { AuthorizationError } from 'type-graphql';
|
|
4
5
|
import { GraphQLError } from 'graphql';
|
|
@@ -6,10 +7,10 @@ import { RefusalExtensions } from './refusalExtensions.js';
|
|
|
6
7
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
7
8
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
8
9
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
9
|
-
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
10
|
-
import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
|
|
10
|
+
import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
|
|
11
|
+
import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator } from '@memberjunction/sql-parser';
|
|
11
12
|
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
12
|
-
import { PublishStatusUpdate } from './PushStatusResolver.js';
|
|
13
|
+
import { PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
|
|
13
14
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
|
|
14
15
|
import { PubSubManager } from './PubSubManager.js';
|
|
15
16
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
@@ -17,16 +18,26 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
|
17
18
|
* Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
|
|
18
19
|
* do not track record changes: their server-side write rules compare against the prior state, which a client could
|
|
19
20
|
* otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
|
|
21
|
+
*
|
|
22
|
+
* `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
|
|
23
|
+
* (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
|
|
24
|
+
* that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
|
|
20
25
|
*/
|
|
21
26
|
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
|
|
22
27
|
'mj: record changes',
|
|
23
28
|
'mj: conversation details',
|
|
24
29
|
'mj: data context items',
|
|
30
|
+
'mj: conversations',
|
|
31
|
+
'mj: ai agent sessions',
|
|
32
|
+
'mj: ai agent runs',
|
|
25
33
|
]);
|
|
34
|
+
const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
|
|
26
36
|
export class ResolverBase {
|
|
27
37
|
static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
|
|
28
38
|
static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
|
|
29
39
|
static { this._eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions'; }
|
|
40
|
+
static { this._activeEntitySessionsKey = '___MJServer___ResolverBase___ActiveEntitySessions'; }
|
|
30
41
|
get eventSubscriptions() {
|
|
31
42
|
// 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
|
|
32
43
|
// could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
|
|
@@ -37,6 +48,13 @@ export class ResolverBase {
|
|
|
37
48
|
}
|
|
38
49
|
return g[ResolverBase._eventSubscriptionKey];
|
|
39
50
|
}
|
|
51
|
+
get activeEntitySessions() {
|
|
52
|
+
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
53
|
+
if (!g[ResolverBase._activeEntitySessionsKey]) {
|
|
54
|
+
g[ResolverBase._activeEntitySessionsKey] = new Map();
|
|
55
|
+
}
|
|
56
|
+
return g[ResolverBase._activeEntitySessionsKey];
|
|
57
|
+
}
|
|
40
58
|
/**
|
|
41
59
|
* Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
|
|
42
60
|
*
|
|
@@ -69,7 +87,7 @@ export class ResolverBase {
|
|
|
69
87
|
const entityInfo = md.EntityByName(entityName);
|
|
70
88
|
if (!entityInfo)
|
|
71
89
|
throw new Error(`Entity ${entityName} not found in metadata`);
|
|
72
|
-
const denied = deniedReadFields ?? (entityInfo.
|
|
90
|
+
const denied = deniedReadFields ?? (entityInfo.FieldLevelSecurityApplies && contextUser
|
|
73
91
|
? entityInfo.GetDeniedReadFields(contextUser)
|
|
74
92
|
: null);
|
|
75
93
|
const mapper = new FieldMapper();
|
|
@@ -284,6 +302,7 @@ export class ResolverBase {
|
|
|
284
302
|
if (!loaded) {
|
|
285
303
|
return null;
|
|
286
304
|
}
|
|
305
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
287
306
|
const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
288
307
|
return mapped;
|
|
289
308
|
}
|
|
@@ -325,28 +344,25 @@ export class ResolverBase {
|
|
|
325
344
|
if (field.AllowDecryptInAPI)
|
|
326
345
|
continue;
|
|
327
346
|
// AllowDecryptInAPI is false - we need to filter the value
|
|
347
|
+
if (!field.SendEncryptedValue) {
|
|
348
|
+
// SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
|
|
349
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
328
352
|
const engine = EncryptionEngine.Instance;
|
|
329
353
|
await engine.Config(false, contextUser);
|
|
330
354
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
342
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
343
|
-
}
|
|
355
|
+
// Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
|
|
356
|
+
if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
|
|
357
|
+
try {
|
|
358
|
+
const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
|
|
359
|
+
dataObject[fieldName] = encryptedValue;
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
// If re-encryption fails, use sentinel for safety
|
|
363
|
+
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
364
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
344
365
|
}
|
|
345
|
-
// If already encrypted (shouldn't happen normally), keep as-is
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
// SendEncryptedValue is false - replace with sentinel
|
|
349
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
350
366
|
}
|
|
351
367
|
}
|
|
352
368
|
return dataObject;
|
|
@@ -449,23 +465,54 @@ export class ResolverBase {
|
|
|
449
465
|
return [];
|
|
450
466
|
}
|
|
451
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Resolves a User View by name, consulting the in-memory {@link UserViewEngine} cache first
|
|
470
|
+
* and falling back to a database `RunView` only on a cache miss.
|
|
471
|
+
*/
|
|
472
|
+
async resolveUserViewByName(viewName, provider, contextUser) {
|
|
473
|
+
if (UserViewEngine.Instance.Loaded) {
|
|
474
|
+
const cached = UserViewEngine.Instance.GetViewByName(viewName);
|
|
475
|
+
if (cached) {
|
|
476
|
+
return cached;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const rv = provider;
|
|
480
|
+
const result = await rv.RunView({
|
|
481
|
+
EntityName: 'MJ: User Views',
|
|
482
|
+
// SECURITY: the view name is client-supplied and lands inside a SQL literal.
|
|
483
|
+
ExtraFilter: "Name='" + EscapeSQLString(viewName) + "'",
|
|
484
|
+
}, contextUser);
|
|
485
|
+
if (result && result.Success && result.Results.length > 0) {
|
|
486
|
+
return result.Results[0];
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Resolves a User View by ID, consulting the in-memory {@link UserViewEngine} cache first
|
|
492
|
+
* and falling back to an entity `Load()` only on a cache miss.
|
|
493
|
+
*/
|
|
494
|
+
async resolveUserViewByID(viewID, provider, contextUser) {
|
|
495
|
+
if (UserViewEngine.Instance.Loaded) {
|
|
496
|
+
const cached = UserViewEngine.Instance.GetViewById(viewID);
|
|
497
|
+
if (cached) {
|
|
498
|
+
return cached;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
|
|
502
|
+
await viewInfo.Load(viewID);
|
|
503
|
+
return viewInfo;
|
|
504
|
+
}
|
|
452
505
|
async RunViewByNameGeneric(viewInput, provider, userPayload, pubSub) {
|
|
453
506
|
try {
|
|
454
507
|
// Log aggregate input for debugging
|
|
455
508
|
if (viewInput.Aggregates?.length) {
|
|
456
509
|
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 })))}`);
|
|
457
510
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
EntityName: 'MJ: User Views',
|
|
461
|
-
// SECURITY: the view name is client-supplied and lands inside a SQL literal.
|
|
462
|
-
ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
|
|
463
|
-
}, userPayload.userRecord);
|
|
464
|
-
if (result && result.Success && result.Results.length > 0) {
|
|
465
|
-
const viewInfo = result.Results[0];
|
|
511
|
+
const viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
|
|
512
|
+
if (viewInfo) {
|
|
466
513
|
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, viewInput.AfterKey
|
|
467
514
|
? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
|
|
468
|
-
: undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
|
|
515
|
+
: undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
|
|
469
516
|
}
|
|
470
517
|
else {
|
|
471
518
|
LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
|
|
@@ -484,9 +531,11 @@ export class ResolverBase {
|
|
|
484
531
|
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 })))}`);
|
|
485
532
|
}
|
|
486
533
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
487
|
-
const viewInfo = await
|
|
488
|
-
|
|
489
|
-
|
|
534
|
+
const viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
|
|
535
|
+
if (!viewInfo) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
|
|
490
539
|
}
|
|
491
540
|
catch (err) {
|
|
492
541
|
console.log(err);
|
|
@@ -509,7 +558,7 @@ export class ResolverBase {
|
|
|
509
558
|
EntityID: entity.ID,
|
|
510
559
|
EntityBaseView: entity.BaseView,
|
|
511
560
|
}; // only providing a few bits of data here, but it's enough to get the view to run
|
|
512
|
-
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, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
|
|
561
|
+
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, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, undefined, viewInput.SkipTotalRowCount);
|
|
513
562
|
}
|
|
514
563
|
catch (err) {
|
|
515
564
|
console.log(err);
|
|
@@ -524,7 +573,7 @@ export class ResolverBase {
|
|
|
524
573
|
try {
|
|
525
574
|
let viewInfo = null;
|
|
526
575
|
if (viewInput.ViewName) {
|
|
527
|
-
viewInfo =
|
|
576
|
+
viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
|
|
528
577
|
// Populate EntityName on the input so callers (e.g. RunViews resolver) can
|
|
529
578
|
// look up the entity without re-querying the view
|
|
530
579
|
if (viewInfo && !viewInput.EntityName) {
|
|
@@ -532,8 +581,7 @@ export class ResolverBase {
|
|
|
532
581
|
}
|
|
533
582
|
}
|
|
534
583
|
else if (viewInput.ViewID) {
|
|
535
|
-
viewInfo = await
|
|
536
|
-
await viewInfo.Load(viewInput.ViewID);
|
|
584
|
+
viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
|
|
537
585
|
// Populate EntityName on the input so callers can look up the entity
|
|
538
586
|
if (viewInfo && !viewInput.EntityName) {
|
|
539
587
|
viewInput.EntityName = viewInfo.Entity;
|
|
@@ -580,6 +628,9 @@ export class ResolverBase {
|
|
|
580
628
|
bypassCache: viewInput.BypassCache,
|
|
581
629
|
dataSource: viewInput.DataSource,
|
|
582
630
|
includeBinaryFields: viewInput.IncludeBinaryFields,
|
|
631
|
+
skipTotalRowCount: viewInput.SkipTotalRowCount,
|
|
632
|
+
alternateBaseView: viewInput.AlternateBaseView,
|
|
633
|
+
properties: viewInput.Properties,
|
|
583
634
|
});
|
|
584
635
|
}
|
|
585
636
|
catch (err) {
|
|
@@ -590,6 +641,30 @@ export class ResolverBase {
|
|
|
590
641
|
let results = await this.RunViewsGenericInternal(params);
|
|
591
642
|
return results;
|
|
592
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* What an entity event may carry to a CloudEvents sink or a log line.
|
|
646
|
+
*
|
|
647
|
+
* The whole row for an entity without field security. For one where it applies (its own flag
|
|
648
|
+
* or an IS-A ancestor's), and for `MJ: Record Changes` (whose payload columns hold other
|
|
649
|
+
* entities' values), only the primary key and the names of the fields that changed: the sink
|
|
650
|
+
* is outside MemberJunction with no reader to project the row for, and these events fire for
|
|
651
|
+
* server-internal saves too, whose objects hold every column.
|
|
652
|
+
*/
|
|
653
|
+
EntityEventPayload(baseEntity, changedFieldNames) {
|
|
654
|
+
const entityInfo = baseEntity.EntityInfo;
|
|
655
|
+
if (!entityInfo?.FieldLevelSecurityApplies && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
|
|
656
|
+
// fls-guard: entity without field security and not the audit trail; whole row is the documented payload
|
|
657
|
+
return baseEntity.GetAll();
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
// Read through Get() rather than PrimaryKey, which builds every field object of a raw-mode record.
|
|
661
|
+
PrimaryKey: (entityInfo.PrimaryKeys ?? []).map((pk) => ({ FieldName: pk.Name, Value: baseEntity.Get(pk.Name) })),
|
|
662
|
+
// A completed save has already rebased the object, so nothing on it is dirty: the event
|
|
663
|
+
// carries the names the save wrote. Other events (save_started) still read them off the object.
|
|
664
|
+
// GetDirtyFieldNames answers without building field objects for a record still in raw mode.
|
|
665
|
+
ChangedFields: changedFieldNames ?? baseEntity.GetDirtyFieldNames(),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
593
668
|
static { this._priorEmittedData = []; }
|
|
594
669
|
async EmitCloudEvent({ component, event, eventCode, args }) {
|
|
595
670
|
if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
|
|
@@ -597,7 +672,7 @@ export class ResolverBase {
|
|
|
597
672
|
const type = `MemberJunction.${event}${extendedType}`;
|
|
598
673
|
const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
|
|
599
674
|
const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
|
|
600
|
-
const data = args?.baseEntity
|
|
675
|
+
const data = args?.baseEntity ? this.EntityEventPayload(args.baseEntity, args.ChangedFieldNames) : {};
|
|
601
676
|
const cloudEvent = new CloudEvent({ type, source, subject, data });
|
|
602
677
|
try {
|
|
603
678
|
// check to see if the combination of Entity and pkey was already emitted, if so, Log that condtion next
|
|
@@ -777,7 +852,7 @@ export class ResolverBase {
|
|
|
777
852
|
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
778
853
|
*
|
|
779
854
|
* The references come from a walk of the whole parse tree
|
|
780
|
-
* (`
|
|
855
|
+
* (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
|
|
781
856
|
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
782
857
|
* a row list).
|
|
783
858
|
*
|
|
@@ -793,14 +868,7 @@ export class ResolverBase {
|
|
|
793
868
|
}
|
|
794
869
|
const dialect = this.dialectForProvider(provider);
|
|
795
870
|
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
796
|
-
const
|
|
797
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
798
|
-
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
799
|
-
}
|
|
800
|
-
if (this.astContainsWriteNode(parser.AST)) {
|
|
801
|
-
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
802
|
-
}
|
|
803
|
-
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
871
|
+
const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
|
|
804
872
|
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
805
873
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
806
874
|
for (const relation of reads.Relations) {
|
|
@@ -857,9 +925,19 @@ export class ResolverBase {
|
|
|
857
925
|
hasBackslashInQuotes(clause, dialect) {
|
|
858
926
|
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
859
927
|
}
|
|
860
|
-
/**
|
|
861
|
-
|
|
862
|
-
|
|
928
|
+
/**
|
|
929
|
+
* Parses a whole statement with the strict validator and returns what it reads; throws unless it
|
|
930
|
+
* is one plain read-only SELECT with no write or DDL at any depth.
|
|
931
|
+
*/
|
|
932
|
+
strictReadOnlyStatementReads(sqlText, dialect, label) {
|
|
933
|
+
const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
|
|
934
|
+
if (result.Refusal === 'NestedWrite') {
|
|
935
|
+
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
936
|
+
}
|
|
937
|
+
if (!result.IsValid || !result.Reads) {
|
|
938
|
+
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
939
|
+
}
|
|
940
|
+
return result.Reads;
|
|
863
941
|
}
|
|
864
942
|
dialectForProvider(provider) {
|
|
865
943
|
return ClientClauseScreen.DialectFor(provider);
|
|
@@ -873,7 +951,7 @@ export class ResolverBase {
|
|
|
873
951
|
* - Field filtering at source (Fix #7)
|
|
874
952
|
* - Improved error handling (Fix #9)
|
|
875
953
|
*/
|
|
876
|
-
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields) {
|
|
954
|
+
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties, skipTotalRowCount) {
|
|
877
955
|
try {
|
|
878
956
|
if (!viewInfo || !userPayload)
|
|
879
957
|
return null;
|
|
@@ -919,6 +997,7 @@ export class ResolverBase {
|
|
|
919
997
|
const result = await rv.RunView({
|
|
920
998
|
ViewID: viewInfo.ID,
|
|
921
999
|
ViewName: viewInfo.Name,
|
|
1000
|
+
ViewEntity: viewInfo.ID ? viewInfo : undefined,
|
|
922
1001
|
EntityName: viewInfo.Entity,
|
|
923
1002
|
ExtraFilter: extraFilter,
|
|
924
1003
|
OrderBy: orderBy,
|
|
@@ -939,7 +1018,20 @@ export class ResolverBase {
|
|
|
939
1018
|
BypassCache: bypassCache,
|
|
940
1019
|
DataSource: dataSource,
|
|
941
1020
|
IncludeBinaryFields: includeBinaryFields,
|
|
1021
|
+
SkipTotalRowCount: skipTotalRowCount,
|
|
1022
|
+
AlternateBaseView: alternateBaseView,
|
|
1023
|
+
Properties: properties,
|
|
1024
|
+
TimeZone: userPayload?.TimeZone,
|
|
1025
|
+
PreserveRestrictedApiCiphertext: true,
|
|
942
1026
|
}, user);
|
|
1027
|
+
if (result && typeof result === 'object') {
|
|
1028
|
+
Object.defineProperty(result, '_resolvedViewEntity', {
|
|
1029
|
+
value: viewInfo,
|
|
1030
|
+
enumerable: false,
|
|
1031
|
+
configurable: true,
|
|
1032
|
+
writable: true,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
943
1035
|
// Log aggregate results for debugging
|
|
944
1036
|
if (aggregates?.length) {
|
|
945
1037
|
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)}`);
|
|
@@ -966,7 +1058,7 @@ export class ResolverBase {
|
|
|
966
1058
|
if (result?.Success && result.Results?.length) {
|
|
967
1059
|
result.Results = result.Results.map(r => mapper.MapFields({ ...r }));
|
|
968
1060
|
// Filter encrypted fields before sending to API client
|
|
969
|
-
await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user);
|
|
1061
|
+
await this.ArrayFilterEncryptedFieldsForAPI(viewInfo.Entity, result.Results, user, md);
|
|
970
1062
|
}
|
|
971
1063
|
return result;
|
|
972
1064
|
}
|
|
@@ -1050,6 +1142,7 @@ export class ResolverBase {
|
|
|
1050
1142
|
runViewParams.push({
|
|
1051
1143
|
ViewID: param.viewInfo.ID,
|
|
1052
1144
|
ViewName: param.viewInfo.Name,
|
|
1145
|
+
ViewEntity: param.viewInfo.ID ? param.viewInfo : undefined,
|
|
1053
1146
|
EntityName: param.viewInfo.Entity,
|
|
1054
1147
|
ExtraFilter: param.extraFilter,
|
|
1055
1148
|
OrderBy: param.orderBy,
|
|
@@ -1070,6 +1163,11 @@ export class ResolverBase {
|
|
|
1070
1163
|
BypassCache: param.bypassCache,
|
|
1071
1164
|
DataSource: param.dataSource,
|
|
1072
1165
|
IncludeBinaryFields: param.includeBinaryFields,
|
|
1166
|
+
SkipTotalRowCount: param.skipTotalRowCount,
|
|
1167
|
+
AlternateBaseView: param.alternateBaseView,
|
|
1168
|
+
Properties: param.properties,
|
|
1169
|
+
TimeZone: param.userPayload?.TimeZone,
|
|
1170
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1073
1171
|
});
|
|
1074
1172
|
}
|
|
1075
1173
|
// Fix #4: Run views in a single batch through RunViews
|
|
@@ -1086,7 +1184,7 @@ export class ResolverBase {
|
|
|
1086
1184
|
// Use the corresponding param's entity name
|
|
1087
1185
|
const entityName = params[i]?.viewInfo?.Entity;
|
|
1088
1186
|
if (entityName && contextUser) {
|
|
1089
|
-
await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser);
|
|
1187
|
+
await this.ArrayFilterEncryptedFieldsForAPI(entityName, runViewResult.Results, contextUser, md);
|
|
1090
1188
|
}
|
|
1091
1189
|
}
|
|
1092
1190
|
}
|
|
@@ -1205,8 +1303,9 @@ export class ResolverBase {
|
|
|
1205
1303
|
* skip redundant re-fetches (it already handled the event locally).
|
|
1206
1304
|
*
|
|
1207
1305
|
* The row itself rides along ONLY for entities opted in via
|
|
1208
|
-
* `cacheSettings.recordDataBroadcastEntities
|
|
1209
|
-
*
|
|
1306
|
+
* `cacheSettings.recordDataBroadcastEntities`, and never for one under field-level security —
|
|
1307
|
+
* this event reaches every connected client unprojected, so the row would otherwise be readable
|
|
1308
|
+
* by sessions that could not read the record or its restricted fields.
|
|
1210
1309
|
*/
|
|
1211
1310
|
PublishCacheInvalidation(entityObject, action, userPayload) {
|
|
1212
1311
|
const entityName = entityObject.EntityInfo.Name;
|
|
@@ -1217,32 +1316,61 @@ export class ResolverBase {
|
|
|
1217
1316
|
sourceServerId: MJGlobal.Instance.ProcessUUID,
|
|
1218
1317
|
timestamp: new Date(),
|
|
1219
1318
|
originSessionId: userPayload?.sessionId || null,
|
|
1220
|
-
recordData: action === 'save' && MayBroadcastRecordData(entityName)
|
|
1319
|
+
recordData: action === 'save' && MayBroadcastRecordData(entityName, entityObject.EntityInfo)
|
|
1320
|
+
// fls-guard: MayBroadcastRecordData refuses FLS entities and Record Changes
|
|
1221
1321
|
? JSON.stringify(entityObject.GetAll())
|
|
1222
1322
|
: undefined,
|
|
1223
1323
|
});
|
|
1224
1324
|
}
|
|
1225
1325
|
/**
|
|
1226
1326
|
* Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1327
|
+
* owner key of `userPayload` (`StatusUpdateOwnerID`: the request's user, or the widget guest an
|
|
1328
|
+
* elevated payload stands in for) so the subscription filter can bind delivery to identity (see B49 /
|
|
1329
|
+
* `StatusUpdatesFilter`). The ergonomic wrapper every resolver should use instead of calling
|
|
1330
|
+
* `pubSub.publish` on the topic directly — it makes omitting identity impossible. Non-resolver
|
|
1331
|
+
* publishers (services, the liveness heartbeat) call the shared `PublishStatusUpdate()` function
|
|
1332
|
+
* directly with an explicit `ownerUserId`.
|
|
1232
1333
|
*/
|
|
1233
1334
|
PublishStatusUpdate(pubSub, sessionId, message, userPayload) {
|
|
1234
1335
|
PublishStatusUpdate(pubSub, {
|
|
1235
1336
|
sessionId,
|
|
1236
|
-
ownerUserId: userPayload
|
|
1337
|
+
ownerUserId: StatusUpdateOwnerID(userPayload),
|
|
1237
1338
|
message,
|
|
1238
1339
|
});
|
|
1239
1340
|
}
|
|
1240
1341
|
ListenForEntityMessages(entityObject, pubSub, userPayload) {
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1342
|
+
// Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
|
|
1343
|
+
// per event, while routing WebSocket status updates dynamically across all active user sessions
|
|
1344
|
+
// that have interacted with this entity type (instead of closing over only the first caller).
|
|
1244
1345
|
const uniqueKey = entityObject.EntityInfo.Name;
|
|
1245
|
-
|
|
1346
|
+
const now = Date.now();
|
|
1347
|
+
const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
|
|
1348
|
+
let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
|
|
1349
|
+
if (!sessionsForEntity) {
|
|
1350
|
+
sessionsForEntity = new Map();
|
|
1351
|
+
this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
// Opportunistically evict expired session entries and enforce a safe per-entity cap
|
|
1355
|
+
for (const [key, entry] of sessionsForEntity) {
|
|
1356
|
+
if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1357
|
+
sessionsForEntity.delete(key);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
|
|
1361
|
+
const oldestKey = sessionsForEntity.keys().next().value;
|
|
1362
|
+
if (oldestKey !== undefined) {
|
|
1363
|
+
sessionsForEntity.delete(oldestKey);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (userPayload) {
|
|
1368
|
+
const sessionKey = userPayload.sessionId
|
|
1369
|
+
? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
|
|
1370
|
+
: (userPayload.userRecord?.ID ?? '__anonymous__');
|
|
1371
|
+
sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
|
|
1372
|
+
}
|
|
1373
|
+
if (isFreshSubscription) {
|
|
1246
1374
|
// 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
|
|
1247
1375
|
LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
|
|
1248
1376
|
const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
|
|
@@ -1253,28 +1381,63 @@ export class ResolverBase {
|
|
|
1253
1381
|
if (baseEntity?.EntityInfo?.Name !== uniqueKey) {
|
|
1254
1382
|
return;
|
|
1255
1383
|
}
|
|
1256
|
-
const baseEntityValues = baseEntity ? baseEntity.
|
|
1257
|
-
const eventToLog = { entityName:
|
|
1384
|
+
const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
|
|
1385
|
+
const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
|
|
1258
1386
|
LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
|
|
1259
1387
|
await this.EmitCloudEvent(event);
|
|
1260
1388
|
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1261
1389
|
if (event.args && event.args instanceof BaseEntityEvent) {
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1390
|
+
const activeSessions = this.activeEntitySessions.get(uniqueKey);
|
|
1391
|
+
if (activeSessions && activeSessions.size > 0) {
|
|
1392
|
+
const eventNow = Date.now();
|
|
1393
|
+
let serializedMessage;
|
|
1394
|
+
for (const [sKey, session] of activeSessions) {
|
|
1395
|
+
if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1396
|
+
activeSessions.delete(sKey);
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
|
|
1400
|
+
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
1401
|
+
serializedMessage ??= this.EntityStatusMessage(event.args);
|
|
1402
|
+
this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1272
1406
|
}
|
|
1273
1407
|
}
|
|
1274
1408
|
});
|
|
1275
1409
|
this.eventSubscriptions.set(uniqueKey, theSub);
|
|
1276
1410
|
}
|
|
1277
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Whether an entity event may be relayed to the session whose call created the listener. The
|
|
1414
|
+
* listener is shared by every caller in the process for that entity, so an event raised by an
|
|
1415
|
+
* object loaded for another user (or for the system user) is never relayed to this session.
|
|
1416
|
+
*/
|
|
1417
|
+
EntityEventBelongsToSession(event, userPayload) {
|
|
1418
|
+
const sessionUserId = userPayload?.userRecord?.ID;
|
|
1419
|
+
const objectUserId = event.baseEntity?.ContextCurrentUser?.ID;
|
|
1420
|
+
return Boolean(sessionUserId && objectUserId && UUIDsEqual(sessionUserId, objectUserId));
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* The status message relayed for an entity event: the entity, key and event type. The event's
|
|
1424
|
+
* payload is included only when it cannot carry record values: never for a delete (its payload
|
|
1425
|
+
* is the whole deleted row), an entity under field-level security, or `MJ: Record Changes`.
|
|
1426
|
+
*/
|
|
1427
|
+
EntityStatusMessage(event) {
|
|
1428
|
+
const entityInfo = event.baseEntity.EntityInfo;
|
|
1429
|
+
const mayCarryPayload = event.type !== 'delete'
|
|
1430
|
+
&& !entityInfo.FieldLevelSecurityApplies
|
|
1431
|
+
&& !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo);
|
|
1432
|
+
return JSON.stringify({
|
|
1433
|
+
status: 'OK',
|
|
1434
|
+
type: 'EntityObjectStatusMessage',
|
|
1435
|
+
entityName: entityInfo.Name,
|
|
1436
|
+
primaryKey: event.baseEntity.PrimaryKey,
|
|
1437
|
+
eventType: event.type,
|
|
1438
|
+
message: mayCarryPayload ? event.payload : undefined,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1278
1441
|
/**
|
|
1279
1442
|
* IS-A PROMOTION over the wire: an EXISTING parent record gaining a subtype — an Animal that is
|
|
1280
1443
|
* now also a Dog. It arrives as the CHILD's create mutation carrying the existing parent's
|
|
@@ -1348,6 +1511,9 @@ export class ResolverBase {
|
|
|
1348
1511
|
// IS-A promotion: bind the new child to its EXISTING parent row BEFORE the field
|
|
1349
1512
|
// assignments, so values the client sent for parent fields land on the loaded parent
|
|
1350
1513
|
// (as an update) instead of being wiped by the load.
|
|
1514
|
+
// Field-level security: refuse a key the caller may not read, or that names no field, before
|
|
1515
|
+
// anything is applied (the same gate as the update path).
|
|
1516
|
+
AssertClientInputWritableForUser(entityObject.EntityInfo, fieldsForSet, this.GetUserFromPayload(userPayload));
|
|
1351
1517
|
await this.attachToExistingParentIfPromotion(entityObject, fieldsForSet);
|
|
1352
1518
|
entityObject.SetMany(fieldsForSet);
|
|
1353
1519
|
// Reconstruct the client-side restore context, if any, on this server
|
|
@@ -1361,6 +1527,7 @@ export class ResolverBase {
|
|
|
1361
1527
|
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1362
1528
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
1363
1529
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1530
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
1364
1531
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
1365
1532
|
}
|
|
1366
1533
|
// save failed, throw error with message
|
|
@@ -1398,12 +1565,11 @@ export class ResolverBase {
|
|
|
1398
1565
|
}
|
|
1399
1566
|
});
|
|
1400
1567
|
// Field-level security: any value the client sends for a field it cannot READ is
|
|
1401
|
-
// fabricated by construction (the field was
|
|
1402
|
-
// received), so
|
|
1403
|
-
// denied we also force the truth-load branch below
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
|
|
1568
|
+
// fabricated by construction (the field was absent from every payload the client ever
|
|
1569
|
+
// received), so the input is refused before anything applies it. When any field is
|
|
1570
|
+
// denied we also force the truth-load branch below, so the denied columns keep what the
|
|
1571
|
+
// database holds rather than anything derived from the client's OldValues.
|
|
1572
|
+
const hasDeniedReadFields = this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
|
|
1407
1573
|
// The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
|
|
1408
1574
|
// has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
|
|
1409
1575
|
// that hydrates from those values — never loading what the database actually holds.
|
|
@@ -1459,6 +1625,7 @@ export class ResolverBase {
|
|
|
1459
1625
|
await this.AfterUpdate(provider, input); // fire event
|
|
1460
1626
|
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1461
1627
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1628
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
1462
1629
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
|
|
1463
1630
|
}
|
|
1464
1631
|
else {
|
|
@@ -1510,7 +1677,7 @@ export class ResolverBase {
|
|
|
1510
1677
|
* writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
|
|
1511
1678
|
* database load (`InnerLoad`) so untouched columns retain their stored values.
|
|
1512
1679
|
*
|
|
1513
|
-
* `
|
|
1680
|
+
* `FieldLevelSecurityApplies` (own flag or an IS-A ancestor's) is the security-critical one, and it is NOT redundant with
|
|
1514
1681
|
* `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
|
|
1515
1682
|
* Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
|
|
1516
1683
|
* `OldValues___`, and a value it supplies there for an update-denied field arrives through
|
|
@@ -1558,60 +1725,43 @@ export class ResolverBase {
|
|
|
1558
1725
|
!this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
|
|
1559
1726
|
hasDeniedReadFields ||
|
|
1560
1727
|
hasNarrowedAuditPayload ||
|
|
1561
|
-
entityInfo.
|
|
1728
|
+
entityInfo.FieldLevelSecurityApplies ||
|
|
1562
1729
|
!user ||
|
|
1563
1730
|
entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0);
|
|
1564
1731
|
}
|
|
1565
1732
|
/**
|
|
1566
|
-
* Field-level security
|
|
1733
|
+
* Field-level security gate on the update path's client input.
|
|
1567
1734
|
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
* save" safe for a restricted user.
|
|
1735
|
+
* For a caller denied at least one field on the entity, refuses (with a `FieldSecurityError`)
|
|
1736
|
+
* any key in the new values or in `OldValues___` that names a field the caller cannot READ, or
|
|
1737
|
+
* that names no field at all, with the same message for both (see
|
|
1738
|
+
* `AssertClientInputWritableForUser`), so the refusal does not reveal which columns exist.
|
|
1573
1739
|
*
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1740
|
+
* Refused rather than dropped: a field the caller cannot read was absent from every payload the
|
|
1741
|
+
* client received, so a value for it can only be fabricated. A well-behaved client never sends
|
|
1742
|
+
* one (it omits fields it did not load), so ordinary traffic is unaffected.
|
|
1576
1743
|
*
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
1579
|
-
* and is restricted).
|
|
1744
|
+
* There is no split by update permission: Read is required for Update, so a user denied read is
|
|
1745
|
+
* denied update too.
|
|
1580
1746
|
*
|
|
1581
|
-
* Returns true when the user has a non-empty denied-read set on this entity. The caller
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1747
|
+
* Returns true when the user has a non-empty denied-read set on this entity. The caller must
|
|
1748
|
+
* then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold
|
|
1749
|
+
* true values the client input never touches.
|
|
1584
1750
|
*/
|
|
1585
|
-
|
|
1586
|
-
if (!entityInfo.
|
|
1751
|
+
AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues) {
|
|
1752
|
+
if (!entityInfo.FieldLevelSecurityApplies || !userInfo) {
|
|
1587
1753
|
return false;
|
|
1588
1754
|
}
|
|
1589
|
-
|
|
1590
|
-
if (deniedReadNames.size === 0) {
|
|
1755
|
+
if (entityInfo.GetDeniedReadFields(userInfo).size === 0) {
|
|
1591
1756
|
return false;
|
|
1592
1757
|
}
|
|
1593
|
-
|
|
1594
|
-
// lowercased field Names — bridge via the field metadata once.
|
|
1595
|
-
const deniedReadCodeNames = new Set();
|
|
1596
|
-
for (const field of entityInfo.Fields) {
|
|
1597
|
-
if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
|
|
1598
|
-
deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
const stripped = [];
|
|
1602
|
-
for (const key of Object.keys(clientNewValues)) {
|
|
1603
|
-
if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
|
|
1604
|
-
delete clientNewValues[key];
|
|
1605
|
-
delete input[key];
|
|
1606
|
-
stripped.push(key);
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1758
|
+
AssertClientInputWritableForUser(entityInfo, clientNewValues, userInfo);
|
|
1609
1759
|
if (Array.isArray(input.OldValues___)) {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1760
|
+
const oldValueKeys = {};
|
|
1761
|
+
for (const item of input.OldValues___) {
|
|
1762
|
+
oldValueKeys[String(item.Key)] = null;
|
|
1763
|
+
}
|
|
1764
|
+
AssertClientInputWritableForUser(entityInfo, oldValueKeys, userInfo);
|
|
1615
1765
|
}
|
|
1616
1766
|
return true;
|
|
1617
1767
|
}
|
|
@@ -1728,132 +1878,268 @@ export class ResolverBase {
|
|
|
1728
1878
|
}
|
|
1729
1879
|
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
1730
1880
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1881
|
+
/**
|
|
1882
|
+
* Whether the API never gives a client this field's stored value: an encrypted field that is not
|
|
1883
|
+
* decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
|
|
1884
|
+
* client's old value for it can never be compared with the database value.
|
|
1885
|
+
*/
|
|
1886
|
+
isFieldOpaqueToClient(field) {
|
|
1887
|
+
return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* The client's new values without the opaque fields it only echoed back: the sentinel, or the same
|
|
1891
|
+
* value it received as the old value. Writing either would replace the stored secret.
|
|
1892
|
+
*/
|
|
1893
|
+
withoutEchoedOpaqueValues(entityInfo, clientNewValues, clientOldValues) {
|
|
1894
|
+
const kept = {};
|
|
1895
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
1896
|
+
const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
|
|
1897
|
+
const newValue = clientNewValues[key];
|
|
1898
|
+
const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
|
|
1899
|
+
const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
|
|
1900
|
+
if (!echoed) {
|
|
1901
|
+
kept[key] = newValue;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return kept;
|
|
1905
|
+
}
|
|
1906
|
+
/**
|
|
1907
|
+
* Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
|
|
1908
|
+
* the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
|
|
1909
|
+
*/
|
|
1910
|
+
redactEncryptedConflictValues(entityInfo, entries) {
|
|
1911
|
+
return entries.map((entry) => {
|
|
1912
|
+
if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
|
|
1913
|
+
return entry;
|
|
1914
|
+
}
|
|
1915
|
+
const redacted = { FieldName: entry.FieldName };
|
|
1916
|
+
if ('ClientOldValue' in entry)
|
|
1917
|
+
redacted.ClientOldValue = ENCRYPTED_SENTINEL;
|
|
1918
|
+
if ('ClientNewValue' in entry)
|
|
1919
|
+
redacted.ClientNewValue = ENCRYPTED_SENTINEL;
|
|
1920
|
+
if ('DBValue' in entry)
|
|
1921
|
+
redacted.DBValue = ENCRYPTED_SENTINEL;
|
|
1922
|
+
return redacted;
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
|
|
1927
|
+
* (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
|
|
1928
|
+
*/
|
|
1929
|
+
areEntityFieldValuesDifferent(field, left, right, allowTimezoneShiftWarning, fieldKeyForLog) {
|
|
1930
|
+
const normalizedLeft = left === undefined ? null : left;
|
|
1931
|
+
const normalizedRight = right === undefined ? null : right;
|
|
1932
|
+
if (normalizedLeft instanceof Date) {
|
|
1933
|
+
const rightDate = normalizedRight instanceof Date
|
|
1934
|
+
? normalizedRight
|
|
1935
|
+
: field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
|
|
1936
|
+
? this.ClientOldValueToFieldValue(field, normalizedRight)
|
|
1937
|
+
: null;
|
|
1938
|
+
if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
|
|
1939
|
+
return true;
|
|
1940
|
+
}
|
|
1941
|
+
if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
|
|
1942
|
+
return false;
|
|
1943
|
+
}
|
|
1944
|
+
const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
|
|
1945
|
+
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
|
|
1946
|
+
if (allowTimezoneShiftWarning && fieldKeyForLog) {
|
|
1947
|
+
console.warn(`Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
|
|
1948
|
+
`Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
|
|
1949
|
+
`Consider using datetimeoffset to avoid timezone ambiguity.`);
|
|
1950
|
+
}
|
|
1951
|
+
return false;
|
|
1952
|
+
}
|
|
1953
|
+
return true;
|
|
1954
|
+
}
|
|
1955
|
+
if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
|
|
1956
|
+
const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
|
|
1957
|
+
if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
|
|
1958
|
+
return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
|
|
1962
|
+
return false;
|
|
1963
|
+
}
|
|
1964
|
+
return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== normalizedRight;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* True when two instants would be stored as the same value in the field's column: SQL Server
|
|
1968
|
+
* `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
|
|
1969
|
+
*/
|
|
1970
|
+
datesEqualAtColumnResolution(field, left, right) {
|
|
1971
|
+
return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
|
|
1972
|
+
}
|
|
1973
|
+
dateColumnResolutionMs(field) {
|
|
1974
|
+
switch ((field?.Type ?? '').trim().toLowerCase()) {
|
|
1975
|
+
case 'datetime':
|
|
1976
|
+
return 1000 / 300;
|
|
1977
|
+
case 'smalldatetime':
|
|
1978
|
+
return 60_000;
|
|
1979
|
+
default:
|
|
1980
|
+
return 1;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* True when both strings are JSON documents with the same content, ignoring whitespace and key order.
|
|
1985
|
+
*/
|
|
1986
|
+
jsonStringsEquivalent(left, right) {
|
|
1987
|
+
const canonicalLeft = this.canonicalJSONDocument(left);
|
|
1988
|
+
return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Canonical form of a JSON object or array string, or null when the text is not one.
|
|
1992
|
+
*/
|
|
1993
|
+
canonicalJSONDocument(text) {
|
|
1994
|
+
const first = text.trimStart().charAt(0);
|
|
1995
|
+
if (first !== '{' && first !== '[') {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
return Canonicalize(JSON.parse(text));
|
|
2000
|
+
}
|
|
2001
|
+
catch {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
async TestAndSetClientOldValuesToDBValues(input, rawClientNewValues, entityObject, contextUser) {
|
|
1733
2006
|
const clientOldValues = {};
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
// 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
|
|
2007
|
+
for (const item of input.OldValues___ ?? []) {
|
|
2008
|
+
// Transform string-serialized wire values to match the field's TypeScript type
|
|
1737
2009
|
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
//
|
|
1742
|
-
// with the DB values
|
|
2010
|
+
clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
|
|
2011
|
+
}
|
|
2012
|
+
const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
|
|
2013
|
+
// fls-guard: compared on the server only, never returned
|
|
1743
2014
|
const dbValues = entityObject.GetAll();
|
|
1744
|
-
// 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
|
|
1745
2015
|
const dbDifferences = [];
|
|
1746
|
-
|
|
2016
|
+
const dbDiffByField = new Map();
|
|
2017
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
1747
2018
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
switch (typeof clientOldValues[key]) {
|
|
1751
|
-
case 'number':
|
|
1752
|
-
different = clientOldValues[key] !== dbVal;
|
|
1753
|
-
break;
|
|
1754
|
-
case 'boolean':
|
|
1755
|
-
different = clientOldValues[key] !== dbVal;
|
|
1756
|
-
break;
|
|
1757
|
-
case 'object':
|
|
1758
|
-
if (clientOldValues[key] instanceof Date) {
|
|
1759
|
-
const clientDate = clientOldValues[key];
|
|
1760
|
-
const dbDate = dbVal;
|
|
1761
|
-
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
1762
|
-
different = true;
|
|
1763
|
-
}
|
|
1764
|
-
else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
1765
|
-
// Check if this is a datetime/datetime2 field with only a timezone hour shift
|
|
1766
|
-
const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
|
|
1767
|
-
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
|
|
1768
|
-
console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
|
|
1769
|
-
`Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
|
|
1770
|
-
`Consider using datetimeoffset to avoid timezone ambiguity.`);
|
|
1771
|
-
different = false; // Allow timezone shifts through for non-datetimeoffset fields
|
|
1772
|
-
}
|
|
1773
|
-
else {
|
|
1774
|
-
different = true;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
else if (clientOldValues[key] === null) {
|
|
1779
|
-
different = dbVal !== null;
|
|
1780
|
-
}
|
|
1781
|
-
break;
|
|
1782
|
-
default:
|
|
1783
|
-
different = clientOldValues[key] !== dbVal;
|
|
1784
|
-
break;
|
|
2019
|
+
if (this.isFieldOpaqueToClient(f)) {
|
|
2020
|
+
continue;
|
|
1785
2021
|
}
|
|
2022
|
+
const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
2023
|
+
const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
|
|
1786
2024
|
if (different && f && !f.ReadOnly) {
|
|
1787
|
-
|
|
1788
|
-
dbDifferences.push({
|
|
2025
|
+
const entry = {
|
|
1789
2026
|
FieldName: key,
|
|
1790
2027
|
ClientOldValue: clientOldValues[key],
|
|
1791
2028
|
DBValue: dbVal,
|
|
1792
|
-
}
|
|
2029
|
+
};
|
|
2030
|
+
dbDifferences.push(entry);
|
|
2031
|
+
dbDiffByField.set(key, entry);
|
|
1793
2032
|
}
|
|
1794
|
-
}
|
|
2033
|
+
}
|
|
2034
|
+
let fieldsToPreserveFromDB;
|
|
1795
2035
|
if (dbDifferences.length > 0) {
|
|
1796
|
-
// 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
|
|
1797
|
-
// 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
|
|
1798
2036
|
const clientDifferences = [];
|
|
1799
|
-
|
|
2037
|
+
const clientDiffFieldNames = new Set();
|
|
2038
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
1800
2039
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1801
|
-
if (key in clientNewValues &&
|
|
1802
|
-
|
|
2040
|
+
if (key in clientNewValues &&
|
|
2041
|
+
f &&
|
|
2042
|
+
f.AllowUpdateAPI &&
|
|
2043
|
+
!f.IsPrimaryKey &&
|
|
2044
|
+
this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)) {
|
|
1803
2045
|
clientDifferences.push({
|
|
1804
2046
|
FieldName: key,
|
|
1805
2047
|
ClientOldValue: clientOldValues[key],
|
|
1806
2048
|
ClientNewValue: clientNewValues[key],
|
|
1807
2049
|
});
|
|
2050
|
+
clientDiffFieldNames.add(key);
|
|
1808
2051
|
}
|
|
2052
|
+
}
|
|
2053
|
+
// True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
|
|
2054
|
+
// ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
|
|
2055
|
+
const overlap = clientDifferences.filter((cd) => {
|
|
2056
|
+
const dd = dbDiffByField.get(cd.FieldName);
|
|
2057
|
+
if (!dd) {
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
2060
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
|
|
2061
|
+
return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
|
|
1809
2062
|
});
|
|
1810
|
-
// 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
|
|
1811
|
-
// 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
|
|
1812
|
-
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
1813
|
-
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
1814
2063
|
if (overlap.length > 0) {
|
|
2064
|
+
const conflictingFields = overlap.map((o) => o.FieldName);
|
|
2065
|
+
const entityInfo = entityObject.EntityInfo;
|
|
2066
|
+
const shownClientDifferences = this.redactEncryptedConflictValues(entityInfo, clientDifferences);
|
|
2067
|
+
const shownDBDifferences = this.redactEncryptedConflictValues(entityInfo, dbDifferences);
|
|
2068
|
+
const shownOverlap = this.redactEncryptedConflictValues(entityInfo, overlap);
|
|
1815
2069
|
const msg = {
|
|
1816
|
-
Message: '
|
|
1817
|
-
ClientDifferences:
|
|
1818
|
-
DBDifferences:
|
|
1819
|
-
Overlap:
|
|
2070
|
+
Message: 'Concurrent modification conflict: one or more fields modified by the client were updated in the database since the record was loaded. Save rejected to prevent overwriting concurrent changes.',
|
|
2071
|
+
ClientDifferences: shownClientDifferences,
|
|
2072
|
+
DBDifferences: shownDBDifferences,
|
|
2073
|
+
Overlap: shownOverlap,
|
|
1820
2074
|
};
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
details: {
|
|
2075
|
+
console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
|
|
2076
|
+
LogErrorEx({
|
|
2077
|
+
message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
|
|
2078
|
+
severity: 'warning',
|
|
2079
|
+
category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
|
|
2080
|
+
metadata: {
|
|
1828
2081
|
entityName: entityObject.EntityInfo.Name,
|
|
1829
|
-
clientDifferences:
|
|
1830
|
-
dbDifferences:
|
|
1831
|
-
overlap:
|
|
1832
|
-
}
|
|
2082
|
+
clientDifferences: shownClientDifferences,
|
|
2083
|
+
dbDifferences: shownDBDifferences,
|
|
2084
|
+
overlap: shownOverlap,
|
|
2085
|
+
},
|
|
1833
2086
|
});
|
|
1834
|
-
//
|
|
1835
|
-
// ErrorLog write goes to the same connection as the entity that triggered it.
|
|
2087
|
+
// Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
|
|
1836
2088
|
try {
|
|
1837
|
-
const
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2089
|
+
const hasEntityObjectFactory = (p) => Boolean(p && 'GetEntityObject' in p);
|
|
2090
|
+
const providerCandidate = entityObject.ProviderToUse;
|
|
2091
|
+
if (hasEntityObjectFactory(providerCandidate)) {
|
|
2092
|
+
const errorLogEntity = await providerCandidate.GetEntityObject('MJ: Error Logs', contextUser);
|
|
2093
|
+
// Code is NCHAR(20); a longer value is rejected at the database
|
|
2094
|
+
errorLogEntity.Code = 'SAVE_INCONSISTENCY';
|
|
2095
|
+
errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
|
|
2096
|
+
errorLogEntity.Status = 'Warning';
|
|
2097
|
+
errorLogEntity.Category = 'Entity Save';
|
|
2098
|
+
errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
|
|
2099
|
+
const saveResult = await errorLogEntity.Save();
|
|
2100
|
+
if (!saveResult) {
|
|
2101
|
+
console.error(`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`);
|
|
2102
|
+
}
|
|
1847
2103
|
}
|
|
1848
2104
|
}
|
|
1849
2105
|
catch (errorLogError) {
|
|
1850
2106
|
console.error('Error creating ErrorLog record:', errorLogError);
|
|
1851
2107
|
}
|
|
2108
|
+
throw new GraphQLError(`Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`, {
|
|
2109
|
+
extensions: {
|
|
2110
|
+
code: 'CONCURRENT_MODIFICATION_CONFLICT',
|
|
2111
|
+
entityName: entityObject.EntityInfo.Name,
|
|
2112
|
+
conflictingFields,
|
|
2113
|
+
},
|
|
2114
|
+
});
|
|
1852
2115
|
}
|
|
2116
|
+
// Non-overlapping concurrent DB edits: preserve any field that changed in the database
|
|
2117
|
+
// when the client itself did NOT modify that field (prevents full-payload clients or forged
|
|
2118
|
+
// OldValues___ from clobbering concurrent DB updates on untouched columns).
|
|
2119
|
+
fieldsToPreserveFromDB = new Set();
|
|
2120
|
+
for (const dd of dbDifferences) {
|
|
2121
|
+
if (!clientDiffFieldNames.has(dd.FieldName)) {
|
|
2122
|
+
fieldsToPreserveFromDB.add(dd.FieldName);
|
|
2123
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
|
|
2124
|
+
if (f) {
|
|
2125
|
+
fieldsToPreserveFromDB.add(f.Name);
|
|
2126
|
+
fieldsToPreserveFromDB.add(f.CodeName);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
|
|
2132
|
+
const mergedValues = {};
|
|
2133
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2134
|
+
if (!fieldsToPreserveFromDB.has(key)) {
|
|
2135
|
+
mergedValues[key] = clientNewValues[key];
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
entityObject.SetMany(mergedValues);
|
|
2139
|
+
}
|
|
2140
|
+
else {
|
|
2141
|
+
entityObject.SetMany(clientNewValues);
|
|
1853
2142
|
}
|
|
1854
|
-
// If we get here that means we've not thrown an exception, so there is
|
|
1855
|
-
// NO OVERLAP, so we can set the new values from the data provided from the client now...
|
|
1856
|
-
entityObject.SetMany(clientNewValues);
|
|
1857
2143
|
}
|
|
1858
2144
|
async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
|
|
1859
2145
|
// Check API key scope authorization for entity delete operations
|
|
@@ -1872,7 +2158,10 @@ export class ResolverBase {
|
|
|
1872
2158
|
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
1873
2159
|
});
|
|
1874
2160
|
}
|
|
1875
|
-
|
|
2161
|
+
// Grab the values before we delete so we can return the last state if the delete succeeds,
|
|
2162
|
+
// projected for the caller in case the object holds a column they may not read.
|
|
2163
|
+
// fls-guard: projected by ProjectEntityValuesForUser
|
|
2164
|
+
const returnValue = ProjectEntityValuesForUser(provider, entityObject.EntityInfo, entityObject.GetAll(), this.GetUserFromPayload(userPayload));
|
|
1876
2165
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
1877
2166
|
if (await entityObject.Delete(options)) {
|
|
1878
2167
|
await this.AfterDelete(provider, key); // fire event
|