@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
|
@@ -7,8 +7,8 @@ import { RefusalExtensions } from './refusalExtensions.js';
|
|
|
7
7
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
8
8
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
9
9
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
10
|
-
import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
11
|
-
import { SQLParser, IsBalancedSQLFragment, LexSQL } from '@memberjunction/sql-parser';
|
|
10
|
+
import { MJEventType, MJGlobal, UUIDsEqual, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift, Canonicalize } from '@memberjunction/global';
|
|
11
|
+
import { SQLParser, IsBalancedSQLFragment, LexSQL, StrictFragmentValidator } from '@memberjunction/sql-parser';
|
|
12
12
|
import { EncryptionEngine, IsEncryptionKeyUnavailable, ReportEncryptionKeyUnavailable } from '@memberjunction/encryption';
|
|
13
13
|
import { PublishStatusUpdate, StatusUpdateOwnerID } from './PushStatusResolver.js';
|
|
14
14
|
import { CACHE_INVALIDATION_TOPIC, MayBroadcastRecordData } from './CacheInvalidationResolver.js';
|
|
@@ -22,6 +22,12 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
|
22
22
|
* `mj: conversations` and `mj: ai agent sessions` carry the anonymous realtime-session ownership key
|
|
23
23
|
* (`ExternalID` / `ConversationID`), which their server entities refuse an anonymous visitor to change — a rule
|
|
24
24
|
* that decides on `Dirty`, so the baseline must be the stored row, not a client-supplied one (#4931).
|
|
25
|
+
*
|
|
26
|
+
* `mj: projects` (conversation folders) is refused a change to `OwnerUserID` or `ParentID` by anyone but the
|
|
27
|
+
* stored owner, a rule that reads the stored owner, so it must hold even if a host stops tracking its changes.
|
|
28
|
+
*
|
|
29
|
+
* `mj: diagnostic sender registrations` is refused a change to its guarded fields by a rule that decides on
|
|
30
|
+
* `Dirty`, so the baseline must be the stored row.
|
|
25
31
|
*/
|
|
26
32
|
const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
|
|
27
33
|
'mj: record changes',
|
|
@@ -30,11 +36,16 @@ const ENTITIES_THAT_ALWAYS_LOAD_STORED_ROW = new Set([
|
|
|
30
36
|
'mj: conversations',
|
|
31
37
|
'mj: ai agent sessions',
|
|
32
38
|
'mj: ai agent runs',
|
|
39
|
+
'mj: projects',
|
|
40
|
+
'mj: diagnostic sender registrations',
|
|
33
41
|
]);
|
|
42
|
+
const ACTIVE_ENTITY_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
const ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY = 2048;
|
|
34
44
|
export class ResolverBase {
|
|
35
45
|
static { this._emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null; }
|
|
36
46
|
static { this._cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {}; }
|
|
37
47
|
static { this._eventSubscriptionKey = '___MJServer___ResolverBase___EventSubscriptions'; }
|
|
48
|
+
static { this._activeEntitySessionsKey = '___MJServer___ResolverBase___ActiveEntitySessions'; }
|
|
38
49
|
get eventSubscriptions() {
|
|
39
50
|
// 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
|
|
40
51
|
// could actually be duplicated and we'd end up with multiple instances of the same map, which would be bad.
|
|
@@ -45,6 +56,13 @@ export class ResolverBase {
|
|
|
45
56
|
}
|
|
46
57
|
return g[ResolverBase._eventSubscriptionKey];
|
|
47
58
|
}
|
|
59
|
+
get activeEntitySessions() {
|
|
60
|
+
const g = MJGlobal.Instance.GetGlobalObjectStore();
|
|
61
|
+
if (!g[ResolverBase._activeEntitySessionsKey]) {
|
|
62
|
+
g[ResolverBase._activeEntitySessionsKey] = new Map();
|
|
63
|
+
}
|
|
64
|
+
return g[ResolverBase._activeEntitySessionsKey];
|
|
65
|
+
}
|
|
48
66
|
/**
|
|
49
67
|
* Maps field names to their GraphQL-safe CodeNames and handles encryption for API responses.
|
|
50
68
|
*
|
|
@@ -334,28 +352,25 @@ export class ResolverBase {
|
|
|
334
352
|
if (field.AllowDecryptInAPI)
|
|
335
353
|
continue;
|
|
336
354
|
// AllowDecryptInAPI is false - we need to filter the value
|
|
355
|
+
if (!field.SendEncryptedValue) {
|
|
356
|
+
// SendEncryptedValue is false - replace with sentinel immediately without touching EncryptionEngine
|
|
357
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
337
360
|
const engine = EncryptionEngine.Instance;
|
|
338
361
|
await engine.Config(false, contextUser);
|
|
339
362
|
const keyMarker = field.EncryptionKeyID ? engine.GetKeyByID(field.EncryptionKeyID)?.Marker : undefined;
|
|
340
|
-
if (
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
351
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
352
|
-
}
|
|
363
|
+
// Only encrypt if the value is not already encrypted (PreserveRestrictedApiCiphertext keeps DB/cache ciphertext intact)
|
|
364
|
+
if (typeof value === 'string' && !IsValueEncrypted(value, keyMarker)) {
|
|
365
|
+
try {
|
|
366
|
+
const encryptedValue = await engine.Encrypt(value, field.EncryptionKeyID, contextUser);
|
|
367
|
+
dataObject[fieldName] = encryptedValue;
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
// If re-encryption fails, use sentinel for safety
|
|
371
|
+
LogError(`Failed to re-encrypt field ${fieldName} for API response: ${err}`);
|
|
372
|
+
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
353
373
|
}
|
|
354
|
-
// If already encrypted (shouldn't happen normally), keep as-is
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
// SendEncryptedValue is false - replace with sentinel
|
|
358
|
-
dataObject[fieldName] = ENCRYPTED_SENTINEL;
|
|
359
374
|
}
|
|
360
375
|
}
|
|
361
376
|
return dataObject;
|
|
@@ -505,7 +520,7 @@ export class ResolverBase {
|
|
|
505
520
|
if (viewInfo) {
|
|
506
521
|
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, viewInput.AfterKey
|
|
507
522
|
? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
|
|
508
|
-
: undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
|
|
523
|
+
: undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
|
|
509
524
|
}
|
|
510
525
|
else {
|
|
511
526
|
LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
|
|
@@ -528,7 +543,7 @@ export class ResolverBase {
|
|
|
528
543
|
if (!viewInfo) {
|
|
529
544
|
return null;
|
|
530
545
|
}
|
|
531
|
-
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties);
|
|
546
|
+
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, viewInput.Properties, viewInput.SkipTotalRowCount);
|
|
532
547
|
}
|
|
533
548
|
catch (err) {
|
|
534
549
|
console.log(err);
|
|
@@ -551,7 +566,7 @@ export class ResolverBase {
|
|
|
551
566
|
EntityID: entity.ID,
|
|
552
567
|
EntityBaseView: entity.BaseView,
|
|
553
568
|
}; // only providing a few bits of data here, but it's enough to get the view to run
|
|
554
|
-
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView);
|
|
569
|
+
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields, viewInput.AlternateBaseView, undefined, viewInput.SkipTotalRowCount);
|
|
555
570
|
}
|
|
556
571
|
catch (err) {
|
|
557
572
|
console.log(err);
|
|
@@ -621,6 +636,7 @@ export class ResolverBase {
|
|
|
621
636
|
bypassCache: viewInput.BypassCache,
|
|
622
637
|
dataSource: viewInput.DataSource,
|
|
623
638
|
includeBinaryFields: viewInput.IncludeBinaryFields,
|
|
639
|
+
skipTotalRowCount: viewInput.SkipTotalRowCount,
|
|
624
640
|
alternateBaseView: viewInput.AlternateBaseView,
|
|
625
641
|
properties: viewInput.Properties,
|
|
626
642
|
});
|
|
@@ -844,7 +860,7 @@ export class ResolverBase {
|
|
|
844
860
|
* Fails closed: no user, or an unresolvable reference, refuses.
|
|
845
861
|
*
|
|
846
862
|
* The references come from a walk of the whole parse tree
|
|
847
|
-
* (`
|
|
863
|
+
* (`StrictFragmentValidator.ValidateReadOnlyStatement`). The statement is refused when that walk does not
|
|
848
864
|
* account for every SELECT in the text, or when a FROM item names no table (a table function or
|
|
849
865
|
* a row list).
|
|
850
866
|
*
|
|
@@ -860,14 +876,7 @@ export class ResolverBase {
|
|
|
860
876
|
}
|
|
861
877
|
const dialect = this.dialectForProvider(provider);
|
|
862
878
|
ClientClauseScreen.AssertNoIdentifierAliasNames(sqlText, dialect, label);
|
|
863
|
-
const
|
|
864
|
-
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
865
|
-
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
866
|
-
}
|
|
867
|
-
if (this.astContainsWriteNode(parser.AST)) {
|
|
868
|
-
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
869
|
-
}
|
|
870
|
-
const reads = ClientClauseScreen.CollectStatementReads(parser);
|
|
879
|
+
const reads = this.strictReadOnlyStatementReads(sqlText, dialect, label);
|
|
871
880
|
ClientClauseScreen.AssertEverySelectAccountedFor(sqlText, dialect, reads, label);
|
|
872
881
|
const allowed = this.entityBaseViewAllowList(provider);
|
|
873
882
|
for (const relation of reads.Relations) {
|
|
@@ -924,9 +933,19 @@ export class ResolverBase {
|
|
|
924
933
|
hasBackslashInQuotes(clause, dialect) {
|
|
925
934
|
return LexSQL(clause, dialect).some((t) => (t.Kind === 'string' || t.Kind === 'identifier') && t.Text.includes('\\'));
|
|
926
935
|
}
|
|
927
|
-
/**
|
|
928
|
-
|
|
929
|
-
|
|
936
|
+
/**
|
|
937
|
+
* Parses a whole statement with the strict validator and returns what it reads; throws unless it
|
|
938
|
+
* is one plain read-only SELECT with no write or DDL at any depth.
|
|
939
|
+
*/
|
|
940
|
+
strictReadOnlyStatementReads(sqlText, dialect, label) {
|
|
941
|
+
const result = StrictFragmentValidator.ValidateReadOnlyStatement(sqlText, dialect, { CollectReads: true });
|
|
942
|
+
if (result.Refusal === 'NestedWrite') {
|
|
943
|
+
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
944
|
+
}
|
|
945
|
+
if (!result.IsValid || !result.Reads) {
|
|
946
|
+
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
947
|
+
}
|
|
948
|
+
return result.Reads;
|
|
930
949
|
}
|
|
931
950
|
dialectForProvider(provider) {
|
|
932
951
|
return ClientClauseScreen.DialectFor(provider);
|
|
@@ -940,7 +959,7 @@ export class ResolverBase {
|
|
|
940
959
|
* - Field filtering at source (Fix #7)
|
|
941
960
|
* - Improved error handling (Fix #9)
|
|
942
961
|
*/
|
|
943
|
-
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties) {
|
|
962
|
+
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields, alternateBaseView, properties, skipTotalRowCount) {
|
|
944
963
|
try {
|
|
945
964
|
if (!viewInfo || !userPayload)
|
|
946
965
|
return null;
|
|
@@ -1007,9 +1026,11 @@ export class ResolverBase {
|
|
|
1007
1026
|
BypassCache: bypassCache,
|
|
1008
1027
|
DataSource: dataSource,
|
|
1009
1028
|
IncludeBinaryFields: includeBinaryFields,
|
|
1029
|
+
SkipTotalRowCount: skipTotalRowCount,
|
|
1010
1030
|
AlternateBaseView: alternateBaseView,
|
|
1011
1031
|
Properties: properties,
|
|
1012
1032
|
TimeZone: userPayload?.TimeZone,
|
|
1033
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1013
1034
|
}, user);
|
|
1014
1035
|
if (result && typeof result === 'object') {
|
|
1015
1036
|
Object.defineProperty(result, '_resolvedViewEntity', {
|
|
@@ -1150,9 +1171,11 @@ export class ResolverBase {
|
|
|
1150
1171
|
BypassCache: param.bypassCache,
|
|
1151
1172
|
DataSource: param.dataSource,
|
|
1152
1173
|
IncludeBinaryFields: param.includeBinaryFields,
|
|
1174
|
+
SkipTotalRowCount: param.skipTotalRowCount,
|
|
1153
1175
|
AlternateBaseView: param.alternateBaseView,
|
|
1154
1176
|
Properties: param.properties,
|
|
1155
1177
|
TimeZone: param.userPayload?.TimeZone,
|
|
1178
|
+
PreserveRestrictedApiCiphertext: true,
|
|
1156
1179
|
});
|
|
1157
1180
|
}
|
|
1158
1181
|
// Fix #4: Run views in a single batch through RunViews
|
|
@@ -1324,11 +1347,38 @@ export class ResolverBase {
|
|
|
1324
1347
|
});
|
|
1325
1348
|
}
|
|
1326
1349
|
ListenForEntityMessages(entityObject, pubSub, userPayload) {
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
//
|
|
1350
|
+
// Maintain a single MJGlobal event listener per entity name so CloudEvents are emitted once
|
|
1351
|
+
// per event, while routing WebSocket status updates dynamically across all active user sessions
|
|
1352
|
+
// that have interacted with this entity type (instead of closing over only the first caller).
|
|
1330
1353
|
const uniqueKey = entityObject.EntityInfo.Name;
|
|
1331
|
-
|
|
1354
|
+
const now = Date.now();
|
|
1355
|
+
const isFreshSubscription = !this.eventSubscriptions.has(uniqueKey);
|
|
1356
|
+
let sessionsForEntity = isFreshSubscription ? undefined : this.activeEntitySessions.get(uniqueKey);
|
|
1357
|
+
if (!sessionsForEntity) {
|
|
1358
|
+
sessionsForEntity = new Map();
|
|
1359
|
+
this.activeEntitySessions.set(uniqueKey, sessionsForEntity);
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
// Opportunistically evict expired session entries and enforce a safe per-entity cap
|
|
1363
|
+
for (const [key, entry] of sessionsForEntity) {
|
|
1364
|
+
if (now - entry.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1365
|
+
sessionsForEntity.delete(key);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (sessionsForEntity.size >= ACTIVE_ENTITY_SESSION_MAX_PER_ENTITY) {
|
|
1369
|
+
const oldestKey = sessionsForEntity.keys().next().value;
|
|
1370
|
+
if (oldestKey !== undefined) {
|
|
1371
|
+
sessionsForEntity.delete(oldestKey);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (userPayload) {
|
|
1376
|
+
const sessionKey = userPayload.sessionId
|
|
1377
|
+
? `${userPayload.userRecord?.ID ?? ''}:${userPayload.sessionId}`
|
|
1378
|
+
: (userPayload.userRecord?.ID ?? '__anonymous__');
|
|
1379
|
+
sessionsForEntity.set(sessionKey, { pubSub, userPayload, lastSeenAt: now });
|
|
1380
|
+
}
|
|
1381
|
+
if (isFreshSubscription) {
|
|
1332
1382
|
// 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
|
|
1333
1383
|
LogDebug(`ResolverBase.ListenForEntityMessages: About to call MJGlobal.Instance.GetEventListener() to get the event listener subscription for ${uniqueKey}`);
|
|
1334
1384
|
const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
|
|
@@ -1340,14 +1390,27 @@ export class ResolverBase {
|
|
|
1340
1390
|
return;
|
|
1341
1391
|
}
|
|
1342
1392
|
const baseEntityValues = baseEntity ? this.EntityEventPayload(baseEntity, event.args instanceof BaseEntityEvent ? event.args.ChangedFieldNames : undefined) : null;
|
|
1343
|
-
const eventToLog = { entityName:
|
|
1393
|
+
const eventToLog = { entityName: uniqueKey, baseEntity: baseEntityValues, event: event.event, eventCode: event.eventCode };
|
|
1344
1394
|
LogDebug(`ResolverBase.ListenForEntityMessages: Received Event from within MJGlobal.Instance.GetEventListener() callback. Will call EmitCloudEvent() next\nEvent data:\n${JSON.stringify(eventToLog)}`);
|
|
1345
1395
|
await this.EmitCloudEvent(event);
|
|
1346
1396
|
LogDebug(`ResolverBase.ListenForEntityMessages: EmitCloudEvent() completed successfully`);
|
|
1347
|
-
if (event.args && event.args instanceof BaseEntityEvent
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1397
|
+
if (event.args && event.args instanceof BaseEntityEvent) {
|
|
1398
|
+
const activeSessions = this.activeEntitySessions.get(uniqueKey);
|
|
1399
|
+
if (activeSessions && activeSessions.size > 0) {
|
|
1400
|
+
const eventNow = Date.now();
|
|
1401
|
+
let serializedMessage;
|
|
1402
|
+
for (const [sKey, session] of activeSessions) {
|
|
1403
|
+
if (eventNow - session.lastSeenAt > ACTIVE_ENTITY_SESSION_TTL_MS) {
|
|
1404
|
+
activeSessions.delete(sKey);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (this.EntityEventBelongsToSession(event.args, session.userPayload)) {
|
|
1408
|
+
LogDebug('ResolverBase.ListenForEntityMessages: About to publish PUSH_STATUS_UPDATES_TOPIC');
|
|
1409
|
+
serializedMessage ??= this.EntityStatusMessage(event.args);
|
|
1410
|
+
this.PublishStatusUpdate(session.pubSub, session.userPayload.sessionId, serializedMessage, session.userPayload);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1351
1414
|
}
|
|
1352
1415
|
}
|
|
1353
1416
|
});
|
|
@@ -1823,134 +1886,268 @@ export class ResolverBase {
|
|
|
1823
1886
|
}
|
|
1824
1887
|
return entityInfo.Fields.find((f) => f.CodeName === key || f.Name === key);
|
|
1825
1888
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1889
|
+
/**
|
|
1890
|
+
* Whether the API never gives a client this field's stored value: an encrypted field that is not
|
|
1891
|
+
* decrypted for the API reaches the client only as the sentinel or as fresh ciphertext, so the
|
|
1892
|
+
* client's old value for it can never be compared with the database value.
|
|
1893
|
+
*/
|
|
1894
|
+
isFieldOpaqueToClient(field) {
|
|
1895
|
+
return Boolean(field?.Encrypt && !field.AllowDecryptInAPI);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* The client's new values without the opaque fields it only echoed back: the sentinel, or the same
|
|
1899
|
+
* value it received as the old value. Writing either would replace the stored secret.
|
|
1900
|
+
*/
|
|
1901
|
+
withoutEchoedOpaqueValues(entityInfo, clientNewValues, clientOldValues) {
|
|
1902
|
+
const kept = {};
|
|
1903
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
1904
|
+
const f = this.findEntityFieldByCodeNameOrName(entityInfo, key);
|
|
1905
|
+
const newValue = clientNewValues[key];
|
|
1906
|
+
const unchanged = key in clientOldValues && !this.areEntityFieldValuesDifferent(f, clientOldValues[key], newValue, false);
|
|
1907
|
+
const echoed = this.isFieldOpaqueToClient(f) && (newValue === ENCRYPTED_SENTINEL || unchanged);
|
|
1908
|
+
if (!echoed) {
|
|
1909
|
+
kept[key] = newValue;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return kept;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Copies concurrency-conflict diagnostic entries with every encrypted field's values replaced by
|
|
1916
|
+
* the sentinel, so the console, the error log and `MJ: Error Logs` never hold a secret.
|
|
1917
|
+
*/
|
|
1918
|
+
redactEncryptedConflictValues(entityInfo, entries) {
|
|
1919
|
+
return entries.map((entry) => {
|
|
1920
|
+
if (!this.findEntityFieldByCodeNameOrName(entityInfo, entry.FieldName)?.Encrypt) {
|
|
1921
|
+
return entry;
|
|
1922
|
+
}
|
|
1923
|
+
const redacted = { FieldName: entry.FieldName };
|
|
1924
|
+
if ('ClientOldValue' in entry)
|
|
1925
|
+
redacted.ClientOldValue = ENCRYPTED_SENTINEL;
|
|
1926
|
+
if ('ClientNewValue' in entry)
|
|
1927
|
+
redacted.ClientNewValue = ENCRYPTED_SENTINEL;
|
|
1928
|
+
if ('DBValue' in entry)
|
|
1929
|
+
redacted.DBValue = ENCRYPTED_SENTINEL;
|
|
1930
|
+
return redacted;
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Compares two typed entity field values for semantic inequality, handling `Date` epoch equality
|
|
1935
|
+
* (plus optional non-`datetimeoffset` timezone-hour-shift tolerance) and `null`/`undefined` normalization.
|
|
1936
|
+
*/
|
|
1937
|
+
areEntityFieldValuesDifferent(field, left, right, allowTimezoneShiftWarning, fieldKeyForLog) {
|
|
1938
|
+
const normalizedLeft = left === undefined ? null : left;
|
|
1939
|
+
const normalizedRight = right === undefined ? null : right;
|
|
1940
|
+
if (normalizedLeft instanceof Date) {
|
|
1941
|
+
const rightDate = normalizedRight instanceof Date
|
|
1942
|
+
? normalizedRight
|
|
1943
|
+
: field?.TSType === EntityFieldTSType.Date && normalizedRight !== null
|
|
1944
|
+
? this.ClientOldValueToFieldValue(field, normalizedRight)
|
|
1945
|
+
: null;
|
|
1946
|
+
if (!(rightDate instanceof Date) || isNaN(rightDate.getTime())) {
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
if (this.datesEqualAtColumnResolution(field, normalizedLeft, rightDate)) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
const sqlType = field?.SQLFullType?.trim().toLowerCase() || '';
|
|
1953
|
+
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(normalizedLeft, rightDate)) {
|
|
1954
|
+
if (allowTimezoneShiftWarning && fieldKeyForLog) {
|
|
1955
|
+
console.warn(`Timezone hour shift detected on field "${fieldKeyForLog}" (${sqlType || 'datetime'}). ` +
|
|
1956
|
+
`Client: ${normalizedLeft.toISOString()}, DB: ${rightDate.toISOString()}. ` +
|
|
1957
|
+
`Consider using datetimeoffset to avoid timezone ambiguity.`);
|
|
1958
|
+
}
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
return true;
|
|
1962
|
+
}
|
|
1963
|
+
if (normalizedRight instanceof Date && field?.TSType === EntityFieldTSType.Date && normalizedLeft !== null) {
|
|
1964
|
+
const leftDate = this.ClientOldValueToFieldValue(field, normalizedLeft);
|
|
1965
|
+
if (leftDate instanceof Date && !isNaN(leftDate.getTime())) {
|
|
1966
|
+
return !this.datesEqualAtColumnResolution(field, leftDate, normalizedRight);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof normalizedLeft === 'string' && typeof normalizedRight === 'string' && this.jsonStringsEquivalent(normalizedLeft, normalizedRight)) {
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
return Object.is(normalizedLeft, normalizedRight) ? false : normalizedLeft !== normalizedRight;
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* True when two instants would be stored as the same value in the field's column: SQL Server
|
|
1976
|
+
* `datetime` resolves to 1/300 s and `smalldatetime` to one minute; every other type keeps milliseconds.
|
|
1977
|
+
*/
|
|
1978
|
+
datesEqualAtColumnResolution(field, left, right) {
|
|
1979
|
+
return Math.abs(left.getTime() - right.getTime()) < this.dateColumnResolutionMs(field);
|
|
1980
|
+
}
|
|
1981
|
+
dateColumnResolutionMs(field) {
|
|
1982
|
+
switch ((field?.Type ?? '').trim().toLowerCase()) {
|
|
1983
|
+
case 'datetime':
|
|
1984
|
+
return 1000 / 300;
|
|
1985
|
+
case 'smalldatetime':
|
|
1986
|
+
return 60_000;
|
|
1987
|
+
default:
|
|
1988
|
+
return 1;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* True when both strings are JSON documents with the same content, ignoring whitespace and key order.
|
|
1993
|
+
*/
|
|
1994
|
+
jsonStringsEquivalent(left, right) {
|
|
1995
|
+
const canonicalLeft = this.canonicalJSONDocument(left);
|
|
1996
|
+
return canonicalLeft !== null && canonicalLeft === this.canonicalJSONDocument(right);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Canonical form of a JSON object or array string, or null when the text is not one.
|
|
2000
|
+
*/
|
|
2001
|
+
canonicalJSONDocument(text) {
|
|
2002
|
+
const first = text.trimStart().charAt(0);
|
|
2003
|
+
if (first !== '{' && first !== '[') {
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
try {
|
|
2007
|
+
return Canonicalize(JSON.parse(text));
|
|
2008
|
+
}
|
|
2009
|
+
catch {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async TestAndSetClientOldValuesToDBValues(input, rawClientNewValues, entityObject, contextUser) {
|
|
1828
2014
|
const clientOldValues = {};
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
// we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
|
|
2015
|
+
for (const item of input.OldValues___ ?? []) {
|
|
2016
|
+
// Transform string-serialized wire values to match the field's TypeScript type
|
|
1832
2017
|
const field = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, item.Key);
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
// clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
|
|
1837
|
-
// with the DB values
|
|
2018
|
+
clientOldValues[item.Key] = this.ClientOldValueToFieldValue(field, item.Value);
|
|
2019
|
+
}
|
|
2020
|
+
const clientNewValues = this.withoutEchoedOpaqueValues(entityObject.EntityInfo, rawClientNewValues, clientOldValues);
|
|
1838
2021
|
// fls-guard: compared on the server only, never returned
|
|
1839
2022
|
const dbValues = entityObject.GetAll();
|
|
1840
|
-
// now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
|
|
1841
2023
|
const dbDifferences = [];
|
|
1842
|
-
|
|
2024
|
+
const dbDiffByField = new Map();
|
|
2025
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
1843
2026
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
switch (typeof clientOldValues[key]) {
|
|
1847
|
-
case 'number':
|
|
1848
|
-
different = clientOldValues[key] !== dbVal;
|
|
1849
|
-
break;
|
|
1850
|
-
case 'boolean':
|
|
1851
|
-
different = clientOldValues[key] !== dbVal;
|
|
1852
|
-
break;
|
|
1853
|
-
case 'object':
|
|
1854
|
-
if (clientOldValues[key] instanceof Date) {
|
|
1855
|
-
const clientDate = clientOldValues[key];
|
|
1856
|
-
const dbDate = dbVal;
|
|
1857
|
-
if (dbDate == null || !(dbDate instanceof Date)) {
|
|
1858
|
-
different = true;
|
|
1859
|
-
}
|
|
1860
|
-
else if (clientDate.getTime() !== dbDate.getTime()) {
|
|
1861
|
-
// Check if this is a datetime/datetime2 field with only a timezone hour shift
|
|
1862
|
-
const sqlType = f?.SQLFullType?.trim().toLowerCase() || '';
|
|
1863
|
-
if (sqlType !== 'datetimeoffset' && IsOnlyTimezoneShift(clientDate, dbDate)) {
|
|
1864
|
-
console.warn(`Timezone hour shift detected on field "${key}" (${sqlType || 'datetime'}). ` +
|
|
1865
|
-
`Client: ${clientDate.toISOString()}, DB: ${dbDate.toISOString()}. ` +
|
|
1866
|
-
`Consider using datetimeoffset to avoid timezone ambiguity.`);
|
|
1867
|
-
different = false; // Allow timezone shifts through for non-datetimeoffset fields
|
|
1868
|
-
}
|
|
1869
|
-
else {
|
|
1870
|
-
different = true;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
else if (clientOldValues[key] === null) {
|
|
1875
|
-
different = dbVal !== null;
|
|
1876
|
-
}
|
|
1877
|
-
break;
|
|
1878
|
-
default:
|
|
1879
|
-
different = clientOldValues[key] !== dbVal;
|
|
1880
|
-
break;
|
|
2027
|
+
if (this.isFieldOpaqueToClient(f)) {
|
|
2028
|
+
continue;
|
|
1881
2029
|
}
|
|
2030
|
+
const dbVal = f && f.Name in dbValues ? dbValues[f.Name] : dbValues[key];
|
|
2031
|
+
const different = this.areEntityFieldValuesDifferent(f, clientOldValues[key], dbVal, true, key);
|
|
1882
2032
|
if (different && f && !f.ReadOnly) {
|
|
1883
|
-
|
|
1884
|
-
dbDifferences.push({
|
|
2033
|
+
const entry = {
|
|
1885
2034
|
FieldName: key,
|
|
1886
2035
|
ClientOldValue: clientOldValues[key],
|
|
1887
2036
|
DBValue: dbVal,
|
|
1888
|
-
}
|
|
2037
|
+
};
|
|
2038
|
+
dbDifferences.push(entry);
|
|
2039
|
+
dbDiffByField.set(key, entry);
|
|
1889
2040
|
}
|
|
1890
|
-
}
|
|
2041
|
+
}
|
|
2042
|
+
let fieldsToPreserveFromDB;
|
|
1891
2043
|
if (dbDifferences.length > 0) {
|
|
1892
|
-
// now we have an array of any dbDifferences with length > 0, between the clientOldValues and the dbValues, we need to check to see if any of the differences are on fields that the client is trying to update
|
|
1893
|
-
// first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
|
|
1894
2044
|
const clientDifferences = [];
|
|
1895
|
-
|
|
2045
|
+
const clientDiffFieldNames = new Set();
|
|
2046
|
+
for (const key of Object.keys(clientOldValues)) {
|
|
1896
2047
|
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, key);
|
|
1897
|
-
if (key in clientNewValues &&
|
|
1898
|
-
|
|
2048
|
+
if (key in clientNewValues &&
|
|
2049
|
+
f &&
|
|
2050
|
+
f.AllowUpdateAPI &&
|
|
2051
|
+
!f.IsPrimaryKey &&
|
|
2052
|
+
this.areEntityFieldValuesDifferent(f, clientOldValues[key], clientNewValues[key], false)) {
|
|
1899
2053
|
clientDifferences.push({
|
|
1900
2054
|
FieldName: key,
|
|
1901
2055
|
ClientOldValue: clientOldValues[key],
|
|
1902
2056
|
ClientNewValue: clientNewValues[key],
|
|
1903
2057
|
});
|
|
2058
|
+
clientDiffFieldNames.add(key);
|
|
1904
2059
|
}
|
|
2060
|
+
}
|
|
2061
|
+
// True conflicting overlap: client modified a field whose authoritative DB value differs from BOTH
|
|
2062
|
+
// ClientOldValue AND ClientNewValue (idempotent concurrent writes to the same value converge cleanly).
|
|
2063
|
+
const overlap = clientDifferences.filter((cd) => {
|
|
2064
|
+
const dd = dbDiffByField.get(cd.FieldName);
|
|
2065
|
+
if (!dd) {
|
|
2066
|
+
return false;
|
|
2067
|
+
}
|
|
2068
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, cd.FieldName);
|
|
2069
|
+
return this.areEntityFieldValuesDifferent(f, cd.ClientNewValue, dd.DBValue, true);
|
|
1905
2070
|
});
|
|
1906
|
-
// 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
|
|
1907
|
-
// 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
|
|
1908
|
-
const dbDiffFieldNames = new Set(dbDifferences.map((dd) => dd.FieldName));
|
|
1909
|
-
const overlap = clientDifferences.filter((cd) => dbDiffFieldNames.has(cd.FieldName));
|
|
1910
2071
|
if (overlap.length > 0) {
|
|
2072
|
+
const conflictingFields = overlap.map((o) => o.FieldName);
|
|
2073
|
+
const entityInfo = entityObject.EntityInfo;
|
|
2074
|
+
const shownClientDifferences = this.redactEncryptedConflictValues(entityInfo, clientDifferences);
|
|
2075
|
+
const shownDBDifferences = this.redactEncryptedConflictValues(entityInfo, dbDifferences);
|
|
2076
|
+
const shownOverlap = this.redactEncryptedConflictValues(entityInfo, overlap);
|
|
1911
2077
|
const msg = {
|
|
1912
|
-
Message: '
|
|
1913
|
-
ClientDifferences:
|
|
1914
|
-
DBDifferences:
|
|
1915
|
-
Overlap:
|
|
2078
|
+
Message: 'Concurrent modification conflict: one or more fields modified by the client were updated in the database since the record was loaded. Save rejected to prevent overwriting concurrent changes.',
|
|
2079
|
+
ClientDifferences: shownClientDifferences,
|
|
2080
|
+
DBDifferences: shownDBDifferences,
|
|
2081
|
+
Overlap: shownOverlap,
|
|
1916
2082
|
};
|
|
1917
|
-
|
|
1918
|
-
console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
|
|
2083
|
+
console.warn('Entity save concurrency conflict detected — rejecting conflicting update:', JSON.stringify(msg));
|
|
1919
2084
|
LogErrorEx({
|
|
1920
2085
|
message: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
|
|
1921
2086
|
severity: 'warning',
|
|
1922
2087
|
category: 'ResolverBase.TestAndSetClientOldValuesToDBValues',
|
|
1923
2088
|
metadata: {
|
|
1924
2089
|
entityName: entityObject.EntityInfo.Name,
|
|
1925
|
-
clientDifferences:
|
|
1926
|
-
dbDifferences:
|
|
1927
|
-
overlap:
|
|
2090
|
+
clientDifferences: shownClientDifferences,
|
|
2091
|
+
dbDifferences: shownDBDifferences,
|
|
2092
|
+
overlap: shownOverlap,
|
|
1928
2093
|
},
|
|
1929
2094
|
});
|
|
1930
|
-
//
|
|
1931
|
-
// ErrorLog write goes to the same connection as the entity that triggered it.
|
|
2095
|
+
// Persist SAVE_INCONSISTENCY ErrorLog record on the entity's bound provider before rejecting
|
|
1932
2096
|
try {
|
|
1933
|
-
const
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2097
|
+
const hasEntityObjectFactory = (p) => Boolean(p && 'GetEntityObject' in p);
|
|
2098
|
+
const providerCandidate = entityObject.ProviderToUse;
|
|
2099
|
+
if (hasEntityObjectFactory(providerCandidate)) {
|
|
2100
|
+
const errorLogEntity = await providerCandidate.GetEntityObject('MJ: Error Logs', contextUser);
|
|
2101
|
+
// Code is NCHAR(20); a longer value is rejected at the database
|
|
2102
|
+
errorLogEntity.Code = 'SAVE_INCONSISTENCY';
|
|
2103
|
+
errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
|
|
2104
|
+
errorLogEntity.Status = 'Warning';
|
|
2105
|
+
errorLogEntity.Category = 'Entity Save';
|
|
2106
|
+
errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
|
|
2107
|
+
const saveResult = await errorLogEntity.Save();
|
|
2108
|
+
if (!saveResult) {
|
|
2109
|
+
console.error(`Failed to save ErrorLog record for ${entityObject.EntityInfo.Name}: ${errorLogEntity.LatestResult?.CompleteMessage}`);
|
|
2110
|
+
}
|
|
1944
2111
|
}
|
|
1945
2112
|
}
|
|
1946
2113
|
catch (errorLogError) {
|
|
1947
2114
|
console.error('Error creating ErrorLog record:', errorLogError);
|
|
1948
2115
|
}
|
|
2116
|
+
throw new GraphQLError(`Concurrent modification conflict on ${entityObject.EntityInfo.Name} for field(s): ${conflictingFields.join(', ')}. Reload the record and retry your changes.`, {
|
|
2117
|
+
extensions: {
|
|
2118
|
+
code: 'CONCURRENT_MODIFICATION_CONFLICT',
|
|
2119
|
+
entityName: entityObject.EntityInfo.Name,
|
|
2120
|
+
conflictingFields,
|
|
2121
|
+
},
|
|
2122
|
+
});
|
|
1949
2123
|
}
|
|
2124
|
+
// Non-overlapping concurrent DB edits: preserve any field that changed in the database
|
|
2125
|
+
// when the client itself did NOT modify that field (prevents full-payload clients or forged
|
|
2126
|
+
// OldValues___ from clobbering concurrent DB updates on untouched columns).
|
|
2127
|
+
fieldsToPreserveFromDB = new Set();
|
|
2128
|
+
for (const dd of dbDifferences) {
|
|
2129
|
+
if (!clientDiffFieldNames.has(dd.FieldName)) {
|
|
2130
|
+
fieldsToPreserveFromDB.add(dd.FieldName);
|
|
2131
|
+
const f = this.findEntityFieldByCodeNameOrName(entityObject.EntityInfo, dd.FieldName);
|
|
2132
|
+
if (f) {
|
|
2133
|
+
fieldsToPreserveFromDB.add(f.Name);
|
|
2134
|
+
fieldsToPreserveFromDB.add(f.CodeName);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
if (fieldsToPreserveFromDB && fieldsToPreserveFromDB.size > 0) {
|
|
2140
|
+
const mergedValues = {};
|
|
2141
|
+
for (const key of Object.keys(clientNewValues)) {
|
|
2142
|
+
if (!fieldsToPreserveFromDB.has(key)) {
|
|
2143
|
+
mergedValues[key] = clientNewValues[key];
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
entityObject.SetMany(mergedValues);
|
|
2147
|
+
}
|
|
2148
|
+
else {
|
|
2149
|
+
entityObject.SetMany(clientNewValues);
|
|
1950
2150
|
}
|
|
1951
|
-
// If we get here that means we've not thrown an exception, so there is
|
|
1952
|
-
// NO OVERLAP, so we can set the new values from the data provided from the client now...
|
|
1953
|
-
entityObject.SetMany(clientNewValues);
|
|
1954
2151
|
}
|
|
1955
2152
|
async DeleteRecord(entityName, key, options, provider, userPayload, pubSub) {
|
|
1956
2153
|
// Check API key scope authorization for entity delete operations
|