@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,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AggregateExpression,
|
|
3
|
+
AssertClientInputWritableForUser,
|
|
3
4
|
BaseEntity,
|
|
4
5
|
BaseEntityEvent,
|
|
5
6
|
CompositeKey,
|
|
@@ -14,8 +15,10 @@ import {
|
|
|
14
15
|
KeyValuePair,
|
|
15
16
|
LogDebug,
|
|
16
17
|
LogError,
|
|
18
|
+
LogErrorEx,
|
|
17
19
|
LogStatus,
|
|
18
20
|
Metadata,
|
|
21
|
+
ProjectEntityValuesForUser,
|
|
19
22
|
ReadableFieldsTransportKey,
|
|
20
23
|
RecordChangeFieldSecurityProjector,
|
|
21
24
|
RecordChangePayloadFields,
|
|
@@ -23,8 +26,17 @@ import {
|
|
|
23
26
|
RunViewParams,
|
|
24
27
|
RunViewResult,
|
|
25
28
|
UserInfo,
|
|
29
|
+
ViewPropertyValues,
|
|
26
30
|
} from '@memberjunction/core';
|
|
27
|
-
import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
31
|
+
import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended, UserViewEngine } from '@memberjunction/core-entities';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extended RunViewResult carrying a non-enumerable reference to the resolved view entity
|
|
35
|
+
* so downstream resolvers (e.g. RunViewResolver) do not re-query `MJ: User Views` a third time.
|
|
36
|
+
*/
|
|
37
|
+
export type RunViewResultWithResolvedView<T = unknown> = RunViewResult<T> & {
|
|
38
|
+
_resolvedViewEntity?: MJUserViewEntityExtended;
|
|
39
|
+
};
|
|
28
40
|
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
29
41
|
import { ClientClauseScreen, UserCache, type EntityBaseViewAllowList } from '@memberjunction/generic-database-provider';
|
|
30
42
|
import { PubSubEngine, AuthorizationError } from 'type-graphql';
|
|
@@ -37,11 +49,11 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
|
37
49
|
import { RunViewGenericParams, UserPayload } from '../types.js';
|
|
38
50
|
import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
|
|
39
51
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
40
|
-
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
41
|
-
import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
|
|
52
|
+
import { MJEvent, MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
|
|
53
|
+
import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator, type StrictStatementReads } from '@memberjunction/sql-parser';
|
|
42
54
|
import type { SQLParserDialect } from '@memberjunction/sql-dialect';
|
|
43
55
|
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
44
|
-
import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate } from './PushStatusResolver.js';
|
|
56
|
+
import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
|
|
45
57
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
|
|
46
58
|
import { PubSubManager } from './PubSubManager.js';
|
|
47
59
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
@@ -51,18 +63,43 @@ import { Subscription } from 'rxjs';
|
|
|
51
63
|
* Entities whose update is always hydrated from the stored row, never from the client's `OldValues___`, though they
|
|
52
64
|
* do not track record changes: their server-side write rules compare against the prior state, which a client could
|
|
53
65
|
* otherwise supply itself (lower-case names). See `ResolverBase.MustLoadTruthFromDatabase`.
|
|
66
|
+
*
|
|
67
|
+
* `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
|
|
68
|
+
* (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
|
|
69
|
+
* that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
|
|
54
70
|
*/
|
|
55
71
|
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
56
72
|
'mj: record changes',
|
|
57
73
|
'mj: conversation details',
|
|
58
74
|
'mj: data context items',
|
|
75
|
+
'mj: conversations',
|
|
76
|
+
'mj: ai agent sessions',
|
|
77
|
+
'mj: ai agent runs',
|
|
59
78
|
]);
|
|
60
79
|
|
|
80
|
+
/** One field's values in a concurrency-conflict diagnostic. */
|
|
81
|
+
interface ConflictDiagnosticEntry<TValue> {
|
|
82
|
+
FieldName: string;
|
|
83
|
+
ClientOldValue?: TValue;
|
|
84
|
+
ClientNewValue?: TValue;
|
|
85
|
+
DBValue?: TValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ActiveEntitySessionListener {
|
|
89
|
+
pubSub: PubSubEngine;
|
|
90
|
+
userPayload: UserPayload;
|
|
91
|
+
lastSeenAt: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
95
|
+
const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
|
|
96
|
+
|
|
61
97
|
export class ResolverBase {
|
|
62
98
|
private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
|
|
63
99
|
private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
|
|
64
100
|
|
|
65
101
|
private static _eventSubscriptionKey: string = '___MJServer___ResolverBase___EventSubscriptions';
|
|
102
|
+
private static _activeEntitySessionsKey: string = '___MJServer___ResolverBase___ActiveEntitySessions';
|
|
66
103
|
private get eventSubscriptions(): Map<string, Subscription> {
|
|
67
104
|
// 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
|
|
68
105
|
// could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
|
|
@@ -71,7 +108,15 @@ export class ResolverBase {
|
|
|
71
108
|
LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
|
|
72
109
|
g[ResolverBase._eventSubscriptionKey] = new Map<string, Subscription>();
|
|
73
110
|
}
|
|
74
|
-
return g[ResolverBase._eventSubscriptionKey]
|
|
111
|
+
return g[ResolverBase._eventSubscriptionKey] as Map<string, Subscription>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private get activeEntitySessions(): Map<string, Map<string, ActiveEntitySessionListener>> {
|
|
115
|
+
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
116
|
+
if (!g[ResolverBase._activeEntitySessionsKey]) {
|
|
117
|
+
g[ResolverBase._activeEntitySessionsKey] = new Map<string, Map<string, ActiveEntitySessionListener>>();
|
|
118
|
+
}
|
|
119
|
+
return g[ResolverBase._activeEntitySessionsKey] as Map<string, Map<string, ActiveEntitySessionListener>>;
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
/**
|
|
@@ -120,7 +165,7 @@ export class ResolverBase {
|
|
|
120
165
|
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
121
166
|
|
|
122
167
|
const denied = deniedReadFields ?? (
|
|
123
|
-
entityInfo.
|
|
168
|
+
entityInfo.FieldLevelSecurityApplies && contextUser
|
|
124
169
|
? entityInfo.GetDeniedReadFields(contextUser)
|
|
125
170
|
: null
|
|
126
171
|
);
|
|
@@ -369,6 +414,7 @@ export class ResolverBase {
|
|
|
369
414
|
if (!loaded) {
|
|
370
415
|
return null;
|
|
371
416
|
}
|
|
417
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
372
418
|
const mapped = await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
373
419
|
return mapped as T;
|
|
374
420
|
}
|
|
@@ -418,30 +464,29 @@ export class ResolverBase {
|
|
|
418
464
|
if (field.AllowDecryptInAPI) continue;
|
|
419
465
|
|
|
420
466
|
// AllowDecryptInAPI is false - we need to filter the value
|
|
467
|
+
if (!field.SendEncryptedValue) {
|
|
468
|
+
// SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
|
|
469
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
421
473
|
const engine = EncryptionEngine.Instance;
|
|
422
474
|
await engine.Config(false, contextUser);
|
|
423
475
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
438
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
439
|
-
}
|
|
476
|
+
// Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
|
|
477
|
+
if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
|
|
478
|
+
try {
|
|
479
|
+
const encryptedValue = await engine.Encrypt(
|
|
480
|
+
value,
|
|
481
|
+
field.EncryptionKeyID as string,
|
|
482
|
+
contextUser
|
|
483
|
+
);
|
|
484
|
+
dataObject[fieldName] = encryptedValue;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
// If re-encryption fails, use sentinel for safety
|
|
487
|
+
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
488
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
440
489
|
}
|
|
441
|
-
// If already encrypted (shouldn't happen normally), keep as-is
|
|
442
|
-
} else {
|
|
443
|
-
// SendEncryptedValue is false - replace with sentinel
|
|
444
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
445
490
|
}
|
|
446
491
|
}
|
|
447
492
|
|
|
@@ -559,6 +604,53 @@ export class ResolverBase {
|
|
|
559
604
|
}
|
|
560
605
|
}
|
|
561
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Resolves a User View by name, consulting the in-memory {@link UserViewEngine} cache first
|
|
609
|
+
* and falling back to a database `RunView` only on a cache miss.
|
|
610
|
+
*/
|
|
611
|
+
protected async resolveUserViewByName(
|
|
612
|
+
viewName: string,
|
|
613
|
+
provider: DatabaseProviderBase,
|
|
614
|
+
contextUser?: UserInfo,
|
|
615
|
+
): Promise<MJUserViewEntityExtended | null> {
|
|
616
|
+
if (UserViewEngine.Instance.Loaded) {
|
|
617
|
+
const cached = UserViewEngine.Instance.GetViewByName(viewName);
|
|
618
|
+
if (cached) {
|
|
619
|
+
return cached;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const rv = provider as unknown as IRunViewProvider;
|
|
623
|
+
const result = await rv.RunView<MJUserViewEntityExtended>({
|
|
624
|
+
EntityName: 'MJ: User Views',
|
|
625
|
+
// SECURITY: the view name is client-supplied and lands inside a SQL literal.
|
|
626
|
+
ExtraFilter: "Name='" + EscapeSQLString(viewName) + "'",
|
|
627
|
+
}, contextUser);
|
|
628
|
+
if (result && result.Success && result.Results.length > 0) {
|
|
629
|
+
return result.Results[0];
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Resolves a User View by ID, consulting the in-memory {@link UserViewEngine} cache first
|
|
636
|
+
* and falling back to an entity `Load()` only on a cache miss.
|
|
637
|
+
*/
|
|
638
|
+
protected async resolveUserViewByID(
|
|
639
|
+
viewID: string,
|
|
640
|
+
provider: DatabaseProviderBase,
|
|
641
|
+
contextUser?: UserInfo,
|
|
642
|
+
): Promise<MJUserViewEntityExtended | null> {
|
|
643
|
+
if (UserViewEngine.Instance.Loaded) {
|
|
644
|
+
const cached = UserViewEngine.Instance.GetViewById(viewID);
|
|
645
|
+
if (cached) {
|
|
646
|
+
return cached;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
|
|
650
|
+
await viewInfo.Load(viewID);
|
|
651
|
+
return viewInfo;
|
|
652
|
+
}
|
|
653
|
+
|
|
562
654
|
async RunViewByNameGeneric(viewInput: RunViewByNameInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
563
655
|
try {
|
|
564
656
|
// Log aggregate input for debugging
|
|
@@ -566,14 +658,8 @@ export class ResolverBase {
|
|
|
566
658
|
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 })))}`);
|
|
567
659
|
}
|
|
568
660
|
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
EntityName: 'MJ: User Views',
|
|
572
|
-
// SECURITY: the view name is client-supplied and lands inside a SQL literal.
|
|
573
|
-
ExtraFilter: "Name='" + EscapeSQLString(viewInput.ViewName) + "'",
|
|
574
|
-
}, userPayload.userRecord);
|
|
575
|
-
if (result && result.Success && result.Results.length > 0) {
|
|
576
|
-
const viewInfo = result.Results[0];
|
|
661
|
+
const viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
|
|
662
|
+
if (viewInfo) {
|
|
577
663
|
return this.RunViewGenericInternal(
|
|
578
664
|
provider,
|
|
579
665
|
viewInfo,
|
|
@@ -598,7 +684,10 @@ export class ResolverBase {
|
|
|
598
684
|
: undefined,
|
|
599
685
|
viewInput.BypassCache,
|
|
600
686
|
viewInput.DataSource,
|
|
601
|
-
viewInput.IncludeBinaryFields
|
|
687
|
+
viewInput.IncludeBinaryFields,
|
|
688
|
+
viewInput.AlternateBaseView,
|
|
689
|
+
viewInput.Properties,
|
|
690
|
+
viewInput.SkipTotalRowCount
|
|
602
691
|
);
|
|
603
692
|
}
|
|
604
693
|
else {
|
|
@@ -619,8 +708,10 @@ export class ResolverBase {
|
|
|
619
708
|
}
|
|
620
709
|
|
|
621
710
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
622
|
-
const viewInfo = await
|
|
623
|
-
|
|
711
|
+
const viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
|
|
712
|
+
if (!viewInfo) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
624
715
|
return this.RunViewGenericInternal(
|
|
625
716
|
provider,
|
|
626
717
|
viewInfo,
|
|
@@ -643,7 +734,10 @@ export class ResolverBase {
|
|
|
643
734
|
undefined,
|
|
644
735
|
viewInput.BypassCache,
|
|
645
736
|
viewInput.DataSource,
|
|
646
|
-
viewInput.IncludeBinaryFields
|
|
737
|
+
viewInput.IncludeBinaryFields,
|
|
738
|
+
viewInput.AlternateBaseView,
|
|
739
|
+
viewInput.Properties,
|
|
740
|
+
viewInput.SkipTotalRowCount
|
|
647
741
|
);
|
|
648
742
|
} catch (err) {
|
|
649
743
|
console.log(err);
|
|
@@ -691,7 +785,10 @@ export class ResolverBase {
|
|
|
691
785
|
undefined,
|
|
692
786
|
viewInput.BypassCache,
|
|
693
787
|
viewInput.DataSource,
|
|
694
|
-
viewInput.IncludeBinaryFields
|
|
788
|
+
viewInput.IncludeBinaryFields,
|
|
789
|
+
viewInput.AlternateBaseView,
|
|
790
|
+
undefined,
|
|
791
|
+
viewInput.SkipTotalRowCount
|
|
695
792
|
);
|
|
696
793
|
} catch (err) {
|
|
697
794
|
console.log(err);
|
|
@@ -712,15 +809,14 @@ export class ResolverBase {
|
|
|
712
809
|
let viewInfo: MJUserViewEntityExtended | null = null;
|
|
713
810
|
|
|
714
811
|
if (viewInput.ViewName) {
|
|
715
|
-
viewInfo =
|
|
812
|
+
viewInfo = await this.resolveUserViewByName(viewInput.ViewName, provider, userPayload.userRecord);
|
|
716
813
|
// Populate EntityName on the input so callers (e.g. RunViews resolver) can
|
|
717
814
|
// look up the entity without re-querying the view
|
|
718
815
|
if (viewInfo && !viewInput.EntityName) {
|
|
719
816
|
viewInput.EntityName = viewInfo.Entity;
|
|
720
817
|
}
|
|
721
818
|
} else if (viewInput.ViewID) {
|
|
722
|
-
viewInfo = await
|
|
723
|
-
await viewInfo.Load(viewInput.ViewID);
|
|
819
|
+
viewInfo = await this.resolveUserViewByID(viewInput.ViewID, provider, contextUser);
|
|
724
820
|
// Populate EntityName on the input so callers can look up the entity
|
|
725
821
|
if (viewInfo && !viewInput.EntityName) {
|
|
726
822
|
viewInput.EntityName = viewInfo.Entity;
|
|
@@ -767,6 +863,9 @@ export class ResolverBase {
|
|
|
767
863
|
bypassCache: viewInput.BypassCache,
|
|
768
864
|
dataSource: viewInput.DataSource,
|
|
769
865
|
includeBinaryFields: viewInput.IncludeBinaryFields,
|
|
866
|
+
skipTotalRowCount: viewInput.SkipTotalRowCount,
|
|
867
|
+
alternateBaseView: viewInput.AlternateBaseView,
|
|
868
|
+
properties: viewInput.Properties,
|
|
770
869
|
});
|
|
771
870
|
} catch (err) {
|
|
772
871
|
LogError(err);
|
|
@@ -778,6 +877,31 @@ export class ResolverBase {
|
|
|
778
877
|
return results;
|
|
779
878
|
}
|
|
780
879
|
|
|
880
|
+
/**
|
|
881
|
+
* What an entity event may carry to a CloudEvents sink or a log line.
|
|
882
|
+
*
|
|
883
|
+
* The whole row for an entity without field security. For one where it applies (its own flag
|
|
884
|
+
* or an IS-A ancestor's), and for `MJ: Record Changes` (whose payload columns hold other
|
|
885
|
+
* entities' values), only the primary key and the names of the fields that changed: the sink
|
|
886
|
+
* is outside MemberJunction with no reader to project the row for, and these events fire for
|
|
887
|
+
* server-internal saves too, whose objects hold every column.
|
|
888
|
+
*/
|
|
889
|
+
protected EntityEventPayload(baseEntity: BaseEntity, changedFieldNames?: string[]): Record<string, unknown> {
|
|
890
|
+
const entityInfo = baseEntity.EntityInfo;
|
|
891
|
+
if (!entityInfo?.FieldLevelSecurityApplies && !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo)) {
|
|
892
|
+
// fls-guard: entity without field security and not the audit trail; whole row is the documented payload
|
|
893
|
+
return baseEntity.GetAll();
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
// Read through Get() rather than PrimaryKey, which builds every field object of a raw-mode record.
|
|
897
|
+
PrimaryKey: (entityInfo.PrimaryKeys ?? []).map((pk) => ({ FieldName: pk.Name, Value: baseEntity.Get(pk.Name) })),
|
|
898
|
+
// A completed save has already rebased the object, so nothing on it is dirty: the event
|
|
899
|
+
// carries the names the save wrote. Other events (save_started) still read them off the object.
|
|
900
|
+
// GetDirtyFieldNames answers without building field objects for a record still in raw mode.
|
|
901
|
+
ChangedFields: changedFieldNames ?? baseEntity.GetDirtyFieldNames(),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
781
905
|
private static _priorEmittedData: {Entity: string, PKey: CompositeKey}[] = [];
|
|
782
906
|
protected async EmitCloudEvent({ component, event, eventCode, args }: MJEvent) {
|
|
783
907
|
if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
|
|
@@ -785,7 +909,7 @@ export class ResolverBase {
|
|
|
785
909
|
const type = `MemberJunction.${event}${extendedType}`;
|
|
786
910
|
const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
|
|
787
911
|
const subject = args instanceof BaseEntityEvent ? args.baseEntity.EntityInfo.CodeName : undefined;
|
|
788
|
-
const data = args?.baseEntity
|
|
912
|
+
const data = args?.baseEntity ? this.EntityEventPayload(args.baseEntity, args.ChangedFieldNames) : {};
|
|
789
913
|
|
|
790
914
|
const cloudEvent = new CloudEvent({ type, source, subject, data });
|
|
791
915
|
|
|
@@ -1007,7 +1131,7 @@ export class ResolverBase {
|
|
|
1007
1131
|
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
1008
1132
|
*
|
|
1009
1133
|
* The references come from a walk of the whole parse tree
|
|
1010
|
-
* (`
|
|
1134
|
+
* (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
|
|
1011
1135
|
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
1012
1136
|
* a row list).
|
|
1013
1137
|
*
|
|
@@ -1028,14 +1152,7 @@ export class ResolverBase {
|
|
|
1028
1152
|
}
|
|
1029
1153
|
const dialect = this.dialectForProvider(provider);
|
|
1030
1154
|
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
1031
|
-
const
|
|
1032
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
1033
|
-
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
1034
|
-
}
|
|
1035
|
-
if (this.astContainsWriteNode(parser.AST)) {
|
|
1036
|
-
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
1037
|
-
}
|
|
1038
|
-
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
1155
|
+
const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
|
|
1039
1156
|
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
1040
1157
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
1041
1158
|
for (const relation of reads.Relations) {
|
|
@@ -1104,9 +1221,19 @@ export class ResolverBase {
|
|
|
1104
1221
|
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
1105
1222
|
}
|
|
1106
1223
|
|
|
1107
|
-
/**
|
|
1108
|
-
|
|
1109
|
-
|
|
1224
|
+
/**
|
|
1225
|
+
* Parses a whole statement with the strict validator and returns what it reads; throws unless it
|
|
1226
|
+
* is one plain read-only SELECT with no write or DDL at any depth.
|
|
1227
|
+
*/
|
|
1228
|
+
private strictReadOnlyStatementReads(sqlText: string, dialect: SQLParserDialect, label: string): StrictStatementReads {
|
|
1229
|
+
const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
|
|
1230
|
+
if (result.Refusal === 'NestedWrite') {
|
|
1231
|
+
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
1232
|
+
}
|
|
1233
|
+
if (!result.IsValid || !result.Reads) {
|
|
1234
|
+
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
1235
|
+
}
|
|
1236
|
+
return result.Reads;
|
|
1110
1237
|
}
|
|
1111
1238
|
|
|
1112
1239
|
private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
|
|
@@ -1145,7 +1272,10 @@ export class ResolverBase {
|
|
|
1145
1272
|
afterKey?: CompositeKey,
|
|
1146
1273
|
bypassCache?: boolean,
|
|
1147
1274
|
dataSource?: 'Live' | 'Materialized',
|
|
1148
|
-
includeBinaryFields?: boolean
|
|
1275
|
+
includeBinaryFields?: boolean,
|
|
1276
|
+
alternateBaseView?: string,
|
|
1277
|
+
properties?: ViewPropertyValues,
|
|
1278
|
+
skipTotalRowCount?: boolean
|
|
1149
1279
|
) {
|
|
1150
1280
|
try {
|
|
1151
1281
|
if (!viewInfo || !userPayload) return null;
|
|
@@ -1207,6 +1337,7 @@ export class ResolverBase {
|
|
|
1207
1337
|
{
|
|
1208
1338
|
ViewID: viewInfo.ID,
|
|
1209
1339
|
ViewName: viewInfo.Name,
|
|
1340
|
+
ViewEntity: viewInfo.ID ? viewInfo : undefined,
|
|
1210
1341
|
EntityName: viewInfo.Entity,
|
|
1211
1342
|
ExtraFilter: extraFilter,
|
|
1212
1343
|
OrderBy: orderBy,
|
|
@@ -1227,10 +1358,24 @@ export class ResolverBase {
|
|
|
1227
1358
|
BypassCache: bypassCache,
|
|
1228
1359
|
DataSource: dataSource,
|
|
1229
1360
|
IncludeBinaryFields: includeBinaryFields,
|
|
1361
|
+
SkipTotalRowCount: skipTotalRowCount,
|
|
1362
|
+
AlternateBaseView: alternateBaseView,
|
|
1363
|
+
Properties: properties,
|
|
1364
|
+
TimeZone: userPayload?.TimeZone,
|
|
1365
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1230
1366
|
},
|
|
1231
1367
|
user
|
|
1232
1368
|
);
|
|
1233
1369
|
|
|
1370
|
+
if (result && typeof result === 'object') {
|
|
1371
|
+
Object.defineProperty(result, '_resolvedViewEntity', {
|
|
1372
|
+
value: viewInfo,
|
|
1373
|
+
enumerable: false,
|
|
1374
|
+
configurable: true,
|
|
1375
|
+
writable: true,
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1234
1379
|
// Log aggregate results for debugging
|
|
1235
1380
|
if (aggregates?.length) {
|
|
1236
1381
|
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)}`);
|
|
@@ -1261,7 +1406,8 @@ export class ResolverBase {
|
|
|
1261
1406
|
await this.ArrayFilterEncryptedFieldsForAPI(
|
|
1262
1407
|
viewInfo.Entity,
|
|
1263
1408
|
result.Results as Record<string, unknown>[],
|
|
1264
|
-
user
|
|
1409
|
+
user,
|
|
1410
|
+
md
|
|
1265
1411
|
);
|
|
1266
1412
|
}
|
|
1267
1413
|
|
|
@@ -1357,6 +1503,7 @@ export class ResolverBase {
|
|
|
1357
1503
|
runViewParams.push({
|
|
1358
1504
|
ViewID: param.viewInfo.ID,
|
|
1359
1505
|
ViewName: param.viewInfo.Name,
|
|
1506
|
+
ViewEntity: param.viewInfo.ID ? param.viewInfo : undefined,
|
|
1360
1507
|
EntityName: param.viewInfo.Entity,
|
|
1361
1508
|
ExtraFilter: param.extraFilter,
|
|
1362
1509
|
OrderBy: param.orderBy,
|
|
@@ -1377,6 +1524,11 @@ export class ResolverBase {
|
|
|
1377
1524
|
BypassCache: param.bypassCache,
|
|
1378
1525
|
DataSource: param.dataSource,
|
|
1379
1526
|
IncludeBinaryFields: param.includeBinaryFields,
|
|
1527
|
+
SkipTotalRowCount: param.skipTotalRowCount,
|
|
1528
|
+
AlternateBaseView: param.alternateBaseView,
|
|
1529
|
+
Properties: param.properties,
|
|
1530
|
+
TimeZone: param.userPayload?.TimeZone,
|
|
1531
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1380
1532
|
});
|
|
1381
1533
|
}
|
|
1382
1534
|
|
|
@@ -1398,7 +1550,8 @@ export class ResolverBase {
|
|
|
1398
1550
|
await this.ArrayFilterEncryptedFieldsForAPI(
|
|
1399
1551
|
entityName,
|
|
1400
1552
|
runViewResult.Results as Record<string, unknown>[],
|
|
1401
|
-
contextUser
|
|
1553
|
+
contextUser,
|
|
1554
|
+
md
|
|
1402
1555
|
);
|
|
1403
1556
|
}
|
|
1404
1557
|
}
|
|
@@ -1537,8 +1690,9 @@ export class ResolverBase {
|
|
|
1537
1690
|
* skip redundant re-fetches (it already handled the event locally).
|
|
1538
1691
|
*
|
|
1539
1692
|
* The row itself rides along ONLY for entities opted in via
|
|
1540
|
-
* `cacheSettings.recordDataBroadcastEntities
|
|
1541
|
-
*
|
|
1693
|
+
* `cacheSettings.recordDataBroadcastEntities`, and never for one under field-level security —
|
|
1694
|
+
* this event reaches every connected client unprojected, so the row would otherwise be readable
|
|
1695
|
+
* by sessions that could not read the record or its restricted fields.
|
|
1542
1696
|
*/
|
|
1543
1697
|
protected PublishCacheInvalidation(entityObject: BaseEntity, action: 'save' | 'delete', userPayload: UserPayload): void {
|
|
1544
1698
|
const entityName = entityObject.EntityInfo.Name;
|
|
@@ -1550,7 +1704,8 @@ export class ResolverBase {
|
|
|
1550
1704
|
timestamp: new Date(),
|
|
1551
1705
|
originSessionId: userPayload?.sessionId || null,
|
|
1552
1706
|
recordData:
|
|
1553
|
-
action === 'save' && MayBroadcastRecordData(entityName)
|
|
1707
|
+
action === 'save' && MayBroadcastRecordData(entityName, entityObject.EntityInfo)
|
|
1708
|
+
// fls-guard: MayBroadcastRecordData refuses FLS entities and Record Changes
|
|
1554
1709
|
? JSON.stringify(entityObject.GetAll())
|
|
1555
1710
|
: undefined,
|
|
1556
1711
|
});
|
|
@@ -1558,27 +1713,56 @@ export class ResolverBase {
|
|
|
1558
1713
|
|
|
1559
1714
|
/**
|
|
1560
1715
|
* Publishes a push-status update to the client on {@link PUSH_STATUS_UPDATES_TOPIC}, stamping the
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1716
|
+
* owner key of `userPayload` (`StatusUpdateOwnerID`: the request's user, or the widget guest an
|
|
1717
|
+
* elevated payload stands in for) so the subscription filter can bind delivery to identity (see B49 /
|
|
1718
|
+
* `StatusUpdatesFilter`). The ergonomic wrapper every resolver should use instead of calling
|
|
1719
|
+
* `pubSub.publish` on the topic directly — it makes omitting identity impossible. Non-resolver
|
|
1720
|
+
* publishers (services, the liveness heartbeat) call the shared `PublishStatusUpdate()` function
|
|
1721
|
+
* directly with an explicit `ownerUserId`.
|
|
1566
1722
|
*/
|
|
1567
1723
|
protected PublishStatusUpdate(pubSub: PubSubEngine, sessionId: string, message: string | undefined, userPayload: UserPayload): void {
|
|
1568
1724
|
PublishStatusUpdate(pubSub, {
|
|
1569
1725
|
sessionId,
|
|
1570
|
-
ownerUserId: userPayload
|
|
1726
|
+
ownerUserId: StatusUpdateOwnerID(userPayload),
|
|
1571
1727
|
message,
|
|
1572
1728
|
});
|
|
1573
1729
|
}
|
|
1574
1730
|
|
|
1575
1731
|
protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1732
|
+
// Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
|
|
1733
|
+
// per event, while routing WebSocket status updates dynamically across all active user sessions
|
|
1734
|
+
// that have interacted with this entity type (instead of closing over only the first caller).
|
|
1579
1735
|
const uniqueKey = entityObject.EntityInfo.Name;
|
|
1736
|
+
const now = Date.now();
|
|
1737
|
+
const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
|
|
1738
|
+
|
|
1739
|
+
let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
|
|
1740
|
+
if (!sessionsForEntity) {
|
|
1741
|
+
sessionsForEntity = new Map<string, ActiveEntitySessionListener>();
|
|
1742
|
+
this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
|
|
1743
|
+
} else {
|
|
1744
|
+
// Opportunistically evict expired session entries and enforce a safe per-entity cap
|
|
1745
|
+
for (const [key, entry] of sessionsForEntity) {
|
|
1746
|
+
if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1747
|
+
sessionsForEntity.delete(key);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
|
|
1751
|
+
const oldestKey = sessionsForEntity.keys().next().value;
|
|
1752
|
+
if (oldestKey !== undefined) {
|
|
1753
|
+
sessionsForEntity.delete(oldestKey);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (userPayload) {
|
|
1759
|
+
const sessionKey = userPayload.sessionId
|
|
1760
|
+
? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
|
|
1761
|
+
: (userPayload.userRecord?.ID ?? '__anonymous__');
|
|
1762
|
+
sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
|
|
1763
|
+
}
|
|
1580
1764
|
|
|
1581
|
-
if (
|
|
1765
|
+
if (isFreshSubscription) {
|
|
1582
1766
|
// 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
|
|
1583
1767
|
LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
|
|
1584
1768
|
const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event: MJEvent) => {
|
|
@@ -1591,23 +1775,29 @@ export class ResolverBase {
|
|
|
1591
1775
|
return;
|
|
1592
1776
|
}
|
|
1593
1777
|
|
|
1594
|
-
const baseEntityValues = baseEntity ? baseEntity.
|
|
1595
|
-
const eventToLog = { entityName:
|
|
1778
|
+
const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
|
|
1779
|
+
const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
|
|
1596
1780
|
LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
|
|
1597
1781
|
await this.EmitCloudEvent(event);
|
|
1598
|
-
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1782
|
+
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1599
1783
|
|
|
1600
1784
|
if (event.args && event.args instanceof BaseEntityEvent) {
|
|
1601
|
-
const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1785
|
+
const activeSessions = this.activeEntitySessions.get(uniqueKey);
|
|
1786
|
+
if (activeSessions && activeSessions.size > 0) {
|
|
1787
|
+
const eventNow = Date.now();
|
|
1788
|
+
let serializedMessage: string | undefined;
|
|
1789
|
+
for (const [sKey, session] of activeSessions) {
|
|
1790
|
+
if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1791
|
+
activeSessions.delete(sKey);
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
|
|
1795
|
+
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
1796
|
+
serializedMessage ??= this.EntityStatusMessage(event.args);
|
|
1797
|
+
this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1611
1801
|
}
|
|
1612
1802
|
}
|
|
1613
1803
|
});
|
|
@@ -1615,6 +1805,37 @@ export class ResolverBase {
|
|
|
1615
1805
|
}
|
|
1616
1806
|
}
|
|
1617
1807
|
|
|
1808
|
+
/**
|
|
1809
|
+
* Whether an entity event may be relayed to the session whose call created the listener. The
|
|
1810
|
+
* listener is shared by every caller in the process for that entity, so an event raised by an
|
|
1811
|
+
* object loaded for another user (or for the system user) is never relayed to this session.
|
|
1812
|
+
*/
|
|
1813
|
+
protected EntityEventBelongsToSession(event: BaseEntityEvent, userPayload: UserPayload): boolean {
|
|
1814
|
+
const sessionUserId = userPayload?.userRecord?.ID;
|
|
1815
|
+
const objectUserId = event.baseEntity?.ContextCurrentUser?.ID;
|
|
1816
|
+
return Boolean(sessionUserId && objectUserId && UUIDsEqual(sessionUserId, objectUserId));
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* The status message relayed for an entity event: the entity, key and event type. The event's
|
|
1821
|
+
* payload is included only when it cannot carry record values: never for a delete (its payload
|
|
1822
|
+
* is the whole deleted row), an entity under field-level security, or `MJ: Record Changes`.
|
|
1823
|
+
*/
|
|
1824
|
+
protected EntityStatusMessage(event: BaseEntityEvent): string {
|
|
1825
|
+
const entityInfo = event.baseEntity.EntityInfo;
|
|
1826
|
+
const mayCarryPayload = event.type !== 'delete'
|
|
1827
|
+
&& !entityInfo.FieldLevelSecurityApplies
|
|
1828
|
+
&& !RecordChangeFieldSecurityProjector.IsRecordChangesEntity(entityInfo);
|
|
1829
|
+
return JSON.stringify({
|
|
1830
|
+
status: 'OK',
|
|
1831
|
+
type: 'EntityObjectStatusMessage',
|
|
1832
|
+
entityName: entityInfo.Name,
|
|
1833
|
+
primaryKey: event.baseEntity.PrimaryKey,
|
|
1834
|
+
eventType: event.type,
|
|
1835
|
+
message: mayCarryPayload ? event.payload : undefined,
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1618
1839
|
/**
|
|
1619
1840
|
* IS-A PROMOTION over the wire: an EXISTING parent record gaining a subtype — an Animal that is
|
|
1620
1841
|
* now also a Dog. It arrives as the CHILD's create mutation carrying the existing parent's
|
|
@@ -1691,6 +1912,9 @@ export class ResolverBase {
|
|
|
1691
1912
|
// IS-A promotion: bind the new child to its EXISTING parent row BEFORE the field
|
|
1692
1913
|
// assignments, so values the client sent for parent fields land on the loaded parent
|
|
1693
1914
|
// (as an update) instead of being wiped by the load.
|
|
1915
|
+
// Field-level security: refuse a key the caller may not read, or that names no field, before
|
|
1916
|
+
// anything is applied (the same gate as the update path).
|
|
1917
|
+
AssertClientInputWritableForUser(entityObject.EntityInfo, fieldsForSet, this.GetUserFromPayload(userPayload));
|
|
1694
1918
|
await this.attachToExistingParentIfPromotion(entityObject, fieldsForSet);
|
|
1695
1919
|
entityObject.SetMany(fieldsForSet);
|
|
1696
1920
|
|
|
@@ -1707,6 +1931,7 @@ export class ResolverBase {
|
|
|
1707
1931
|
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1708
1932
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
1709
1933
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1934
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
1710
1935
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
1711
1936
|
}
|
|
1712
1937
|
// save failed, throw error with message
|
|
@@ -1747,12 +1972,11 @@ export class ResolverBase {
|
|
|
1747
1972
|
});
|
|
1748
1973
|
|
|
1749
1974
|
// Field-level security: any value the client sends for a field it cannot READ is
|
|
1750
|
-
// fabricated by construction (the field was
|
|
1751
|
-
// received), so
|
|
1752
|
-
// denied we also force the truth-load branch below
|
|
1753
|
-
//
|
|
1754
|
-
|
|
1755
|
-
const hasDeniedReadFields = this.StripDeniedReadFieldsFromClientInput(entityInfo, userInfo, input, clientNewValues);
|
|
1975
|
+
// fabricated by construction (the field was absent from every payload the client ever
|
|
1976
|
+
// received), so the input is refused before anything applies it. When any field is
|
|
1977
|
+
// denied we also force the truth-load branch below, so the denied columns keep what the
|
|
1978
|
+
// database holds rather than anything derived from the client's OldValues.
|
|
1979
|
+
const hasDeniedReadFields = this.AssertClientInputFieldSecurity(entityInfo, userInfo, input, clientNewValues);
|
|
1756
1980
|
// The audit-trail equivalent. Its `true` matters most on THIS entity: 'MJ: Record Changes'
|
|
1757
1981
|
// has TrackRecordChanges off, so without it a client sending OldValues takes the branch below
|
|
1758
1982
|
// that hydrates from those values — never loading what the database actually holds.
|
|
@@ -1815,6 +2039,7 @@ export class ResolverBase {
|
|
|
1815
2039
|
// Cache invalidation is now handled globally by the MJGlobal listener in index.ts
|
|
1816
2040
|
|
|
1817
2041
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
2042
|
+
// fls-guard: MapFieldNamesToCodeNames drops the fields the caller may not read
|
|
1818
2043
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
|
|
1819
2044
|
} else {
|
|
1820
2045
|
// CompleteMessage, not Message. A validation refusal puts its reasons in `Errors` and leaves
|
|
@@ -1871,7 +2096,7 @@ export class ResolverBase {
|
|
|
1871
2096
|
* writable fields in `OldValues___`, so {@link hasCompleteOldValues} returns `false` and forces the
|
|
1872
2097
|
* database load (`InnerLoad`) so untouched columns retain their stored values.
|
|
1873
2098
|
*
|
|
1874
|
-
* `
|
|
2099
|
+
* `FieldLevelSecurityApplies` (own flag or an IS-A ancestor's) is the security-critical one, and it is NOT redundant with
|
|
1875
2100
|
* `hasDeniedReadFields`. That flag reports READ denials, and the canonical FLS configuration is
|
|
1876
2101
|
* Read Allow + Update Deny — which leaves it false. Such a caller would be hydrated from its own
|
|
1877
2102
|
* `OldValues___`, and a value it supplies there for an update-denied field arrives through
|
|
@@ -1926,73 +2151,50 @@ export class ResolverBase {
|
|
|
1926
2151
|
!this.hasCompleteOldValues(entityInfo, input.OldValues___) ||
|
|
1927
2152
|
hasDeniedReadFields ||
|
|
1928
2153
|
hasNarrowedAuditPayload ||
|
|
1929
|
-
entityInfo.
|
|
2154
|
+
entityInfo.FieldLevelSecurityApplies ||
|
|
1930
2155
|
!user ||
|
|
1931
2156
|
entityInfo.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Update, '').length > 0
|
|
1932
2157
|
);
|
|
1933
2158
|
}
|
|
1934
2159
|
|
|
1935
2160
|
/**
|
|
1936
|
-
* Field-level security
|
|
2161
|
+
* Field-level security gate on the update path's client input.
|
|
1937
2162
|
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
*
|
|
1942
|
-
* save" safe for a restricted user.
|
|
2163
|
+
* For a caller denied at least one field on the entity, refuses (with a `FieldSecurityError`)
|
|
2164
|
+
* any key in the new values or in `OldValues___` that names a field the caller cannot READ, or
|
|
2165
|
+
* that names no field at all, with the same message for both (see
|
|
2166
|
+
* `AssertClientInputWritableForUser`), so the refusal does not reveal which columns exist.
|
|
1943
2167
|
*
|
|
1944
|
-
*
|
|
1945
|
-
*
|
|
2168
|
+
* Refused rather than dropped: a field the caller cannot read was absent from every payload the
|
|
2169
|
+
* client received, so a value for it can only be fabricated. A well-behaved client never sends
|
|
2170
|
+
* one (it omits fields it did not load), so ordinary traffic is unaffected.
|
|
1946
2171
|
*
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1949
|
-
* and is restricted).
|
|
2172
|
+
* There is no split by update permission: Read is required for Update, so a user denied read is
|
|
2173
|
+
* denied update too.
|
|
1950
2174
|
*
|
|
1951
|
-
* Returns true when the user has a non-empty denied-read set on this entity. The caller
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
2175
|
+
* Returns true when the user has a non-empty denied-read set on this entity. The caller must
|
|
2176
|
+
* then hydrate the entity from the DATABASE (never from client OldValues), so denied fields hold
|
|
2177
|
+
* true values the client input never touches.
|
|
1954
2178
|
*/
|
|
1955
|
-
protected
|
|
2179
|
+
protected AssertClientInputFieldSecurity(
|
|
1956
2180
|
entityInfo: EntityInfo,
|
|
1957
2181
|
userInfo: UserInfo,
|
|
1958
2182
|
input: { OldValues___?: Array<{ Key: string; Value: unknown }> } & Record<string, unknown>,
|
|
1959
2183
|
clientNewValues: Record<string, unknown>
|
|
1960
2184
|
): boolean {
|
|
1961
|
-
if (!entityInfo.
|
|
2185
|
+
if (!entityInfo.FieldLevelSecurityApplies || !userInfo) {
|
|
1962
2186
|
return false;
|
|
1963
2187
|
}
|
|
1964
|
-
|
|
1965
|
-
if (deniedReadNames.size === 0) {
|
|
2188
|
+
if (entityInfo.GetDeniedReadFields(userInfo).size === 0) {
|
|
1966
2189
|
return false;
|
|
1967
2190
|
}
|
|
1968
|
-
|
|
1969
|
-
// Input keys are entity CodeNames (post ReverseMapInputFieldNames); the denied set holds
|
|
1970
|
-
// lowercased field Names — bridge via the field metadata once.
|
|
1971
|
-
const deniedReadCodeNames = new Set<string>();
|
|
1972
|
-
for (const field of entityInfo.Fields) {
|
|
1973
|
-
if (deniedReadNames.has(field.Name.trim().toLowerCase())) {
|
|
1974
|
-
deniedReadCodeNames.add(field.CodeName.trim().toLowerCase());
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
const stripped: string[] = [];
|
|
1979
|
-
for (const key of Object.keys(clientNewValues)) {
|
|
1980
|
-
if (deniedReadCodeNames.has(key.trim().toLowerCase())) {
|
|
1981
|
-
delete clientNewValues[key];
|
|
1982
|
-
delete input[key];
|
|
1983
|
-
stripped.push(key);
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
2191
|
+
AssertClientInputWritableForUser(entityInfo, clientNewValues, userInfo);
|
|
1986
2192
|
if (Array.isArray(input.OldValues___)) {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
LogDebug(
|
|
1993
|
-
`[FieldSecurity] UpdateRecord on '${entityInfo.Name}' for user ${userInfo.Email}: ` +
|
|
1994
|
-
`stripped client-sent value(s) for denied-read field(s) ${stripped.join(', ')}`
|
|
1995
|
-
);
|
|
2193
|
+
const oldValueKeys: Record<string, unknown> = {};
|
|
2194
|
+
for (const item of input.OldValues___) {
|
|
2195
|
+
oldValueKeys[String(item.Key)] = null;
|
|
2196
|
+
}
|
|
2197
|
+
AssertClientInputWritableForUser(entityInfo, oldValueKeys, userInfo);
|
|
1996
2198
|
}
|
|
1997
2199
|
return true;
|
|
1998
2200
|
}
|
|
@@ -2120,139 +2322,335 @@ export class ResolverBase {
|
|
|
2120
2322
|
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
2121
2323
|
}
|
|
2122
2324
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2325
|
+
/**
|
|
2326
|
+
* Whether the API never gives a client this field's stored value: an encrypted field that is not
|
|
2327
|
+
* decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
|
|
2328
|
+
* client's old value for it can never be compared with the database value.
|
|
2329
|
+
*/
|
|
2330
|
+
private isFieldOpaqueToClient(field: EntityFieldInfo | undefined): boolean {
|
|
2331
|
+
return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* The client's new values without the opaque fields it only echoed back: the sentinel, or the same
|
|
2336
|
+
* value it received as the old value. Writing either would replace the stored secret.
|
|
2337
|
+
*/
|
|
2338
|
+
private withoutEchoedOpaqueValues<TOldVal, TNewVal>(
|
|
2339
|
+
entityInfo: EntityInfo,
|
|
2340
|
+
clientNewValues: Record<string, TNewVal>,
|
|
2341
|
+
clientOldValues: Record<string, TOldVal>,
|
|
2342
|
+
): Record<string, TNewVal> {
|
|
2343
|
+
const kept: Record<string, TNewVal> = {};
|
|
2344
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2345
|
+
const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
|
|
2346
|
+
const newValue = clientNewValues[key];
|
|
2347
|
+
const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
|
|
2348
|
+
const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
|
|
2349
|
+
if (!echoed) {
|
|
2350
|
+
kept[key] = newValue;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return kept;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
|
|
2358
|
+
* the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
|
|
2359
|
+
*/
|
|
2360
|
+
private redactEncryptedConflictValues<TValue>(
|
|
2361
|
+
entityInfo: EntityInfo,
|
|
2362
|
+
entries: ReadonlyArray<ConflictDiagnosticEntry<TValue>>,
|
|
2363
|
+
): ConflictDiagnosticEntry<TValue | string>[] {
|
|
2364
|
+
return entries.map((entry) => {
|
|
2365
|
+
if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
|
|
2366
|
+
return entry;
|
|
2367
|
+
}
|
|
2368
|
+
const redacted: ConflictDiagnosticEntry<TValue | string> = { FieldName: entry.FieldName };
|
|
2369
|
+
if ('ClientOldValue' in entry) redacted.ClientOldValue = ENCRYPTED_SENTINEL;
|
|
2370
|
+
if ('ClientNewValue' in entry) redacted.ClientNewValue = ENCRYPTED_SENTINEL;
|
|
2371
|
+
if ('DBValue' in entry) redacted.DBValue = ENCRYPTED_SENTINEL;
|
|
2372
|
+
return redacted;
|
|
2132
2373
|
});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
/**
|
|
2377
|
+
* Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
|
|
2378
|
+
* (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
|
|
2379
|
+
*/
|
|
2380
|
+
private areEntityFieldValuesDifferent<TLeft, TRight>(
|
|
2381
|
+
field: EntityFieldInfo | undefined,
|
|
2382
|
+
left: TLeft,
|
|
2383
|
+
right: TRight,
|
|
2384
|
+
allowTimezoneShiftWarning: boolean,
|
|
2385
|
+
fieldKeyForLog?: string
|
|
2386
|
+
): boolean {
|
|
2387
|
+
const normalizedLeft = left === undefined ? null : left;
|
|
2388
|
+
const normalizedRight = right === undefined ? null : right;
|
|
2389
|
+
|
|
2390
|
+
if (normalizedLeft instanceof Date) {
|
|
2391
|
+
const rightDate =
|
|
2392
|
+
normalizedRight instanceof Date
|
|
2393
|
+
? normalizedRight
|
|
2394
|
+
: field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
|
|
2395
|
+
? this.ClientOldValueToFieldValue(field, normalizedRight)
|
|
2396
|
+
: null;
|
|
2397
|
+
if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
|
|
2398
|
+
return true;
|
|
2399
|
+
}
|
|
2400
|
+
if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
|
|
2401
|
+
return false;
|
|
2402
|
+
}
|
|
2403
|
+
const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
|
|
2404
|
+
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
|
|
2405
|
+
if (allowTimezoneShiftWarning && fieldKeyForLog) {
|
|
2406
|
+
console.warn(
|
|
2407
|
+
`Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
|
|
2408
|
+
`Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
|
|
2409
|
+
`Consider using datetimeoffset to avoid timezone ambiguity.`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
return false;
|
|
2413
|
+
}
|
|
2414
|
+
return true;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
|
|
2418
|
+
const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
|
|
2419
|
+
if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
|
|
2420
|
+
return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
|
|
2425
|
+
return false;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== (normalizedRight as never);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* True when two instants would be stored as the same value in the field's column: SQL Server
|
|
2433
|
+
* `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
|
|
2434
|
+
*/
|
|
2435
|
+
private datesEqualAtColumnResolution(field: EntityFieldInfo | undefined, left: Date, right: Date): boolean {
|
|
2436
|
+
return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
private dateColumnResolutionMs(field: EntityFieldInfo | undefined): number {
|
|
2440
|
+
switch ((field?.Type ?? '').trim().toLowerCase()) {
|
|
2441
|
+
case 'datetime':
|
|
2442
|
+
return 1000 / 300;
|
|
2443
|
+
case 'smalldatetime':
|
|
2444
|
+
return 60_000;
|
|
2445
|
+
default:
|
|
2446
|
+
return 1;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/**
|
|
2451
|
+
* True when both strings are JSON documents with the same content, ignoring whitespace and key order.
|
|
2452
|
+
*/
|
|
2453
|
+
private jsonStringsEquivalent(left: string, right: string): boolean {
|
|
2454
|
+
const canonicalLeft = this.canonicalJSONDocument(left);
|
|
2455
|
+
return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
|
|
2456
|
+
}
|
|
2133
2457
|
|
|
2134
|
-
|
|
2135
|
-
|
|
2458
|
+
/**
|
|
2459
|
+
* Canonical form of a JSON object or array string, or null when the text is not one.
|
|
2460
|
+
*/
|
|
2461
|
+
private canonicalJSONDocument(text: string): string | null {
|
|
2462
|
+
const first = text.trimStart().charAt(0);
|
|
2463
|
+
if (first !== '{' && first !== '[') {
|
|
2464
|
+
return null;
|
|
2465
|
+
}
|
|
2466
|
+
try {
|
|
2467
|
+
return Canonicalize(JSON.parse(text));
|
|
2468
|
+
} catch {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
protected async TestAndSetClientOldValuesToDBValues<
|
|
2474
|
+
TOldVal,
|
|
2475
|
+
TNewVal,
|
|
2476
|
+
TInput extends { OldValues___?: ReadonlyArray<{ Key: string; Value: TOldVal }> | null }
|
|
2477
|
+
>(
|
|
2478
|
+
input: TInput,
|
|
2479
|
+
rawClientNewValues: Record<string, TNewVal>,
|
|
2480
|
+
entityObject: BaseEntity,
|
|
2481
|
+
contextUser: UserInfo
|
|
2482
|
+
): Promise<void> {
|
|
2483
|
+
type ConvertedFieldValue = ReturnType<ResolverBase['ClientOldValueToFieldValue']>;
|
|
2484
|
+
const clientOldValues: Record<string, ConvertedFieldValue> = {};
|
|
2485
|
+
for (const item of input.OldValues___ ?? []) {
|
|
2486
|
+
// Transform string-serialized wire values to match the field's TypeScript type
|
|
2487
|
+
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
2488
|
+
clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
|
|
2489
|
+
}
|
|
2490
|
+
const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
|
|
2491
|
+
|
|
2492
|
+
// fls-guard: compared on the server only, never returned
|
|
2136
2493
|
const dbValues = entityObject.GetAll();
|
|
2137
2494
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2495
|
+
interface DBDifferenceEntry {
|
|
2496
|
+
FieldName: string;
|
|
2497
|
+
ClientOldValue: ConvertedFieldValue;
|
|
2498
|
+
DBValue: ConvertedFieldValue;
|
|
2499
|
+
}
|
|
2500
|
+
interface ClientDifferenceEntry {
|
|
2501
|
+
FieldName: string;
|
|
2502
|
+
ClientOldValue: ConvertedFieldValue;
|
|
2503
|
+
ClientNewValue: TNewVal;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
const dbDifferences: DBDifferenceEntry[] = [];
|
|
2507
|
+
const dbDiffByField = new Map<string, DBDifferenceEntry>();
|
|
2508
|
+
|
|
2509
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
2141
2510
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
switch (typeof clientOldValues[key]) {
|
|
2145
|
-
case 'number':
|
|
2146
|
-
different = clientOldValues[key] !== dbVal;
|
|
2147
|
-
break;
|
|
2148
|
-
case 'boolean':
|
|
2149
|
-
different = clientOldValues[key] !== dbVal;
|
|
2150
|
-
break;
|
|
2151
|
-
case 'object':
|
|
2152
|
-
if (clientOldValues[key] instanceof Date) {
|
|
2153
|
-
const clientDate = clientOldValues[key] as Date;
|
|
2154
|
-
const dbDate = dbVal;
|
|
2155
|
-
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
2156
|
-
different = true;
|
|
2157
|
-
} else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
2158
|
-
// Check if this is a datetime/datetime2 field with only a timezone hour shift
|
|
2159
|
-
const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
|
|
2160
|
-
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
|
|
2161
|
-
console.warn(
|
|
2162
|
-
`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
|
|
2163
|
-
`Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
|
|
2164
|
-
`Consider using datetimeoffset to avoid timezone ambiguity.`
|
|
2165
|
-
);
|
|
2166
|
-
different = false; // Allow timezone shifts through for non-datetimeoffset fields
|
|
2167
|
-
} else {
|
|
2168
|
-
different = true;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
} else if (clientOldValues[key] === null) {
|
|
2172
|
-
different = dbVal !== null;
|
|
2173
|
-
}
|
|
2174
|
-
break;
|
|
2175
|
-
default:
|
|
2176
|
-
different = clientOldValues[key] !== dbVal;
|
|
2177
|
-
break;
|
|
2511
|
+
if (this.isFieldOpaqueToClient(f)) {
|
|
2512
|
+
continue;
|
|
2178
2513
|
}
|
|
2514
|
+
const dbVal: ConvertedFieldValue = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
2515
|
+
const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
|
|
2179
2516
|
if (different && f && !f.ReadOnly) {
|
|
2180
|
-
|
|
2181
|
-
dbDifferences.push({
|
|
2517
|
+
const entry: DBDifferenceEntry = {
|
|
2182
2518
|
FieldName: key,
|
|
2183
2519
|
ClientOldValue: clientOldValues[key],
|
|
2184
2520
|
DBValue: dbVal,
|
|
2185
|
-
}
|
|
2521
|
+
};
|
|
2522
|
+
dbDifferences.push(entry);
|
|
2523
|
+
dbDiffByField.set(key, entry);
|
|
2186
2524
|
}
|
|
2187
|
-
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
let fieldsToPreserveFromDB: Set<string> | undefined;
|
|
2188
2528
|
|
|
2189
2529
|
if (dbDifferences.length > 0) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
Object.keys(clientOldValues)
|
|
2530
|
+
const clientDifferences: ClientDifferenceEntry[] = [];
|
|
2531
|
+
const clientDiffFieldNames = new Set<string>();
|
|
2532
|
+
|
|
2533
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
2194
2534
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2195
|
-
if (
|
|
2196
|
-
|
|
2535
|
+
if (
|
|
2536
|
+
key in clientNewValues &&
|
|
2537
|
+
f &&
|
|
2538
|
+
f.AllowUpdateAPI &&
|
|
2539
|
+
!f.IsPrimaryKey &&
|
|
2540
|
+
this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)
|
|
2541
|
+
) {
|
|
2197
2542
|
clientDifferences.push({
|
|
2198
2543
|
FieldName: key,
|
|
2199
2544
|
ClientOldValue: clientOldValues[key],
|
|
2200
2545
|
ClientNewValue: clientNewValues[key],
|
|
2201
2546
|
});
|
|
2547
|
+
clientDiffFieldNames.add(key);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
|
|
2552
|
+
// ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
|
|
2553
|
+
const overlap = clientDifferences.filter((cd) => {
|
|
2554
|
+
const dd = dbDiffByField.get(cd.FieldName);
|
|
2555
|
+
if (!dd) {
|
|
2556
|
+
return false;
|
|
2202
2557
|
}
|
|
2558
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
|
|
2559
|
+
return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
|
|
2203
2560
|
});
|
|
2204
2561
|
|
|
2205
|
-
// 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
|
|
2206
|
-
// 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
|
|
2207
|
-
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
2208
|
-
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
2209
2562
|
if (overlap.length > 0) {
|
|
2563
|
+
const conflictingFields = overlap.map((o) => o.FieldName);
|
|
2564
|
+
const entityInfo = entityObject.EntityInfo;
|
|
2565
|
+
const shownClientDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, clientDifferences);
|
|
2566
|
+
const shownDBDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, dbDifferences);
|
|
2567
|
+
const shownOverlap = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, overlap);
|
|
2210
2568
|
const msg = {
|
|
2211
2569
|
Message:
|
|
2212
|
-
'
|
|
2213
|
-
ClientDifferences:
|
|
2214
|
-
DBDifferences:
|
|
2215
|
-
Overlap:
|
|
2570
|
+
'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.',
|
|
2571
|
+
ClientDifferences: shownClientDifferences,
|
|
2572
|
+
DBDifferences: shownDBDifferences,
|
|
2573
|
+
Overlap: shownOverlap,
|
|
2216
2574
|
};
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
details: {
|
|
2575
|
+
|
|
2576
|
+
console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
|
|
2577
|
+
LogErrorEx({
|
|
2578
|
+
message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
|
|
2579
|
+
severity: 'warning',
|
|
2580
|
+
category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
|
|
2581
|
+
metadata: {
|
|
2225
2582
|
entityName: entityObject.EntityInfo.Name,
|
|
2226
|
-
clientDifferences:
|
|
2227
|
-
dbDifferences:
|
|
2228
|
-
overlap:
|
|
2229
|
-
}
|
|
2583
|
+
clientDifferences: shownClientDifferences,
|
|
2584
|
+
dbDifferences: shownDBDifferences,
|
|
2585
|
+
overlap: shownOverlap,
|
|
2586
|
+
},
|
|
2230
2587
|
});
|
|
2231
2588
|
|
|
2232
|
-
//
|
|
2233
|
-
// ErrorLog write goes to the same connection as the entity that triggered it.
|
|
2589
|
+
// Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
|
|
2234
2590
|
try {
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2591
|
+
const hasEntityObjectFactory = (
|
|
2592
|
+
p: object | null | undefined
|
|
2593
|
+
): p is Pick<IMetadataProvider, 'GetEntityObject'> => Boolean(p && 'GetEntityObject' in p);
|
|
2594
|
+
const providerCandidate = entityObject.ProviderToUse;
|
|
2595
|
+
if (hasEntityObjectFactory(providerCandidate)) {
|
|
2596
|
+
const errorLogEntity = await providerCandidate.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
|
|
2597
|
+
// Code is NCHAR(20); a longer value is rejected at the database
|
|
2598
|
+
errorLogEntity.Code = 'SAVE_INCONSISTENCY';
|
|
2599
|
+
errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
|
|
2600
|
+
errorLogEntity.Status = 'Warning';
|
|
2601
|
+
errorLogEntity.Category = 'Entity Save';
|
|
2602
|
+
errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
|
|
2603
|
+
|
|
2604
|
+
const saveResult = await errorLogEntity.Save();
|
|
2605
|
+
if (!saveResult) {
|
|
2606
|
+
console.error(
|
|
2607
|
+
`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2246
2610
|
}
|
|
2247
2611
|
} catch (errorLogError) {
|
|
2248
2612
|
console.error('Error creating ErrorLog record:', errorLogError);
|
|
2249
2613
|
}
|
|
2614
|
+
|
|
2615
|
+
throw new GraphQLError(
|
|
2616
|
+
`Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`,
|
|
2617
|
+
{
|
|
2618
|
+
extensions: {
|
|
2619
|
+
code: 'CONCURRENT_MODIFICATION_CONFLICT',
|
|
2620
|
+
entityName: entityObject.EntityInfo.Name,
|
|
2621
|
+
conflictingFields,
|
|
2622
|
+
},
|
|
2623
|
+
}
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// Non-overlapping concurrent DB edits: preserve any field that changed in the database
|
|
2628
|
+
// when the client itself did NOT modify that field (prevents full-payload clients or forged
|
|
2629
|
+
// OldValues___ from clobbering concurrent DB updates on untouched columns).
|
|
2630
|
+
fieldsToPreserveFromDB = new Set<string>();
|
|
2631
|
+
for (const dd of dbDifferences) {
|
|
2632
|
+
if (!clientDiffFieldNames.has(dd.FieldName)) {
|
|
2633
|
+
fieldsToPreserveFromDB.add(dd.FieldName);
|
|
2634
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
|
|
2635
|
+
if (f) {
|
|
2636
|
+
fieldsToPreserveFromDB.add(f.Name);
|
|
2637
|
+
fieldsToPreserveFromDB.add(f.CodeName);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2250
2640
|
}
|
|
2251
2641
|
}
|
|
2252
2642
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2643
|
+
if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
|
|
2644
|
+
const mergedValues: Record<string, TNewVal> = {};
|
|
2645
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2646
|
+
if (!fieldsToPreserveFromDB.has(key)) {
|
|
2647
|
+
mergedValues[key] = clientNewValues[key];
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
entityObject.SetMany(mergedValues);
|
|
2651
|
+
} else {
|
|
2652
|
+
entityObject.SetMany(clientNewValues);
|
|
2653
|
+
}
|
|
2256
2654
|
}
|
|
2257
2655
|
|
|
2258
2656
|
protected async DeleteRecord(
|
|
@@ -2281,7 +2679,10 @@ export class ResolverBase {
|
|
|
2281
2679
|
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
2282
2680
|
});
|
|
2283
2681
|
}
|
|
2284
|
-
|
|
2682
|
+
// Grab the values before we delete so we can return the last state if the delete succeeds,
|
|
2683
|
+
// projected for the caller in case the object holds a column they may not read.
|
|
2684
|
+
// fls-guard: projected by ProjectEntityValuesForUser
|
|
2685
|
+
const returnValue = ProjectEntityValuesForUser(provider, entityObject.EntityInfo, entityObject.GetAll(), this.GetUserFromPayload(userPayload));
|
|
2285
2686
|
|
|
2286
2687
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
2287
2688
|
|