@memberjunction/server 6.2.0-edge.6 → 6.2.0-edge.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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/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 +35 -4
- 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 +228 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +1163 -6
- 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 +321 -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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +157 -35
- 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/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/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.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +2 -1
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/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/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 +36 -0
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +174 -87
- 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/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +2 -0
- package/dist/resolvers/meetingRecordingRegistration.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/package.json +98 -98
- 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 +323 -26
- 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__/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__/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__/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__/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/config.ts +69 -0
- package/src/context.ts +39 -6
- package/src/coreRoutes.ts +14 -7
- package/src/generated/graphql-schemas/__mj.ts +822 -7
- package/src/generic/OrphanedConversationDetailReconciler.ts +32 -1
- package/src/generic/ResolverBase.ts +414 -143
- package/src/generic/RunViewResolver.ts +96 -49
- package/src/index.ts +201 -35
- 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/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/AgentSessionResolver.ts +6 -2
- package/src/resolvers/AutotagPipelineResolver.ts +2 -1
- package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
- package/src/resolvers/GetDataResolver.ts +35 -13
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1522 -203
- package/src/resolvers/MCPResolver.ts +15 -2
- package/src/resolvers/QuerySystemUserResolver.ts +197 -98
- 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/__tests__/GetDataResolver.tokens.test.ts +52 -0
- package/src/resolvers/meetingRecordingRegistration.ts +2 -0
- 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/services/TaskOrchestration-Integration.md +0 -188
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A table selection's ids resolve against the CONNECTION's catalog, and an apply that resolves
|
|
3
|
+
* nothing builds nothing.
|
|
4
|
+
*
|
|
5
|
+
* Inside a catalog scope, `IntegrationListSourceObjects` hands the picker per-connection
|
|
6
|
+
* (CompanyIntegrationObject) ids. Every name lookup the apply paths ran read only the shared
|
|
7
|
+
* `MJ: Integration Objects` catalog, so an id-only selection was dropped with a LogError, the
|
|
8
|
+
* migration ran with an empty table list, CodeGen and the restart still ran, and the apply reported
|
|
9
|
+
* Success with 0 tables — 118 picked, 0 built, twice, on a WordPress connection.
|
|
10
|
+
*
|
|
11
|
+
* These pin the lookup the four resolver paths now share, and — against the source, because the
|
|
12
|
+
* resolver needs a provider, a connector and an RSU pipeline to invoke — that the apply refuses an
|
|
13
|
+
* empty resolution before it builds anything.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { RunView, type UserInfo } from '@memberjunction/core';
|
|
19
|
+
import { IntegrationEngineBase } from '@memberjunction/integration-engine-base';
|
|
20
|
+
import { WithCatalogScope } from '@memberjunction/integration-engine';
|
|
21
|
+
import { LookupObjectNamesByID, OBJECT_ID_LOOKUP_CHUNK } from '../integration/ObjectNamesByID.js';
|
|
22
|
+
|
|
23
|
+
const USER = { ID: 'user-1' } as UserInfo;
|
|
24
|
+
type RunViewParams = Parameters<RunView['RunView']>[0];
|
|
25
|
+
type RunViewResult = Awaited<ReturnType<RunView['RunView']>>;
|
|
26
|
+
|
|
27
|
+
/** An engine holding a per-connection catalog: each row is a CIO id, its shared IO id, and a name. */
|
|
28
|
+
function installEngine(rows: Array<{ ID: string; IntegrationObjectID: string | null; Name: string }>, hasCatalog = true) {
|
|
29
|
+
vi.spyOn(IntegrationEngineBase, 'Instance', 'get').mockReturnValue({
|
|
30
|
+
HasCompanyIntegrationCatalog: () => hasCatalog,
|
|
31
|
+
GetCompanyIntegrationObjects: () => rows,
|
|
32
|
+
} as unknown as IntegrationEngineBase);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('LookupObjectNamesByID', () => {
|
|
36
|
+
let runView: ReturnType<typeof vi.spyOn>;
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
runView = vi.spyOn(RunView.prototype, 'RunView');
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
41
|
+
|
|
42
|
+
it('resolves per-connection ids from the scope\'s catalog with no query, whatever their case', async () => {
|
|
43
|
+
installEngine([
|
|
44
|
+
{ ID: 'cio-aaa', IntegrationObjectID: 'io-111', Name: 'posts' },
|
|
45
|
+
{ ID: 'cio-bbb', IntegrationObjectID: null, Name: 'pages' },
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const map = await WithCatalogScope('CI-1', () => LookupObjectNamesByID(['CIO-AAA', 'cio-bbb'], USER));
|
|
49
|
+
|
|
50
|
+
expect(map.get('CIO-AAA')).toBe('posts');
|
|
51
|
+
expect(map.get('CIO-BBB')).toBe('pages');
|
|
52
|
+
expect(runView).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('also resolves a shared IntegrationObject id through the row that carries it', async () => {
|
|
56
|
+
installEngine([{ ID: 'cio-aaa', IntegrationObjectID: 'io-111', Name: 'posts' }]);
|
|
57
|
+
|
|
58
|
+
const map = await WithCatalogScope('CI-1', () => LookupObjectNamesByID(['IO-111'], USER));
|
|
59
|
+
|
|
60
|
+
expect(map.get('IO-111')).toBe('posts');
|
|
61
|
+
expect(runView).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('reads a cache miss straight from the connection\'s catalog: one uncached query, scoped to the connection', async () => {
|
|
65
|
+
installEngine([{ ID: 'cio-aaa', IntegrationObjectID: null, Name: 'posts' }]);
|
|
66
|
+
runView.mockResolvedValue({ Success: true, Results: [{ ID: 'cio-new', Name: 'comments' }] } as unknown as RunViewResult);
|
|
67
|
+
|
|
68
|
+
const map = await WithCatalogScope('CI-1', () => LookupObjectNamesByID(['cio-aaa', 'cio-new'], USER));
|
|
69
|
+
|
|
70
|
+
expect(runView).toHaveBeenCalledTimes(1);
|
|
71
|
+
const params = runView.mock.calls[0][0] as RunViewParams;
|
|
72
|
+
expect(params.EntityName).toBe('MJ: Company Integration Objects');
|
|
73
|
+
expect(params.ExtraFilter).toBe("CompanyIntegrationID='CI-1' AND ID IN ('CIO-NEW')");
|
|
74
|
+
expect(params.BypassCache).toBe(true);
|
|
75
|
+
expect(params.IgnoreMaxRows).toBe(true);
|
|
76
|
+
expect(map.get('CIO-NEW')).toBe('comments');
|
|
77
|
+
expect(map.get('CIO-AAA')).toBe('posts');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it(`reads cache misses ${OBJECT_ID_LOOKUP_CHUNK} ids per query`, async () => {
|
|
81
|
+
installEngine([]);
|
|
82
|
+
runView.mockResolvedValue({ Success: true, Results: [] } as unknown as RunViewResult);
|
|
83
|
+
const ids = Array.from({ length: OBJECT_ID_LOOKUP_CHUNK + 1 }, (_, i) => `cio-${i}`);
|
|
84
|
+
|
|
85
|
+
await WithCatalogScope('CI-1', () => LookupObjectNamesByID(ids, USER));
|
|
86
|
+
|
|
87
|
+
const perConnection = runView.mock.calls.filter(c => (c[0] as RunViewParams).EntityName === 'MJ: Company Integration Objects');
|
|
88
|
+
expect(perConnection).toHaveLength(2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('falls through to the shared catalog only for ids neither tier resolved', async () => {
|
|
92
|
+
installEngine([]);
|
|
93
|
+
runView
|
|
94
|
+
.mockResolvedValueOnce({ Success: true, Results: [] } as unknown as RunViewResult)
|
|
95
|
+
.mockResolvedValueOnce({ Success: true, Results: [{ ID: 'io-9', Name: 'users' }] } as unknown as RunViewResult);
|
|
96
|
+
|
|
97
|
+
const map = await WithCatalogScope('CI-1', () => LookupObjectNamesByID(['io-9'], USER));
|
|
98
|
+
|
|
99
|
+
expect(runView).toHaveBeenCalledTimes(2);
|
|
100
|
+
expect((runView.mock.calls[1][0] as RunViewParams).EntityName).toBe('MJ: Integration Objects');
|
|
101
|
+
expect(map.get('IO-9')).toBe('users');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('outside a catalog scope, queries only the shared catalog — exactly as before', async () => {
|
|
105
|
+
installEngine([{ ID: 'cio-aaa', IntegrationObjectID: null, Name: 'posts' }]);
|
|
106
|
+
runView.mockResolvedValue({ Success: true, Results: [{ ID: 'io-1', Name: 'users' }] } as unknown as RunViewResult);
|
|
107
|
+
|
|
108
|
+
const map = await LookupObjectNamesByID(['io-1'], USER);
|
|
109
|
+
|
|
110
|
+
expect(runView).toHaveBeenCalledTimes(1);
|
|
111
|
+
const params = runView.mock.calls[0][0] as RunViewParams;
|
|
112
|
+
expect(params.EntityName).toBe('MJ: Integration Objects');
|
|
113
|
+
expect(params.ExtraFilter).toBe("ID='IO-1'");
|
|
114
|
+
expect(map.get('IO-1')).toBe('users');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('quotes ids, so a stray quote cannot break out of the filter', async () => {
|
|
118
|
+
installEngine([]);
|
|
119
|
+
runView.mockResolvedValue({ Success: true, Results: [] } as unknown as RunViewResult);
|
|
120
|
+
|
|
121
|
+
await LookupObjectNamesByID(["x' OR 1=1 --"], USER);
|
|
122
|
+
|
|
123
|
+
expect((runView.mock.calls[0][0] as RunViewParams).ExtraFilter).toBe("ID='X'' OR 1=1 --'");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('escapes ids the way every filter does: a NUL byte never reaches the query', async () => {
|
|
127
|
+
installEngine([]);
|
|
128
|
+
runView.mockResolvedValue({ Success: true, Results: [] } as unknown as RunViewResult);
|
|
129
|
+
|
|
130
|
+
await LookupObjectNamesByID(['io-1\0'], USER);
|
|
131
|
+
|
|
132
|
+
expect((runView.mock.calls[0][0] as RunViewParams).ExtraFilter).toBe("ID='IO-1'");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('asks nothing for an empty selection', async () => {
|
|
136
|
+
installEngine([]);
|
|
137
|
+
expect((await LookupObjectNamesByID([], USER)).size).toBe(0);
|
|
138
|
+
expect(runView).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('the resolver uses the lookup, and refuses to build an empty resolution', () => {
|
|
143
|
+
const source = readFileSync(join(__dirname, '../resolvers/IntegrationDiscoveryResolver.ts'), 'utf8');
|
|
144
|
+
const body = (signature: string): string => {
|
|
145
|
+
const start = source.indexOf(signature);
|
|
146
|
+
expect(start, `not found: ${signature}`).toBeGreaterThan(-1);
|
|
147
|
+
return source.slice(start, source.indexOf('\n }\n', start));
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
it.each([
|
|
151
|
+
'private async resolveSelectionPlan(',
|
|
152
|
+
'private async resolveSourceObjectNames(',
|
|
153
|
+
'private async resolveSourceObjectsToNames(',
|
|
154
|
+
'private async resolveObjectInputs(',
|
|
155
|
+
])('%s resolves ids through LookupObjectNamesByID, never the shared catalog alone', (signature) => {
|
|
156
|
+
const fn = body(signature);
|
|
157
|
+
expect(fn).toContain('LookupObjectNamesByID(');
|
|
158
|
+
expect(fn).not.toContain("EntityName: 'MJ: Integration Objects'");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('IntegrationApplyAll refuses an empty resolution BEFORE the schema build', () => {
|
|
162
|
+
const resolve = source.indexOf('const resolved = await this.resolveSourceObjectsToNames(input.SourceObjects, sourceSchema, user);');
|
|
163
|
+
expect(resolve).toBeGreaterThan(-1);
|
|
164
|
+
const refusal = source.indexOf('input.SourceObjects.length > 0 && resolvedNames.length === 0', resolve);
|
|
165
|
+
const build = source.indexOf('this.buildSchemaForConnector(', resolve);
|
|
166
|
+
expect(refusal).toBeGreaterThan(resolve);
|
|
167
|
+
expect(refusal).toBeLessThan(build);
|
|
168
|
+
expect(source.slice(refusal, build)).toContain('Success: false');
|
|
169
|
+
expect(source.slice(refusal, build)).toContain('Apply refused');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('the batch flow throws on an empty resolution, mirroring the filtered flow', () => {
|
|
173
|
+
expect(source).toContain('connInput.SourceObjects.length > 0 && resolvedNames.length === 0');
|
|
174
|
+
expect(source).toContain('No source objects resolved');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-connection catalog answers correctly only when a read happens INSIDE a scope naming the
|
|
3
|
+
* connection; an unscoped read falls back to the shared catalog by design. Every request-time entry
|
|
4
|
+
* point enters the scope in the resolver. The one entry point that is NOT request-time is the
|
|
5
|
+
* post-restart consumer in index.ts: after the RSU restarts the process it introspects the connector
|
|
6
|
+
* to build the entity and field maps, and nothing upstream of it has entered a scope.
|
|
7
|
+
*
|
|
8
|
+
* Unscoped, that introspection sees only the declared fields, so every column this connection's own
|
|
9
|
+
* discovery sampled gets a table column (the RSU built the schema in scope) but no field map, and its
|
|
10
|
+
* values land in the overflow JSON instead of the column. On the sandbox that was 69 of 556 columns.
|
|
11
|
+
*
|
|
12
|
+
* Entering the scope is not enough on its own. In a freshly restarted process nothing has loaded the
|
|
13
|
+
* connection's catalog, so a synchronous entry reads the shared one; and an async entry loads objects
|
|
14
|
+
* but not fields, so a connector reading fields gets the connection's objects with none. These run
|
|
15
|
+
* the real catalog scope from a cold cache, with only the database reads stubbed.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { RunView, type UserInfo } from '@memberjunction/core';
|
|
21
|
+
import {
|
|
22
|
+
CATALOG_FIELD_COLUMNS,
|
|
23
|
+
CATALOG_OBJECT_COLUMNS,
|
|
24
|
+
ENTITY_COMPANY_INTEGRATION_OBJECTS,
|
|
25
|
+
ENTITY_COMPANY_INTEGRATION_OBJECT_FIELDS,
|
|
26
|
+
IntegrationEngineBase,
|
|
27
|
+
} from '@memberjunction/integration-engine-base';
|
|
28
|
+
import { CatalogFieldCacheStats, EvictCatalogScope, RunInCatalogScope, WithCatalogScope } from '@memberjunction/integration-engine';
|
|
29
|
+
import { IntrospectInCatalogScope } from '../integration/PostRestartIntrospect.js';
|
|
30
|
+
|
|
31
|
+
const SRC = readFileSync(join(__dirname, '..', 'index.ts'), 'utf-8');
|
|
32
|
+
|
|
33
|
+
const INTEGRATION = 'AAAAAAAA-1111-2222-3333-444444444444';
|
|
34
|
+
const CONN = 'CCCCCCCC-0000-0000-0000-000000000001';
|
|
35
|
+
const MEMBERS = 'BBBBBBBB-0000-0000-0000-000000000001';
|
|
36
|
+
const USER = { ID: 'u-1' } as UserInfo;
|
|
37
|
+
type RunViewParams = Parameters<RunView['RunView']>[0];
|
|
38
|
+
type RunViewResult = Awaited<ReturnType<RunView['RunView']>>;
|
|
39
|
+
|
|
40
|
+
function row(columns: readonly string[], values: Record<string, unknown>) {
|
|
41
|
+
const all: Record<string, unknown> = {};
|
|
42
|
+
for (const c of columns) all[c] = null;
|
|
43
|
+
Object.assign(all, values);
|
|
44
|
+
return { Fields: Object.keys(all).map(Name => ({ Name })), Get: (c: string) => all[c] };
|
|
45
|
+
}
|
|
46
|
+
const membersRow = row(CATALOG_OBJECT_COLUMNS, {
|
|
47
|
+
ID: MEMBERS, CompanyIntegrationID: CONN, IntegrationID: INTEGRATION, Name: 'Members', Status: 'Active', Sequence: 1,
|
|
48
|
+
});
|
|
49
|
+
// `region` is the column this connection's discovery sampled; the connector never declared it.
|
|
50
|
+
const memberFields = [
|
|
51
|
+
row(CATALOG_FIELD_COLUMNS, { ID: 'f0', CompanyIntegrationObjectID: MEMBERS, Name: 'id', Status: 'Active', Sequence: 1 }),
|
|
52
|
+
row(CATALOG_FIELD_COLUMNS, { ID: 'f1', CompanyIntegrationObjectID: MEMBERS, Name: 'region', Status: 'Active', Sequence: 2 }),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
let objectsInDb: ReturnType<typeof row>[] = [];
|
|
56
|
+
let fieldReadFails = false;
|
|
57
|
+
let fieldReads = 0;
|
|
58
|
+
|
|
59
|
+
function answer(p: RunViewParams): RunViewResult {
|
|
60
|
+
const ok = (Results: unknown[]) => ({ Success: true, Results } as unknown as RunViewResult);
|
|
61
|
+
if (p.EntityName === ENTITY_COMPANY_INTEGRATION_OBJECTS) return ok(objectsInDb);
|
|
62
|
+
if (p.EntityName === ENTITY_COMPANY_INTEGRATION_OBJECT_FIELDS && p.ResultType === 'simple') return ok([]); // the edge set
|
|
63
|
+
if (p.EntityName === ENTITY_COMPANY_INTEGRATION_OBJECT_FIELDS) {
|
|
64
|
+
fieldReads++;
|
|
65
|
+
if (fieldReadFails) return { Success: false, ErrorMessage: 'connection reset', Results: [] } as unknown as RunViewResult;
|
|
66
|
+
const oid = /CompanyIntegrationObjectID = '([^']+)'/.exec(String(p.ExtraFilter ?? ''))?.[1] ?? '';
|
|
67
|
+
return ok(oid.toUpperCase() === MEMBERS ? memberFields : []);
|
|
68
|
+
}
|
|
69
|
+
return ok([]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What a REST connector's IntrospectSchema reads: the object by integration + name, then its fields. */
|
|
73
|
+
async function introspectMembers(): Promise<string[]> {
|
|
74
|
+
const base = IntegrationEngineBase.Instance;
|
|
75
|
+
const obj = base.GetIntegrationObject(INTEGRATION, 'Members');
|
|
76
|
+
return obj ? base.GetIntegrationObjectFields(obj.ID).map(f => f.Name) : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
EvictCatalogScope(); // a freshly restarted process: nothing loaded, nothing warm
|
|
81
|
+
objectsInDb = [membersRow];
|
|
82
|
+
fieldReadFails = false;
|
|
83
|
+
fieldReads = 0;
|
|
84
|
+
vi.spyOn(RunView.prototype, 'RunView').mockImplementation(async (p: RunViewParams) => answer(p));
|
|
85
|
+
vi.spyOn(IntegrationEngineBase.Instance, 'ProviderToUse', 'get')
|
|
86
|
+
.mockImplementation(() => ({ EntityByName: (n: string) => ({ Name: n }) }) as never);
|
|
87
|
+
IntegrationEngineBase.Instance.SeedForTesting({
|
|
88
|
+
IntegrationObjects: [], IntegrationObjectFields: [], CompanyIntegrationObjects: [], CompanyIntegrationObjectFields: [],
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
92
|
+
|
|
93
|
+
describe('post-restart introspection sees the connection\'s own fields from a cold catalog', () => {
|
|
94
|
+
it('introspects the per-connection fields, sampled column included', async () => {
|
|
95
|
+
const fields = await IntrospectInCatalogScope(CONN, ['Members'], introspectMembers, USER);
|
|
96
|
+
expect(fields).toEqual(['id', 'region']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('releases its pins after the introspection', async () => {
|
|
100
|
+
await IntrospectInCatalogScope(CONN, ['Members'], introspectMembers, USER);
|
|
101
|
+
expect(CatalogFieldCacheStats().Pinned).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('releases its pins when the introspection throws, and the error propagates', async () => {
|
|
105
|
+
await expect(IntrospectInCatalogScope(CONN, ['Members'], async () => { throw new Error('vendor 503'); }, USER))
|
|
106
|
+
.rejects.toThrow('vendor 503');
|
|
107
|
+
expect(CatalogFieldCacheStats().Pinned).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('a failed field read throws instead of introspecting an object with no fields', async () => {
|
|
111
|
+
fieldReadFails = true;
|
|
112
|
+
const introspect = vi.fn(introspectMembers);
|
|
113
|
+
await expect(IntrospectInCatalogScope(CONN, ['Members'], introspect, USER)).rejects.toThrow();
|
|
114
|
+
expect(introspect).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('a connection with no catalog of its own warms nothing and introspects as before', async () => {
|
|
118
|
+
objectsInDb = [];
|
|
119
|
+
const introspect = vi.fn(async () => 'shared');
|
|
120
|
+
expect(await IntrospectInCatalogScope(CONN, ['Members'], introspect, USER)).toBe('shared');
|
|
121
|
+
expect(introspect).toHaveBeenCalledOnce();
|
|
122
|
+
expect(fieldReads).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('why: a synchronous scope on a cold process reads the shared catalog — no object at all', async () => {
|
|
126
|
+
expect(await RunInCatalogScope(CONN, introspectMembers)).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('why: an async scope without the warm finds the object but none of its fields', async () => {
|
|
130
|
+
expect(await WithCatalogScope(CONN, introspectMembers, USER)).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('index.ts wires the post-restart introspection through it', () => {
|
|
135
|
+
it('introspects through IntrospectInCatalogScope for the item\'s connection and objects', () => {
|
|
136
|
+
// Whitespace-tolerant: formatters may wrap the call.
|
|
137
|
+
expect(SRC).toMatch(
|
|
138
|
+
/const schema = await IntrospectInCatalogScope\(\s*item\.CompanyIntegrationID\s*,\s*item\.SourceObjectNames\s*,\s*\(\)\s*=>\s*introspect\(\s*companyIntegration\s*,\s*systemUser\s*\)\s*,\s*systemUser\s*,?\s*\)/,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('has no bare or synchronously scoped post-restart introspect call left', () => {
|
|
143
|
+
expect(SRC).not.toMatch(/const schema = await introspect\(companyIntegration, systemUser\)/);
|
|
144
|
+
expect(SRC).not.toMatch(/RunInCatalogScope\(\s*item\.CompanyIntegrationID/);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child rows `CreateQueryExtended` / `UpdateQueryExtended` return are mapped column by column
|
|
3
|
+
* from the saved query's rows. A column left out of that mapping reaches the client as `null`
|
|
4
|
+
* whatever is stored, so every column the GraphQL types carry must be mapped: a field's
|
|
5
|
+
* nullability, entity-field source and `schema.object` source, a parameter's server-set flag and
|
|
6
|
+
* a Query Entities row's alternate base view.
|
|
7
|
+
*/
|
|
8
|
+
import 'reflect-metadata';
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
|
|
11
|
+
vi.mock('type-graphql', () => {
|
|
12
|
+
const decoratorFactory = (..._args: unknown[]) => (..._decorated: unknown[]) => undefined;
|
|
13
|
+
const exportNames = ['Arg', 'Ctx', 'Field', 'InputType', 'Mutation', 'ObjectType', 'Resolver', 'PubSub', 'Int', 'Float'];
|
|
14
|
+
return { ...Object.fromEntries(exportNames.map((name) => [name, decoratorFactory])), registerEnumType: () => undefined };
|
|
15
|
+
});
|
|
16
|
+
vi.mock('@memberjunction/core-entities-server', () => ({ MJQueryEntityServer: class {} }));
|
|
17
|
+
vi.mock('../generic/DeleteOptionsInput.js', () => ({ DeleteOptionsInput: class {} }));
|
|
18
|
+
vi.mock('../generated/generated.js', () => ({
|
|
19
|
+
MJQuery_: class {}, MJQueryField_: class {}, MJQueryParameter_: class {}, MJQueryEntity_: class {}, MJQueryPermission_: class {},
|
|
20
|
+
MJQueryResolver: class {
|
|
21
|
+
protected async MapFieldNamesToCodeNames(_entityName: string, values: Record<string, unknown>) {
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { MJQueryResolverExtended } from '../resolvers/QuerySystemUserResolver.js';
|
|
28
|
+
|
|
29
|
+
type MappedRows = { Fields: Record<string, unknown>[]; Parameters: Record<string, unknown>[]; Entities: Record<string, unknown>[] };
|
|
30
|
+
|
|
31
|
+
const STAMP = new Date('2026-10-28T00:00:00Z');
|
|
32
|
+
|
|
33
|
+
const FIELD = {
|
|
34
|
+
ID: 'field-1', QueryID: 'query-1', Query: 'Q', Name: 'UserName', Description: 'The name.', Sequence: 1, SQLBaseType: 'nvarchar', SQLFullType: 'nvarchar(100)',
|
|
35
|
+
SourceEntityID: 'entity-users', SourceEntity: 'MJ: Users', SourceFieldName: 'Name', SourceEntityFieldID: 'field-users-name', SourceEntityField: 'Name',
|
|
36
|
+
SourceObjectName: null, IsNullable: false, IsComputed: false, ComputationDescription: null, IsSummary: false, SummaryDescription: null,
|
|
37
|
+
DetectionMethod: 'Database', AutoDetectConfidenceScore: null, __mj_CreatedAt: STAMP, __mj_UpdatedAt: STAMP,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const OBJECT_FIELD = { ...FIELD, ID: 'field-2', Name: 'Code', SourceEntityID: null, SourceEntity: null, SourceEntityFieldID: null, SourceEntityField: null, SourceFieldName: 'Code', SourceObjectName: 'fixture.Customer', IsNullable: true };
|
|
41
|
+
|
|
42
|
+
const PARAMETER = {
|
|
43
|
+
ID: 'parameter-1', QueryID: 'query-1', Query: 'Q', Name: 'OwnerID', Description: null, Type: 'string', IsRequired: true, DefaultValue: null, SampleValue: null,
|
|
44
|
+
ValidationFilters: null, IsServerSet: true, DetectionMethod: 'Manual', AutoDetectConfidenceScore: null, __mj_CreatedAt: STAMP, __mj_UpdatedAt: STAMP,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ENTITY = {
|
|
48
|
+
ID: 'link-1', QueryID: 'query-1', Query: 'Q', EntityID: 'entity-users', Entity: 'MJ: Users', AlternateBaseViewID: 'abv-active', AlternateBaseView: 'Active Users',
|
|
49
|
+
DetectionMethod: 'Parser', AutoDetectConfidenceScore: 1, __mj_CreatedAt: STAMP, __mj_UpdatedAt: STAMP,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function mapped(): Promise<MappedRows> {
|
|
53
|
+
const build = (MJQueryResolverExtended.prototype as unknown as {
|
|
54
|
+
buildSuccessResult(entity: unknown, user: unknown, provider: unknown): Promise<MappedRows>;
|
|
55
|
+
}).buildSuccessResult;
|
|
56
|
+
const entity = { GetAll: () => ({ ID: 'query-1' }), QueryFields: [FIELD, OBJECT_FIELD], QueryParameters: [PARAMETER], QueryEntities: [ENTITY], QueryPermissions: [] };
|
|
57
|
+
return build.call(new MJQueryResolverExtended(), entity, {}, {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('QuerySystemUserResolver: the saved query\'s child rows are returned with every column', () => {
|
|
61
|
+
it('returns a field\'s nullability and its entity-field source', async () => {
|
|
62
|
+
const { Fields } = await mapped();
|
|
63
|
+
expect(Fields[0]).toMatchObject({ IsNullable: false, SourceEntityFieldID: 'field-users-name', SourceEntityField: 'Name', SourceObjectName: null });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns the schema.object source of a field no entity backs', async () => {
|
|
67
|
+
const { Fields } = await mapped();
|
|
68
|
+
expect(Fields[1]).toMatchObject({ IsNullable: true, SourceObjectName: 'fixture.Customer', SourceEntityFieldID: null });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns a parameter\'s server-set flag', async () => {
|
|
72
|
+
const { Parameters } = await mapped();
|
|
73
|
+
expect(Parameters[0]).toMatchObject({ IsServerSet: true });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns a Query Entities row\'s alternate base view', async () => {
|
|
77
|
+
const { Entities } = await mapped();
|
|
78
|
+
expect(Entities[0]).toMatchObject({ AlternateBaseViewID: 'abv-active', AlternateBaseView: 'Active Users' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #6007: concurrent `CreateQueryExtended` calls with the same name.
|
|
3
|
+
*
|
|
4
|
+
* Every request first checks that the name is free, so N requests that arrive together all pass
|
|
5
|
+
* the check and then race to insert. The database's unique index on (Name, CategoryID) lets one
|
|
6
|
+
* win. Before the fix, every loser found the winner's row and returned it as its own result, even
|
|
7
|
+
* with `AutoResolveCollision`, so N callers shared one query. Now:
|
|
8
|
+
*
|
|
9
|
+
* - with `AutoResolveCollision`, a loser picks a new unique name and inserts again (bounded), so
|
|
10
|
+
* every request gets its own query;
|
|
11
|
+
* - without it, the existing create-or-get behaviour stays: losers get the query that now exists;
|
|
12
|
+
* - the existence lookup reads the database, never a cache.
|
|
13
|
+
*
|
|
14
|
+
* The provider is a stub whose saves yield before inserting, so concurrent saves interleave the
|
|
15
|
+
* way they do against a real database, and whose insert enforces the unique name.
|
|
16
|
+
*/
|
|
17
|
+
import 'reflect-metadata';
|
|
18
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
19
|
+
import type { RunViewParams } from '@memberjunction/core';
|
|
20
|
+
|
|
21
|
+
vi.mock('type-graphql', () => {
|
|
22
|
+
const decoratorFactory = (..._args: unknown[]) => (..._decorated: unknown[]) => undefined;
|
|
23
|
+
const exportNames = ['Arg', 'Ctx', 'Field', 'InputType', 'Mutation', 'ObjectType', 'Resolver', 'PubSub', 'Int', 'Float'];
|
|
24
|
+
return { ...Object.fromEntries(exportNames.map((name) => [name, decoratorFactory])), registerEnumType: () => undefined };
|
|
25
|
+
});
|
|
26
|
+
vi.mock('@memberjunction/core-entities-server', () => ({ MJQueryEntityServer: class {} }));
|
|
27
|
+
vi.mock('../generic/DeleteOptionsInput.js', () => ({ DeleteOptionsInput: class {} }));
|
|
28
|
+
vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
29
|
+
const actual = await importOriginal<typeof import('@memberjunction/core')>();
|
|
30
|
+
return { ...actual, AssertClientInputWritableForUser: () => undefined, LogStatus: () => undefined };
|
|
31
|
+
});
|
|
32
|
+
vi.mock('../generated/generated.js', () => ({
|
|
33
|
+
MJQuery_: class {}, MJQueryField_: class {}, MJQueryParameter_: class {}, MJQueryEntity_: class {}, MJQueryPermission_: class {},
|
|
34
|
+
MJQueryResolver: class {
|
|
35
|
+
protected async CheckAPIKeyScopeAuthorization(): Promise<void> { /* authorized */ }
|
|
36
|
+
protected async AfterCreate(): Promise<void> { /* no event */ }
|
|
37
|
+
protected ListenForEntityMessages(): void { /* no pub/sub */ }
|
|
38
|
+
protected async MapFieldNamesToCodeNames(_entityName: string, values: Record<string, unknown>) { return values; }
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import { MJQueryResolverExtended, CreateQuerySystemUserInput } from '../resolvers/QuerySystemUserResolver.js';
|
|
43
|
+
import type { AppContext } from '../types.js';
|
|
44
|
+
|
|
45
|
+
/** One stored query row. */
|
|
46
|
+
interface StoredQuery { ID: string; Name: string; CategoryID: string | null }
|
|
47
|
+
|
|
48
|
+
const CATEGORY = 'C0000000-0000-0000-0000-000000000001';
|
|
49
|
+
let rows: StoredQuery[];
|
|
50
|
+
let lookups: RunViewParams[];
|
|
51
|
+
/** The child-row reads the resolver made, with every cache bypassed or not. */
|
|
52
|
+
let childReads: RunViewParams[];
|
|
53
|
+
/** The field rows stored per query ID, as the database holds them (the query cache holds none). */
|
|
54
|
+
let storedFields: Map<string, Array<{ ID: string; QueryID: string; Name: string; Sequence: number }>>;
|
|
55
|
+
let nextID: number;
|
|
56
|
+
|
|
57
|
+
/** A query record as the stub provider hands out: its insert yields first and enforces the unique name. */
|
|
58
|
+
class StubQueryRecord {
|
|
59
|
+
public ID = '';
|
|
60
|
+
public Name = '';
|
|
61
|
+
public CategoryID: string | null = null;
|
|
62
|
+
public ParameterHints?: Map<string, string>;
|
|
63
|
+
public EntityInfo = {};
|
|
64
|
+
public LatestResult: { Message: string; CompleteMessage: string } | null = null;
|
|
65
|
+
public QueryFields = [];
|
|
66
|
+
public QueryParameters = [];
|
|
67
|
+
public QueryEntities = [];
|
|
68
|
+
public QueryPermissions = [];
|
|
69
|
+
|
|
70
|
+
SetMany(values: Record<string, unknown>): void {
|
|
71
|
+
if (typeof values.Name === 'string') this.Name = values.Name;
|
|
72
|
+
if (typeof values.CategoryID === 'string') this.CategoryID = values.CategoryID;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async Save(): Promise<boolean> {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
77
|
+
if (rows.some(r => r.Name.toLowerCase() === this.Name.toLowerCase() && r.CategoryID === this.CategoryID)) {
|
|
78
|
+
const message = `Cannot insert duplicate key row in object '__mj.Query' with unique index 'UX_Query_Name_CategoryID'. The duplicate key value is (${this.Name}).`;
|
|
79
|
+
this.LatestResult = { Message: message, CompleteMessage: message };
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
this.ID = `Q${++nextID}`;
|
|
83
|
+
rows.push({ ID: this.ID, Name: this.Name, CategoryID: this.CategoryID });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async Load(id: string): Promise<boolean> {
|
|
88
|
+
const row = rows.find(r => r.ID === id);
|
|
89
|
+
if (!row) return false;
|
|
90
|
+
Object.assign(this, row);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
GetAll(): Record<string, unknown> {
|
|
95
|
+
return { ID: this.ID, Name: this.Name, CategoryID: this.CategoryID };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const provider = {
|
|
100
|
+
async GetEntityObject(): Promise<StubQueryRecord> {
|
|
101
|
+
return new StubQueryRecord();
|
|
102
|
+
},
|
|
103
|
+
async RunViews(params: RunViewParams[]) {
|
|
104
|
+
childReads.push(...params);
|
|
105
|
+
return params.map(p => {
|
|
106
|
+
const queryID = /QueryID='(.*)'/.exec(p.ExtraFilter ?? '')?.[1] ?? '';
|
|
107
|
+
const results = p.EntityName === 'MJ: Query Fields' ? (storedFields.get(queryID) ?? []) : [];
|
|
108
|
+
return { Success: true, Results: results, RowCount: results.length, TotalRowCount: results.length, ExecutionTime: 0, ErrorMessage: '', UserViewRunID: '' };
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async RunView(params: RunViewParams) {
|
|
112
|
+
lookups.push(params);
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
114
|
+
const name = /LOWER\(Name\) = LOWER\('(.*)'\)/.exec(params.ExtraFilter ?? '')?.[1]?.replace(/''/g, "'") ?? '';
|
|
115
|
+
const matches = rows.filter(r => r.Name.toLowerCase() === name.toLowerCase() && r.CategoryID === CATEGORY);
|
|
116
|
+
return { Success: true, Results: matches, RowCount: matches.length, TotalRowCount: matches.length, ExecutionTime: 0, ErrorMessage: '', UserViewRunID: '' };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const context = {
|
|
121
|
+
providers: [{ provider, type: 'Read-Write' }],
|
|
122
|
+
userPayload: { userRecord: { ID: 'user-1', Email: 'u@example.com', UserRoles: [] } },
|
|
123
|
+
} as unknown as AppContext;
|
|
124
|
+
|
|
125
|
+
function input(name: string, autoResolve: boolean): CreateQuerySystemUserInput {
|
|
126
|
+
return Object.assign(new CreateQuerySystemUserInput(), { Name: name, CategoryID: CATEGORY, SQL: 'SELECT 1 AS One', AutoResolveCollision: autoResolve });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function createConcurrently(count: number, name: string, autoResolve: boolean) {
|
|
130
|
+
const resolver = new MJQueryResolverExtended();
|
|
131
|
+
const pubSub = { publish: async () => undefined } as unknown as Parameters<MJQueryResolverExtended['CreateQueryExtended']>[2];
|
|
132
|
+
return Promise.all(Array.from({ length: count }, () => resolver.CreateQueryExtended(input(name, autoResolve), context, pubSub)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
rows = [];
|
|
137
|
+
lookups = [];
|
|
138
|
+
childReads = [];
|
|
139
|
+
storedFields = new Map();
|
|
140
|
+
nextID = 0;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('CreateQueryExtended — concurrent creates with the same name (#6007)', () => {
|
|
144
|
+
it('with AutoResolveCollision, gives every concurrent request its own query with its own name', async () => {
|
|
145
|
+
const results = await createConcurrently(8, 'Revenue By Region', true);
|
|
146
|
+
|
|
147
|
+
expect(results.every(r => r.Success)).toBe(true);
|
|
148
|
+
const ids = new Set(results.map(r => r.Query?.ID));
|
|
149
|
+
const names = new Set(results.map(r => r.Query?.Name));
|
|
150
|
+
expect(ids.size).toBe(8);
|
|
151
|
+
expect(names.size).toBe(8);
|
|
152
|
+
expect(rows).toHaveLength(8);
|
|
153
|
+
expect([...names]).toContain('Revenue By Region');
|
|
154
|
+
expect([...names].filter(n => n !== 'Revenue By Region').every(n => /^Revenue By Region \(\d+\)$/.test(String(n)))).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('without AutoResolveCollision, keeps create-or-get: the requests that lose the race get the query that now exists', async () => {
|
|
158
|
+
const results = await createConcurrently(5, 'Orders Today', false);
|
|
159
|
+
|
|
160
|
+
expect(results.every(r => r.Success)).toBe(true);
|
|
161
|
+
expect(rows).toHaveLength(1);
|
|
162
|
+
expect(new Set(results.map(r => r.Query?.ID))).toEqual(new Set([rows[0].ID]));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('without AutoResolveCollision, a request that loses the race gets the other query\'s fields, read from the database', async () => {
|
|
166
|
+
const saveOnce = StubQueryRecord.prototype.Save;
|
|
167
|
+
vi.spyOn(StubQueryRecord.prototype, 'Save').mockImplementation(async function (this: StubQueryRecord) {
|
|
168
|
+
const saved = await saveOnce.call(this);
|
|
169
|
+
if (saved) storedFields.set(this.ID, [{ ID: `${this.ID}-f1`, QueryID: this.ID, Name: 'One', Sequence: 1 }]);
|
|
170
|
+
return saved;
|
|
171
|
+
});
|
|
172
|
+
const results = await createConcurrently(3, 'With Fields', false);
|
|
173
|
+
|
|
174
|
+
const winnerID = rows[0].ID;
|
|
175
|
+
const losers = results.filter(r => r.Fields?.length === 1);
|
|
176
|
+
expect(losers).toHaveLength(2);
|
|
177
|
+
expect(losers.every(r => r.Query?.ID === winnerID && r.Fields?.[0].Name === 'One')).toBe(true);
|
|
178
|
+
expect(childReads.length).toBeGreaterThan(0);
|
|
179
|
+
expect(childReads.every(r => r.BypassCache === true)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('reads every existence check from the database, never a cache', async () => {
|
|
183
|
+
await createConcurrently(3, 'Uncached', true);
|
|
184
|
+
|
|
185
|
+
expect(lookups.length).toBeGreaterThan(0);
|
|
186
|
+
expect(lookups.every(l => l.BypassCache === true)).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns the save failure itself when it is not a name race', async () => {
|
|
190
|
+
vi.spyOn(StubQueryRecord.prototype, 'Save').mockImplementationOnce(async function (this: StubQueryRecord) {
|
|
191
|
+
this.LatestResult = { Message: 'Query "Bad" was not saved: MJQ_TAG_IN_STRING_LITERAL', CompleteMessage: 'Query "Bad" was not saved: MJQ_TAG_IN_STRING_LITERAL' };
|
|
192
|
+
return false;
|
|
193
|
+
});
|
|
194
|
+
const [result] = await createConcurrently(1, 'Bad', true);
|
|
195
|
+
|
|
196
|
+
expect(result.Success).toBe(false);
|
|
197
|
+
expect(result.ErrorMessage).toContain('MJQ_TAG_IN_STRING_LITERAL');
|
|
198
|
+
expect(rows).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
});
|