@memberjunction/server 6.2.0-edge.6 → 6.2.0-edge.8
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 +69 -0
- 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 +31 -8
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +85 -41
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +14 -4
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +28 -4
- 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/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 +50 -2
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +129 -12
- 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 +27 -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/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.map +1 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +2 -1
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -1
- package/dist/config.d.ts +906 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +41 -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 +6 -4
- package/dist/coreRoutes.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +1072 -5
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +10174 -4618
- package/dist/generated/graphql-schemas/__mj.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 +27 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +50 -5
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +329 -132
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +47 -7
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/globalCors.d.ts.map +1 -1
- package/dist/globalCors.js +1 -0
- package/dist/globalCors.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +201 -43
- 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 +87 -46
- 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/StartupLogger.d.ts +50 -0
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +15 -22
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +21 -3
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +4 -2
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.js +2 -3
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
- package/dist/realtimeSessions/sessionAccess.d.ts +15 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +22 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +2 -0
- package/dist/realtimeWidget/WidgetSessionService.js.map +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.map +1 -1
- package/dist/realtimeWidget/widgetCore.js +4 -7
- package/dist/realtimeWidget/widgetCore.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -0
- 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/AgentRunStatusPublisher.d.ts +10 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +18 -2
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/AgentSessionResolver.js +6 -2
- package/dist/resolvers/AgentSessionResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +61 -16
- 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/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/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +34 -13
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.js +1 -1
- package/dist/resolvers/InfoResolver.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 +2690 -1305
- 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/QuerySystemUserResolver.d.ts +38 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +181 -88
- package/dist/resolvers/QuerySystemUserResolver.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 +6 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +36 -5
- 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 +217 -34
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +24 -8
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +185 -67
- 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 +3 -1
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +7 -8
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.d.ts +12 -0
- package/dist/resolvers/TagGovernanceResolver.d.ts.map +1 -1
- package/dist/resolvers/TagGovernanceResolver.js +84 -0
- package/dist/resolvers/TagGovernanceResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +13 -8
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
- package/dist/resolvers/TelemetryResolver.js +40 -9
- package/dist/resolvers/TelemetryResolver.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/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +34 -4
- package/dist/resolvers/meetings/meetingMutations.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/RealtimeRecordingHandler.d.ts +3 -3
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.js +2 -4
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +2 -1
- 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/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.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +1 -0
- 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 +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +55 -2
- package/dist/util.js.map +1 -1
- package/package.json +107 -100
- package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
- package/src/__tests__/ActiveOperations.test.ts +68 -0
- package/src/__tests__/AgentSessionResolver.ownership.test.ts +13 -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__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
- package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
- package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
- package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
- package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -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__/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.permissions.test.ts +132 -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.test.ts +22 -0
- 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 +6 -4
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +327 -26
- package/src/__tests__/ResolverBase.diagnosticSenderRegistrations.test.ts +26 -0
- package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
- package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +53 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +60 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +4 -0
- package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
- package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +2 -0
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +2 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +87 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +28 -4
- 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 +69 -0
- package/src/__tests__/TagGovernanceAndAutotagResolvers.test.ts +261 -0
- package/src/__tests__/TagGovernanceResolver.test.ts +60 -2
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
- package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
- package/src/__tests__/ViewOperationsHandler.test.ts +2 -5
- package/src/__tests__/WorkQueueHostService.test.ts +25 -7
- package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
- package/src/__tests__/contextRequestOrigin.test.ts +105 -0
- package/src/__tests__/conversationBrowsers.test.ts +62 -0
- package/src/__tests__/globalCors.test.ts +12 -0
- package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
- package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
- package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +7 -2
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +79 -1
- package/src/__tests__/requestOrigin.test.ts +104 -0
- package/src/__tests__/review-capture-valid-token-log.test.ts +98 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +5 -5
- package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
- package/src/__tests__/sessionDeadline.test.ts +51 -0
- package/src/__tests__/sharedCache.test.ts +11 -0
- package/src/__tests__/unifiedAuth.test.ts +73 -1
- 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 +92 -42
- package/src/agentSessions/SessionManager.ts +38 -8
- package/src/agentSessions/conversationBrowsers.ts +143 -0
- package/src/agentSessions/sessionDeadline.ts +81 -0
- package/src/auth/APIKeyScopeAuth.ts +5 -4
- package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
- package/src/auth/loginEmailResolution.ts +67 -0
- package/src/auth/magicLink/MagicLinkKeys.ts +146 -14
- 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 +28 -3
- package/src/auth/newUsers.ts +1 -0
- package/src/backgroundSweepConfigUnits.ts +213 -0
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +2 -1
- package/src/config.ts +69 -0
- package/src/context.ts +46 -15
- package/src/coreRoutes.ts +14 -7
- package/src/generated/graphql-schemas/__mj.ts +10360 -6479
- package/src/generic/OrphanedConversationDetailReconciler.ts +32 -1
- package/src/generic/ResolverBase.ts +422 -143
- package/src/generic/RunViewResolver.ts +96 -49
- package/src/globalCors.ts +1 -0
- package/src/index.ts +246 -44
- package/src/integration/ConnectionPause.ts +222 -0
- package/src/integration/CustomColumnPromoter.ts +123 -53
- 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/StartupLogger.ts +24 -0
- package/src/middleware/BaseServerMiddleware.ts +20 -3
- package/src/realtimeSessions/ownedRealtimeSession.ts +2 -6
- package/src/realtimeSessions/sessionAccess.ts +30 -0
- package/src/realtimeWidget/WidgetSessionService.ts +2 -0
- package/src/realtimeWidget/visitorIdentity.ts +1 -0
- package/src/realtimeWidget/widgetCore.ts +4 -8
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -0
- package/src/requestOrigin.ts +64 -0
- package/src/resolvers/AgentRunStatusPublisher.ts +27 -2
- package/src/resolvers/AgentSessionResolver.ts +6 -2
- package/src/resolvers/AutotagPipelineResolver.ts +59 -14
- package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
- package/src/resolvers/GetDataResolver.ts +35 -13
- package/src/resolvers/InfoResolver.ts +1 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1522 -203
- package/src/resolvers/MCPResolver.ts +15 -2
- package/src/resolvers/QuerySystemUserResolver.ts +211 -100
- package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +40 -7
- package/src/resolvers/RemoteBrowserActionResolver.ts +242 -32
- package/src/resolvers/RunAIAgentResolver.ts +265 -86
- package/src/resolvers/RunAIPromptResolver.ts +18 -18
- package/src/resolvers/RunTemplateResolver.ts +3 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +7 -9
- package/src/resolvers/TagGovernanceResolver.ts +65 -0
- package/src/resolvers/TelemetryResolver.ts +44 -9
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
- package/src/resolvers/meetingRecordingRegistration.ts +2 -0
- package/src/resolvers/meetings/meetingMutations.ts +37 -7
- package/src/rest/MediaAccessKeys.ts +6 -2
- package/src/rest/OAuthCallbackHandler.ts +13 -5
- package/src/rest/RealtimeRecordingHandler.ts +5 -9
- package/src/rest/ViewOperationsHandler.ts +2 -1
- package/src/rest/setupRESTEndpoints.ts +11 -2
- 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 +1 -0
- package/src/services/WorkQueueHostService.ts +26 -4
- package/src/services/workQueueConfig.ts +5 -5
- package/src/sharedCache.ts +1 -1
- package/src/types.ts +18 -0
- package/src/util.ts +55 -3
- package/src/services/TaskOrchestration-Integration.md +0 -188
|
@@ -49,8 +49,8 @@ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
|
49
49
|
import { RunViewGenericParams, UserPayload } from '../types.js';
|
|
50
50
|
import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
|
|
51
51
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
52
|
-
import { MJEvent, MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
53
|
-
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';
|
|
54
54
|
import type { SQLParserDialect } from '@memberjunction/sql-dialect';
|
|
55
55
|
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
56
56
|
import { PUSH_STATUS_UPDATES_TOPIC, PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
|
|
@@ -67,6 +67,12 @@ import { Subscription } from 'rxjs';
|
|
|
67
67
|
* `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
|
|
68
68
|
* (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
|
|
69
69
|
* that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
|
|
70
|
+
*
|
|
71
|
+
* `mj: projects` (conversation folders) is refused a change to `OwnerUserID` or `ParentID` by anyone but the
|
|
72
|
+
* stored owner, a rule that reads the stored owner, so it must hold even if a host stops tracking its changes.
|
|
73
|
+
*
|
|
74
|
+
* `mj: diagnostic sender registrations` is refused a change to its guarded fields by a rule that decides on
|
|
75
|
+
* `Dirty`, so the baseline must be the stored row.
|
|
70
76
|
*/
|
|
71
77
|
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
72
78
|
'mj: record changes',
|
|
@@ -75,13 +81,33 @@ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW: ReadonlySet<string> = new Set([
|
|
|
75
81
|
'mj: conversations',
|
|
76
82
|
'mj: ai agent sessions',
|
|
77
83
|
'mj: ai agent runs',
|
|
84
|
+
'mj: projects',
|
|
85
|
+
'mj: diagnostic sender registrations',
|
|
78
86
|
]);
|
|
79
87
|
|
|
88
|
+
/** One field's values in a concurrency-conflict diagnostic. */
|
|
89
|
+
interface ConflictDiagnosticEntry<TValue> {
|
|
90
|
+
FieldName: string;
|
|
91
|
+
ClientOldValue?: TValue;
|
|
92
|
+
ClientNewValue?: TValue;
|
|
93
|
+
DBValue?: TValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ActiveEntitySessionListener {
|
|
97
|
+
pubSub: PubSubEngine;
|
|
98
|
+
userPayload: UserPayload;
|
|
99
|
+
lastSeenAt: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
103
|
+
const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
|
|
104
|
+
|
|
80
105
|
export class ResolverBase {
|
|
81
106
|
private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
|
|
82
107
|
private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
|
|
83
108
|
|
|
84
109
|
private static _eventSubscriptionKey: string = '___MJServer___ResolverBase___EventSubscriptions';
|
|
110
|
+
private static _activeEntitySessionsKey: string = '___MJServer___ResolverBase___ActiveEntitySessions';
|
|
85
111
|
private get eventSubscriptions(): Map<string, Subscription> {
|
|
86
112
|
// 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
|
|
87
113
|
// could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
|
|
@@ -90,7 +116,15 @@ export class ResolverBase {
|
|
|
90
116
|
LogDebug(`>>>>> MJServer.ResolverBase.EventSubscriptions: Creating new Map - this should only happen once per server instance <<<<<<`);
|
|
91
117
|
g[ResolverBase._eventSubscriptionKey] = new Map<string, Subscription>();
|
|
92
118
|
}
|
|
93
|
-
return g[ResolverBase._eventSubscriptionKey]
|
|
119
|
+
return g[ResolverBase._eventSubscriptionKey] as Map<string, Subscription>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private get activeEntitySessions(): Map<string, Map<string, ActiveEntitySessionListener>> {
|
|
123
|
+
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
124
|
+
if (!g[ResolverBase._activeEntitySessionsKey]) {
|
|
125
|
+
g[ResolverBase._activeEntitySessionsKey] = new Map<string, Map<string, ActiveEntitySessionListener>>();
|
|
126
|
+
}
|
|
127
|
+
return g[ResolverBase._activeEntitySessionsKey] as Map<string, Map<string, ActiveEntitySessionListener>>;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
130
|
/**
|
|
@@ -438,30 +472,29 @@ export class ResolverBase {
|
|
|
438
472
|
if (field.AllowDecryptInAPI) continue;
|
|
439
473
|
|
|
440
474
|
// AllowDecryptInAPI is false - we need to filter the value
|
|
475
|
+
if (!field.SendEncryptedValue) {
|
|
476
|
+
// SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
|
|
477
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
441
481
|
const engine = EncryptionEngine.Instance;
|
|
442
482
|
await engine.Config(false, contextUser);
|
|
443
483
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
458
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
459
|
-
}
|
|
484
|
+
// Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
|
|
485
|
+
if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
|
|
486
|
+
try {
|
|
487
|
+
const encryptedValue = await engine.Encrypt(
|
|
488
|
+
value,
|
|
489
|
+
field.EncryptionKeyID as string,
|
|
490
|
+
contextUser
|
|
491
|
+
);
|
|
492
|
+
dataObject[fieldName] = encryptedValue;
|
|
493
|
+
} catch (err) {
|
|
494
|
+
// If re-encryption fails, use sentinel for safety
|
|
495
|
+
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
496
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
460
497
|
}
|
|
461
|
-
// If already encrypted (shouldn't happen normally), keep as-is
|
|
462
|
-
} else {
|
|
463
|
-
// SendEncryptedValue is false - replace with sentinel
|
|
464
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
465
498
|
}
|
|
466
499
|
}
|
|
467
500
|
|
|
@@ -661,7 +694,8 @@ export class ResolverBase {
|
|
|
661
694
|
viewInput.DataSource,
|
|
662
695
|
viewInput.IncludeBinaryFields,
|
|
663
696
|
viewInput.AlternateBaseView,
|
|
664
|
-
viewInput.Properties
|
|
697
|
+
viewInput.Properties,
|
|
698
|
+
viewInput.SkipTotalRowCount
|
|
665
699
|
);
|
|
666
700
|
}
|
|
667
701
|
else {
|
|
@@ -710,7 +744,8 @@ export class ResolverBase {
|
|
|
710
744
|
viewInput.DataSource,
|
|
711
745
|
viewInput.IncludeBinaryFields,
|
|
712
746
|
viewInput.AlternateBaseView,
|
|
713
|
-
viewInput.Properties
|
|
747
|
+
viewInput.Properties,
|
|
748
|
+
viewInput.SkipTotalRowCount
|
|
714
749
|
);
|
|
715
750
|
} catch (err) {
|
|
716
751
|
console.log(err);
|
|
@@ -759,7 +794,9 @@ export class ResolverBase {
|
|
|
759
794
|
viewInput.BypassCache,
|
|
760
795
|
viewInput.DataSource,
|
|
761
796
|
viewInput.IncludeBinaryFields,
|
|
762
|
-
viewInput.AlternateBaseView
|
|
797
|
+
viewInput.AlternateBaseView,
|
|
798
|
+
undefined,
|
|
799
|
+
viewInput.SkipTotalRowCount
|
|
763
800
|
);
|
|
764
801
|
} catch (err) {
|
|
765
802
|
console.log(err);
|
|
@@ -834,6 +871,7 @@ export class ResolverBase {
|
|
|
834
871
|
bypassCache: viewInput.BypassCache,
|
|
835
872
|
dataSource: viewInput.DataSource,
|
|
836
873
|
includeBinaryFields: viewInput.IncludeBinaryFields,
|
|
874
|
+
skipTotalRowCount: viewInput.SkipTotalRowCount,
|
|
837
875
|
alternateBaseView: viewInput.AlternateBaseView,
|
|
838
876
|
properties: viewInput.Properties,
|
|
839
877
|
});
|
|
@@ -1101,7 +1139,7 @@ export class ResolverBase {
|
|
|
1101
1139
|
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
1102
1140
|
*
|
|
1103
1141
|
* The references come from a walk of the whole parse tree
|
|
1104
|
-
* (`
|
|
1142
|
+
* (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
|
|
1105
1143
|
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
1106
1144
|
* a row list).
|
|
1107
1145
|
*
|
|
@@ -1122,14 +1160,7 @@ export class ResolverBase {
|
|
|
1122
1160
|
}
|
|
1123
1161
|
const dialect = this.dialectForProvider(provider);
|
|
1124
1162
|
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
1125
|
-
const
|
|
1126
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
1127
|
-
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
1128
|
-
}
|
|
1129
|
-
if (this.astContainsWriteNode(parser.AST)) {
|
|
1130
|
-
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
1131
|
-
}
|
|
1132
|
-
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
1163
|
+
const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
|
|
1133
1164
|
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
1134
1165
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
1135
1166
|
for (const relation of reads.Relations) {
|
|
@@ -1198,9 +1229,19 @@ export class ResolverBase {
|
|
|
1198
1229
|
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
1199
1230
|
}
|
|
1200
1231
|
|
|
1201
|
-
/**
|
|
1202
|
-
|
|
1203
|
-
|
|
1232
|
+
/**
|
|
1233
|
+
* Parses a whole statement with the strict validator and returns what it reads; throws unless it
|
|
1234
|
+
* is one plain read-only SELECT with no write or DDL at any depth.
|
|
1235
|
+
*/
|
|
1236
|
+
private strictReadOnlyStatementReads(sqlText: string, dialect: SQLParserDialect, label: string): StrictStatementReads {
|
|
1237
|
+
const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
|
|
1238
|
+
if (result.Refusal === 'NestedWrite') {
|
|
1239
|
+
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
1240
|
+
}
|
|
1241
|
+
if (!result.IsValid || !result.Reads) {
|
|
1242
|
+
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
1243
|
+
}
|
|
1244
|
+
return result.Reads;
|
|
1204
1245
|
}
|
|
1205
1246
|
|
|
1206
1247
|
private dialectForProvider(provider?: IMetadataProvider): SQLParserDialect {
|
|
@@ -1241,7 +1282,8 @@ export class ResolverBase {
|
|
|
1241
1282
|
dataSource?: 'Live' | 'Materialized',
|
|
1242
1283
|
includeBinaryFields?: boolean,
|
|
1243
1284
|
alternateBaseView?: string,
|
|
1244
|
-
properties?: ViewPropertyValues
|
|
1285
|
+
properties?: ViewPropertyValues,
|
|
1286
|
+
skipTotalRowCount?: boolean
|
|
1245
1287
|
) {
|
|
1246
1288
|
try {
|
|
1247
1289
|
if (!viewInfo || !userPayload) return null;
|
|
@@ -1324,9 +1366,11 @@ export class ResolverBase {
|
|
|
1324
1366
|
BypassCache: bypassCache,
|
|
1325
1367
|
DataSource: dataSource,
|
|
1326
1368
|
IncludeBinaryFields: includeBinaryFields,
|
|
1369
|
+
SkipTotalRowCount: skipTotalRowCount,
|
|
1327
1370
|
AlternateBaseView: alternateBaseView,
|
|
1328
1371
|
Properties: properties,
|
|
1329
1372
|
TimeZone: userPayload?.TimeZone,
|
|
1373
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1330
1374
|
},
|
|
1331
1375
|
user
|
|
1332
1376
|
);
|
|
@@ -1488,9 +1532,11 @@ export class ResolverBase {
|
|
|
1488
1532
|
BypassCache: param.bypassCache,
|
|
1489
1533
|
DataSource: param.dataSource,
|
|
1490
1534
|
IncludeBinaryFields: param.includeBinaryFields,
|
|
1535
|
+
SkipTotalRowCount: param.skipTotalRowCount,
|
|
1491
1536
|
AlternateBaseView: param.alternateBaseView,
|
|
1492
1537
|
Properties: param.properties,
|
|
1493
1538
|
TimeZone: param.userPayload?.TimeZone,
|
|
1539
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1494
1540
|
});
|
|
1495
1541
|
}
|
|
1496
1542
|
|
|
@@ -1691,12 +1737,40 @@ export class ResolverBase {
|
|
|
1691
1737
|
}
|
|
1692
1738
|
|
|
1693
1739
|
protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1740
|
+
// Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
|
|
1741
|
+
// per event, while routing WebSocket status updates dynamically across all active user sessions
|
|
1742
|
+
// that have interacted with this entity type (instead of closing over only the first caller).
|
|
1697
1743
|
const uniqueKey = entityObject.EntityInfo.Name;
|
|
1744
|
+
const now = Date.now();
|
|
1745
|
+
const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
|
|
1698
1746
|
|
|
1699
|
-
|
|
1747
|
+
let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
|
|
1748
|
+
if (!sessionsForEntity) {
|
|
1749
|
+
sessionsForEntity = new Map<string, ActiveEntitySessionListener>();
|
|
1750
|
+
this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
|
|
1751
|
+
} else {
|
|
1752
|
+
// Opportunistically evict expired session entries and enforce a safe per-entity cap
|
|
1753
|
+
for (const [key, entry] of sessionsForEntity) {
|
|
1754
|
+
if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1755
|
+
sessionsForEntity.delete(key);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
|
|
1759
|
+
const oldestKey = sessionsForEntity.keys().next().value;
|
|
1760
|
+
if (oldestKey !== undefined) {
|
|
1761
|
+
sessionsForEntity.delete(oldestKey);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
if (userPayload) {
|
|
1767
|
+
const sessionKey = userPayload.sessionId
|
|
1768
|
+
? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
|
|
1769
|
+
: (userPayload.userRecord?.ID ?? '__anonymous__');
|
|
1770
|
+
sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (isFreshSubscription) {
|
|
1700
1774
|
// 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
|
|
1701
1775
|
LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
|
|
1702
1776
|
const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event: MJEvent) => {
|
|
@@ -1710,15 +1784,28 @@ export class ResolverBase {
|
|
|
1710
1784
|
}
|
|
1711
1785
|
|
|
1712
1786
|
const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
|
|
1713
|
-
const eventToLog = { entityName:
|
|
1787
|
+
const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
|
|
1714
1788
|
LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
|
|
1715
1789
|
await this.EmitCloudEvent(event);
|
|
1716
|
-
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1717
|
-
|
|
1718
|
-
if (event.args && event.args instanceof BaseEntityEvent
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1790
|
+
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1791
|
+
|
|
1792
|
+
if (event.args && event.args instanceof BaseEntityEvent) {
|
|
1793
|
+
const activeSessions = this.activeEntitySessions.get(uniqueKey);
|
|
1794
|
+
if (activeSessions && activeSessions.size > 0) {
|
|
1795
|
+
const eventNow = Date.now();
|
|
1796
|
+
let serializedMessage: string | undefined;
|
|
1797
|
+
for (const [sKey, session] of activeSessions) {
|
|
1798
|
+
if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1799
|
+
activeSessions.delete(sKey);
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
|
|
1803
|
+
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
1804
|
+
serializedMessage ??= this.EntityStatusMessage(event.args);
|
|
1805
|
+
this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1722
1809
|
}
|
|
1723
1810
|
}
|
|
1724
1811
|
});
|
|
@@ -2243,143 +2330,335 @@ export class ResolverBase {
|
|
|
2243
2330
|
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
2244
2331
|
}
|
|
2245
2332
|
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2333
|
+
/**
|
|
2334
|
+
* Whether the API never gives a client this field's stored value: an encrypted field that is not
|
|
2335
|
+
* decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
|
|
2336
|
+
* client's old value for it can never be compared with the database value.
|
|
2337
|
+
*/
|
|
2338
|
+
private isFieldOpaqueToClient(field: EntityFieldInfo | undefined): boolean {
|
|
2339
|
+
return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* The client's new values without the opaque fields it only echoed back: the sentinel, or the same
|
|
2344
|
+
* value it received as the old value. Writing either would replace the stored secret.
|
|
2345
|
+
*/
|
|
2346
|
+
private withoutEchoedOpaqueValues<TOldVal, TNewVal>(
|
|
2347
|
+
entityInfo: EntityInfo,
|
|
2348
|
+
clientNewValues: Record<string, TNewVal>,
|
|
2349
|
+
clientOldValues: Record<string, TOldVal>,
|
|
2350
|
+
): Record<string, TNewVal> {
|
|
2351
|
+
const kept: Record<string, TNewVal> = {};
|
|
2352
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2353
|
+
const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
|
|
2354
|
+
const newValue = clientNewValues[key];
|
|
2355
|
+
const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
|
|
2356
|
+
const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
|
|
2357
|
+
if (!echoed) {
|
|
2358
|
+
kept[key] = newValue;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return kept;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/**
|
|
2365
|
+
* Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
|
|
2366
|
+
* the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
|
|
2367
|
+
*/
|
|
2368
|
+
private redactEncryptedConflictValues<TValue>(
|
|
2369
|
+
entityInfo: EntityInfo,
|
|
2370
|
+
entries: ReadonlyArray<ConflictDiagnosticEntry<TValue>>,
|
|
2371
|
+
): ConflictDiagnosticEntry<TValue | string>[] {
|
|
2372
|
+
return entries.map((entry) => {
|
|
2373
|
+
if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
|
|
2374
|
+
return entry;
|
|
2375
|
+
}
|
|
2376
|
+
const redacted: ConflictDiagnosticEntry<TValue | string> = { FieldName: entry.FieldName };
|
|
2377
|
+
if ('ClientOldValue' in entry) redacted.ClientOldValue = ENCRYPTED_SENTINEL;
|
|
2378
|
+
if ('ClientNewValue' in entry) redacted.ClientNewValue = ENCRYPTED_SENTINEL;
|
|
2379
|
+
if ('DBValue' in entry) redacted.DBValue = ENCRYPTED_SENTINEL;
|
|
2380
|
+
return redacted;
|
|
2255
2381
|
});
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
|
|
2386
|
+
* (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
|
|
2387
|
+
*/
|
|
2388
|
+
private areEntityFieldValuesDifferent<TLeft, TRight>(
|
|
2389
|
+
field: EntityFieldInfo | undefined,
|
|
2390
|
+
left: TLeft,
|
|
2391
|
+
right: TRight,
|
|
2392
|
+
allowTimezoneShiftWarning: boolean,
|
|
2393
|
+
fieldKeyForLog?: string
|
|
2394
|
+
): boolean {
|
|
2395
|
+
const normalizedLeft = left === undefined ? null : left;
|
|
2396
|
+
const normalizedRight = right === undefined ? null : right;
|
|
2397
|
+
|
|
2398
|
+
if (normalizedLeft instanceof Date) {
|
|
2399
|
+
const rightDate =
|
|
2400
|
+
normalizedRight instanceof Date
|
|
2401
|
+
? normalizedRight
|
|
2402
|
+
: field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
|
|
2403
|
+
? this.ClientOldValueToFieldValue(field, normalizedRight)
|
|
2404
|
+
: null;
|
|
2405
|
+
if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
|
|
2406
|
+
return true;
|
|
2407
|
+
}
|
|
2408
|
+
if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
|
|
2409
|
+
return false;
|
|
2410
|
+
}
|
|
2411
|
+
const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
|
|
2412
|
+
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
|
|
2413
|
+
if (allowTimezoneShiftWarning && fieldKeyForLog) {
|
|
2414
|
+
console.warn(
|
|
2415
|
+
`Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
|
|
2416
|
+
`Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
|
|
2417
|
+
`Consider using datetimeoffset to avoid timezone ambiguity.`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
return true;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
|
|
2426
|
+
const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
|
|
2427
|
+
if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
|
|
2428
|
+
return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== (normalizedRight as never);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
/**
|
|
2440
|
+
* True when two instants would be stored as the same value in the field's column: SQL Server
|
|
2441
|
+
* `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
|
|
2442
|
+
*/
|
|
2443
|
+
private datesEqualAtColumnResolution(field: EntityFieldInfo | undefined, left: Date, right: Date): boolean {
|
|
2444
|
+
return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
private dateColumnResolutionMs(field: EntityFieldInfo | undefined): number {
|
|
2448
|
+
switch ((field?.Type ?? '').trim().toLowerCase()) {
|
|
2449
|
+
case 'datetime':
|
|
2450
|
+
return 1000 / 300;
|
|
2451
|
+
case 'smalldatetime':
|
|
2452
|
+
return 60_000;
|
|
2453
|
+
default:
|
|
2454
|
+
return 1;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* True when both strings are JSON documents with the same content, ignoring whitespace and key order.
|
|
2460
|
+
*/
|
|
2461
|
+
private jsonStringsEquivalent(left: string, right: string): boolean {
|
|
2462
|
+
const canonicalLeft = this.canonicalJSONDocument(left);
|
|
2463
|
+
return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Canonical form of a JSON object or array string, or null when the text is not one.
|
|
2468
|
+
*/
|
|
2469
|
+
private canonicalJSONDocument(text: string): string | null {
|
|
2470
|
+
const first = text.trimStart().charAt(0);
|
|
2471
|
+
if (first !== '{' && first !== '[') {
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
return Canonicalize(JSON.parse(text));
|
|
2476
|
+
} catch {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
protected async TestAndSetClientOldValuesToDBValues<
|
|
2482
|
+
TOldVal,
|
|
2483
|
+
TNewVal,
|
|
2484
|
+
TInput extends { OldValues___?: ReadonlyArray<{ Key: string; Value: TOldVal }> | null }
|
|
2485
|
+
>(
|
|
2486
|
+
input: TInput,
|
|
2487
|
+
rawClientNewValues: Record<string, TNewVal>,
|
|
2488
|
+
entityObject: BaseEntity,
|
|
2489
|
+
contextUser: UserInfo
|
|
2490
|
+
): Promise<void> {
|
|
2491
|
+
type ConvertedFieldValue = ReturnType<ResolverBase['ClientOldValueToFieldValue']>;
|
|
2492
|
+
const clientOldValues: Record<string, ConvertedFieldValue> = {};
|
|
2493
|
+
for (const item of input.OldValues___ ?? []) {
|
|
2494
|
+
// Transform string-serialized wire values to match the field's TypeScript type
|
|
2495
|
+
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
2496
|
+
clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
|
|
2497
|
+
}
|
|
2498
|
+
const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
|
|
2256
2499
|
|
|
2257
|
-
// clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
|
|
2258
|
-
// with the DB values
|
|
2259
2500
|
// fls-guard: compared on the server only, never returned
|
|
2260
2501
|
const dbValues = entityObject.GetAll();
|
|
2261
2502
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2503
|
+
interface DBDifferenceEntry {
|
|
2504
|
+
FieldName: string;
|
|
2505
|
+
ClientOldValue: ConvertedFieldValue;
|
|
2506
|
+
DBValue: ConvertedFieldValue;
|
|
2507
|
+
}
|
|
2508
|
+
interface ClientDifferenceEntry {
|
|
2509
|
+
FieldName: string;
|
|
2510
|
+
ClientOldValue: ConvertedFieldValue;
|
|
2511
|
+
ClientNewValue: TNewVal;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const dbDifferences: DBDifferenceEntry[] = [];
|
|
2515
|
+
const dbDiffByField = new Map<string, DBDifferenceEntry>();
|
|
2516
|
+
|
|
2517
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
2265
2518
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
switch (typeof clientOldValues[key]) {
|
|
2269
|
-
case 'number':
|
|
2270
|
-
different = clientOldValues[key] !== dbVal;
|
|
2271
|
-
break;
|
|
2272
|
-
case 'boolean':
|
|
2273
|
-
different = clientOldValues[key] !== dbVal;
|
|
2274
|
-
break;
|
|
2275
|
-
case 'object':
|
|
2276
|
-
if (clientOldValues[key] instanceof Date) {
|
|
2277
|
-
const clientDate = clientOldValues[key] as Date;
|
|
2278
|
-
const dbDate = dbVal;
|
|
2279
|
-
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
2280
|
-
different = true;
|
|
2281
|
-
} else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
2282
|
-
// Check if this is a datetime/datetime2 field with only a timezone hour shift
|
|
2283
|
-
const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
|
|
2284
|
-
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
|
|
2285
|
-
console.warn(
|
|
2286
|
-
`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
|
|
2287
|
-
`Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
|
|
2288
|
-
`Consider using datetimeoffset to avoid timezone ambiguity.`
|
|
2289
|
-
);
|
|
2290
|
-
different = false; // Allow timezone shifts through for non-datetimeoffset fields
|
|
2291
|
-
} else {
|
|
2292
|
-
different = true;
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
} else if (clientOldValues[key] === null) {
|
|
2296
|
-
different = dbVal !== null;
|
|
2297
|
-
}
|
|
2298
|
-
break;
|
|
2299
|
-
default:
|
|
2300
|
-
different = clientOldValues[key] !== dbVal;
|
|
2301
|
-
break;
|
|
2519
|
+
if (this.isFieldOpaqueToClient(f)) {
|
|
2520
|
+
continue;
|
|
2302
2521
|
}
|
|
2522
|
+
const dbVal: ConvertedFieldValue = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
2523
|
+
const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
|
|
2303
2524
|
if (different && f && !f.ReadOnly) {
|
|
2304
|
-
|
|
2305
|
-
dbDifferences.push({
|
|
2525
|
+
const entry: DBDifferenceEntry = {
|
|
2306
2526
|
FieldName: key,
|
|
2307
2527
|
ClientOldValue: clientOldValues[key],
|
|
2308
2528
|
DBValue: dbVal,
|
|
2309
|
-
}
|
|
2529
|
+
};
|
|
2530
|
+
dbDifferences.push(entry);
|
|
2531
|
+
dbDiffByField.set(key, entry);
|
|
2310
2532
|
}
|
|
2311
|
-
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
let fieldsToPreserveFromDB: Set<string> | undefined;
|
|
2312
2536
|
|
|
2313
2537
|
if (dbDifferences.length > 0) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
Object.keys(clientOldValues)
|
|
2538
|
+
const clientDifferences: ClientDifferenceEntry[] = [];
|
|
2539
|
+
const clientDiffFieldNames = new Set<string>();
|
|
2540
|
+
|
|
2541
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
2318
2542
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
2319
|
-
if (
|
|
2320
|
-
|
|
2543
|
+
if (
|
|
2544
|
+
key in clientNewValues &&
|
|
2545
|
+
f &&
|
|
2546
|
+
f.AllowUpdateAPI &&
|
|
2547
|
+
!f.IsPrimaryKey &&
|
|
2548
|
+
this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)
|
|
2549
|
+
) {
|
|
2321
2550
|
clientDifferences.push({
|
|
2322
2551
|
FieldName: key,
|
|
2323
2552
|
ClientOldValue: clientOldValues[key],
|
|
2324
2553
|
ClientNewValue: clientNewValues[key],
|
|
2325
2554
|
});
|
|
2555
|
+
clientDiffFieldNames.add(key);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
|
|
2560
|
+
// ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
|
|
2561
|
+
const overlap = clientDifferences.filter((cd) => {
|
|
2562
|
+
const dd = dbDiffByField.get(cd.FieldName);
|
|
2563
|
+
if (!dd) {
|
|
2564
|
+
return false;
|
|
2326
2565
|
}
|
|
2566
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
|
|
2567
|
+
return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
|
|
2327
2568
|
});
|
|
2328
2569
|
|
|
2329
|
-
// 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
|
|
2330
|
-
// 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
|
|
2331
|
-
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
2332
|
-
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
2333
2570
|
if (overlap.length > 0) {
|
|
2571
|
+
const conflictingFields = overlap.map((o) => o.FieldName);
|
|
2572
|
+
const entityInfo = entityObject.EntityInfo;
|
|
2573
|
+
const shownClientDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, clientDifferences);
|
|
2574
|
+
const shownDBDifferences = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, dbDifferences);
|
|
2575
|
+
const shownOverlap = this.redactEncryptedConflictValues<ConvertedFieldValue | TNewVal>(entityInfo, overlap);
|
|
2334
2576
|
const msg = {
|
|
2335
2577
|
Message:
|
|
2336
|
-
'
|
|
2337
|
-
ClientDifferences:
|
|
2338
|
-
DBDifferences:
|
|
2339
|
-
Overlap:
|
|
2578
|
+
'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.',
|
|
2579
|
+
ClientDifferences: shownClientDifferences,
|
|
2580
|
+
DBDifferences: shownDBDifferences,
|
|
2581
|
+
Overlap: shownOverlap,
|
|
2340
2582
|
};
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
|
|
2583
|
+
|
|
2584
|
+
console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
|
|
2344
2585
|
LogErrorEx({
|
|
2345
2586
|
message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
|
|
2346
2587
|
severity: 'warning',
|
|
2347
2588
|
category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
|
|
2348
2589
|
metadata: {
|
|
2349
2590
|
entityName: entityObject.EntityInfo.Name,
|
|
2350
|
-
clientDifferences:
|
|
2351
|
-
dbDifferences:
|
|
2352
|
-
overlap:
|
|
2591
|
+
clientDifferences: shownClientDifferences,
|
|
2592
|
+
dbDifferences: shownDBDifferences,
|
|
2593
|
+
overlap: shownOverlap,
|
|
2353
2594
|
},
|
|
2354
2595
|
});
|
|
2355
2596
|
|
|
2356
|
-
//
|
|
2357
|
-
// ErrorLog write goes to the same connection as the entity that triggered it.
|
|
2597
|
+
// Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
|
|
2358
2598
|
try {
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
);
|
|
2599
|
+
const hasEntityObjectFactory = (
|
|
2600
|
+
p: object | null | undefined
|
|
2601
|
+
): p is Pick<IMetadataProvider, 'GetEntityObject'> => Boolean(p && 'GetEntityObject' in p);
|
|
2602
|
+
const providerCandidate = entityObject.ProviderToUse;
|
|
2603
|
+
if (hasEntityObjectFactory(providerCandidate)) {
|
|
2604
|
+
const errorLogEntity = await providerCandidate.GetEntityObject<MJErrorLogEntity>('MJ: Error Logs', contextUser);
|
|
2605
|
+
// Code is NCHAR(20); a longer value is rejected at the database
|
|
2606
|
+
errorLogEntity.Code = 'SAVE_INCONSISTENCY';
|
|
2607
|
+
errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
|
|
2608
|
+
errorLogEntity.Status = 'Warning';
|
|
2609
|
+
errorLogEntity.Category = 'Entity Save';
|
|
2610
|
+
errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
|
|
2611
|
+
|
|
2612
|
+
const saveResult = await errorLogEntity.Save();
|
|
2613
|
+
if (!saveResult) {
|
|
2614
|
+
console.error(
|
|
2615
|
+
`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2373
2618
|
}
|
|
2374
2619
|
} catch (errorLogError) {
|
|
2375
2620
|
console.error('Error creating ErrorLog record:', errorLogError);
|
|
2376
2621
|
}
|
|
2622
|
+
|
|
2623
|
+
throw new GraphQLError(
|
|
2624
|
+
`Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`,
|
|
2625
|
+
{
|
|
2626
|
+
extensions: {
|
|
2627
|
+
code: 'CONCURRENT_MODIFICATION_CONFLICT',
|
|
2628
|
+
entityName: entityObject.EntityInfo.Name,
|
|
2629
|
+
conflictingFields,
|
|
2630
|
+
},
|
|
2631
|
+
}
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// Non-overlapping concurrent DB edits: preserve any field that changed in the database
|
|
2636
|
+
// when the client itself did NOT modify that field (prevents full-payload clients or forged
|
|
2637
|
+
// OldValues___ from clobbering concurrent DB updates on untouched columns).
|
|
2638
|
+
fieldsToPreserveFromDB = new Set<string>();
|
|
2639
|
+
for (const dd of dbDifferences) {
|
|
2640
|
+
if (!clientDiffFieldNames.has(dd.FieldName)) {
|
|
2641
|
+
fieldsToPreserveFromDB.add(dd.FieldName);
|
|
2642
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
|
|
2643
|
+
if (f) {
|
|
2644
|
+
fieldsToPreserveFromDB.add(f.Name);
|
|
2645
|
+
fieldsToPreserveFromDB.add(f.CodeName);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2377
2648
|
}
|
|
2378
2649
|
}
|
|
2379
2650
|
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2651
|
+
if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
|
|
2652
|
+
const mergedValues: Record<string, TNewVal> = {};
|
|
2653
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2654
|
+
if (!fieldsToPreserveFromDB.has(key)) {
|
|
2655
|
+
mergedValues[key] = clientNewValues[key];
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
entityObject.SetMany(mergedValues);
|
|
2659
|
+
} else {
|
|
2660
|
+
entityObject.SetMany(clientNewValues);
|
|
2661
|
+
}
|
|
2383
2662
|
}
|
|
2384
2663
|
|
|
2385
2664
|
protected async DeleteRecord(
|