@memberjunction/server 6.2.0-edge.5 → 6.2.0-edge.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +8 -3
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -1
- package/dist/agentSessions/BridgeHostInstance.js +14 -4
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -1
- package/dist/agentSessions/HandoffOfferExpiry.d.ts +10 -0
- package/dist/agentSessions/HandoffOfferExpiry.d.ts.map +1 -0
- package/dist/agentSessions/HandoffOfferExpiry.js +18 -0
- package/dist/agentSessions/HandoffOfferExpiry.js.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +34 -11
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +88 -44
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +29 -9
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +62 -16
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/conversationBrowsers.d.ts +61 -0
- package/dist/agentSessions/conversationBrowsers.d.ts.map +1 -0
- package/dist/agentSessions/conversationBrowsers.js +118 -0
- package/dist/agentSessions/conversationBrowsers.js.map +1 -0
- package/dist/agentSessions/sessionDeadline.d.ts +43 -0
- package/dist/agentSessions/sessionDeadline.d.ts.map +1 -0
- package/dist/agentSessions/sessionDeadline.js +68 -0
- package/dist/agentSessions/sessionDeadline.js.map +1 -0
- package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.js +5 -4
- package/dist/auth/APIKeyScopeAuth.js.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +2 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +51 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +10 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +37 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/localDevAuth.d.ts +79 -0
- package/dist/auth/localDevAuth.d.ts.map +1 -0
- package/dist/auth/localDevAuth.js +439 -0
- package/dist/auth/localDevAuth.js.map +1 -0
- package/dist/auth/loginEmailResolution.d.ts +41 -0
- package/dist/auth/loginEmailResolution.d.ts.map +1 -0
- package/dist/auth/loginEmailResolution.js +47 -0
- package/dist/auth/loginEmailResolution.js.map +1 -0
- package/dist/auth/magicLink/MagicLinkKeys.d.ts +51 -3
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +130 -13
- package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +3 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +23 -5
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +39 -5
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +221 -9
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/redeemLanding.d.ts +2 -2
- package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -1
- package/dist/auth/magicLink/redeemLanding.js +7 -4
- package/dist/auth/magicLink/redeemLanding.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +39 -3
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/auth/magicLink/types.js +0 -4
- package/dist/auth/magicLink/types.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +1 -0
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/sessionUserClone.d.ts +1 -1
- package/dist/auth/sessionUserClone.d.ts.map +1 -1
- package/dist/auth/sessionUserClone.js +3 -2
- package/dist/auth/sessionUserClone.js.map +1 -1
- package/dist/backgroundSweepConfigUnits.d.ts +231 -0
- package/dist/backgroundSweepConfigUnits.d.ts.map +1 -0
- package/dist/backgroundSweepConfigUnits.js +180 -0
- package/dist/backgroundSweepConfigUnits.js.map +1 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts +292 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js +442 -0
- package/dist/broadcast/BaseCacheInvalidationBroadcastProvider.js.map +1 -0
- package/dist/broadcast/index.d.ts +6 -0
- package/dist/broadcast/index.d.ts.map +1 -0
- package/dist/broadcast/index.js +6 -0
- package/dist/broadcast/index.js.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts +43 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js +144 -0
- package/dist/broadcast/providers/AWSIoTCoreBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts +46 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js +149 -0
- package/dist/broadcast/providers/AzureWebPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts +49 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js +131 -0
- package/dist/broadcast/providers/GCPPubSubBroadcastProvider.js.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts +24 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.d.ts.map +1 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js +51 -0
- package/dist/broadcast/providers/SequencedRoleBucketBroadcastProvider.js.map +1 -0
- package/dist/cache.d.ts +3 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js.map +1 -1
- package/dist/config.d.ts +1402 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +120 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +5 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -12
- package/dist/context.js.map +1 -1
- package/dist/coreRoutes.d.ts +3 -1
- package/dist/coreRoutes.d.ts.map +1 -1
- package/dist/coreRoutes.js +20 -4
- package/dist/coreRoutes.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +863 -14
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +4167 -62
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +50 -30
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +154 -79
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +11 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +29 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/PubSubManager.d.ts +10 -0
- package/dist/generic/PubSubManager.d.ts.map +1 -1
- package/dist/generic/PubSubManager.js +44 -1
- package/dist/generic/PubSubManager.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +39 -6
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +93 -11
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +114 -29
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +509 -220
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +15 -3
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +126 -20
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/generic/actionOutputFieldSecurity.d.ts +26 -0
- package/dist/generic/actionOutputFieldSecurity.d.ts.map +1 -0
- package/dist/generic/actionOutputFieldSecurity.js +47 -0
- package/dist/generic/actionOutputFieldSecurity.js.map +1 -0
- package/dist/generic/fieldSecurityTransport.d.ts +30 -0
- package/dist/generic/fieldSecurityTransport.d.ts.map +1 -0
- package/dist/generic/fieldSecurityTransport.js +30 -0
- package/dist/generic/fieldSecurityTransport.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +262 -84
- package/dist/index.js.map +1 -1
- package/dist/integration/ConnectionPause.d.ts +121 -0
- package/dist/integration/ConnectionPause.d.ts.map +1 -0
- package/dist/integration/ConnectionPause.js +183 -0
- package/dist/integration/ConnectionPause.js.map +1 -0
- package/dist/integration/CustomColumnPromoter.d.ts +72 -6
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +93 -47
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/EntityMapLifecycle.d.ts +78 -4
- package/dist/integration/EntityMapLifecycle.d.ts.map +1 -1
- package/dist/integration/EntityMapLifecycle.js +199 -5
- package/dist/integration/EntityMapLifecycle.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +31 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -1
- package/dist/integration/InactiveRowWarnings.js +29 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -1
- package/dist/integration/ObjectNamesByID.d.ts +28 -0
- package/dist/integration/ObjectNamesByID.d.ts.map +1 -0
- package/dist/integration/ObjectNamesByID.js +87 -0
- package/dist/integration/ObjectNamesByID.js.map +1 -0
- package/dist/integration/PostRestartIntrospect.d.ts +26 -0
- package/dist/integration/PostRestartIntrospect.d.ts.map +1 -0
- package/dist/integration/PostRestartIntrospect.js +27 -0
- package/dist/integration/PostRestartIntrospect.js.map +1 -0
- package/dist/integration/RSUPostRestartProgress.d.ts +176 -0
- package/dist/integration/RSUPostRestartProgress.d.ts.map +1 -0
- package/dist/integration/RSUPostRestartProgress.js +323 -0
- package/dist/integration/RSUPostRestartProgress.js.map +1 -0
- package/dist/integration/RSUProgressBridge.d.ts +41 -2
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -1
- package/dist/integration/RSUProgressBridge.js +84 -1
- package/dist/integration/RSUProgressBridge.js.map +1 -1
- package/dist/integration/RunArtifactAuthorization.d.ts +39 -0
- package/dist/integration/RunArtifactAuthorization.d.ts.map +1 -0
- package/dist/integration/RunArtifactAuthorization.js +45 -0
- package/dist/integration/RunArtifactAuthorization.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +9 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -1
- package/dist/integration/SchemaRefreshLaunch.js +3 -2
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -1
- package/dist/integration/ScopedFieldsForBuild.d.ts +46 -0
- package/dist/integration/ScopedFieldsForBuild.d.ts.map +1 -0
- package/dist/integration/ScopedFieldsForBuild.js +91 -0
- package/dist/integration/ScopedFieldsForBuild.js.map +1 -0
- package/dist/integration/TargetConfigGuard.d.ts +40 -0
- package/dist/integration/TargetConfigGuard.d.ts.map +1 -0
- package/dist/integration/TargetConfigGuard.js +40 -0
- package/dist/integration/TargetConfigGuard.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts +12 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
- package/dist/logging/variablesLoggingMiddleware.js +50 -1
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -1
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +8 -6
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.js +14 -6
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -1
- package/dist/realtimeSessions/sessionAccess.d.ts +103 -11
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +147 -22
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js +1 -1
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -1
- package/dist/realtimeSessions/subscriptionAuthorization.js +5 -2
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.d.ts +2 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js +1 -1
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +19 -3
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts +67 -7
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +203 -19
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/host-identity.d.ts +1 -1
- package/dist/realtimeWidget/host-identity.js +1 -1
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
- package/dist/realtimeWidget/visitorIdentity.js +1 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
- package/dist/realtimeWidget/widgetCore.d.ts +20 -1
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetCore.js +21 -8
- package/dist/realtimeWidget/widgetCore.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +3 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/requestOrigin.d.ts +22 -0
- package/dist/requestOrigin.d.ts.map +1 -0
- package/dist/requestOrigin.js +60 -0
- package/dist/requestOrigin.js.map +1 -0
- package/dist/resolvers/ActionResolver.d.ts +19 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +63 -12
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +1 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +18 -4
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.js +32 -8
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -1
- package/dist/resolvers/AgentSessionResolver.d.ts +10 -4
- package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/AgentSessionResolver.js +44 -17
- package/dist/resolvers/AgentSessionResolver.js.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.d.ts.map +1 -1
- package/dist/resolvers/AutotagPipelineResolver.js +2 -1
- package/dist/resolvers/AutotagPipelineResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +2 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +13 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +50 -14
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +6 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +30 -7
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +28 -0
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +117 -21
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -0
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +5 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +13 -4
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +34 -13
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +401 -11
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +2713 -1290
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -2
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +8 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +31 -3
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.d.ts +6 -0
- package/dist/resolvers/PipelineProgressResolver.d.ts.map +1 -1
- package/dist/resolvers/PipelineProgressResolver.js +5 -3
- package/dist/resolvers/PipelineProgressResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +20 -6
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +38 -0
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +184 -88
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts.map +1 -1
- package/dist/resolvers/RSUResolver.js +12 -0
- package/dist/resolvers/RSUResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +22 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +118 -33
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +62 -10
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +224 -39
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +80 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +388 -125
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +5 -3
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +19 -18
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +7 -5
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +3 -1
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +4 -0
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +1 -0
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +17 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +60 -15
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js +2 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +2 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +6 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +19 -5
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +6 -2
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +11 -5
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +8 -5
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.d.ts +3 -3
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -1
- package/dist/rest/RealtimeRecordingHandler.js +6 -8
- package/dist/rest/RealtimeRecordingHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +6 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +24 -6
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +3 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +11 -2
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverRoleConfigUnits.d.ts +29 -0
- package/dist/serverRoleConfigUnits.d.ts.map +1 -0
- package/dist/serverRoleConfigUnits.js +33 -0
- package/dist/serverRoleConfigUnits.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -1
- package/dist/services/DurableEntityActionTaskSubmitter.js +6 -1
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +2 -26
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +6 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +38 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +5 -0
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +43 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +2 -1
- package/dist/services/WorkQueueHostService.d.ts.map +1 -1
- package/dist/services/WorkQueueHostService.js +19 -4
- package/dist/services/WorkQueueHostService.js.map +1 -1
- package/dist/services/workQueueConfig.d.ts +2 -2
- package/dist/services/workQueueConfig.js +5 -5
- package/dist/services/workQueueConfig.js.map +1 -1
- package/dist/sharedCache.js +1 -1
- package/dist/sharedCache.js.map +1 -1
- package/dist/types.d.ts +40 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +98 -98
- package/src/__tests__/APIKeyScopeAuth.test.ts +4 -8
- package/src/__tests__/ActionResolver.fieldSecurity.test.ts +237 -0
- package/src/__tests__/ActiveOperations.test.ts +68 -0
- package/src/__tests__/AdhocQueryResolver.dialect.test.ts +137 -0
- package/src/__tests__/AgentRunStatusPublisher.test.ts +49 -0
- package/src/__tests__/AgentSessionResolver.ownership.test.ts +150 -0
- package/src/__tests__/AssertTargetConfigs.test.ts +75 -0
- package/src/__tests__/BatchApplyParity.test.ts +109 -0
- package/src/__tests__/BridgeHostInstance.test.ts +42 -0
- package/src/__tests__/CollectInactiveRowWarnings.test.ts +115 -0
- package/src/__tests__/ConnectionPause.test.ts +192 -0
- package/src/__tests__/ConnectionPauseResolver.test.ts +166 -0
- package/src/__tests__/ConnectionPauseSave.test.ts +143 -0
- package/src/__tests__/CustomColumnPromoterCompletion.test.ts +142 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +15 -1
- package/src/__tests__/DeleteConnectionRemovesPerConnectionCatalog.test.ts +51 -0
- package/src/__tests__/DetachedDiscoveryStart.test.ts +65 -0
- package/src/__tests__/DurableEntityActionTaskSubmitter.test.ts +73 -0
- package/src/__tests__/EntityCommunicationsResolver.actingUser.test.ts +151 -0
- package/src/__tests__/EntityMapLifecycle.rekey.test.ts +252 -0
- package/src/__tests__/EvolutionActsOnWhatItDetects.test.ts +113 -0
- package/src/__tests__/ExecuteRemoteOperationResolver.test.ts +213 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +206 -3
- package/src/__tests__/FileResolver.fieldSecurity.test.ts +82 -0
- package/src/__tests__/HandoffOfferExpiry.test.ts +40 -0
- package/src/__tests__/IntegrationRunArtifactAuthorization.test.ts +126 -0
- package/src/__tests__/MagicLinkKeys.test.ts +102 -0
- package/src/__tests__/MediaAccessKeys.test.ts +57 -10
- package/src/__tests__/NewArrivalsDefaultDisabled.test.ts +50 -0
- package/src/__tests__/ObjectNamesByID.test.ts +176 -0
- package/src/__tests__/PipelineProgressResolver.filter.test.ts +58 -0
- package/src/__tests__/PostRestartMapsInCatalogScope.test.ts +146 -0
- package/src/__tests__/QuerySystemUserResolver.childRows.test.ts +80 -0
- package/src/__tests__/QuerySystemUserResolver.collision.test.ts +200 -0
- package/src/__tests__/QuerySystemUserResolver.fieldSecurity.test.ts +49 -0
- package/src/__tests__/QuerySystemUserResolver.permissions.test.ts +132 -0
- package/src/__tests__/RESTEndpointHandler.test.ts +25 -0
- package/src/__tests__/RSUPostRestartProgress.test.ts +343 -0
- package/src/__tests__/RSUProgressBridge.test.ts +188 -5
- package/src/__tests__/RSUStartupSweepWiring.test.ts +30 -0
- package/src/__tests__/ReactivateRefreshMessage.test.ts +10 -3
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +23 -3
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +346 -46
- package/src/__tests__/{RefreshAdoptsNewColumns.test.ts → RefreshReportsNewColumns.test.ts} +25 -16
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +287 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +3 -0
- package/src/__tests__/RemoteBrowserActionResolver.ownership.test.ts +100 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +3 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +3 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +344 -14
- package/src/__tests__/ResolverBase.deleteReturnProjection.test.ts +52 -0
- package/src/__tests__/ResolverBase.encryptedApiZeroReencrypt.test.ts +307 -0
- package/src/__tests__/ResolverBase.entityMessageRelay.test.ts +227 -0
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +83 -1
- package/src/__tests__/ResolverBase.publishStatusUpdate.test.ts +68 -0
- package/src/__tests__/ResolverBase.userViewCacheAndDedup.test.ts +130 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +72 -13
- package/src/__tests__/RunAIAgentResolver.consultPredecessor.test.ts +289 -0
- package/src/__tests__/RunAIAgentResolver.conversationWriter.test.ts +210 -0
- package/src/__tests__/RunAIAgentResolver.fireAndForgetCompletion.test.ts +209 -0
- package/src/__tests__/RunAIAgentResolver.handoff.test.ts +649 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +19 -27
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +10 -2
- package/src/__tests__/RunAIAgentResolver.serverReply.test.ts +358 -0
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +91 -13
- package/src/__tests__/RunTemplateResolver.test.ts +35 -2
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +11 -0
- package/src/__tests__/SchemaEvolutionCustomColumns.test.ts +119 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +24 -2
- package/src/__tests__/ScopedFieldsForBuild.test.ts +192 -0
- package/src/__tests__/SessionJanitor.test.ts +195 -4
- package/src/__tests__/SessionManager.test.ts +164 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -2
- package/src/__tests__/TaskGraphAgentRunner.test.ts +272 -16
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +114 -3
- package/src/__tests__/TransactionGroupResolver.fieldSecurity.test.ts +199 -0
- package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +15 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +9 -7
- package/src/__tests__/WidgetSessionService.test.ts +583 -4
- package/src/__tests__/WorkQueueHostService.test.ts +25 -7
- package/src/__tests__/apiRootBoot.test.ts +1 -1
- package/src/__tests__/backgroundSweepConfigUnits.test.ts +153 -0
- package/src/__tests__/buildMagicLinkSessionUser.test.ts +96 -0
- package/src/__tests__/cacheBroadcastProviders.test.ts +403 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +45 -0
- package/src/__tests__/contextRequestOrigin.test.ts +105 -0
- package/src/__tests__/conversationBrowsers.test.ts +62 -0
- package/src/__tests__/fieldSecurityFixtures.ts +91 -0
- package/src/__tests__/globalCors.test.ts +3 -3
- package/src/__tests__/localDevAuth.test.ts +196 -0
- package/src/__tests__/magicLinkExtensionCapabilities.test.ts +413 -0
- package/src/__tests__/orphanedConversationDetailReconciler.start.test.ts +82 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +10 -0
- package/src/__tests__/ownedRealtimeSession.test.ts +14 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +57 -1
- package/src/__tests__/realtimeRecordingHandler.test.ts +24 -2
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +29 -1
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.anonymous.test.ts +31 -0
- package/src/__tests__/realtimeSessions/ownedRealtimeSession.namedScoped.test.ts +37 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +224 -7
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +1 -1
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +25 -4
- package/src/__tests__/requestOrigin.test.ts +104 -0
- package/src/__tests__/resolverBase.cloudEventChangedFields.test.ts +157 -0
- package/src/__tests__/resolverBase.fieldSecurityTransport.test.ts +222 -0
- package/src/__tests__/resolverBase.fls.test.ts +146 -97
- package/src/__tests__/restTransport.fieldSecurity.test.ts +148 -0
- package/src/__tests__/runParamsTimeZone.test.ts +179 -0
- package/src/__tests__/runStreams.fieldSecurity.test.ts +100 -0
- package/src/__tests__/runViewProperties.test.ts +114 -0
- package/src/__tests__/scalabilityBenchmarks.test.ts +765 -0
- package/src/__tests__/scalabilityDatabaseIntegration.test.ts +207 -0
- package/src/__tests__/securityHardeningPr6034.test.ts +120 -0
- package/src/__tests__/sessionDeadline.test.ts +51 -0
- package/src/__tests__/sessionUserClone.test.ts +5 -1
- package/src/__tests__/sharedCache.test.ts +11 -0
- package/src/__tests__/startupSweepOrder.test.ts +40 -0
- package/src/__tests__/unifiedAuth.test.ts +101 -1
- package/src/__tests__/widgetGuestElevation.test.ts +2 -0
- package/src/agentSessions/BridgeHostInstance.ts +20 -5
- package/src/agentSessions/HandoffOfferExpiry.ts +18 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +1 -0
- package/src/agentSessions/SessionJanitor.ts +95 -45
- package/src/agentSessions/SessionManager.ts +73 -20
- package/src/agentSessions/conversationBrowsers.ts +143 -0
- package/src/agentSessions/sessionDeadline.ts +81 -0
- package/src/auth/APIKeyScopeAuth.ts +5 -4
- package/src/auth/AuthProviderCatalogRouter.ts +3 -0
- package/src/auth/__tests__/loginEmailResolution.test.ts +67 -0
- package/src/auth/index.ts +56 -12
- package/src/auth/initializeProviders.ts +43 -1
- package/src/auth/localDevAuth.ts +528 -0
- package/src/auth/loginEmailResolution.ts +67 -0
- package/src/auth/magicLink/MagicLinkKeys.ts +147 -15
- package/src/auth/magicLink/MagicLinkRouter.ts +33 -6
- package/src/auth/magicLink/MagicLinkService.ts +250 -10
- package/src/auth/magicLink/index.ts +1 -1
- package/src/auth/magicLink/redeemLanding.ts +7 -4
- package/src/auth/magicLink/types.ts +38 -3
- package/src/auth/newUsers.ts +1 -0
- package/src/auth/sessionUserClone.ts +3 -2
- package/src/backgroundSweepConfigUnits.ts +213 -0
- package/src/broadcast/BaseCacheInvalidationBroadcastProvider.ts +674 -0
- package/src/broadcast/index.ts +5 -0
- package/src/broadcast/providers/AWSIoTCoreBroadcastProvider.ts +180 -0
- package/src/broadcast/providers/AzureWebPubSubBroadcastProvider.ts +182 -0
- package/src/broadcast/providers/GCPPubSubBroadcastProvider.ts +169 -0
- package/src/broadcast/providers/SequencedRoleBucketBroadcastProvider.ts +57 -0
- package/src/cache.ts +2 -1
- package/src/config.ts +138 -0
- package/src/context.ts +71 -14
- package/src/coreRoutes.ts +29 -7
- package/src/generated/graphql-schemas/__mj.ts +4145 -1193
- package/src/generic/CacheInvalidationResolver.ts +135 -78
- package/src/generic/OrphanedConversationDetailReconciler.ts +36 -3
- package/src/generic/PubSubManager.ts +54 -1
- package/src/generic/PushStatusResolver.ts +108 -11
- package/src/generic/ResolverBase.ts +635 -234
- package/src/generic/RunViewResolver.ts +176 -64
- package/src/generic/actionOutputFieldSecurity.ts +75 -0
- package/src/generic/fieldSecurityTransport.ts +62 -0
- package/src/index.ts +310 -85
- package/src/integration/ConnectionPause.ts +222 -0
- package/src/integration/CustomColumnPromoter.ts +129 -54
- package/src/integration/EntityMapLifecycle.ts +229 -6
- package/src/integration/InactiveRowWarnings.ts +50 -0
- package/src/integration/ObjectNamesByID.ts +97 -0
- package/src/integration/PostRestartIntrospect.ts +49 -0
- package/src/integration/RSUPostRestartProgress.ts +359 -0
- package/src/integration/RSUProgressBridge.ts +85 -2
- package/src/integration/RunArtifactAuthorization.ts +65 -0
- package/src/integration/SchemaRefreshLaunch.ts +12 -2
- package/src/integration/ScopedFieldsForBuild.ts +113 -0
- package/src/integration/TargetConfigGuard.ts +71 -0
- package/src/logging/variablesLoggingMiddleware.ts +59 -3
- package/src/realtimeSessions/README.md +7 -3
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +1 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +16 -6
- package/src/realtimeSessions/sessionAccess.ts +211 -25
- package/src/realtimeSessions/sessionConfigStamps.ts +1 -1
- package/src/realtimeSessions/subscriptionAuthorization.ts +4 -2
- package/src/realtimeSessions/verificationWorkflow.ts +3 -1
- package/src/realtimeWidget/WidgetRouter.ts +22 -4
- package/src/realtimeWidget/WidgetSessionService.ts +254 -17
- package/src/realtimeWidget/host-identity.ts +1 -1
- package/src/realtimeWidget/visitorIdentity.ts +1 -0
- package/src/realtimeWidget/widgetCore.ts +34 -9
- package/src/realtimeWidget/widgetGuestElevation.ts +3 -1
- package/src/requestOrigin.ts +64 -0
- package/src/resolvers/ActionResolver.ts +78 -15
- package/src/resolvers/AdhocQueryResolver.ts +1 -0
- package/src/resolvers/AgentRunStatusPublisher.ts +36 -8
- package/src/resolvers/AgentSessionResolver.ts +50 -18
- package/src/resolvers/AutotagPipelineResolver.ts +2 -1
- package/src/resolvers/ConversationRunEventsResolver.ts +2 -0
- package/src/resolvers/EntityCommunicationsResolver.ts +56 -15
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +41 -9
- package/src/resolvers/FetchEntityVectorsResolver.ts +132 -21
- package/src/resolvers/FileCategoryResolver.ts +1 -0
- package/src/resolvers/FileResolver.ts +14 -4
- package/src/resolvers/GetDataResolver.ts +35 -13
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1570 -209
- package/src/resolvers/MCPResolver.ts +15 -2
- package/src/resolvers/MergeRecordsResolver.ts +23 -2
- package/src/resolvers/PipelineProgressResolver.ts +12 -3
- package/src/resolvers/QueryResolver.ts +20 -6
- package/src/resolvers/QuerySystemUserResolver.ts +207 -99
- package/src/resolvers/RSUResolver.ts +12 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +1 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +144 -40
- package/src/resolvers/RemoteBrowserActionResolver.ts +252 -37
- package/src/resolvers/RunAIAgentResolver.ts +518 -163
- package/src/resolvers/RunAIPromptResolver.ts +18 -18
- package/src/resolvers/RunTemplateResolver.ts +9 -5
- package/src/resolvers/RunTestResolver.ts +3 -1
- package/src/resolvers/SyncDataResolver.ts +4 -0
- package/src/resolvers/TestQuerySQLResolver.ts +1 -0
- package/src/resolvers/TransactionGroupResolver.ts +66 -15
- package/src/resolvers/UserViewResolver.ts +2 -1
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +52 -0
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +43 -0
- package/src/resolvers/meetingRecordingRegistration.ts +2 -0
- package/src/rest/EntityCRUDHandler.ts +22 -5
- package/src/rest/MediaAccessKeys.ts +6 -2
- package/src/rest/OAuthCallbackHandler.ts +13 -5
- package/src/rest/RESTEndpointHandler.ts +8 -5
- package/src/rest/RealtimeRecordingHandler.ts +9 -10
- package/src/rest/ViewOperationsHandler.ts +26 -7
- package/src/rest/setupRESTEndpoints.ts +11 -2
- package/src/serverRoleConfigUnits.ts +48 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +10 -2
- package/src/services/TaskGraphActionRunner.ts +2 -23
- package/src/services/TaskGraphAgentRunner.ts +43 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +53 -20
- package/src/services/WorkQueueHostService.ts +26 -4
- package/src/services/workQueueConfig.ts +5 -5
- package/src/sharedCache.ts +1 -1
- package/src/types.ts +41 -1
- package/src/services/TaskOrchestration-Integration.md +0 -188
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType, Int, Float } from "type-graphql";
|
|
2
|
-
import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
|
|
2
|
+
import { BaseEntity, CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
|
|
3
3
|
import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
|
|
4
4
|
import { NoLog } from "../logging/NoLog.js";
|
|
5
|
-
import { UUIDsEqual } from "@memberjunction/global";
|
|
5
|
+
import { EscapeSQLString, UUIDsEqual } from "@memberjunction/global";
|
|
6
6
|
import { DrainResponseBody, SafeFetch } from "@memberjunction/network-utils";
|
|
7
7
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
8
8
|
import {
|
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
IntegrationSchemaSync,
|
|
34
34
|
decideSchemaLimitViolations,
|
|
35
35
|
IntegrationConnectorCreationPipeline,
|
|
36
|
-
IntegrationActionGenerator
|
|
36
|
+
IntegrationActionGenerator,
|
|
37
|
+
DescribePersistCounts,
|
|
38
|
+
BuildCatalogWriter, WithCatalogScope,
|
|
39
|
+
ObjectsWithWatermarkFieldChange
|
|
37
40
|
} from "@memberjunction/integration-engine";
|
|
38
41
|
import type {
|
|
39
42
|
IntegrationActionVerb,
|
|
@@ -53,11 +56,19 @@ import {
|
|
|
53
56
|
import { RuntimeSchemaManager, type RSUPipelineStep, type RSUPipelineInput, type RSUPendingWork } from "@memberjunction/schema-engine";
|
|
54
57
|
import type { SchemaBuilderOutput } from "@memberjunction/integration-schema-builder";
|
|
55
58
|
import { IntegrationProgressEmitter, IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
59
|
+
import { AuthorizeRunArtifact } from "../integration/RunArtifactAuthorization.js";
|
|
60
|
+
import type { RunArtifactAuthorizationResult } from "../integration/RunArtifactAuthorization.js";
|
|
56
61
|
import type { IntegrationRunSnapshot, IntegrationRunKind } from "@memberjunction/integration-progress-artifacts";
|
|
57
62
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
58
63
|
import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
|
|
59
|
-
import { ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DecideFieldMapReconcile, DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
|
|
60
|
-
import {
|
|
64
|
+
import { CatalogIdentityKey, ClearRekeyedObjectData, ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DecideFieldMapReconcile, DecideRekeyed, DisableUnselectedEntityMaps, IdentityKeyFields, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
|
|
65
|
+
import { CollectInactiveRowWarnings } from "../integration/InactiveRowWarnings.js";
|
|
66
|
+
import { DecidePauseWrite, DecideSchedulesToPause, DecideSchedulesToResume, DescribeCancelOutcome, DescribeCancelScope, DescribePauseOutcome, ReadPausedSchedules, WritePausedSchedules } from "../integration/ConnectionPause.js";
|
|
67
|
+
import type { CancelScope, ScheduleJobState } from "../integration/ConnectionPause.js";
|
|
68
|
+
import { ReadResourcePressure, EvaluatePressure } from "@memberjunction/integration-engine";
|
|
69
|
+
import { FieldsForBuild, LoadScopedFieldsForBuild, type ScopedBuildFields } from "../integration/ScopedFieldsForBuild.js";
|
|
70
|
+
import { LookupObjectNamesByID } from "../integration/ObjectNamesByID.js";
|
|
71
|
+
import { AssertTargetConfigs, NewTargetConfigDrops, type TargetConfigDrops } from "../integration/TargetConfigGuard.js";
|
|
61
72
|
import { BuildCreateConnectionMessage, BuildDetachedRefreshMessage, BuildReactivateMessage, BuildUpdateConnectionMessage } from "../integration/SchemaRefreshLaunch.js";
|
|
62
73
|
// Type-only: the registered runtime class for 'MJ: Company Integrations'. Lets the create path name the
|
|
63
74
|
// server subclass it actually gets back from GetEntityObject with a real type rather than a cast.
|
|
@@ -66,6 +77,22 @@ import { AppContext } from "../types.js";
|
|
|
66
77
|
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
67
78
|
import { UserCache } from "@memberjunction/generic-database-provider";
|
|
68
79
|
|
|
80
|
+
// ─── SQL filter guard ───────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const UUID_SHAPE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates that a caller-supplied record id is a UUID before it is interpolated into a SQL
|
|
86
|
+
* filter, and returns it unchanged. Every id this resolver family accepts is a UNIQUEIDENTIFIER,
|
|
87
|
+
* so anything else is refused outright rather than escaped — a validated UUID cannot carry SQL.
|
|
88
|
+
*/
|
|
89
|
+
function sqlUuid(value: string, argName: string): string {
|
|
90
|
+
if (typeof value !== 'string' || !UUID_SHAPE.test(value)) {
|
|
91
|
+
throw new Error(`${argName} must be a UUID`);
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
// ─── RSU Pipeline Output Types ──────────────────────────────────────────────
|
|
70
97
|
|
|
71
98
|
@ObjectType()
|
|
@@ -175,7 +202,11 @@ class ApplyAllBatchConnectorInput {
|
|
|
175
202
|
class ApplyAllBatchInput {
|
|
176
203
|
@Field(() => [ApplyAllBatchConnectorInput]) Connectors: ApplyAllBatchConnectorInput[];
|
|
177
204
|
@Field(() => Boolean, { nullable: true, defaultValue: true, description: 'If false, skips sync after schema + entity maps' }) StartSync?: boolean;
|
|
178
|
-
|
|
205
|
+
// NO defaultValue. It must stay UNDEFINED when the caller says nothing, so the resolver can
|
|
206
|
+
// tell "not specified" from "explicitly incremental" and apply the first-apply rule
|
|
207
|
+
// (everything.txt: "always full generally as the default" for a connector's first RSU).
|
|
208
|
+
// A defaultValue of false made that determination dead code on this path for its whole life.
|
|
209
|
+
@Field(() => Boolean, { nullable: true, description: 'Ignore watermarks and do a full re-fetch. Omit to let the server choose: FULL on a connection first apply, incremental after.' }) FullSync?: boolean;
|
|
179
210
|
@Field({ nullable: true, defaultValue: 'created', description: 'Sync scope: "created" = only newly created entity maps, "all" = all maps for the connector' }) SyncScope?: string;
|
|
180
211
|
@Field({ nullable: true, description: 'Override sync direction for the initial sync: Pull | Push | Bidirectional. Defaults to entity map SyncDirection.' }) SyncDirection?: string;
|
|
181
212
|
@Field({ nullable: true, description: 'Override sync direction stored in the created schedule: Pull | Push | Bidirectional.' }) ScheduleSyncDirection?: string;
|
|
@@ -241,10 +272,60 @@ class SchemaEvolutionOutput {
|
|
|
241
272
|
@Field(() => [String], { nullable: true }) ChangedObjects?: string[];
|
|
242
273
|
/** U10 — objects whose Pull watermark was reset so the next sync backfills the schema change. */
|
|
243
274
|
@Field(() => [String], { nullable: true }) WatermarksReset?: string[];
|
|
275
|
+
/**
|
|
276
|
+
* Objects whose stored rows were CLEARED because the connector changed what identifies a row.
|
|
277
|
+
* Reported separately from WatermarksReset: a reset means "fetch it all again", this means
|
|
278
|
+
* "what was here could not be reconciled with the source and has been rebuilt from it".
|
|
279
|
+
*/
|
|
280
|
+
@Field(() => [String], { nullable: true }) RekeyedObjects?: string[];
|
|
281
|
+
/**
|
|
282
|
+
* Custom columns the source has been sending that are still sitting in the overflow JSON,
|
|
283
|
+
* offered here so a schema refresh is where you see them.
|
|
284
|
+
*
|
|
285
|
+
* everything.txt: promoting one "requires that the user accepts" it, so this is REPORT-ONLY.
|
|
286
|
+
* Pass the column names back in `acceptCustomColumns` to actually materialise them — in the
|
|
287
|
+
* same migration and the same restart as the refresh's own DDL.
|
|
288
|
+
*/
|
|
289
|
+
@Field(() => [CustomColumnCandidate], { nullable: true }) CustomColumnCandidates?: CustomColumnCandidate[];
|
|
290
|
+
/** The accepted columns that were folded into this run's migration. */
|
|
291
|
+
@Field(() => [PromotedColumn], { nullable: true }) PromotedColumns?: PromotedColumn[];
|
|
244
292
|
}
|
|
245
293
|
|
|
246
294
|
// ─── Connector Capabilities Output Type ─────────────────────────────────────
|
|
247
295
|
|
|
296
|
+
/**
|
|
297
|
+
* One tunable setting, described by the connector.
|
|
298
|
+
*
|
|
299
|
+
* Exists so a surface can render the settings for a connection it knows nothing about, and so the
|
|
300
|
+
* DEFAULT it shows is the engine's real one. The alternative — a hardcoded list per surface — is
|
|
301
|
+
* how a sample size of 500 came to be shown and stamped against an engine whose default is 50.
|
|
302
|
+
*/
|
|
303
|
+
@ObjectType()
|
|
304
|
+
class ConnectorSettingOutput {
|
|
305
|
+
@Field() Key: string;
|
|
306
|
+
@Field() Label: string;
|
|
307
|
+
@Field() Group: string;
|
|
308
|
+
@Field() Type: string;
|
|
309
|
+
@Field(() => [String], { nullable: true }) Options?: string[];
|
|
310
|
+
/** JSON-encoded: the value is polymorphic per setting, and Type says how to read it. */
|
|
311
|
+
@Field({ nullable: true }) DefaultJSON?: string;
|
|
312
|
+
@Field(() => Float, { nullable: true }) Min?: number;
|
|
313
|
+
@Field(() => Float, { nullable: true }) Max?: number;
|
|
314
|
+
/** 'basic' | 'advanced' | 'expert' — how a surface decides what to show. */
|
|
315
|
+
@Field() Tier: string;
|
|
316
|
+
/** 'connection' | 'object' */
|
|
317
|
+
@Field() AppliesTo: string;
|
|
318
|
+
@Field() Description: string;
|
|
319
|
+
@Field() Sensitive: boolean;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@ObjectType()
|
|
323
|
+
class ConnectorSettingsSchemaOutput {
|
|
324
|
+
@Field() Success: boolean;
|
|
325
|
+
@Field() Message: string;
|
|
326
|
+
@Field(() => [ConnectorSettingOutput]) Settings: ConnectorSettingOutput[];
|
|
327
|
+
}
|
|
328
|
+
|
|
248
329
|
@ObjectType()
|
|
249
330
|
class ConnectorCapabilitiesOutput {
|
|
250
331
|
@Field() Success: boolean;
|
|
@@ -409,6 +490,22 @@ class RefreshConnectorSchemaOutput {
|
|
|
409
490
|
@Field({ nullable: true }) FailureMessage?: string;
|
|
410
491
|
}
|
|
411
492
|
|
|
493
|
+
/**
|
|
494
|
+
* The reply to a DETACHED discovery start. Deliberately a different shape from
|
|
495
|
+
* RefreshConnectorSchemaOutput: that one reports real counts because it waited, and handing back
|
|
496
|
+
* placeholder zeros from a run that has not begun is exactly the confusion this type avoids.
|
|
497
|
+
*/
|
|
498
|
+
@ObjectType()
|
|
499
|
+
class StartSchemaRefreshOutput {
|
|
500
|
+
@Field() Success: boolean;
|
|
501
|
+
@Field() Message: string;
|
|
502
|
+
/** Tail this with IntegrationTailRunEvents. 'not-started' when Success is false. */
|
|
503
|
+
@Field() RunID: string;
|
|
504
|
+
@Field() InProgress: boolean;
|
|
505
|
+
/** Set when the refusal was a lock: names what is holding it, so the UI can say why. */
|
|
506
|
+
@Field({ nullable: true }) BlockedBy?: string;
|
|
507
|
+
}
|
|
508
|
+
|
|
412
509
|
// ─── Generate Integration Action (on-demand Integration-as-Actions) ─────────
|
|
413
510
|
// Generates + persists a strongly-typed Action (DriverClass='IntegrationActionExecutor')
|
|
414
511
|
// for one integration/object/verb (or all applicable verbs when verb is omitted) via
|
|
@@ -622,6 +719,28 @@ class MutationResultOutput {
|
|
|
622
719
|
@Field() Message: string;
|
|
623
720
|
}
|
|
624
721
|
|
|
722
|
+
/**
|
|
723
|
+
* Deactivate returns more than a boolean because pausing a connection is not one act.
|
|
724
|
+
*
|
|
725
|
+
* A caller has to be able to tell "paused, and the sync you were watching is stopping" from
|
|
726
|
+
* "paused, and the sync you were watching will run to completion in a process I cannot reach".
|
|
727
|
+
* Both are successful pauses. Only one of them means the work has stopped, and a UI that says
|
|
728
|
+
* "stopping" for the second is lying to the operator about the state of their data.
|
|
729
|
+
*/
|
|
730
|
+
@ObjectType()
|
|
731
|
+
class DeactivateConnectionOutput {
|
|
732
|
+
@Field() Success: boolean;
|
|
733
|
+
@Field() Message: string;
|
|
734
|
+
/** How many scheduled jobs this pause actually moved from Active to Paused. */
|
|
735
|
+
@Field(() => Int) SchedulesPaused: number;
|
|
736
|
+
/** The live sync run at the moment of pause, when there was one. */
|
|
737
|
+
@Field({ nullable: true }) InFlightSyncRunID?: string;
|
|
738
|
+
/** True when a live run existed AND a cancel was signalled for it. */
|
|
739
|
+
@Field() CancelRequested: boolean;
|
|
740
|
+
/** 'durable' | 'this-process' | 'unknown' | 'none' — see ConnectionPause.DescribeCancelScope. */
|
|
741
|
+
@Field() CancelScope: string;
|
|
742
|
+
}
|
|
743
|
+
|
|
625
744
|
// ─── Typed sync-config (rate-limit / concurrency / time-budget as STRUCTURED fields, not a raw
|
|
626
745
|
// Configuration JSON blob). These map to the CompanyIntegration.Configuration keys the engine
|
|
627
746
|
// reads at runtime, so they are customizable per-connection via the API instead of hidden code
|
|
@@ -864,6 +983,10 @@ class SyncHistoryOutput {
|
|
|
864
983
|
@Field() Success: boolean;
|
|
865
984
|
@Field() Message: string;
|
|
866
985
|
@Field(() => [SyncRunSummaryOutput], { nullable: true }) Runs?: SyncRunSummaryOutput[];
|
|
986
|
+
/** Total runs for this connection, so a pager can size itself without fetching every row. */
|
|
987
|
+
@Field({ nullable: true }) TotalKnown?: number;
|
|
988
|
+
/** True when rows exist beyond this page. */
|
|
989
|
+
@Field({ nullable: true }) HasMore?: boolean;
|
|
867
990
|
}
|
|
868
991
|
|
|
869
992
|
@ObjectType()
|
|
@@ -879,6 +1002,8 @@ class OperationProgressOutput {
|
|
|
879
1002
|
@Field({ nullable: true }) RecordsCreated?: number;
|
|
880
1003
|
@Field({ nullable: true }) RecordsUpdated?: number;
|
|
881
1004
|
@Field({ nullable: true }) RecordsErrored?: number;
|
|
1005
|
+
/** Records the sync chose not to write because the content hash matched. */
|
|
1006
|
+
@Field({ nullable: true }) RecordsSkipped?: number;
|
|
882
1007
|
@Field({ nullable: true }) RSUStep?: string;
|
|
883
1008
|
@Field({ nullable: true }) RSURunning?: boolean;
|
|
884
1009
|
/** U11 — 1-based index of the current RSU step (determinate stepper). */
|
|
@@ -889,6 +1014,73 @@ class OperationProgressOutput {
|
|
|
889
1014
|
@Field({ nullable: true }) StartedAt?: string;
|
|
890
1015
|
}
|
|
891
1016
|
|
|
1017
|
+
@ObjectType()
|
|
1018
|
+
class ActiveOperationOutput {
|
|
1019
|
+
/** 'sync' | 'rsu' | 'discovery' | 'maintenance' */
|
|
1020
|
+
@Field() Kind: string;
|
|
1021
|
+
/**
|
|
1022
|
+
* 'connection' when this operation provably belongs to the requested connection;
|
|
1023
|
+
* 'workspace' when it is process-wide and cannot be attributed to one. The UI must not claim a
|
|
1024
|
+
* workspace-scoped RSU belongs to the connector being viewed - that mislabelling is why an
|
|
1025
|
+
* unrelated schema update read as "your connector is building".
|
|
1026
|
+
*/
|
|
1027
|
+
@Field() Scope: string;
|
|
1028
|
+
@Field() Label: string;
|
|
1029
|
+
@Field({ nullable: true }) RunID?: string;
|
|
1030
|
+
@Field({ nullable: true }) StartedAt?: Date;
|
|
1031
|
+
@Field({ nullable: true }) StepLabel?: string;
|
|
1032
|
+
@Field(() => Int, { nullable: true }) StepIndex?: number;
|
|
1033
|
+
@Field(() => Int, { nullable: true }) StepTotal?: number;
|
|
1034
|
+
@Field(() => Int, { nullable: true }) RecordsCreated?: number;
|
|
1035
|
+
@Field(() => Int, { nullable: true }) RecordsUpdated?: number;
|
|
1036
|
+
@Field(() => Int, { nullable: true }) RecordsSkipped?: number;
|
|
1037
|
+
@Field(() => Int, { nullable: true }) RecordsErrored?: number;
|
|
1038
|
+
/** Only a sync can be cancelled, and only from the process running it. */
|
|
1039
|
+
@Field() Cancellable: boolean;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
@ObjectType()
|
|
1043
|
+
class ActiveOperationsOutput {
|
|
1044
|
+
@Field() Success: boolean;
|
|
1045
|
+
@Field() Message: string;
|
|
1046
|
+
@Field(() => [ActiveOperationOutput], { nullable: true }) Operations?: ActiveOperationOutput[];
|
|
1047
|
+
/** What holds this connection's maintenance lock, if anything. */
|
|
1048
|
+
@Field({ nullable: true }) MaintenanceLockReason?: string;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
@ObjectType()
|
|
1052
|
+
class ResourcePressureFindingOutput {
|
|
1053
|
+
@Field() Code: string;
|
|
1054
|
+
@Field() Message: string;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
@ObjectType()
|
|
1058
|
+
class ResourcePressureOutput {
|
|
1059
|
+
@Field() Success: boolean;
|
|
1060
|
+
@Field() Message: string;
|
|
1061
|
+
@Field(() => Float, { nullable: true }) HeapUsedFraction?: number;
|
|
1062
|
+
@Field(() => Float, { nullable: true }) HeapUsedMB?: number;
|
|
1063
|
+
@Field(() => Float, { nullable: true }) HeapLimitMB?: number;
|
|
1064
|
+
@Field(() => Float, { nullable: true }) ResidentMB?: number;
|
|
1065
|
+
/**
|
|
1066
|
+
* Resident set as a fraction of HOST memory, plus the host totals it is derived from.
|
|
1067
|
+
*
|
|
1068
|
+
* ResidentMB alone is a number with no denominator: a client cannot tell whether 3,478 MB is
|
|
1069
|
+
* comfortable or one step from a SIGKILL without knowing the box. It was the latter on the
|
|
1070
|
+
* sandbox on 2026-09-14 — 3,478 MB of 3,830 MB, killed by the kernel, while HeapUsedFraction
|
|
1071
|
+
* (the only judgeable number this type exposed) sat unremarkable because `--max-old-space-size`
|
|
1072
|
+
* bounds V8's old space and not the response buffers that actually filled the machine.
|
|
1073
|
+
*/
|
|
1074
|
+
@Field(() => Float, { nullable: true }) ResidentFraction?: number;
|
|
1075
|
+
@Field(() => Float, { nullable: true }) HostMemTotalMB?: number;
|
|
1076
|
+
@Field(() => Float, { nullable: true }) HostMemAvailableMB?: number;
|
|
1077
|
+
@Field(() => Float, { nullable: true }) ArtifactDiskFreeMB?: number;
|
|
1078
|
+
@Field(() => Float, { nullable: true }) WorkDirFreeMB?: number;
|
|
1079
|
+
@Field(() => Int, { nullable: true }) ActiveSyncCount?: number;
|
|
1080
|
+
/** Empty when nothing is under pressure. Ordered most severe first. */
|
|
1081
|
+
@Field(() => [ResourcePressureFindingOutput], { nullable: true }) Findings?: ResourcePressureFindingOutput[];
|
|
1082
|
+
}
|
|
1083
|
+
|
|
892
1084
|
// ── STRUCTURED RUN ARTIFACTS (durable JSONL progress streams) ─────────
|
|
893
1085
|
// These expose the IntegrationProgressReader over GraphQL so a tenant can ask,
|
|
894
1086
|
// at any time, "what exactly happened (or is happening) on this run?" — backed
|
|
@@ -911,6 +1103,12 @@ class IntegrationRunSummaryArtifactOutput {
|
|
|
911
1103
|
@Field() RunKind: string;
|
|
912
1104
|
@Field({ nullable: true }) IntegrationID?: string;
|
|
913
1105
|
@Field({ nullable: true }) CompanyIntegrationID?: string;
|
|
1106
|
+
/**
|
|
1107
|
+
* Every connection the run touched. Populated for runs that can span more than one (an RSU
|
|
1108
|
+
* batch); `CompanyIntegrationID` above is set only when this holds exactly one. A client should
|
|
1109
|
+
* read this rather than the singular field when it needs to know what a run covers.
|
|
1110
|
+
*/
|
|
1111
|
+
@Field(() => [String], { nullable: true }) CompanyIntegrationIDs?: string[];
|
|
914
1112
|
@Field({ nullable: true }) ObjectName?: string;
|
|
915
1113
|
@Field({ nullable: true }) TriggerType?: string;
|
|
916
1114
|
@Field() StartedAt: string;
|
|
@@ -949,6 +1147,10 @@ class IntegrationListRunsOutput {
|
|
|
949
1147
|
@Field() Success: boolean;
|
|
950
1148
|
@Field() Message: string;
|
|
951
1149
|
@Field(() => [IntegrationRunSummaryArtifactOutput], { nullable: true }) Runs?: IntegrationRunSummaryArtifactOutput[];
|
|
1150
|
+
/** Feed back as `offset` for the next page. Absent when there is no next page. */
|
|
1151
|
+
@Field({ nullable: true }) NextOffset?: number;
|
|
1152
|
+
/** True when the store holds more runs past this page. */
|
|
1153
|
+
@Field({ nullable: true }) HasMore?: boolean;
|
|
952
1154
|
}
|
|
953
1155
|
|
|
954
1156
|
@ObjectType()
|
|
@@ -1107,6 +1309,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1107
1309
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
1108
1310
|
@Ctx() ctx: AppContext
|
|
1109
1311
|
): Promise<DiscoverObjectsOutput> {
|
|
1312
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
1110
1313
|
try {
|
|
1111
1314
|
const user = this.getAuthenticatedUser(ctx);
|
|
1112
1315
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
@@ -1132,6 +1335,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1132
1335
|
} catch (e) {
|
|
1133
1336
|
return this.handleDiscoveryError(e);
|
|
1134
1337
|
}
|
|
1338
|
+
});
|
|
1135
1339
|
}
|
|
1136
1340
|
|
|
1137
1341
|
/**
|
|
@@ -1146,6 +1350,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1146
1350
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
1147
1351
|
@Ctx() ctx: AppContext
|
|
1148
1352
|
): Promise<ListSourceObjectsOutput> {
|
|
1353
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
1149
1354
|
try {
|
|
1150
1355
|
const user = this.getAuthenticatedUser(ctx);
|
|
1151
1356
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
@@ -1211,6 +1416,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1211
1416
|
LogError(`IntegrationListSourceObjects error: ${e}`);
|
|
1212
1417
|
return { Success: false, Message: this.formatError(e) };
|
|
1213
1418
|
}
|
|
1419
|
+
});
|
|
1214
1420
|
}
|
|
1215
1421
|
|
|
1216
1422
|
private async loadIntegrationObjectsByIntegrationID(
|
|
@@ -1220,7 +1426,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1220
1426
|
const rv = new RunView();
|
|
1221
1427
|
const result = await rv.RunView<MJIntegrationObjectEntity>({
|
|
1222
1428
|
EntityName: 'MJ: Integration Objects',
|
|
1223
|
-
ExtraFilter: `IntegrationID='${integrationID}'`,
|
|
1429
|
+
ExtraFilter: `IntegrationID='${sqlUuid(integrationID, 'integrationID')}'`,
|
|
1224
1430
|
ResultType: 'entity_object',
|
|
1225
1431
|
}, user);
|
|
1226
1432
|
if (!result.Success) {
|
|
@@ -1250,6 +1456,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1250
1456
|
* (IntegrationObjectField.RelatedIntegrationObjectID) plus soft-FK declarations in the IO's
|
|
1251
1457
|
* Configuration (parentObjectName / parentObjectNames map / ReferencedType). Reads the
|
|
1252
1458
|
* engine cache only (no DB round-trip); objects with no persisted IO row have no entry.
|
|
1459
|
+
*
|
|
1460
|
+
* Hard FK parents come from `GetRelatedIntegrationObjectIDs`, which answers a per-connection
|
|
1461
|
+
* object from the scope's edge set. Its full field rows are cold here (only the sync loop warms
|
|
1462
|
+
* them), so reading them would find no parents at all.
|
|
1253
1463
|
*/
|
|
1254
1464
|
private computeObjectDependencyNames(integrationID: string): Map<string, string[]> {
|
|
1255
1465
|
const engine = IntegrationEngineBase.Instance;
|
|
@@ -1259,9 +1469,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1259
1469
|
const deps = new Map<string, string[]>();
|
|
1260
1470
|
for (const io of ios) {
|
|
1261
1471
|
const parents = new Set<string>();
|
|
1262
|
-
for (const
|
|
1263
|
-
const parentID =
|
|
1264
|
-
if (
|
|
1472
|
+
for (const relatedID of engine.GetRelatedIntegrationObjectIDs(io.ID)) {
|
|
1473
|
+
const parentID = relatedID.toUpperCase();
|
|
1474
|
+
if (UUIDsEqual(parentID, io.ID)) continue;
|
|
1265
1475
|
const parentName = nameById.get(parentID);
|
|
1266
1476
|
if (parentName) parents.add(parentName);
|
|
1267
1477
|
}
|
|
@@ -1297,6 +1507,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1297
1507
|
@Arg("objectName") objectName: string,
|
|
1298
1508
|
@Ctx() ctx: AppContext
|
|
1299
1509
|
): Promise<DiscoverFieldsOutput> {
|
|
1510
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
1300
1511
|
try {
|
|
1301
1512
|
const user = this.getAuthenticatedUser(ctx);
|
|
1302
1513
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
@@ -1322,6 +1533,193 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1322
1533
|
} catch (e) {
|
|
1323
1534
|
return this.handleDiscoveryError(e);
|
|
1324
1535
|
}
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Tests credentials that have NOT been saved anywhere.
|
|
1541
|
+
*
|
|
1542
|
+
* ── Why this exists ───────────────────────────────────────────────────────
|
|
1543
|
+
*
|
|
1544
|
+
* `IntegrationTestConnection` takes a `companyIntegrationID`, so it can only test a
|
|
1545
|
+
* connection that already exists. That forces every setup wizard into create-then-test:
|
|
1546
|
+
* the workspace writes a Credential and a CompanyIntegration, THEN finds out the
|
|
1547
|
+
* password was wrong, and something has to go back and delete them. Two cases escape
|
|
1548
|
+
* that cleanup — a credential test that outlives the caller's gateway (most likely
|
|
1549
|
+
* precisely when a host or tenant id is wrong, because that is what makes a vendor call
|
|
1550
|
+
* hang rather than refuse), and a test that passes before a later step fails. Both leave
|
|
1551
|
+
* a connection that answers every listing and syncs nothing.
|
|
1552
|
+
*
|
|
1553
|
+
* This inverts the order. Nothing is written, so there is nothing to clean up.
|
|
1554
|
+
*
|
|
1555
|
+
* ── How it can work without persisting ────────────────────────────────────
|
|
1556
|
+
*
|
|
1557
|
+
* The connector is resolved from the INTEGRATION row, not from a connection —
|
|
1558
|
+
* `ConnectorFactory.Resolve` already only needs the former. The credentials then ride in
|
|
1559
|
+
* on a transient CompanyIntegration that is constructed with `NewRecord()` and
|
|
1560
|
+
* DELIBERATELY NEVER SAVED, carrying the values in `Configuration`.
|
|
1561
|
+
*
|
|
1562
|
+
* That works because every connector resolves its auth material the same way: prefer the
|
|
1563
|
+
* linked Credential when `CredentialID` is set, otherwise fall back to the
|
|
1564
|
+
* `Configuration` JSON. It is a de-facto framework convention rather than a documented
|
|
1565
|
+
* one — verified across all seven connectors this was written for (NetSuite, Totara,
|
|
1566
|
+
* Nimble AMS, OpenWater, PheedLoop, PropFuel, Elevate), each of which reads
|
|
1567
|
+
* `CredentialID` first and `Configuration` second. A connector that reads ONLY
|
|
1568
|
+
* `CredentialID` would report missing credentials here rather than misbehave, which is
|
|
1569
|
+
* the safe direction to fail: the caller learns the probe cannot help and falls back to
|
|
1570
|
+
* create-then-test, exactly as before.
|
|
1571
|
+
*
|
|
1572
|
+
* ── Rules this endpoint holds itself to ───────────────────────────────────
|
|
1573
|
+
*
|
|
1574
|
+
* - NOTHING IS PERSISTED. No Credential, no CompanyIntegration, no Company. The
|
|
1575
|
+
* transient entity is a local and never leaves this method.
|
|
1576
|
+
* - THE VALUES ARE NEVER LOGGED. Not on success, not on failure, not inside an error
|
|
1577
|
+
* message. Only the integration id and the outcome are ever written down.
|
|
1578
|
+
* - IT CANNOT HANG. A hard deadline applies regardless of what the connector does,
|
|
1579
|
+
* because a probe that inherits the gateway-timeout problem solves nothing. A
|
|
1580
|
+
* timeout is reported as a timeout, which is itself the diagnosis for an unreachable
|
|
1581
|
+
* host.
|
|
1582
|
+
* - IT IS RATE LIMITED per (user, integration). The endpoint turns caller-supplied
|
|
1583
|
+
* input into an outbound vendor request, so an unbounded one is a request amplifier.
|
|
1584
|
+
*/
|
|
1585
|
+
@Query(() => ConnectionTestOutput)
|
|
1586
|
+
async IntegrationProbeCredentials(
|
|
1587
|
+
@Arg("integrationID") integrationID: string,
|
|
1588
|
+
@Arg("credentialValues") credentialValues: string,
|
|
1589
|
+
@Ctx() ctx: AppContext
|
|
1590
|
+
): Promise<ConnectionTestOutput> {
|
|
1591
|
+
try {
|
|
1592
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
1593
|
+
|
|
1594
|
+
const limited = IntegrationDiscoveryResolver.probeRateLimited(user.ID, integrationID, Date.now());
|
|
1595
|
+
if (limited) {
|
|
1596
|
+
return { Success: false, Message: limited };
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Must be a JSON OBJECT. A connector's parser is entitled to assume that much, and
|
|
1600
|
+
// handing it a bare string or an array is a way to reach a parse path nobody tests.
|
|
1601
|
+
let parsed: unknown;
|
|
1602
|
+
try {
|
|
1603
|
+
parsed = JSON.parse(credentialValues);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return { Success: false, Message: 'The credentials could not be read as JSON.' };
|
|
1606
|
+
}
|
|
1607
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1608
|
+
return { Success: false, Message: 'The credentials must be a JSON object of field names to values.' };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
1612
|
+
|
|
1613
|
+
const integration = await provider.GetEntityObject<MJIntegrationEntity>('MJ: Integrations', user);
|
|
1614
|
+
if (!(await integration.Load(integrationID))) {
|
|
1615
|
+
return { Success: false, Message: `Integration with ID "${integrationID}" not found` };
|
|
1616
|
+
}
|
|
1617
|
+
const connector = ConnectorFactory.Resolve(integration);
|
|
1618
|
+
|
|
1619
|
+
// The transient carrier. NewRecord() and never Save() — see the header.
|
|
1620
|
+
const probeCI = await provider.GetEntityObject<MJCompanyIntegrationEntity>('MJ: Company Integrations', user);
|
|
1621
|
+
probeCI.NewRecord();
|
|
1622
|
+
probeCI.Set('IntegrationID', integrationID);
|
|
1623
|
+
probeCI.Set('Configuration', credentialValues);
|
|
1624
|
+
|
|
1625
|
+
const testConnection = connector.TestConnection.bind(connector) as
|
|
1626
|
+
(ci: unknown, u: unknown) => Promise<ConnectionTestResult>;
|
|
1627
|
+
|
|
1628
|
+
const result = await IntegrationDiscoveryResolver.withProbeDeadline(
|
|
1629
|
+
testConnection(probeCI, user),
|
|
1630
|
+
integration.Name
|
|
1631
|
+
);
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
Success: result.Success,
|
|
1635
|
+
Message: result.Message,
|
|
1636
|
+
ServerVersion: result.ServerVersion
|
|
1637
|
+
};
|
|
1638
|
+
} catch (e) {
|
|
1639
|
+
// formatError only ever sees the thrown message. The credential values are not in
|
|
1640
|
+
// scope of anything logged here, and must never be added to it.
|
|
1641
|
+
LogError(`IntegrationProbeCredentials error for integration ${integrationID}: ${this.formatError(e)}`);
|
|
1642
|
+
return {
|
|
1643
|
+
Success: false,
|
|
1644
|
+
Message: `Error: ${this.formatError(e)}`
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* The probe's hard deadline.
|
|
1651
|
+
*
|
|
1652
|
+
* `TestConnectionMs` exists on the connector config but is advisory — an implementation
|
|
1653
|
+
* that does not consult it can block for as long as the vendor's socket does, which is
|
|
1654
|
+
* the whole failure this endpoint exists to avoid. So the bound is applied here, where it
|
|
1655
|
+
* cannot be opted out of.
|
|
1656
|
+
*
|
|
1657
|
+
* 20 seconds: comfortably longer than any healthy handshake, comfortably shorter than the
|
|
1658
|
+
* ~240s gateway ceiling that create-then-test keeps running into.
|
|
1659
|
+
*/
|
|
1660
|
+
private static readonly PROBE_DEADLINE_MS = 20_000;
|
|
1661
|
+
|
|
1662
|
+
private static withProbeDeadline(
|
|
1663
|
+
work: Promise<ConnectionTestResult>,
|
|
1664
|
+
vendorName: string
|
|
1665
|
+
): Promise<ConnectionTestResult> {
|
|
1666
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1667
|
+
const deadline = new Promise<ConnectionTestResult>(resolve => {
|
|
1668
|
+
timer = setTimeout(
|
|
1669
|
+
() =>
|
|
1670
|
+
resolve({
|
|
1671
|
+
Success: false,
|
|
1672
|
+
// A timeout IS the diagnosis for an unreachable host or a wrong
|
|
1673
|
+
// tenant path, so it says that rather than "unknown error".
|
|
1674
|
+
Message:
|
|
1675
|
+
`${vendorName} did not respond within ` +
|
|
1676
|
+
`${Math.round(IntegrationDiscoveryResolver.PROBE_DEADLINE_MS / 1000)} seconds. ` +
|
|
1677
|
+
`Check the host and any account or organization identifier in the values above.`
|
|
1678
|
+
}),
|
|
1679
|
+
IntegrationDiscoveryResolver.PROBE_DEADLINE_MS
|
|
1680
|
+
);
|
|
1681
|
+
timer.unref?.();
|
|
1682
|
+
});
|
|
1683
|
+
return Promise.race([work, deadline]).finally(() => {
|
|
1684
|
+
if (timer) {
|
|
1685
|
+
clearTimeout(timer);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Per (user, integration) rate limit.
|
|
1692
|
+
*
|
|
1693
|
+
* This endpoint converts caller-supplied input into an outbound request to a third party,
|
|
1694
|
+
* which makes an unbounded version a request amplifier pointed at somebody else's API —
|
|
1695
|
+
* and the fastest way to get a tenant's key rate-limited by its own vendor. In-process
|
|
1696
|
+
* and per-instance on purpose: it is a courtesy bound on a human typing into a form, not
|
|
1697
|
+
* a security boundary, and giving it a durable store would buy nothing a form's own pace
|
|
1698
|
+
* does not already provide.
|
|
1699
|
+
*/
|
|
1700
|
+
private static readonly PROBE_WINDOW_MS = 60_000;
|
|
1701
|
+
private static readonly PROBE_MAX_PER_WINDOW = 10;
|
|
1702
|
+
private static readonly probeHits = new Map<string, number[]>();
|
|
1703
|
+
|
|
1704
|
+
private static probeRateLimited(userID: string, integrationID: string, now: number): string | null {
|
|
1705
|
+
const key = `${userID}:${integrationID}`.toLowerCase();
|
|
1706
|
+
const window = now - IntegrationDiscoveryResolver.PROBE_WINDOW_MS;
|
|
1707
|
+
const hits = (IntegrationDiscoveryResolver.probeHits.get(key) ?? []).filter(t => t > window);
|
|
1708
|
+
if (hits.length >= IntegrationDiscoveryResolver.PROBE_MAX_PER_WINDOW) {
|
|
1709
|
+
return 'Too many credential checks in the last minute — wait a moment and try again.';
|
|
1710
|
+
}
|
|
1711
|
+
hits.push(now);
|
|
1712
|
+
IntegrationDiscoveryResolver.probeHits.set(key, hits);
|
|
1713
|
+
// Bound the map so a long-lived process cannot accumulate a key per (user, integration)
|
|
1714
|
+
// pair forever. Anything with no hits inside the window is already spent.
|
|
1715
|
+
if (IntegrationDiscoveryResolver.probeHits.size > 500) {
|
|
1716
|
+
for (const [k, v] of IntegrationDiscoveryResolver.probeHits) {
|
|
1717
|
+
if (!v.some(t => t > window)) {
|
|
1718
|
+
IntegrationDiscoveryResolver.probeHits.delete(k);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return null;
|
|
1325
1723
|
}
|
|
1326
1724
|
|
|
1327
1725
|
/**
|
|
@@ -1372,6 +1770,59 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1372
1770
|
* both on stdout (visible in the MJAPI log file) and in a per-run
|
|
1373
1771
|
* `<cwd>/logs/integration-runs/<runID>/progress.jsonl` artifact.
|
|
1374
1772
|
*/
|
|
1773
|
+
/**
|
|
1774
|
+
* Start discovery and return immediately with a tailable RunID.
|
|
1775
|
+
*
|
|
1776
|
+
* plan.md is emphatic that this must exist: "going to a page should NEVER, NEVER trigger such
|
|
1777
|
+
* things automatically, it must in a very clean way tell the user to click a button to start
|
|
1778
|
+
* discovery of tables". A button needs a call that returns at once; the synchronous mutation
|
|
1779
|
+
* runs the whole pipeline inline and times out at the gateway on a large catalog, which is why
|
|
1780
|
+
* both surfaces ended up auto-triggering on page entry and adopting whatever run they found.
|
|
1781
|
+
*
|
|
1782
|
+
* A DISTINCT mutation rather than a flag on the synchronous one: that reply promises real
|
|
1783
|
+
* counts because it waited, and a detached launch can only offer placeholder zeros.
|
|
1784
|
+
*
|
|
1785
|
+
* The lock is PROBED here so a second click is refused with a reason the UI can show. The
|
|
1786
|
+
* pipeline still acquires it for real - this probe is not the guard, it is the clean answer in
|
|
1787
|
+
* the common case. Losing the race just means the launch fails on the run stream instead.
|
|
1788
|
+
*/
|
|
1789
|
+
@Mutation(() => StartSchemaRefreshOutput)
|
|
1790
|
+
@RequireSystemUser()
|
|
1791
|
+
async IntegrationStartSchemaRefresh(
|
|
1792
|
+
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
1793
|
+
@Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK convention hint (e.g. 'id' for HubSpot)" }) universalPKConvention: string | undefined,
|
|
1794
|
+
@Ctx() ctx: AppContext
|
|
1795
|
+
): Promise<StartSchemaRefreshOutput> {
|
|
1796
|
+
const user = this.GetUserFromPayload(ctx.userPayload);
|
|
1797
|
+
// The pipeline WRITES the catalog, so it needs the read-write provider - the same one the
|
|
1798
|
+
// synchronous refresh mutation uses. A read-only provider here would fail at the first save.
|
|
1799
|
+
const md = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
1800
|
+
|
|
1801
|
+
const paused = await this.describeIfPaused(companyIntegrationID, user, md);
|
|
1802
|
+
if (paused) {
|
|
1803
|
+
return { Success: false, InProgress: false, RunID: 'not-started', BlockedBy: 'connection paused', Message: `Discovery not started: ${paused}` };
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const held = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
1807
|
+
if (held) {
|
|
1808
|
+
return {
|
|
1809
|
+
Success: false,
|
|
1810
|
+
InProgress: false,
|
|
1811
|
+
RunID: 'not-started',
|
|
1812
|
+
BlockedBy: held.Reason,
|
|
1813
|
+
Message: `Discovery not started: ${held.Reason} is already running for this connection. It will be available when that finishes.`,
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const summary = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md, universalPKConvention);
|
|
1818
|
+
return {
|
|
1819
|
+
Success: true,
|
|
1820
|
+
InProgress: true,
|
|
1821
|
+
RunID: summary.RunID,
|
|
1822
|
+
Message: 'Discovery started.',
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1375
1826
|
@Mutation(() => RefreshConnectorSchemaOutput)
|
|
1376
1827
|
@RequireSystemUser()
|
|
1377
1828
|
async IntegrationRefreshConnectorSchema(
|
|
@@ -1380,6 +1831,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1380
1831
|
@Arg("deactivateAbsent", { nullable: true, description: "Comprehensive refresh (default true): objects/fields ABSENT from this discovery are deactivated (Status='Disabled', never deleted, reversible on a later rediscovery). Pass false for a scoped/partial discovery so it never disables what it didn't probe." }) deactivateAbsent: boolean | undefined,
|
|
1381
1832
|
@Ctx() ctx: AppContext
|
|
1382
1833
|
): Promise<RefreshConnectorSchemaOutput> {
|
|
1834
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
1835
|
+
// Checked BEFORE the lock is taken, so a refusal owes no release.
|
|
1836
|
+
{
|
|
1837
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
1838
|
+
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
1839
|
+
const paused = await this.describeIfPaused(companyIntegrationID, user, md);
|
|
1840
|
+
if (paused) return { Success: false, RunID: 'not-started', Message: `Refresh not started: ${paused}` };
|
|
1841
|
+
}
|
|
1383
1842
|
// sync lock: a metadata refresh rewrites the IO/IOF rows a sync reads.
|
|
1384
1843
|
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'metadata refresh')) {
|
|
1385
1844
|
return {
|
|
@@ -1423,7 +1882,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1423
1882
|
return {
|
|
1424
1883
|
Success: result.Success,
|
|
1425
1884
|
Message: result.Success
|
|
1426
|
-
|
|
1885
|
+
// Objects AND fields. A PRESUPPOSED connector creates no objects on any run, so
|
|
1886
|
+
// the object counts alone report a refresh that reshaped hundreds of fields as
|
|
1887
|
+
// "0 created, 0 updated" — see DescribePersistCounts.
|
|
1888
|
+
? `Refresh complete: ${DescribePersistCounts(result.PersistResult)}, ${result.UnresolvedObjects.length} IOs still PK-less (deferred to additionalSchemaInfo authoring)`
|
|
1427
1889
|
: `Refresh failed: ${result.FailureMessage ?? 'unknown error'}`,
|
|
1428
1890
|
RunID: result.RunID,
|
|
1429
1891
|
ObjectsCreated: result.PersistResult?.ObjectsCreated,
|
|
@@ -1451,6 +1913,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1451
1913
|
} finally {
|
|
1452
1914
|
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
1453
1915
|
}
|
|
1916
|
+
});
|
|
1454
1917
|
}
|
|
1455
1918
|
|
|
1456
1919
|
/**
|
|
@@ -1597,6 +2060,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1597
2060
|
@Arg("platform", { defaultValue: "sqlserver" }) platform: string,
|
|
1598
2061
|
@Ctx() ctx: AppContext
|
|
1599
2062
|
): Promise<SchemaPreviewOutput> {
|
|
2063
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
1600
2064
|
try {
|
|
1601
2065
|
const user = this.getAuthenticatedUser(ctx);
|
|
1602
2066
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
@@ -1624,7 +2088,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1624
2088
|
Platform: validatedPlatform,
|
|
1625
2089
|
MJVersion: process.env.MJ_VERSION ?? '5.11.0',
|
|
1626
2090
|
SourceType: companyIntegration.Integration,
|
|
1627
|
-
AdditionalSchemaInfoPath:
|
|
2091
|
+
AdditionalSchemaInfoPath: RuntimeSchemaManager.Instance.AdditionalSchemaInfoPath,
|
|
1628
2092
|
MigrationsDir: process.env.RSU_MIGRATIONS_PATH ?? 'migrations/rsu',
|
|
1629
2093
|
MetadataDir: process.env.RSU_METADATA_DIR ?? 'metadata',
|
|
1630
2094
|
ExistingTables: this.buildExistingTables(targetConfigs, provider),
|
|
@@ -1665,6 +2129,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1665
2129
|
Message: `Error: ${this.formatError(e)}`
|
|
1666
2130
|
};
|
|
1667
2131
|
}
|
|
2132
|
+
});
|
|
1668
2133
|
}
|
|
1669
2134
|
|
|
1670
2135
|
/**
|
|
@@ -1717,7 +2182,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1717
2182
|
objects: SchemaPreviewObjectInput[],
|
|
1718
2183
|
sourceSchema: SourceSchemaInfo,
|
|
1719
2184
|
platform: 'sqlserver' | 'postgresql',
|
|
1720
|
-
connector?: BaseIntegrationConnector
|
|
2185
|
+
connector?: BaseIntegrationConnector,
|
|
2186
|
+
drops: TargetConfigDrops = NewTargetConfigDrops(),
|
|
1721
2187
|
): TargetTableConfig[] {
|
|
1722
2188
|
const mapper = new TypeMapper();
|
|
1723
2189
|
|
|
@@ -1731,9 +2197,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1731
2197
|
// out how many selections actually made it. The picker → ApplyAll →
|
|
1732
2198
|
// RSU pipeline already had three layers of silent O(N) drops; this
|
|
1733
2199
|
// makes them at least summarised.
|
|
1734
|
-
|
|
1735
|
-
const
|
|
1736
|
-
const
|
|
2200
|
+
// The caller's lists when it passes them, so the NO_TARGET_TABLES error can name each reason.
|
|
2201
|
+
const droppedNotInSchema = drops.NotInSchema;
|
|
2202
|
+
const droppedNoFields = drops.NoFields;
|
|
2203
|
+
const droppedNoPrimaryKey = drops.NoPrimaryKey;
|
|
1737
2204
|
|
|
1738
2205
|
const results: TargetTableConfig[] = [];
|
|
1739
2206
|
for (const obj of objects) {
|
|
@@ -1961,21 +2428,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
1961
2428
|
.filter(so => !so.SourceObjectName && so.SourceObjectID)
|
|
1962
2429
|
.map(so => so.SourceObjectID!);
|
|
1963
2430
|
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
const rv = new RunView();
|
|
1967
|
-
const result = await rv.RunView<{ ID: string; Name: string }>({
|
|
1968
|
-
EntityName: 'MJ: Integration Objects',
|
|
1969
|
-
ExtraFilter: idsToLookup.map(id => `ID='${id}'`).join(' OR '),
|
|
1970
|
-
ResultType: 'simple',
|
|
1971
|
-
Fields: ['ID', 'Name'],
|
|
1972
|
-
}, user);
|
|
1973
|
-
if (result.Success) {
|
|
1974
|
-
for (const row of result.Results) {
|
|
1975
|
-
idToName.set(row.ID.toUpperCase(), row.Name);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
2431
|
+
// Per-connection ids first (the picker's ids inside a catalog scope), then the shared catalog.
|
|
2432
|
+
const idToName = await LookupObjectNamesByID(idsToLookup.map(id => sqlUuid(id, 'ID')), user);
|
|
1979
2433
|
|
|
1980
2434
|
const plan: Array<{ Name: string; Fields?: string[] }> = [];
|
|
1981
2435
|
for (const so of sourceObjects) {
|
|
@@ -2014,16 +2468,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2014
2468
|
// newly-discovered objects with no persisted row).
|
|
2015
2469
|
void integrationID;
|
|
2016
2470
|
if (ids && ids.length > 0) {
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}, user);
|
|
2024
|
-
if (result.Success) {
|
|
2025
|
-
return result.Results.map(r => r.Name);
|
|
2026
|
-
}
|
|
2471
|
+
// Through the connection's catalog first, and in INPUT order: the batch flow zips these
|
|
2472
|
+
// names back onto its id cursor, which a database-ordered result did not match.
|
|
2473
|
+
const idToName = await LookupObjectNamesByID(ids.map(id => sqlUuid(id, 'ID')), user);
|
|
2474
|
+
return ids
|
|
2475
|
+
.map(id => idToName.get(String(id).toUpperCase()))
|
|
2476
|
+
.filter((n): n is string => !!n);
|
|
2027
2477
|
}
|
|
2028
2478
|
if (names && names.length > 0) {
|
|
2029
2479
|
const nameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
|
|
@@ -2065,19 +2515,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2065
2515
|
const idsToLookup = sourceObjects
|
|
2066
2516
|
.map(so => so.SourceObjectID)
|
|
2067
2517
|
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
const rv = new RunView();
|
|
2071
|
-
const result = await rv.RunView<{ ID: string; Name: string }>({
|
|
2072
|
-
EntityName: 'MJ: Integration Objects',
|
|
2073
|
-
ExtraFilter: idsToLookup.map(id => `ID='${id}'`).join(' OR '),
|
|
2074
|
-
ResultType: 'simple',
|
|
2075
|
-
Fields: ['ID', 'Name'],
|
|
2076
|
-
}, user);
|
|
2077
|
-
if (result.Success) {
|
|
2078
|
-
for (const r of result.Results) idToName.set(r.ID, r.Name);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2518
|
+
// Per-connection ids first (the picker's ids inside a catalog scope), then the shared catalog.
|
|
2519
|
+
const idToName = await LookupObjectNamesByID(idsToLookup.map(id => sqlUuid(id, 'ID')), user);
|
|
2081
2520
|
|
|
2082
2521
|
const schemaNameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
|
|
2083
2522
|
const resolvedNames: string[] = [];
|
|
@@ -2085,8 +2524,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2085
2524
|
const dropped: SourceObjectInput[] = [];
|
|
2086
2525
|
for (const so of sourceObjects) {
|
|
2087
2526
|
let name: string | undefined;
|
|
2088
|
-
if (so.SourceObjectID && idToName.has(so.SourceObjectID)) {
|
|
2089
|
-
name = idToName.get(so.SourceObjectID);
|
|
2527
|
+
if (so.SourceObjectID && idToName.has(so.SourceObjectID.toUpperCase())) {
|
|
2528
|
+
name = idToName.get(so.SourceObjectID.toUpperCase());
|
|
2090
2529
|
} else if (so.SourceObjectName) {
|
|
2091
2530
|
// Normalize case against the schema when the connector reports it
|
|
2092
2531
|
name = schemaNameMap.get(so.SourceObjectName.toLowerCase()) ?? so.SourceObjectName;
|
|
@@ -2120,20 +2559,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2120
2559
|
): Promise<void> {
|
|
2121
2560
|
const idsToResolve = objects.filter(o => o.SourceObjectID && !o.SourceObjectName).map(o => o.SourceObjectID!);
|
|
2122
2561
|
if (idsToResolve.length > 0) {
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
Fields: ['ID', 'Name'],
|
|
2129
|
-
}, user);
|
|
2130
|
-
if (result.Success) {
|
|
2131
|
-
const idToName = new Map(result.Results.map(r => [r.ID.toUpperCase(), r.Name]));
|
|
2132
|
-
for (const obj of objects) {
|
|
2133
|
-
if (obj.SourceObjectID) {
|
|
2134
|
-
const resolved = idToName.get(obj.SourceObjectID.toUpperCase());
|
|
2135
|
-
if (resolved) obj.SourceObjectName = resolved;
|
|
2136
|
-
}
|
|
2562
|
+
const idToName = await LookupObjectNamesByID(idsToResolve.map(id => sqlUuid(id, 'ID')), user);
|
|
2563
|
+
for (const obj of objects) {
|
|
2564
|
+
if (obj.SourceObjectID) {
|
|
2565
|
+
const resolved = idToName.get(obj.SourceObjectID.toUpperCase());
|
|
2566
|
+
if (resolved) obj.SourceObjectName = resolved;
|
|
2137
2567
|
}
|
|
2138
2568
|
}
|
|
2139
2569
|
}
|
|
@@ -2190,8 +2620,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2190
2620
|
const rv = new RunView();
|
|
2191
2621
|
const result = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
2192
2622
|
EntityName: 'MJ: Company Integrations',
|
|
2193
|
-
ExtraFilter: `ID='${companyIntegrationID}'`,
|
|
2623
|
+
ExtraFilter: `ID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
2194
2624
|
MaxRows: 1,
|
|
2625
|
+
SkipTotalRowCount: true,
|
|
2195
2626
|
ResultType: 'simple',
|
|
2196
2627
|
Fields: ['ID']
|
|
2197
2628
|
}, user);
|
|
@@ -2202,23 +2633,27 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2202
2633
|
}
|
|
2203
2634
|
|
|
2204
2635
|
/**
|
|
2205
|
-
* Authorizes the caller for a single run artifact
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2210
|
-
* these per-company endpoints).
|
|
2636
|
+
* Authorizes the caller for a single run artifact against EVERY connection the run touched.
|
|
2637
|
+
*
|
|
2638
|
+
* The rule itself — AND over the run's whole connection set, and why anything weaker leaks —
|
|
2639
|
+
* lives in {@link AuthorizeRunArtifact}. This method supplies the per-connection check, which is
|
|
2640
|
+
* the part that needs the caller's context and the data provider's row-level security.
|
|
2211
2641
|
*/
|
|
2642
|
+
private async authorizeRunArtifact(
|
|
2643
|
+
snap: IntegrationRunSnapshot,
|
|
2644
|
+
user: UserInfo,
|
|
2645
|
+
cache: Map<string, boolean>
|
|
2646
|
+
): Promise<RunArtifactAuthorizationResult> {
|
|
2647
|
+
return AuthorizeRunArtifact(snap.manifest, ciID => this.userCanReadCompanyIntegration(ciID, user, cache));
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/** Boolean form of {@link authorizeRunArtifact}, for the list path which needs no message. */
|
|
2212
2651
|
private async userCanReadRunArtifact(
|
|
2213
2652
|
snap: IntegrationRunSnapshot,
|
|
2214
2653
|
user: UserInfo,
|
|
2215
2654
|
cache: Map<string, boolean>
|
|
2216
2655
|
): Promise<boolean> {
|
|
2217
|
-
|
|
2218
|
-
if (!ciID) {
|
|
2219
|
-
return false;
|
|
2220
|
-
}
|
|
2221
|
-
return this.userCanReadCompanyIntegration(ciID, user, cache);
|
|
2656
|
+
return (await this.authorizeRunArtifact(snap, user, cache)).Authorized;
|
|
2222
2657
|
}
|
|
2223
2658
|
|
|
2224
2659
|
/** Standard authorization-failure message for run-artifact endpoints. */
|
|
@@ -2226,6 +2661,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2226
2661
|
return `Not authorized to access runs for CompanyIntegration '${companyIntegrationID}'`;
|
|
2227
2662
|
}
|
|
2228
2663
|
|
|
2664
|
+
/** The denial message for a run, naming the connection that blocked it when there is one. */
|
|
2665
|
+
private notAuthorizedForRunMessage(runID: string, deniedCompanyIntegrationID?: string): string {
|
|
2666
|
+
return deniedCompanyIntegrationID
|
|
2667
|
+
? this.notAuthorizedForCompanyIntegrationMessage(deniedCompanyIntegrationID)
|
|
2668
|
+
: `Not authorized to access run '${runID}'`;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2229
2671
|
/**
|
|
2230
2672
|
* Loads the CompanyIntegration + its parent Integration, then resolves the
|
|
2231
2673
|
* appropriate connector via ConnectorFactory.
|
|
@@ -2336,11 +2778,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2336
2778
|
* `IsPrimaryKey=true`.
|
|
2337
2779
|
* - Foreign-key relationships are reconstructed from
|
|
2338
2780
|
* `RelatedIntegrationObjectID` lookups against the same cache.
|
|
2781
|
+
* - Under a per-connection catalog the FIELD rows come from `scopedFields`
|
|
2782
|
+
* (`LoadScopedFieldsForBuild`), never from the engine: a connection's field rows
|
|
2783
|
+
* are warmed on demand by the sync loop only, so the engine answers `[]` for them
|
|
2784
|
+
* during an apply and every object would build with no columns.
|
|
2339
2785
|
*/
|
|
2340
2786
|
private buildSourceSchemaFromPersistedRows(
|
|
2341
2787
|
integrationID: string,
|
|
2342
2788
|
requestedNames?: string[],
|
|
2343
2789
|
warningsOut?: string[],
|
|
2790
|
+
scopedFields?: ScopedBuildFields | null,
|
|
2344
2791
|
): SourceSchemaInfo {
|
|
2345
2792
|
const engine = IntegrationEngineBase.Instance;
|
|
2346
2793
|
// ACTIVE-only materialization: an object/field a given tenant doesn't expose is marked
|
|
@@ -2359,15 +2806,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2359
2806
|
for (const io of ios) ioByID.set(io.ID, io.Name);
|
|
2360
2807
|
|
|
2361
2808
|
const result: SourceSchemaInfo = { Objects: [] };
|
|
2362
|
-
const fieldsByObjectName: Record<string, Array<{ Name: string; Status: string | null }>> = {};
|
|
2363
2809
|
for (const io of ios) {
|
|
2364
2810
|
if (filter && !filter.has(io.Name.toLowerCase())) continue;
|
|
2365
2811
|
// Active fields only — an inactive (source-absent / deactivated) field is not materialized.
|
|
2366
|
-
const allFields =
|
|
2812
|
+
const allFields = FieldsForBuild(io.ID, scopedFields, engine);
|
|
2367
2813
|
const iofs = allFields.filter(iof => iof.Status === 'Active');
|
|
2368
|
-
// Remember what this object declared, active or not — the caller's warning collector
|
|
2369
|
-
// turns the difference into the one message that explains a column that never appeared.
|
|
2370
|
-
if (warningsOut) fieldsByObjectName[io.Name] = allFields.map(f => ({ Name: f.Name, Status: f.Status }));
|
|
2371
2814
|
|
|
2372
2815
|
const fields = iofs.map(iof => {
|
|
2373
2816
|
const targetIOName = iof.RelatedIntegrationObjectID
|
|
@@ -2410,19 +2853,63 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2410
2853
|
}
|
|
2411
2854
|
|
|
2412
2855
|
// Declared-but-inactive rows this rebuild left out. The caller decides what to do with the
|
|
2413
|
-
// strings (the apply path puts them on its Warnings);
|
|
2414
|
-
//
|
|
2415
|
-
if (warningsOut)
|
|
2416
|
-
warningsOut.push(...ComputeInactiveRowWarnings({
|
|
2417
|
-
RequestedNames: requestedNames ?? null,
|
|
2418
|
-
AllObjects: engine.GetIntegrationObjectsByIntegrationID(integrationID)
|
|
2419
|
-
.map(io => ({ Name: io.Name, Status: io.Status })),
|
|
2420
|
-
FieldsByObjectName: fieldsByObjectName,
|
|
2421
|
-
}));
|
|
2422
|
-
}
|
|
2856
|
+
// strings (the apply path puts them on its Warnings); nothing is computed when no collector
|
|
2857
|
+
// was passed.
|
|
2858
|
+
if (warningsOut) warningsOut.push(...this.collectInactiveRowWarnings(integrationID, requestedNames, scopedFields));
|
|
2423
2859
|
return result;
|
|
2424
2860
|
}
|
|
2425
2861
|
|
|
2862
|
+
/**
|
|
2863
|
+
* Brings the integration catalog caches to committed state, then re-configures the engine from it.
|
|
2864
|
+
*
|
|
2865
|
+
* Both halves are needed and the ORDER is the point. `mj sync push` writes IO/IOF rows through
|
|
2866
|
+
* stored procedures, which fire no `BaseEntity` change events, so nothing invalidates the RunView
|
|
2867
|
+
* cache and `Config(true)` re-queries straight back into the stale entries. The per-connection
|
|
2868
|
+
* catalog entities are guarded individually because a workspace without that migration has not
|
|
2869
|
+
* registered them, and a missing entity must not fail an apply.
|
|
2870
|
+
*
|
|
2871
|
+
* One method so the single apply and the batch apply cannot drift apart on it again.
|
|
2872
|
+
*/
|
|
2873
|
+
private async refreshIntegrationCatalogCaches(user: UserInfo, provider?: IMetadataProvider): Promise<void> {
|
|
2874
|
+
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Objects');
|
|
2875
|
+
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Object Fields');
|
|
2876
|
+
for (const perConnection of ['MJ: Company Integration Objects', 'MJ: Company Integration Object Fields']) {
|
|
2877
|
+
try { await LocalCacheManager.Instance.InvalidateEntityCaches(perConnection); } catch { /* not registered here */ }
|
|
2878
|
+
}
|
|
2879
|
+
await IntegrationEngine.Instance.Config(true, user, provider);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/**
|
|
2883
|
+
* The declared-but-inactive-row warnings for an apply, computed from the catalog rather than from
|
|
2884
|
+
* whichever schema object the apply happens to be holding.
|
|
2885
|
+
*
|
|
2886
|
+
* Separate from {@link buildSourceSchemaFromPersistedRows} because the two are independent
|
|
2887
|
+
* questions, and conflating them cost the batch path its warnings entirely: the batch builds its
|
|
2888
|
+
* source schema once and hands it down as `prefetchedSourceSchema`, which skips the rebuild — and
|
|
2889
|
+
* with it skipped the only place these warnings were ever collected. The warnings come from the
|
|
2890
|
+
* IO/IOF rows' `Status`, which is knowable whenever the integration and the requested names are,
|
|
2891
|
+
* so it never needed to ride along with a rebuild.
|
|
2892
|
+
*
|
|
2893
|
+
* `requestedNames` empty/undefined means "everything active", which deliberately does NOT report
|
|
2894
|
+
* deactivated objects (a large catalog carries hundreds and announcing them on every apply is
|
|
2895
|
+
* noise) — see ComputeInactiveRowWarnings.
|
|
2896
|
+
*
|
|
2897
|
+
* `scopedFields` is the connection's field rows when the apply runs under a per-connection catalog
|
|
2898
|
+
* (`LoadScopedFieldsForBuild`). The FIELD half of the warnings reads through it for the same reason
|
|
2899
|
+
* the rebuild does: the engine answers `[]` for a connection's fields during an apply, so reading
|
|
2900
|
+
* them from the engine would silently drop every field warning. Without it the engine is read, as
|
|
2901
|
+
* for the shared catalog.
|
|
2902
|
+
*/
|
|
2903
|
+
private collectInactiveRowWarnings(integrationID: string, requestedNames?: string[], scopedFields?: ScopedBuildFields | null): string[] {
|
|
2904
|
+
const engine = IntegrationEngineBase.Instance;
|
|
2905
|
+
if (!scopedFields) return CollectInactiveRowWarnings(engine, integrationID, requestedNames);
|
|
2906
|
+
return CollectInactiveRowWarnings({
|
|
2907
|
+
GetActiveIntegrationObjects: id => engine.GetActiveIntegrationObjects(id),
|
|
2908
|
+
GetIntegrationObjectsByIntegrationID: id => engine.GetIntegrationObjectsByIntegrationID(id),
|
|
2909
|
+
GetIntegrationObjectFields: objectID => FieldsForBuild(objectID, scopedFields, engine),
|
|
2910
|
+
}, integrationID, requestedNames);
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2426
2913
|
private async runSchemaRefreshPipeline(
|
|
2427
2914
|
companyIntegrationID: string,
|
|
2428
2915
|
user: UserInfo,
|
|
@@ -2629,7 +3116,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2629
3116
|
// Shared-credential safety: do not delete if any OTHER CompanyIntegration uses it.
|
|
2630
3117
|
const sharedResult = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
2631
3118
|
EntityName: 'MJ: Company Integrations',
|
|
2632
|
-
ExtraFilter: `CredentialID='${credentialID}' AND ID<>'${companyIntegrationID}'`,
|
|
3119
|
+
ExtraFilter: `CredentialID='${sqlUuid(credentialID, 'credentialID')}' AND ID<>'${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
2633
3120
|
ResultType: 'simple',
|
|
2634
3121
|
Fields: ['ID']
|
|
2635
3122
|
}, user);
|
|
@@ -2714,7 +3201,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2714
3201
|
// Filter stays server-side; no client-side `.filter()` post-pass.
|
|
2715
3202
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as DatabaseProviderBase;
|
|
2716
3203
|
const filters: string[] = [];
|
|
2717
|
-
if (companyID) filters.push(`CompanyID='${companyID}'`);
|
|
3204
|
+
if (companyID) filters.push(`CompanyID='${sqlUuid(companyID, 'companyID')}'`);
|
|
2718
3205
|
if (activeOnly) filters.push(`IsActive = ${provider.Dialect.BooleanLiteral(true)}`);
|
|
2719
3206
|
const filter = filters.join(' AND ');
|
|
2720
3207
|
const result = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
@@ -3066,7 +3553,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3066
3553
|
const rv = new RunView();
|
|
3067
3554
|
const res = await rv.RunView<{ Entity: string }>({
|
|
3068
3555
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3069
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
3556
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
3070
3557
|
Fields: ['Entity'],
|
|
3071
3558
|
ResultType: 'simple',
|
|
3072
3559
|
}, user);
|
|
@@ -3087,6 +3574,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3087
3574
|
@Arg("entityName", { nullable: true }) entityName: string | undefined,
|
|
3088
3575
|
@Ctx() ctx: AppContext
|
|
3089
3576
|
): Promise<CustomColumnCandidatesOutput> {
|
|
3577
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
3090
3578
|
try {
|
|
3091
3579
|
const user = this.getAuthenticatedUser(ctx);
|
|
3092
3580
|
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
@@ -3101,6 +3589,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3101
3589
|
LogError(`IntegrationListCustomColumnCandidates error: ${e}`);
|
|
3102
3590
|
return { Success: false, Message: this.formatError(e), Candidates: [] };
|
|
3103
3591
|
}
|
|
3592
|
+
});
|
|
3104
3593
|
}
|
|
3105
3594
|
|
|
3106
3595
|
/**
|
|
@@ -3117,6 +3606,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3117
3606
|
@Arg("entityNames", () => [String], { nullable: true }) entityNames: string[] | undefined,
|
|
3118
3607
|
@Ctx() ctx: AppContext
|
|
3119
3608
|
): Promise<PromoteCustomColumnsOutput> {
|
|
3609
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
3120
3610
|
try {
|
|
3121
3611
|
const user = this.getAuthenticatedUser(ctx);
|
|
3122
3612
|
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
@@ -3137,28 +3627,118 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3137
3627
|
LogError(`IntegrationPromoteCustomColumns error: ${e}`);
|
|
3138
3628
|
return { Success: false, Message: this.formatError(e), Promoted: false, ColumnsAdded: [], SchemaUpdatePending: false };
|
|
3139
3629
|
}
|
|
3630
|
+
});
|
|
3140
3631
|
}
|
|
3141
3632
|
|
|
3142
3633
|
/**
|
|
3143
|
-
*
|
|
3634
|
+
* Pauses a connection: IsActive=false, both scheduled jobs paused, and a stop requested for
|
|
3635
|
+
* any sync already in flight.
|
|
3636
|
+
*
|
|
3637
|
+
* It used to be the first of those three only. The engine checks IsActive before a sync, so
|
|
3638
|
+
* manual syncs did stop — but the CRON jobs kept firing (a sync schedule woke, was refused,
|
|
3639
|
+
* and wrote a failed run, every tick, for as long as the connection stayed paused), the
|
|
3640
|
+
* DISCOVERY schedule kept firing and was not refused at all (a paused connection went on
|
|
3641
|
+
* rescanning the vendor on a timer, spending rate budget and rewriting the catalog the pause
|
|
3642
|
+
* was meant to freeze), and an in-flight sync ran to completion.
|
|
3643
|
+
*
|
|
3644
|
+
* ORDER IS DELIBERATE. IsActive is written FIRST, because it is the flag every other actor
|
|
3645
|
+
* reads: once it is false, nothing new can start, so a failure in any later step degrades the
|
|
3646
|
+
* pause rather than voiding it. The schedules come next. The cancel is last and is
|
|
3647
|
+
* best-effort by nature — a running sync belongs to whichever process owns it.
|
|
3648
|
+
*
|
|
3649
|
+
* WHAT PAUSE DOES NOT TOUCH: entity maps. Their Status is the user's own selection of which
|
|
3650
|
+
* tables sync, and a pause that silently deselected tables would be undone wrong on resume.
|
|
3651
|
+
*
|
|
3652
|
+
* The reply distinguishes a stop that will happen from one that will not — see
|
|
3653
|
+
* {@link DeactivateConnectionOutput}.
|
|
3144
3654
|
*/
|
|
3145
|
-
@Mutation(() =>
|
|
3655
|
+
@Mutation(() => DeactivateConnectionOutput)
|
|
3146
3656
|
async IntegrationDeactivateConnection(
|
|
3147
3657
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
3148
3658
|
@Ctx() ctx: AppContext
|
|
3149
|
-
): Promise<
|
|
3659
|
+
): Promise<DeactivateConnectionOutput> {
|
|
3660
|
+
const empty = { SchedulesPaused: 0, CancelRequested: false, CancelScope: 'none' as CancelScope };
|
|
3150
3661
|
try {
|
|
3151
3662
|
const user = this.getAuthenticatedUser(ctx);
|
|
3152
3663
|
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
3153
3664
|
const ci = await md.GetEntityObject<MJCompanyIntegrationEntity>('MJ: Company Integrations', user);
|
|
3154
3665
|
const loaded = await ci.InnerLoad(CompositeKey.FromID(companyIntegrationID));
|
|
3155
|
-
if (!loaded) return { Success: false, Message: 'CompanyIntegration not found' };
|
|
3666
|
+
if (!loaded) return { Success: false, Message: 'CompanyIntegration not found', ...empty };
|
|
3667
|
+
|
|
3668
|
+
// 1. The flag first — see ORDER IS DELIBERATE above.
|
|
3156
3669
|
ci.IsActive = false;
|
|
3157
|
-
if (!await ci.Save()) return { Success: false, Message: 'Failed to deactivate' };
|
|
3158
|
-
|
|
3670
|
+
if (!await ci.Save()) return { Success: false, Message: 'Failed to deactivate', ...empty };
|
|
3671
|
+
|
|
3672
|
+
const notes: string[] = [];
|
|
3673
|
+
|
|
3674
|
+
// 2. Schedules. A pause of an already-paused connection moves nothing and must not
|
|
3675
|
+
// overwrite the record the FIRST pause left — DecidePauseWrite is that guard.
|
|
3676
|
+
// The record is saved BEFORE any job moves: a job paused without a saved record is
|
|
3677
|
+
// one resume can never put back, so a record that will not save pauses nothing.
|
|
3678
|
+
let schedulesPaused = 0;
|
|
3679
|
+
try {
|
|
3680
|
+
const jobs = await this.findScheduledJobsForConnection(companyIntegrationID, user);
|
|
3681
|
+
const toPause = DecideSchedulesToPause(jobs);
|
|
3682
|
+
const stored = ReadPausedSchedules(ci.Configuration);
|
|
3683
|
+
const write = DecidePauseWrite(stored, toPause);
|
|
3684
|
+
if (write) {
|
|
3685
|
+
ci.Configuration = WritePausedSchedules(ci.Configuration, write);
|
|
3686
|
+
if (!await ci.Save()) {
|
|
3687
|
+
throw new Error(`recording the schedules to pause failed: ${ci.LatestResult?.Message ?? 'unknown error'}`);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
for (const rec of toPause) {
|
|
3691
|
+
if (await this.setScheduledJobStatus(rec.ID, 'Paused', user, md)) schedulesPaused++;
|
|
3692
|
+
}
|
|
3693
|
+
// Mirror onto the connection row the way the post-restart scheduler does, so a
|
|
3694
|
+
// reader that only knows about ScheduleEnabled still sees a paused connection.
|
|
3695
|
+
// The record is already saved, so a failure here costs only the mirror.
|
|
3696
|
+
if (schedulesPaused > 0) {
|
|
3697
|
+
ci.ScheduleEnabled = false;
|
|
3698
|
+
if (!await ci.Save()) {
|
|
3699
|
+
LogError(`IntegrationDeactivateConnection: clearing ScheduleEnabled failed — ${ci.LatestResult?.Message ?? 'unknown error'}`);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
notes.push(DescribePauseOutcome(toPause, stored.length > 0 && toPause.length === 0));
|
|
3703
|
+
} catch (schedErr) {
|
|
3704
|
+
// A schedule we could not pause is worth saying out loud: the connection IS
|
|
3705
|
+
// paused, but a cron may still wake it and write refused runs.
|
|
3706
|
+
LogError(`IntegrationDeactivateConnection: pausing schedules failed — ${schedErr}`);
|
|
3707
|
+
notes.push('Could not pause the schedules — a scheduled run may still fire and be refused.');
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
// 3. Cancel whatever is running, and report how far the request actually reached.
|
|
3711
|
+
let inFlightRunID: string | undefined;
|
|
3712
|
+
let scope: CancelScope = 'none';
|
|
3713
|
+
try {
|
|
3714
|
+
const live = await IntegrationEngine.GetSyncProgressAsync(companyIntegrationID, user, md);
|
|
3715
|
+
if (live) {
|
|
3716
|
+
inFlightRunID = live.RunID ?? undefined;
|
|
3717
|
+
const durable = await IntegrationEngine.CancelSyncAsync(companyIntegrationID, user, md);
|
|
3718
|
+
scope = DescribeCancelScope({
|
|
3719
|
+
liveRunPresent: true,
|
|
3720
|
+
durableRequestRecorded: durable,
|
|
3721
|
+
runningInThisProcess: IntegrationEngine.IsSyncRunningInThisProcess(companyIntegrationID),
|
|
3722
|
+
});
|
|
3723
|
+
const sentence = DescribeCancelOutcome(scope);
|
|
3724
|
+
if (sentence) notes.push(sentence);
|
|
3725
|
+
}
|
|
3726
|
+
} catch (cancelErr) {
|
|
3727
|
+
LogError(`IntegrationDeactivateConnection: cancel request failed — ${cancelErr}`);
|
|
3728
|
+
notes.push('A sync may still be running; no new sync will start.');
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
return {
|
|
3732
|
+
Success: true,
|
|
3733
|
+
Message: ['Deactivated.', ...notes.filter(Boolean)].join(' '),
|
|
3734
|
+
SchedulesPaused: schedulesPaused,
|
|
3735
|
+
InFlightSyncRunID: inFlightRunID,
|
|
3736
|
+
CancelRequested: scope === 'durable' || scope === 'this-process',
|
|
3737
|
+
CancelScope: scope,
|
|
3738
|
+
};
|
|
3159
3739
|
} catch (e) {
|
|
3160
3740
|
LogError(`IntegrationDeactivateConnection error: ${e}`);
|
|
3161
|
-
return { Success: false, Message: this.formatError(e) };
|
|
3741
|
+
return { Success: false, Message: this.formatError(e), ...empty };
|
|
3162
3742
|
}
|
|
3163
3743
|
}
|
|
3164
3744
|
|
|
@@ -3218,25 +3798,53 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3218
3798
|
ci.IsActive = true;
|
|
3219
3799
|
if (!await ci.Save()) return { Success: false, Message: `Failed to reactivate: ${ci.LatestResult?.Message ?? 'Unknown error'}` };
|
|
3220
3800
|
|
|
3801
|
+
// Put back EXACTLY what the pause took. Not "set every schedule Active": a connection
|
|
3802
|
+
// can be paused while one of its schedules was already off because the operator turned
|
|
3803
|
+
// it off, and inferring would silently restart work nobody asked for. The record
|
|
3804
|
+
// written at pause is the only thing that can tell those apart.
|
|
3805
|
+
let schedulesResumed = 0;
|
|
3806
|
+
let scheduleNote = '';
|
|
3807
|
+
try {
|
|
3808
|
+
const stored = ReadPausedSchedules(ci.Configuration);
|
|
3809
|
+
if (stored.length > 0) {
|
|
3810
|
+
const jobs = await this.findScheduledJobsForConnection(companyIntegrationID, user);
|
|
3811
|
+
for (const id of DecideSchedulesToResume(stored, jobs)) {
|
|
3812
|
+
if (await this.setScheduledJobStatus(id, 'Active', user, md)) schedulesResumed++;
|
|
3813
|
+
}
|
|
3814
|
+
// Consume the record whether or not every job came back: a job that was
|
|
3815
|
+
// deleted while paused is never coming back, and keeping the record would make
|
|
3816
|
+
// the next pause a no-op via DecidePauseWrite's already-paused guard. That is
|
|
3817
|
+
// also why a record that will not clear is said out loud rather than logged.
|
|
3818
|
+
ci.Configuration = WritePausedSchedules(ci.Configuration, []);
|
|
3819
|
+
if (schedulesResumed > 0) ci.ScheduleEnabled = true;
|
|
3820
|
+
if (!await ci.Save()) {
|
|
3821
|
+
LogError(`IntegrationReactivateConnection: clearing the paused-schedule record failed — ${ci.LatestResult?.Message ?? 'unknown error'}`);
|
|
3822
|
+
scheduleNote = ' The record of paused schedules could not be cleared, so the next pause will not record the schedules it stops.';
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
} catch (resumeErr) {
|
|
3826
|
+
LogError(`IntegrationReactivateConnection: restoring schedules failed — ${resumeErr}`);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3221
3829
|
if (runSchemaRefresh && !awaitSchemaRefresh) {
|
|
3222
3830
|
const detached = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md);
|
|
3223
|
-
return { Success: true, Message: BuildReactivateMessage(detached) };
|
|
3831
|
+
return { Success: true, Message: BuildReactivateMessage(detached) + scheduleNote };
|
|
3224
3832
|
}
|
|
3225
3833
|
|
|
3226
3834
|
if (runSchemaRefresh) {
|
|
3227
3835
|
try {
|
|
3228
3836
|
const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md);
|
|
3229
|
-
return { Success: true, Message: BuildReactivateMessage(refreshResult) };
|
|
3837
|
+
return { Success: true, Message: BuildReactivateMessage(refreshResult) + scheduleNote };
|
|
3230
3838
|
} catch (refreshErr) {
|
|
3231
3839
|
// Refresh failure does NOT undo the reactivation — the
|
|
3232
3840
|
// connection is active, only discovery failed, and the
|
|
3233
3841
|
// operator can re-run IntegrationRefreshConnectorSchema.
|
|
3234
3842
|
LogError(`IntegrationReactivateConnection: pipeline error — ${refreshErr}`);
|
|
3235
|
-
return { Success: true, Message: `Reactivated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
3843
|
+
return { Success: true, Message: `Reactivated (schema refresh failed: ${this.formatError(refreshErr)})${scheduleNote}` };
|
|
3236
3844
|
}
|
|
3237
3845
|
}
|
|
3238
3846
|
|
|
3239
|
-
return { Success: true, Message: BuildReactivateMessage(undefined) };
|
|
3847
|
+
return { Success: true, Message: BuildReactivateMessage(undefined) + scheduleNote };
|
|
3240
3848
|
} catch (e) {
|
|
3241
3849
|
LogError(`IntegrationReactivateConnection error: ${e}`);
|
|
3242
3850
|
return { Success: false, Message: this.formatError(e) };
|
|
@@ -3255,6 +3863,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3255
3863
|
@Arg("entityMaps", () => [EntityMapInput]) entityMaps: EntityMapInput[],
|
|
3256
3864
|
@Ctx() ctx: AppContext
|
|
3257
3865
|
): Promise<CreateEntityMapsOutput> {
|
|
3866
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
3258
3867
|
try {
|
|
3259
3868
|
const user = this.getAuthenticatedUser(ctx);
|
|
3260
3869
|
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
@@ -3297,9 +3906,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3297
3906
|
const escapedObjectName = mapDef.ExternalObjectName.replace(/'/g, "''");
|
|
3298
3907
|
const existingMapResult = await new RunView().RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
3299
3908
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3300
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND ExternalObjectName='${escapedObjectName}'`,
|
|
3909
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}' AND ExternalObjectName='${escapedObjectName}'`,
|
|
3301
3910
|
OrderBy: '__mj_CreatedAt ASC',
|
|
3302
3911
|
MaxRows: 1,
|
|
3912
|
+
SkipTotalRowCount: true,
|
|
3303
3913
|
ResultType: 'entity_object',
|
|
3304
3914
|
BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
|
|
3305
3915
|
}, user);
|
|
@@ -3328,7 +3938,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3328
3938
|
if (mapDef.FieldMaps) {
|
|
3329
3939
|
const existingFieldMaps = await new RunView().RunView<{ SourceFieldName: string }>({
|
|
3330
3940
|
EntityName: 'MJ: Company Integration Field Maps',
|
|
3331
|
-
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
3941
|
+
ExtraFilter: `EntityMapID='${sqlUuid(entityMapID, 'entityMapID')}'`,
|
|
3332
3942
|
Fields: ['SourceFieldName'],
|
|
3333
3943
|
ResultType: 'simple',
|
|
3334
3944
|
BypassCache: true, // read committed field-map state for the idempotency skip
|
|
@@ -3362,6 +3972,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3362
3972
|
LogError(`IntegrationCreateEntityMaps error: ${e}`);
|
|
3363
3973
|
return { Success: false, Message: this.formatError(e) };
|
|
3364
3974
|
}
|
|
3975
|
+
});
|
|
3365
3976
|
}
|
|
3366
3977
|
|
|
3367
3978
|
// ── SCHEMA EXECUTION ────────────────────────────────────────────────
|
|
@@ -3384,6 +3995,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3384
3995
|
@Arg("skipRestart", { defaultValue: false }) skipRestart: boolean,
|
|
3385
3996
|
@Ctx() ctx: AppContext
|
|
3386
3997
|
): Promise<ApplySchemaOutput> {
|
|
3998
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
3387
3999
|
try {
|
|
3388
4000
|
const user = this.getAuthenticatedUser(ctx);
|
|
3389
4001
|
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
@@ -3400,6 +4012,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3400
4012
|
companyIntegration.IntegrationID,
|
|
3401
4013
|
Array.from(requestedNames),
|
|
3402
4014
|
inactiveWarnings,
|
|
4015
|
+
await LoadScopedFieldsForBuild(companyIntegration.IntegrationID, user),
|
|
3403
4016
|
);
|
|
3404
4017
|
if (sourceSchema.Objects.length === 0) {
|
|
3405
4018
|
LogError(`[IntegrationApplySchema] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
|
|
@@ -3418,12 +4031,19 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3418
4031
|
|
|
3419
4032
|
await this.resolveObjectInputs(objects, sourceSchema, user);
|
|
3420
4033
|
|
|
4034
|
+
// Filtered on the names AFTER resolveObjectInputs has corrected their casing, as
|
|
4035
|
+
// buildSchemaForConnector does. The raw input set is case-sensitive, so a caller passing
|
|
4036
|
+
// `Clicks` for a stored `clicks` selected nothing, and NO_TARGET_TABLES then told them to
|
|
4037
|
+
// re-run discovery, which cannot fix a casing mismatch.
|
|
4038
|
+
const resolvedNames = new Set(objects.map(o => o.SourceObjectName));
|
|
3421
4039
|
const filteredSchema: SourceSchemaInfo = {
|
|
3422
|
-
Objects: sourceSchema.Objects.filter(o =>
|
|
4040
|
+
Objects: sourceSchema.Objects.filter(o => resolvedNames.has(o.ExternalName))
|
|
3423
4041
|
};
|
|
3424
4042
|
|
|
3425
4043
|
const validatedPlatform = this.validatePlatform(platform);
|
|
3426
|
-
const
|
|
4044
|
+
const drops = NewTargetConfigDrops();
|
|
4045
|
+
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, validatedPlatform, connector, drops);
|
|
4046
|
+
AssertTargetConfigs(objects, targetConfigs, companyIntegration.Integration, drops);
|
|
3427
4047
|
|
|
3428
4048
|
const input: SchemaBuilderInput = {
|
|
3429
4049
|
SourceSchema: filteredSchema,
|
|
@@ -3431,7 +4051,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3431
4051
|
Platform: validatedPlatform,
|
|
3432
4052
|
MJVersion: process.env.MJ_VERSION ?? '5.11.0',
|
|
3433
4053
|
SourceType: companyIntegration.Integration,
|
|
3434
|
-
AdditionalSchemaInfoPath:
|
|
4054
|
+
AdditionalSchemaInfoPath: RuntimeSchemaManager.Instance.AdditionalSchemaInfoPath,
|
|
3435
4055
|
MigrationsDir: process.env.RSU_MIGRATIONS_PATH ?? 'migrations/rsu',
|
|
3436
4056
|
MetadataDir: process.env.RSU_METADATA_DIR ?? 'metadata',
|
|
3437
4057
|
ExistingTables: this.buildExistingTables(targetConfigs, provider),
|
|
@@ -3466,6 +4086,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3466
4086
|
LogError(`IntegrationApplySchema error: ${e}`);
|
|
3467
4087
|
return { Success: false, Message: this.formatError(e) };
|
|
3468
4088
|
}
|
|
4089
|
+
});
|
|
3469
4090
|
}
|
|
3470
4091
|
|
|
3471
4092
|
/**
|
|
@@ -3492,9 +4113,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3492
4113
|
// Phase 1: Build schema artifacts for each connector's objects
|
|
3493
4114
|
for (const item of items) {
|
|
3494
4115
|
try {
|
|
3495
|
-
const { schemaOutput, rsuInput } = await this.buildSchemaForConnector(
|
|
4116
|
+
const { schemaOutput, rsuInput } = await WithCatalogScope(item.CompanyIntegrationID, () => this.buildSchemaForConnector(
|
|
3496
4117
|
item.CompanyIntegrationID, item.Objects, validatedPlatform, user, skipGitCommit, skipRestart, provider
|
|
3497
|
-
);
|
|
4118
|
+
));
|
|
3498
4119
|
pipelineInputs.push(rsuInput);
|
|
3499
4120
|
itemResults.push({
|
|
3500
4121
|
CompanyIntegrationID: item.CompanyIntegrationID,
|
|
@@ -3516,8 +4137,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3516
4137
|
return { Success: false, Message: 'No valid schema inputs to process', Items: itemResults };
|
|
3517
4138
|
}
|
|
3518
4139
|
|
|
4140
|
+
// Retrying variant, matching `IntegrationApplySchema` (which retries via
|
|
4141
|
+
// SchemaBuilder.RunSchemaPipeline). This is the same install path over the same
|
|
4142
|
+
// SchemaBuilder-generated DDL, and its expensive middle steps — ExecuteMigration,
|
|
4143
|
+
// RunCodeGen, CompileTypeScript, RestartMJAPI — fail transiently. The wrapper replays
|
|
4144
|
+
// only those four, and only while NOTHING in the batch has been applied, so a partially
|
|
4145
|
+
// succeeded batch is still reported as-is rather than double-applied.
|
|
3519
4146
|
const rsm = RuntimeSchemaManager.Instance;
|
|
3520
|
-
const batchResult = await rsm.
|
|
4147
|
+
const batchResult = await rsm.RunPipelineBatchWithRetry(pipelineInputs);
|
|
3521
4148
|
|
|
3522
4149
|
return {
|
|
3523
4150
|
Success: batchResult.SuccessCount > 0,
|
|
@@ -3555,6 +4182,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3555
4182
|
@Arg("skipRestart", { defaultValue: false }) skipRestart: boolean,
|
|
3556
4183
|
@Ctx() ctx: AppContext
|
|
3557
4184
|
): Promise<ApplyAllOutput> {
|
|
4185
|
+
return WithCatalogScope(input.CompanyIntegrationID, async () => {
|
|
3558
4186
|
try {
|
|
3559
4187
|
const user = this.getAuthenticatedUser(ctx);
|
|
3560
4188
|
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
@@ -3564,22 +4192,47 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3564
4192
|
const { connector, companyIntegration } = await this.resolveConnector(input.CompanyIntegrationID, user, provider);
|
|
3565
4193
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
3566
4194
|
|
|
3567
|
-
// Step 1b: Ensure IntegrationEngine cache
|
|
3568
|
-
//
|
|
3569
|
-
|
|
4195
|
+
// Step 1b: Ensure IntegrationEngine cache holds the CURRENT IO/IOF rows, because the
|
|
4196
|
+
// reconstruction below is entirely made of them.
|
|
4197
|
+
//
|
|
4198
|
+
// This used to be `Config(false)` — "configure if not already configured" — so an apply
|
|
4199
|
+
// in a process that had configured the engine at boot rebuilt its source schema from a
|
|
4200
|
+
// snapshot of arbitrary age, and an apply that followed an `mj sync push` (whose stored
|
|
4201
|
+
// procedures fire no BaseEntity change events, so no cache is invalidated) rebuilt it
|
|
4202
|
+
// from rows that no longer existed. The BATCH mutation has always invalidated and forced;
|
|
4203
|
+
// the single one is not entitled to a cheaper answer to the same question.
|
|
4204
|
+
await this.refreshIntegrationCatalogCaches(user, provider);
|
|
3570
4205
|
|
|
3571
4206
|
// Step 2: Reconstruct SourceSchemaInfo from the persisted IO/IOF rows
|
|
3572
4207
|
// (already freshened by the Phase 0 v5.39.x MJCompanyIntegrationEntityServer
|
|
3573
4208
|
// Save hook on IsActive false→true). Avoids the duplicate vendor-API
|
|
3574
4209
|
// introspect that used to fire here.
|
|
3575
|
-
let sourceSchema: SourceSchemaInfo = this.buildSourceSchemaFromPersistedRows(
|
|
4210
|
+
let sourceSchema: SourceSchemaInfo = this.buildSourceSchemaFromPersistedRows(
|
|
4211
|
+
companyIntegration.IntegrationID,
|
|
4212
|
+
undefined,
|
|
4213
|
+
undefined,
|
|
4214
|
+
await LoadScopedFieldsForBuild(companyIntegration.IntegrationID, user),
|
|
4215
|
+
);
|
|
3576
4216
|
if (sourceSchema.Objects.length === 0) {
|
|
3577
4217
|
// Fallback: the engine cache is empty (Save hook didn't run, or this
|
|
3578
4218
|
// is a direct-API caller bypassing the wizard). Do a one-time live
|
|
3579
4219
|
// introspect + persist + action-generation so the apply still proceeds.
|
|
3580
4220
|
LogError(`[IntegrationApplyAll] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
|
|
4221
|
+
// FILTERED, by the same decision the batch mutation uses. This fallback described the
|
|
4222
|
+
// vendor's ENTIRE surface and then threw all but the selected objects away — about 70
|
|
4223
|
+
// seconds of describes on a large Salesforce account, for a selection of two or three.
|
|
4224
|
+
// The gate is deliberately the batch's own `shouldUseFilteredIntrospection` rather than
|
|
4225
|
+
// a new rule: a connector with dozens of objects is fine describing them all, and
|
|
4226
|
+
// narrowing the describe for one that ISN'T on the filtered flow would change which
|
|
4227
|
+
// objects a legacy apply persists.
|
|
4228
|
+
const filteredNames = this.shouldUseFilteredIntrospection(connector, input.SourceObjects)
|
|
4229
|
+
? (await this.resolveSelectionPlan(input.SourceObjects, user)).map(p => p.Name)
|
|
4230
|
+
: [];
|
|
3581
4231
|
sourceSchema = await (connector.IntrospectSchema.bind(connector) as
|
|
3582
|
-
(ci: unknown, u: unknown) => Promise<SourceSchemaInfo>)(
|
|
4232
|
+
(ci: unknown, u: unknown, opts?: { ObjectNames?: string[] }) => Promise<SourceSchemaInfo>)(
|
|
4233
|
+
companyIntegration, user,
|
|
4234
|
+
filteredNames.length > 0 ? { ObjectNames: filteredNames } : undefined,
|
|
4235
|
+
);
|
|
3583
4236
|
try {
|
|
3584
4237
|
const persistResult = await IntegrationSchemaSync.PersistDiscoveredSchema({
|
|
3585
4238
|
IntegrationID: companyIntegration.IntegrationID,
|
|
@@ -3640,6 +4293,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3640
4293
|
|
|
3641
4294
|
const resolved = await this.resolveSourceObjectsToNames(input.SourceObjects, sourceSchema, user);
|
|
3642
4295
|
const resolvedNames = resolved.names;
|
|
4296
|
+
// An apply that resolved nothing builds nothing: refuse it here, before the migration,
|
|
4297
|
+
// CodeGen and the restart, which otherwise all run for a Success with 0 tables.
|
|
4298
|
+
if (input.SourceObjects.length > 0 && resolvedNames.length === 0) {
|
|
4299
|
+
return {
|
|
4300
|
+
Success: false,
|
|
4301
|
+
Message: `Apply refused: none of the ${input.SourceObjects.length} selected object(s) resolved to this ` +
|
|
4302
|
+
`connection's catalog (${resolved.droppedCount} dropped) — nothing was built. Re-open the table ` +
|
|
4303
|
+
`picker and select the tables again.`,
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
3643
4306
|
|
|
3644
4307
|
// Build SchemaPreviewObjectInput with Fields from the matching
|
|
3645
4308
|
// SourceObjectInput (resolved.sourceObjects is order-aligned with names).
|
|
@@ -3675,9 +4338,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3675
4338
|
// map existed for this connection before this apply. Explicit input always wins.
|
|
3676
4339
|
const priorMapsResult = await new RunView().RunView<{ ID: string }>({
|
|
3677
4340
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3678
|
-
ExtraFilter: `CompanyIntegrationID='${input.CompanyIntegrationID
|
|
4341
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(input.CompanyIntegrationID, 'CompanyIntegrationID')}'`,
|
|
3679
4342
|
Fields: ['ID'],
|
|
3680
4343
|
MaxRows: 1,
|
|
4344
|
+
SkipTotalRowCount: true,
|
|
3681
4345
|
ResultType: 'simple',
|
|
3682
4346
|
BypassCache: true,
|
|
3683
4347
|
}, user);
|
|
@@ -3784,7 +4448,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3784
4448
|
SyncRunID: syncRunID ?? undefined,
|
|
3785
4449
|
ScheduledJobID: scheduledJobID,
|
|
3786
4450
|
GitCommitSuccess: batchResult.Results[0]?.GitCommitSuccess,
|
|
3787
|
-
|
|
4451
|
+
// READ, not assumed — the same field the batch mutation returns. The pipeline sets
|
|
4452
|
+
// it only when the restart step actually succeeded, and `skipRestart` is a request,
|
|
4453
|
+
// not a record of what happened.
|
|
4454
|
+
APIRestarted: batchResult.Results[0]?.APIRestarted ?? false,
|
|
3788
4455
|
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
3789
4456
|
};
|
|
3790
4457
|
}
|
|
@@ -3796,13 +4463,18 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3796
4463
|
Message: `Applied ${objects.length} object(s) — entity maps will be created after restart`,
|
|
3797
4464
|
Steps: pipelineSteps,
|
|
3798
4465
|
GitCommitSuccess: batchResult.Results[0]?.GitCommitSuccess,
|
|
3799
|
-
|
|
4466
|
+
// Reaching this line means pm2 has NOT killed us yet — which is also exactly what a
|
|
4467
|
+
// FAILED restart looks like. Hardcoding `true` here reported a restart that never
|
|
4468
|
+
// happened, and the comment above conceded the guess; RunPipelineBatch sets
|
|
4469
|
+
// APIRestarted only when restartMJAPI() returned ok, so read it.
|
|
4470
|
+
APIRestarted: batchResult.Results[0]?.APIRestarted ?? false,
|
|
3800
4471
|
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
3801
4472
|
};
|
|
3802
4473
|
} catch (e) {
|
|
3803
4474
|
LogError(`IntegrationApplyAll error: ${e}`);
|
|
3804
4475
|
return { Success: false, Message: this.formatError(e) };
|
|
3805
4476
|
}
|
|
4477
|
+
});
|
|
3806
4478
|
}
|
|
3807
4479
|
|
|
3808
4480
|
/** Derives a SQL-safe schema name from the integration name (e.g., "HubSpot" → "hubspot"). */
|
|
@@ -3849,7 +4521,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3849
4521
|
const existingMapByExternalName = new Map<string, MJCompanyIntegrationEntityMapEntity>();
|
|
3850
4522
|
const allMaps = await new RunView().RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
3851
4523
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3852
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
4524
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
3853
4525
|
ResultType: 'entity_object',
|
|
3854
4526
|
BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
|
|
3855
4527
|
}, user);
|
|
@@ -3908,9 +4580,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3908
4580
|
const escapedObjectName = obj.SourceObjectName.replace(/'/g, "''");
|
|
3909
4581
|
const existingMapResult = await new RunView().RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
3910
4582
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3911
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND ExternalObjectName='${escapedObjectName}'`,
|
|
4583
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}' AND ExternalObjectName='${escapedObjectName}'`,
|
|
3912
4584
|
OrderBy: '__mj_CreatedAt ASC',
|
|
3913
4585
|
MaxRows: 1,
|
|
4586
|
+
SkipTotalRowCount: true,
|
|
3914
4587
|
ResultType: 'entity_object',
|
|
3915
4588
|
BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
|
|
3916
4589
|
}, user);
|
|
@@ -3977,7 +4650,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3977
4650
|
// already exist for this entity map, or re-applies multiply them in lockstep.
|
|
3978
4651
|
const existingFieldMaps = await new RunView().RunView<{ SourceFieldName: string }>({
|
|
3979
4652
|
EntityName: 'MJ: Company Integration Field Maps',
|
|
3980
|
-
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
4653
|
+
ExtraFilter: `EntityMapID='${sqlUuid(entityMapID, 'entityMapID')}'`,
|
|
3981
4654
|
Fields: ['SourceFieldName'],
|
|
3982
4655
|
ResultType: 'simple',
|
|
3983
4656
|
BypassCache: true, // read committed field-map state for the idempotency skip
|
|
@@ -4033,9 +4706,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4033
4706
|
const rv = new RunView();
|
|
4034
4707
|
const runResult = await rv.RunView<{ ID: string }>({
|
|
4035
4708
|
EntityName: 'MJ: Company Integration Runs',
|
|
4036
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND Status='In Progress'`,
|
|
4709
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}' AND Status='In Progress'`,
|
|
4037
4710
|
OrderBy: '__mj_CreatedAt DESC',
|
|
4038
4711
|
MaxRows: 1,
|
|
4712
|
+
SkipTotalRowCount: true,
|
|
4039
4713
|
ResultType: 'simple',
|
|
4040
4714
|
Fields: ['ID']
|
|
4041
4715
|
}, user);
|
|
@@ -4085,16 +4759,23 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4085
4759
|
const limit = IntegrationEngine.Instance.MaxColumnsPerTable;
|
|
4086
4760
|
const overLimit = objects.filter(o => colCount(o) > limit);
|
|
4087
4761
|
if (overLimit.length > 0) {
|
|
4762
|
+
// THE SHARPEST CROSS-CONNECTION COLLISION, and the clearest thing the per-connection
|
|
4763
|
+
// catalog fixes. Disabling an over-wide object used to write the SHARED row, so one
|
|
4764
|
+
// customer's column selection silently disabled that object for every other customer
|
|
4765
|
+
// on the same connector — with no error and nothing in either workspace to explain it.
|
|
4766
|
+
// Through the writer, a per-connection connection disables only its own row.
|
|
4767
|
+
const writer = BuildCatalogWriter(md, companyIntegration, user);
|
|
4088
4768
|
const ioIDByName = new Map(
|
|
4089
|
-
|
|
4769
|
+
(await writer.ObjectsInScope())
|
|
4770
|
+
.filter(io => io.Status === 'Active')
|
|
4090
4771
|
.map(io => [io.Name.toLowerCase(), io.ID] as [string, string])
|
|
4091
4772
|
);
|
|
4092
4773
|
for (const o of overLimit) {
|
|
4093
4774
|
warnings.push(`Disabled "${o.SourceObjectName}" (${colCount(o)} columns > limit ${limit}) — too wide to materialize as one table; reduce its columns or it stays disabled.`);
|
|
4094
4775
|
const ioID = ioIDByName.get(o.SourceObjectName.toLowerCase());
|
|
4095
4776
|
if (ioID) {
|
|
4096
|
-
const io = await
|
|
4097
|
-
if (
|
|
4777
|
+
const io = await writer.LoadObject(ioID);
|
|
4778
|
+
if (io) {
|
|
4098
4779
|
io.Status = 'Disabled';
|
|
4099
4780
|
if (!await io.Save()) {
|
|
4100
4781
|
LogError(`enforceSchemaLimits: failed to disable over-wide IO '${o.SourceObjectName}': ${io.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
@@ -4155,14 +4836,26 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4155
4836
|
// Declared rows this apply will silently leave out (deactivated objects/fields). Collected
|
|
4156
4837
|
// during the rebuild and surfaced on the apply's Warnings, alongside the schema-limit ones.
|
|
4157
4838
|
const inactiveWarnings: string[] = [];
|
|
4839
|
+
// Loaded once for both branches: under a per-connection catalog the engine answers [] for
|
|
4840
|
+
// this connection's fields during an apply, so a field-level warning read through it is lost.
|
|
4841
|
+
const scopedFields = await LoadScopedFieldsForBuild(companyIntegration.IntegrationID, user);
|
|
4158
4842
|
if (prefetchedSourceSchema) {
|
|
4159
4843
|
sourceSchema = prefetchedSourceSchema;
|
|
4844
|
+
// A prefetched schema skips the rebuild, and the rebuild used to be the only place these
|
|
4845
|
+
// warnings were collected — so the BATCH apply (the one caller that prefetches) lost them
|
|
4846
|
+
// twice over: once at its own rebuild, which passed no collector, and again here. They do
|
|
4847
|
+
// not depend on the rebuild: they are the catalog's own Status rows, which are exactly as
|
|
4848
|
+
// readable on this branch as on the other one.
|
|
4849
|
+
inactiveWarnings.push(...this.collectInactiveRowWarnings(
|
|
4850
|
+
companyIntegration.IntegrationID, objects.map(o => o.SourceObjectName), scopedFields,
|
|
4851
|
+
));
|
|
4160
4852
|
} else {
|
|
4161
4853
|
const requestedNamesForReuse = objects.map(o => o.SourceObjectName);
|
|
4162
4854
|
sourceSchema = this.buildSourceSchemaFromPersistedRows(
|
|
4163
4855
|
companyIntegration.IntegrationID,
|
|
4164
4856
|
requestedNamesForReuse,
|
|
4165
4857
|
inactiveWarnings,
|
|
4858
|
+
scopedFields,
|
|
4166
4859
|
);
|
|
4167
4860
|
if (sourceSchema.Objects.length === 0) {
|
|
4168
4861
|
LogError(`[buildSchemaForConnector] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
|
|
@@ -4197,7 +4890,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4197
4890
|
// Caps are operator/env guardrails (MJ_INTEGRATION_MAX_TABLES / _MAX_COLUMNS_PER_TABLE).
|
|
4198
4891
|
const limitWarnings = await this.enforceSchemaLimits(objects, filteredSchema, companyIntegration, user, provider);
|
|
4199
4892
|
|
|
4200
|
-
const
|
|
4893
|
+
const drops = NewTargetConfigDrops();
|
|
4894
|
+
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, platform, connector, drops);
|
|
4895
|
+
AssertTargetConfigs(objects, targetConfigs, companyIntegration.Integration, drops);
|
|
4201
4896
|
|
|
4202
4897
|
const input: SchemaBuilderInput = {
|
|
4203
4898
|
SourceSchema: filteredSchema,
|
|
@@ -4205,7 +4900,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4205
4900
|
Platform: platform,
|
|
4206
4901
|
MJVersion: process.env.MJ_VERSION ?? '5.11.0',
|
|
4207
4902
|
SourceType: companyIntegration.Integration,
|
|
4208
|
-
AdditionalSchemaInfoPath:
|
|
4903
|
+
AdditionalSchemaInfoPath: RuntimeSchemaManager.Instance.AdditionalSchemaInfoPath,
|
|
4209
4904
|
MigrationsDir: process.env.RSU_MIGRATIONS_PATH ?? 'migrations/rsu',
|
|
4210
4905
|
MetadataDir: process.env.RSU_METADATA_DIR ?? 'metadata',
|
|
4211
4906
|
ExistingTables: this.buildExistingTables(targetConfigs, provider),
|
|
@@ -4243,6 +4938,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4243
4938
|
@Arg("syncDirection", () => String, { nullable: true, description: 'Override sync direction: Pull | Push | Bidirectional. If omitted, each entity map\'s own SyncDirection is used.' }) syncDirection: 'Pull' | 'Push' | 'Bidirectional' | undefined,
|
|
4244
4939
|
@Ctx() ctx: AppContext
|
|
4245
4940
|
): Promise<StartSyncOutput> {
|
|
4941
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
4246
4942
|
try {
|
|
4247
4943
|
const user = this.getAuthenticatedUser(ctx);
|
|
4248
4944
|
await IntegrationEngine.Instance.Config(false, user);
|
|
@@ -4340,9 +5036,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4340
5036
|
const rv = new RunView();
|
|
4341
5037
|
const runResult = await rv.RunView<MJCompanyIntegrationRunEntity>({
|
|
4342
5038
|
EntityName: 'MJ: Company Integration Runs',
|
|
4343
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND StartedAt >= '${firedAtFilter}'`,
|
|
5039
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}' AND StartedAt >= '${firedAtFilter}'`,
|
|
4344
5040
|
OrderBy: 'StartedAt DESC',
|
|
4345
5041
|
MaxRows: 1,
|
|
5042
|
+
SkipTotalRowCount: true,
|
|
4346
5043
|
ResultType: 'simple',
|
|
4347
5044
|
Fields: ['ID', 'Status', 'StartedAt']
|
|
4348
5045
|
}, user);
|
|
@@ -4370,6 +5067,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4370
5067
|
LogError(`IntegrationStartSync error: ${e}`);
|
|
4371
5068
|
return { Success: false, Message: this.formatError(e) };
|
|
4372
5069
|
}
|
|
5070
|
+
});
|
|
4373
5071
|
}
|
|
4374
5072
|
|
|
4375
5073
|
/**
|
|
@@ -4474,8 +5172,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4474
5172
|
const rv = new RunView();
|
|
4475
5173
|
const ciResult = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
4476
5174
|
EntityName: 'MJ: Company Integrations',
|
|
4477
|
-
ExtraFilter: `ID='${companyIntegrationID}'`,
|
|
5175
|
+
ExtraFilter: `ID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
4478
5176
|
MaxRows: 1,
|
|
5177
|
+
SkipTotalRowCount: true,
|
|
4479
5178
|
ResultType: 'entity_object',
|
|
4480
5179
|
}, user);
|
|
4481
5180
|
|
|
@@ -4493,6 +5192,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4493
5192
|
EntityName: 'MJ: Integrations',
|
|
4494
5193
|
ExtraFilter: `ID='${companyIntegration.IntegrationID}'`,
|
|
4495
5194
|
MaxRows: 1,
|
|
5195
|
+
SkipTotalRowCount: true,
|
|
4496
5196
|
ResultType: 'entity_object',
|
|
4497
5197
|
}, user);
|
|
4498
5198
|
if (!integResult.Success || integResult.Results.length === 0) {
|
|
@@ -4559,6 +5259,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4559
5259
|
EntityName: 'MJ: Scheduled Job Types',
|
|
4560
5260
|
ExtraFilter: `DriverClass='${driverClass}'`,
|
|
4561
5261
|
MaxRows: 1,
|
|
5262
|
+
SkipTotalRowCount: true,
|
|
4562
5263
|
ResultType: 'simple',
|
|
4563
5264
|
Fields: ['ID']
|
|
4564
5265
|
}, user);
|
|
@@ -4623,6 +5324,102 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4623
5324
|
}
|
|
4624
5325
|
}
|
|
4625
5326
|
|
|
5327
|
+
/**
|
|
5328
|
+
* Every scheduled job that targets this connection, with its kind, for pause/resume.
|
|
5329
|
+
*
|
|
5330
|
+
* Deliberately ONE view over both driver classes rather than two calls: pause has to move
|
|
5331
|
+
* the pair together, and a partial failure that pauses the sync job but leaves the discovery
|
|
5332
|
+
* job running is the exact defect this whole change exists to remove.
|
|
5333
|
+
*
|
|
5334
|
+
* Uses the same structured `"CompanyIntegrationID":"<uuid>"` predicate as
|
|
5335
|
+
* {@link findExistingScheduledJob} — a bare-UUID substring would false-match the same id
|
|
5336
|
+
* appearing under a different config key and pause an unrelated connection's job.
|
|
5337
|
+
*/
|
|
5338
|
+
private async findScheduledJobsForConnection(
|
|
5339
|
+
companyIntegrationID: string,
|
|
5340
|
+
user: UserInfo
|
|
5341
|
+
): Promise<ScheduleJobState[]> {
|
|
5342
|
+
const rv = new RunView();
|
|
5343
|
+
const types = await rv.RunView<MJScheduledJobTypeEntity>({
|
|
5344
|
+
EntityName: 'MJ: Scheduled Job Types',
|
|
5345
|
+
ExtraFilter: `DriverClass IN ('IntegrationSyncScheduledJobDriver','IntegrationDiscoveryScheduledJobDriver')`,
|
|
5346
|
+
Fields: ['ID', 'DriverClass'],
|
|
5347
|
+
ResultType: 'simple',
|
|
5348
|
+
}, user);
|
|
5349
|
+
if (!types.Success || types.Results.length === 0) return [];
|
|
5350
|
+
|
|
5351
|
+
const kindByTypeID = new Map<string, 'sync' | 'discovery'>();
|
|
5352
|
+
for (const t of types.Results) {
|
|
5353
|
+
kindByTypeID.set(t.ID, t.DriverClass === 'IntegrationDiscoveryScheduledJobDriver' ? 'discovery' : 'sync');
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
const validCompanyIntegrationID = sqlUuid(companyIntegrationID, 'companyIntegrationID');
|
|
5357
|
+
const lower = validCompanyIntegrationID.toLowerCase();
|
|
5358
|
+
const upper = validCompanyIntegrationID.toUpperCase();
|
|
5359
|
+
const typeList = [...kindByTypeID.keys()].map(id => `'${EscapeSQLString(id)}'`).join(',');
|
|
5360
|
+
const jobs = await rv.RunView<{ ID: string; Status: string | null; JobTypeID: string }>({
|
|
5361
|
+
EntityName: 'MJ: Scheduled Jobs',
|
|
5362
|
+
ExtraFilter:
|
|
5363
|
+
`JobTypeID IN (${typeList}) AND ` +
|
|
5364
|
+
`(Configuration LIKE '%"CompanyIntegrationID":"${lower}"%' OR Configuration LIKE '%"CompanyIntegrationID":"${upper}"%')`,
|
|
5365
|
+
Fields: ['ID', 'Status', 'JobTypeID'],
|
|
5366
|
+
ResultType: 'simple',
|
|
5367
|
+
BypassCache: true, // pause must see committed status, not a stale filtered cache
|
|
5368
|
+
}, user);
|
|
5369
|
+
if (!jobs.Success) return [];
|
|
5370
|
+
return jobs.Results.map(j => ({
|
|
5371
|
+
ID: j.ID,
|
|
5372
|
+
Status: j.Status,
|
|
5373
|
+
Kind: kindByTypeID.get(j.JobTypeID) ?? 'sync',
|
|
5374
|
+
}));
|
|
5375
|
+
}
|
|
5376
|
+
|
|
5377
|
+
/**
|
|
5378
|
+
* A refusal reason when this connection is paused, or null when it is not.
|
|
5379
|
+
*
|
|
5380
|
+
* everything.txt C6 makes deactivation mean "stop touching the source". Syncs already honoured
|
|
5381
|
+
* that — the engine checks IsActive before RunSync — but DISCOVERY never did. A paused
|
|
5382
|
+
* connection could still be re-scanned by hand or by a cron that outlived the pause, which
|
|
5383
|
+
* spends the customer's rate budget and rewrites the very catalog the pause was meant to
|
|
5384
|
+
* freeze. Pausing the schedules stops the timer; this stops everything else.
|
|
5385
|
+
*
|
|
5386
|
+
* Fails OPEN: a connection we cannot read is not declared paused. The gate exists to honour an
|
|
5387
|
+
* explicit pause, not to become a new way for discovery to be unavailable.
|
|
5388
|
+
*/
|
|
5389
|
+
private async describeIfPaused(
|
|
5390
|
+
companyIntegrationID: string,
|
|
5391
|
+
user: UserInfo,
|
|
5392
|
+
md: IMetadataProvider
|
|
5393
|
+
): Promise<string | null> {
|
|
5394
|
+
try {
|
|
5395
|
+
const ci = await md.GetEntityObject<MJCompanyIntegrationEntity>('MJ: Company Integrations', user);
|
|
5396
|
+
if (!await ci.InnerLoad(CompositeKey.FromID(companyIntegrationID))) return null;
|
|
5397
|
+
if (ci.IsActive) return null;
|
|
5398
|
+
return 'This connection is paused. Resume it first — a paused connection is not scanned, so that its saved schema stays as it was.';
|
|
5399
|
+
} catch (e) {
|
|
5400
|
+
LogError(`describeIfPaused(${companyIntegrationID}) failed, treating as active: ${e}`);
|
|
5401
|
+
return null;
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
/** Moves one scheduled job to a status. Returns false on any failure; the caller reports honestly. */
|
|
5406
|
+
private async setScheduledJobStatus(
|
|
5407
|
+
scheduledJobID: string,
|
|
5408
|
+
status: 'Active' | 'Paused',
|
|
5409
|
+
user: UserInfo,
|
|
5410
|
+
md: IMetadataProvider
|
|
5411
|
+
): Promise<boolean> {
|
|
5412
|
+
try {
|
|
5413
|
+
const job = await md.GetEntityObject<MJScheduledJobEntity>('MJ: Scheduled Jobs', user);
|
|
5414
|
+
if (!await job.InnerLoad(CompositeKey.FromID(scheduledJobID))) return false;
|
|
5415
|
+
job.Status = status;
|
|
5416
|
+
return await job.Save();
|
|
5417
|
+
} catch (e) {
|
|
5418
|
+
LogError(`setScheduledJobStatus(${scheduledJobID}, ${status}) failed: ${e}`);
|
|
5419
|
+
return false;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
|
|
4626
5423
|
/**
|
|
4627
5424
|
* U7 — server-side schedule uniqueness: finds an existing (non-deleted) scheduled job of the
|
|
4628
5425
|
* given job type that targets the given CompanyIntegration (its Configuration JSON carries
|
|
@@ -4634,13 +5431,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4634
5431
|
companyIntegrationID: string,
|
|
4635
5432
|
user: UserInfo
|
|
4636
5433
|
): Promise<{ ID: string } | null> {
|
|
4637
|
-
const
|
|
4638
|
-
const
|
|
4639
|
-
const
|
|
5434
|
+
const validJobTypeID = sqlUuid(jobTypeID, 'jobTypeID');
|
|
5435
|
+
const validCompanyIntegrationID = sqlUuid(companyIntegrationID, 'companyIntegrationID');
|
|
5436
|
+
const lower = validCompanyIntegrationID.toLowerCase();
|
|
5437
|
+
const upper = validCompanyIntegrationID.toUpperCase();
|
|
4640
5438
|
const result = await new RunView().RunView<{ ID: string }>({
|
|
4641
5439
|
EntityName: 'MJ: Scheduled Jobs',
|
|
4642
5440
|
ExtraFilter:
|
|
4643
|
-
`JobTypeID='${
|
|
5441
|
+
`JobTypeID='${validJobTypeID}' AND Status IN ('Active','Paused') AND ` +
|
|
4644
5442
|
// Structured "CompanyIntegrationID":"<uuid>" predicate (same form as IntegrationListSchedules),
|
|
4645
5443
|
// NOT a bare-UUID substring — so this CI's id can't false-match the same UUID appearing in
|
|
4646
5444
|
// another config key (e.g. RelatedCompanyIntegrationID) and repurpose an unrelated job.
|
|
@@ -4648,6 +5446,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4648
5446
|
OrderBy: '__mj_CreatedAt DESC',
|
|
4649
5447
|
Fields: ['ID'],
|
|
4650
5448
|
MaxRows: 1,
|
|
5449
|
+
SkipTotalRowCount: true,
|
|
4651
5450
|
ResultType: 'simple',
|
|
4652
5451
|
BypassCache: true, // uniqueness must see committed jobs, not a stale filtered cache
|
|
4653
5452
|
}, user);
|
|
@@ -4736,7 +5535,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4736
5535
|
// Clear ScheduledJobID on any CompanyIntegration that references this job
|
|
4737
5536
|
const ciResult = await rv.RunView<MJCompanyIntegrationEntity>({
|
|
4738
5537
|
EntityName: 'MJ: Company Integrations',
|
|
4739
|
-
ExtraFilter: `ScheduledJobID='${scheduledJobID}'`,
|
|
5538
|
+
ExtraFilter: `ScheduledJobID='${sqlUuid(scheduledJobID, 'scheduledJobID')}'`,
|
|
4740
5539
|
ResultType: 'entity_object',
|
|
4741
5540
|
}, sysUser);
|
|
4742
5541
|
if (ciResult.Success) {
|
|
@@ -4751,7 +5550,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4751
5550
|
// Null out ScheduledJobRunID on CompanyIntegrationRuns that reference this job's runs
|
|
4752
5551
|
const jobRunsResult = await rv.RunView<MJScheduledJobRunEntity>({
|
|
4753
5552
|
EntityName: 'MJ: Scheduled Job Runs',
|
|
4754
|
-
ExtraFilter: `ScheduledJobID='${scheduledJobID}'`,
|
|
5553
|
+
ExtraFilter: `ScheduledJobID='${sqlUuid(scheduledJobID, 'scheduledJobID')}'`,
|
|
4755
5554
|
ResultType: 'entity_object',
|
|
4756
5555
|
}, sysUser);
|
|
4757
5556
|
if (jobRunsResult.Success) {
|
|
@@ -4807,7 +5606,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4807
5606
|
const rv = new RunView();
|
|
4808
5607
|
const result = await rv.RunView<{ ID: string; Name: string; Status: string; CronExpression: string; Timezone: string; NextRunAt: string; LastRunAt: string; RunCount: number; SuccessCount: number; FailureCount: number }>({
|
|
4809
5608
|
EntityName: 'MJ: Scheduled Jobs',
|
|
4810
|
-
ExtraFilter: `Configuration LIKE '%"CompanyIntegrationID":"${companyIntegrationID}"%'`,
|
|
5609
|
+
ExtraFilter: `Configuration LIKE '%"CompanyIntegrationID":"${sqlUuid(companyIntegrationID, 'companyIntegrationID')}"%'`,
|
|
4811
5610
|
OrderBy: '__mj_CreatedAt DESC',
|
|
4812
5611
|
ResultType: 'simple',
|
|
4813
5612
|
Fields: ['ID', 'Name', 'Status', 'CronExpression', 'Timezone', 'NextRunAt', 'LastRunAt', 'RunCount', 'SuccessCount', 'FailureCount']
|
|
@@ -4848,7 +5647,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4848
5647
|
const rv = new RunView();
|
|
4849
5648
|
const result = await rv.RunView<EntityMapSummaryOutput>({
|
|
4850
5649
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
4851
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
5650
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
4852
5651
|
OrderBy: 'Priority ASC',
|
|
4853
5652
|
ResultType: 'simple',
|
|
4854
5653
|
Fields: ['ID', 'EntityID', 'Entity', 'ExternalObjectName', 'SyncDirection', 'Priority', 'Status', 'Configuration'],
|
|
@@ -4877,7 +5676,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4877
5676
|
const rv = new RunView();
|
|
4878
5677
|
const result = await rv.RunView<FieldMapSummaryOutput>({
|
|
4879
5678
|
EntityName: 'MJ: Company Integration Field Maps',
|
|
4880
|
-
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
5679
|
+
ExtraFilter: `EntityMapID='${sqlUuid(entityMapID, 'entityMapID')}'`,
|
|
4881
5680
|
OrderBy: 'SourceFieldName',
|
|
4882
5681
|
ResultType: 'simple',
|
|
4883
5682
|
BypassCache: true, // operational list must reflect COMMITTED state
|
|
@@ -4960,7 +5759,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4960
5759
|
// Delete field maps
|
|
4961
5760
|
const fmResult = await rv.RunView<MJCompanyIntegrationFieldMapEntity>({
|
|
4962
5761
|
EntityName: 'MJ: Company Integration Field Maps',
|
|
4963
|
-
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
5762
|
+
ExtraFilter: `EntityMapID='${sqlUuid(entityMapID, 'entityMapID')}'`,
|
|
4964
5763
|
ResultType: 'entity_object'
|
|
4965
5764
|
}, sysUser);
|
|
4966
5765
|
if (fmResult.Success) {
|
|
@@ -4970,7 +5769,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4970
5769
|
// Delete watermarks
|
|
4971
5770
|
const wmResult = await rv.RunView<MJCompanyIntegrationSyncWatermarkEntity>({
|
|
4972
5771
|
EntityName: 'MJ: Company Integration Sync Watermarks',
|
|
4973
|
-
ExtraFilter: `EntityMapID='${entityMapID}'`,
|
|
5772
|
+
ExtraFilter: `EntityMapID='${sqlUuid(entityMapID, 'entityMapID')}'`,
|
|
4974
5773
|
ResultType: 'entity_object'
|
|
4975
5774
|
}, sysUser);
|
|
4976
5775
|
if (wmResult.Success) {
|
|
@@ -5056,6 +5855,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5056
5855
|
RecordsCreated: syncProgress.RecordsCreated,
|
|
5057
5856
|
RecordsUpdated: syncProgress.RecordsUpdated,
|
|
5058
5857
|
RecordsErrored: syncProgress.RecordsErrored,
|
|
5858
|
+
RecordsSkipped: syncProgress.RecordsSkipped,
|
|
5059
5859
|
StartedAt: syncProgress.StartedAt.toISOString(),
|
|
5060
5860
|
ElapsedMs: Date.now() - syncProgress.StartedAt.getTime(),
|
|
5061
5861
|
};
|
|
@@ -5084,7 +5884,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5084
5884
|
const [mapsResult, runsResult] = await rv.RunViews([
|
|
5085
5885
|
{
|
|
5086
5886
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
5087
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
5887
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5088
5888
|
ResultType: 'simple',
|
|
5089
5889
|
Fields: ['ID', 'Status'],
|
|
5090
5890
|
// Status must reflect the live state immediately after a deselect/reselect
|
|
@@ -5094,9 +5894,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5094
5894
|
},
|
|
5095
5895
|
{
|
|
5096
5896
|
EntityName: 'MJ: Company Integration Runs',
|
|
5097
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
5897
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5098
5898
|
OrderBy: 'StartedAt DESC',
|
|
5099
5899
|
MaxRows: 1,
|
|
5900
|
+
SkipTotalRowCount: true,
|
|
5100
5901
|
ResultType: 'simple',
|
|
5101
5902
|
Fields: ['ID', 'Status', 'StartedAt', 'EndedAt', 'TotalRecords'],
|
|
5102
5903
|
BypassCache: true
|
|
@@ -5135,29 +5936,221 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5135
5936
|
}
|
|
5136
5937
|
}
|
|
5137
5938
|
|
|
5939
|
+
/**
|
|
5940
|
+
* Paged run history, newest first.
|
|
5941
|
+
*
|
|
5942
|
+
* plan.md: "history must show a paginated list with all runs (25 per page) in the history, it
|
|
5943
|
+
* should not shwo just aprt of it". Both surfaces used to fake paging by re-fetching with a
|
|
5944
|
+
* DOUBLED limit and slicing client-side, which cannot reach past the cap and re-transfers
|
|
5945
|
+
* everything already shown.
|
|
5946
|
+
*
|
|
5947
|
+
* `offset` maps to RunView's StartRow. TotalKnown comes from a separate count so a pager can
|
|
5948
|
+
* size itself without fetching every row.
|
|
5949
|
+
*/
|
|
5950
|
+
/**
|
|
5951
|
+
* Everything running for a connection, in one call.
|
|
5952
|
+
*
|
|
5953
|
+
* plan.md: "In the UI for the integration ... there shouldbe an area that shows thigns that are
|
|
5954
|
+
* running actively, this can be syncs, rsu, discovery, etc." and "Let us say i clear browser
|
|
5955
|
+
* cache and then i go to that integration page, i should see the same steps i mentioned
|
|
5956
|
+
* hapepning there to". Every field here is derived SERVER-side, so a cleared browser sees the
|
|
5957
|
+
* same thing — nothing depends on client state.
|
|
5958
|
+
*
|
|
5959
|
+
* Previously a surface had to fan out to four queries and stitch them, and each got the
|
|
5960
|
+
* attribution wrong in its own way. The two rules that fan-out kept breaking are encoded here:
|
|
5961
|
+
*
|
|
5962
|
+
* - RSU status from GetStatus is PROCESS-WIDE. It is only attributed to this connection when
|
|
5963
|
+
* an RSU run artifact actually names it; otherwise it is reported Scope='workspace'.
|
|
5964
|
+
* - A ConnectorCreation run is discovery, never a sync. Narrating it as a sync is what made a
|
|
5965
|
+
* first-time setup claim it was syncing records it had not fetched.
|
|
5966
|
+
*/
|
|
5967
|
+
@Query(() => ActiveOperationsOutput)
|
|
5968
|
+
async IntegrationGetActiveOperations(
|
|
5969
|
+
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
5970
|
+
@Ctx() ctx: AppContext
|
|
5971
|
+
): Promise<ActiveOperationsOutput> {
|
|
5972
|
+
try {
|
|
5973
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
5974
|
+
const authCache = new Map<string, boolean>();
|
|
5975
|
+
if (!(await this.userCanReadCompanyIntegration(companyIntegrationID, user, authCache))) {
|
|
5976
|
+
return { Success: false, Message: this.notAuthorizedForCompanyIntegrationMessage(companyIntegrationID) };
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5979
|
+
const ops: ActiveOperationOutput[] = [];
|
|
5980
|
+
|
|
5981
|
+
// ── the sync, from the DURABLE snapshot so any process can answer ──
|
|
5982
|
+
// GetSyncProgressAsync prefers the durable run-row snapshot and falls back to this
|
|
5983
|
+
// process's own — necessary because the ProgressJSON column arrives in MJ 6.1.x and
|
|
5984
|
+
// every tenant here is 5.51, where the durable read silently finds nothing.
|
|
5985
|
+
const syncProgress = await IntegrationEngine.GetSyncProgressAsync(companyIntegrationID, user);
|
|
5986
|
+
if (syncProgress) {
|
|
5987
|
+
ops.push({
|
|
5988
|
+
Kind: 'sync',
|
|
5989
|
+
Scope: 'connection',
|
|
5990
|
+
Label: `Syncing ${syncProgress.CurrentEntity || 'data'}`,
|
|
5991
|
+
StartedAt: syncProgress.StartedAt,
|
|
5992
|
+
StepLabel: syncProgress.CurrentEntity || undefined,
|
|
5993
|
+
StepIndex: syncProgress.EntityMapsCompleted,
|
|
5994
|
+
StepTotal: syncProgress.EntityMapsTotal,
|
|
5995
|
+
RecordsCreated: syncProgress.RecordsCreated,
|
|
5996
|
+
RecordsUpdated: syncProgress.RecordsUpdated,
|
|
5997
|
+
RecordsSkipped: syncProgress.RecordsSkipped,
|
|
5998
|
+
RecordsErrored: syncProgress.RecordsErrored,
|
|
5999
|
+
Cancellable: true,
|
|
6000
|
+
});
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
// ── in-flight run artifacts for THIS connection ──
|
|
6004
|
+
const reader = new IntegrationProgressReader();
|
|
6005
|
+
const inFlight = await reader.ListRuns({ companyIntegrationID, inFlightOnly: true }, 25);
|
|
6006
|
+
let rsuNamesThisConnection = false;
|
|
6007
|
+
for (const snap of inFlight) {
|
|
6008
|
+
const kind = snap.manifest.runKind;
|
|
6009
|
+
// 'SyncRun' is already reported above from the durable snapshot. Everything else
|
|
6010
|
+
// that is not RSU is a discovery-shaped run - ConnectorCreation especially, which
|
|
6011
|
+
// must NEVER be narrated as a sync: it fetches nothing.
|
|
6012
|
+
if (kind === 'SyncRun') continue;
|
|
6013
|
+
if (kind === 'RSU') rsuNamesThisConnection = true;
|
|
6014
|
+
ops.push({
|
|
6015
|
+
Kind: kind === 'RSU' ? 'rsu' : 'discovery',
|
|
6016
|
+
Scope: 'connection',
|
|
6017
|
+
Label: kind === 'RSU' ? 'Updating your workspace schema' : 'Discovering tables and columns',
|
|
6018
|
+
RunID: snap.manifest.runID,
|
|
6019
|
+
StartedAt: new Date(snap.manifest.startedAt),
|
|
6020
|
+
StepLabel: snap.latestEvent?.message ?? undefined,
|
|
6021
|
+
Cancellable: false,
|
|
6022
|
+
});
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
// ── RSU reported by the process, when no artifact tied it to this connection ──
|
|
6026
|
+
const rsu = RuntimeSchemaManager.Instance.GetStatus();
|
|
6027
|
+
if (rsu?.Running && !rsuNamesThisConnection) {
|
|
6028
|
+
ops.push({
|
|
6029
|
+
Kind: 'rsu',
|
|
6030
|
+
Scope: 'workspace',
|
|
6031
|
+
Label: 'Your workspace is updating',
|
|
6032
|
+
StepLabel: rsu.CurrentStepName ?? undefined,
|
|
6033
|
+
StepIndex: rsu.CurrentStepIndex ?? undefined,
|
|
6034
|
+
StepTotal: rsu.StepTotal ?? undefined,
|
|
6035
|
+
Cancellable: false,
|
|
6036
|
+
});
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
const lock = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
6040
|
+
return {
|
|
6041
|
+
Success: true,
|
|
6042
|
+
Message: `${ops.length} active operation(s)`,
|
|
6043
|
+
Operations: ops,
|
|
6044
|
+
MaintenanceLockReason: lock?.Reason,
|
|
6045
|
+
};
|
|
6046
|
+
} catch (e) {
|
|
6047
|
+
LogError(`IntegrationGetActiveOperations error: ${e}`);
|
|
6048
|
+
return { Success: false, Message: this.formatError(e) };
|
|
6049
|
+
}
|
|
6050
|
+
}
|
|
6051
|
+
|
|
6052
|
+
/**
|
|
6053
|
+
* What the tenant can see about its own resource headroom.
|
|
6054
|
+
*
|
|
6055
|
+
* plan.md line 158: "what happens if we run out of storage when we sync, how do we alert the
|
|
6056
|
+
* user, OOM (MJC should handle)". The tenant MEASURES - it is the process that fills the heap
|
|
6057
|
+
* and the disk - and the control plane decides. Before this there was no measurement at all,
|
|
6058
|
+
* so a sync that died of OOM or a full disk did so with no warning ahead of it and no
|
|
6059
|
+
* explanation after.
|
|
6060
|
+
*
|
|
6061
|
+
* Deliberately not authorized per connection: this is a property of the workspace, not of any
|
|
6062
|
+
* one connector, and the control plane polls it to decide admission.
|
|
6063
|
+
*/
|
|
6064
|
+
@Query(() => ResourcePressureOutput)
|
|
6065
|
+
@RequireSystemUser()
|
|
6066
|
+
async IntegrationGetResourcePressure(
|
|
6067
|
+
@Ctx() _ctx: AppContext
|
|
6068
|
+
): Promise<ResourcePressureOutput> {
|
|
6069
|
+
try {
|
|
6070
|
+
let runDirCount: number | null = null;
|
|
6071
|
+
try {
|
|
6072
|
+
runDirCount = (await new IntegrationProgressReader().ListRuns({}, 100000)).length;
|
|
6073
|
+
} catch { /* the artifact dir may not exist yet on a fresh workspace */ }
|
|
6074
|
+
|
|
6075
|
+
const reading = await ReadResourcePressure({ runDirCount });
|
|
6076
|
+
const findings = EvaluatePressure(reading);
|
|
6077
|
+
const mb = (b: number | null) => (b === null ? undefined : Math.round(b / (1024 * 1024)));
|
|
6078
|
+
return {
|
|
6079
|
+
Success: true,
|
|
6080
|
+
Message: findings.length === 0 ? 'No resource pressure' : findings[0].Message,
|
|
6081
|
+
HeapUsedFraction: reading.HeapUsedFraction,
|
|
6082
|
+
HeapUsedMB: mb(reading.HeapUsedBytes),
|
|
6083
|
+
HeapLimitMB: mb(reading.HeapLimitBytes),
|
|
6084
|
+
ResidentMB: mb(reading.ResidentBytes),
|
|
6085
|
+
ResidentFraction: reading.ResidentFraction ?? undefined,
|
|
6086
|
+
HostMemTotalMB: mb(reading.HostMemTotalBytes),
|
|
6087
|
+
HostMemAvailableMB: mb(reading.HostMemAvailableBytes),
|
|
6088
|
+
ArtifactDiskFreeMB: mb(reading.ArtifactDiskFreeBytes),
|
|
6089
|
+
WorkDirFreeMB: mb(reading.WorkDirFreeBytes),
|
|
6090
|
+
ActiveSyncCount: reading.ActiveSyncCount,
|
|
6091
|
+
Findings: findings.map(f => ({ Code: f.Code, Message: f.Message })),
|
|
6092
|
+
};
|
|
6093
|
+
} catch (e) {
|
|
6094
|
+
LogError(`IntegrationGetResourcePressure error: ${e}`);
|
|
6095
|
+
return { Success: false, Message: this.formatError(e) };
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5138
6099
|
@Query(() => SyncHistoryOutput)
|
|
5139
6100
|
async IntegrationGetSyncHistory(
|
|
5140
6101
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
5141
|
-
@Arg("limit", { defaultValue:
|
|
6102
|
+
@Arg("limit", { defaultValue: 25 }) limit: number,
|
|
6103
|
+
@Arg("offset", { nullable: true, defaultValue: 0, description: 'Rows to skip. With limit, this is the page.' }) offset: number,
|
|
5142
6104
|
@Ctx() ctx: AppContext
|
|
5143
6105
|
): Promise<SyncHistoryOutput> {
|
|
5144
6106
|
try {
|
|
5145
6107
|
const user = this.getAuthenticatedUser(ctx);
|
|
6108
|
+
const filter = `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`;
|
|
5146
6109
|
const rv = new RunView();
|
|
5147
6110
|
const result = await rv.RunView<SyncRunSummaryOutput>({
|
|
5148
6111
|
EntityName: 'MJ: Company Integration Runs',
|
|
5149
|
-
ExtraFilter:
|
|
6112
|
+
ExtraFilter: filter,
|
|
5150
6113
|
OrderBy: 'StartedAt DESC',
|
|
5151
6114
|
MaxRows: limit,
|
|
6115
|
+
StartRow: Math.max(0, offset ?? 0),
|
|
5152
6116
|
ResultType: 'simple',
|
|
5153
6117
|
Fields: ['ID', 'Status', 'StartedAt', 'EndedAt', 'TotalRecords', 'RunByUserID']
|
|
5154
6118
|
}, user);
|
|
5155
6119
|
|
|
5156
6120
|
if (!result.Success) return { Success: false, Message: result.ErrorMessage || 'Query failed' };
|
|
6121
|
+
|
|
6122
|
+
// A separate count, not result.TotalRowCount: the row query is capped by MaxRows, so its
|
|
6123
|
+
// count reflects the PAGE. A pager needs the whole set.
|
|
6124
|
+
let totalKnown: number | undefined;
|
|
6125
|
+
const countRes = await new RunView().RunView({
|
|
6126
|
+
EntityName: 'MJ: Company Integration Runs',
|
|
6127
|
+
ExtraFilter: filter,
|
|
6128
|
+
ResultType: 'count_only',
|
|
6129
|
+
}, user);
|
|
6130
|
+
if (countRes?.Success) totalKnown = countRes.TotalRowCount;
|
|
6131
|
+
|
|
6132
|
+
// Per-entity created / updated / skipped / errored, recovered from each run's artifact.
|
|
6133
|
+
// The run ROW carries only TotalRecords, so EntityDetails existed on this type and was
|
|
6134
|
+
// never populated — the empty breakdown thing.txt reports. Artifacts are pruned by the
|
|
6135
|
+
// retention cap and are node-local, so an old run yields nothing; that is left ABSENT
|
|
6136
|
+
// rather than zeroed, so the UI can say "not recorded" instead of claiming zero.
|
|
6137
|
+
const reader = new IntegrationProgressReader();
|
|
6138
|
+
await Promise.all(result.Results.map(async (run) => {
|
|
6139
|
+
try {
|
|
6140
|
+
const outcomes = await reader.EntityOutcomes(run.ID);
|
|
6141
|
+
if (outcomes.length > 0) run.EntityDetails = outcomes;
|
|
6142
|
+
} catch { /* a missing or pruned artifact is not an error */ }
|
|
6143
|
+
}));
|
|
6144
|
+
|
|
6145
|
+
const start = Math.max(0, offset ?? 0);
|
|
5157
6146
|
return {
|
|
5158
6147
|
Success: true,
|
|
5159
6148
|
Message: `${result.Results.length} runs`,
|
|
5160
|
-
Runs: result.Results
|
|
6149
|
+
Runs: result.Results,
|
|
6150
|
+
TotalKnown: totalKnown,
|
|
6151
|
+
HasMore: totalKnown === undefined
|
|
6152
|
+
? result.Results.length === limit
|
|
6153
|
+
: start + result.Results.length < totalKnown,
|
|
5161
6154
|
};
|
|
5162
6155
|
} catch (e) {
|
|
5163
6156
|
LogError(`IntegrationGetSyncHistory error: ${e}`);
|
|
@@ -5180,6 +6173,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5180
6173
|
@Arg("runKind", { nullable: true }) runKind?: string,
|
|
5181
6174
|
@Arg("inFlightOnly", { nullable: true }) inFlightOnly?: boolean,
|
|
5182
6175
|
@Arg("limit", { defaultValue: 50 }) limit?: number,
|
|
6176
|
+
@Arg("offset", { nullable: true, defaultValue: 0, description: 'Runs to skip in the underlying store. Use NextOffset from the previous page.' }) offset?: number,
|
|
5183
6177
|
): Promise<IntegrationListRunsOutput> {
|
|
5184
6178
|
try {
|
|
5185
6179
|
const user = this.getAuthenticatedUser(ctx);
|
|
@@ -5195,18 +6189,43 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5195
6189
|
}
|
|
5196
6190
|
|
|
5197
6191
|
const reader = new IntegrationProgressReader();
|
|
5198
|
-
const
|
|
6192
|
+
const want = limit ?? 50;
|
|
6193
|
+
const filter = {
|
|
5199
6194
|
companyIntegrationID,
|
|
5200
6195
|
runKind: runKind as IntegrationRunKind | undefined,
|
|
5201
6196
|
inFlightOnly: inFlightOnly ?? false,
|
|
5202
|
-
}
|
|
5203
|
-
|
|
5204
|
-
//
|
|
5205
|
-
//
|
|
5206
|
-
//
|
|
5207
|
-
//
|
|
5208
|
-
|
|
5209
|
-
|
|
6197
|
+
};
|
|
6198
|
+
|
|
6199
|
+
// AUTHORIZE-THEN-PAGE. Authorization happens after the store returns, so a page can
|
|
6200
|
+
// come back short because the caller could not read some of it — not because the store
|
|
6201
|
+
// ran out. Returning that short page as if it were the end is what made "load older
|
|
6202
|
+
// runs" stop early. So refill from the next offset until the page is full or the store
|
|
6203
|
+
// is genuinely exhausted.
|
|
6204
|
+
//
|
|
6205
|
+
// The refill is BOUNDED: an unauthorized caller would otherwise walk the entire store
|
|
6206
|
+
// one page at a time on a single request.
|
|
6207
|
+
const MAX_REFILLS = 5;
|
|
6208
|
+
let cursor = Math.max(0, offset ?? 0);
|
|
6209
|
+
let exhausted = false;
|
|
6210
|
+
const authorizedSnaps: IntegrationRunSnapshot[] = [];
|
|
6211
|
+
for (let round = 0; round <= MAX_REFILLS && authorizedSnaps.length < want; round++) {
|
|
6212
|
+
const batch = await reader.ListRuns(filter, want, cursor);
|
|
6213
|
+
if (batch.length === 0) { exhausted = true; break; }
|
|
6214
|
+
cursor += batch.length;
|
|
6215
|
+
if (batch.length < want) exhausted = true;
|
|
6216
|
+
const ok = await this.filterAuthorizedRuns(batch, user, authCache);
|
|
6217
|
+
authorizedSnaps.push(...ok);
|
|
6218
|
+
if (exhausted) break;
|
|
6219
|
+
}
|
|
6220
|
+
const page = authorizedSnaps.slice(0, want);
|
|
6221
|
+
const hasMore = !exhausted || authorizedSnaps.length > want;
|
|
6222
|
+
return {
|
|
6223
|
+
Success: true,
|
|
6224
|
+
Message: `${page.length} run(s)`,
|
|
6225
|
+
Runs: page.map(s => this.toRunSummaryArtifact(s)),
|
|
6226
|
+
NextOffset: hasMore ? cursor : undefined,
|
|
6227
|
+
HasMore: hasMore,
|
|
6228
|
+
};
|
|
5210
6229
|
} catch (e) {
|
|
5211
6230
|
LogError(`IntegrationListRuns error: ${e}`);
|
|
5212
6231
|
return { Success: false, Message: this.formatError(e) };
|
|
@@ -5243,14 +6262,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5243
6262
|
const snap = await reader.GetRun(runID);
|
|
5244
6263
|
if (!snap) return { Success: false, Message: `Run '${runID}' not found` };
|
|
5245
6264
|
|
|
5246
|
-
const
|
|
5247
|
-
if (!
|
|
5248
|
-
const ciID = snap.manifest.companyIntegrationID;
|
|
6265
|
+
const auth = await this.authorizeRunArtifact(snap, user, new Map<string, boolean>());
|
|
6266
|
+
if (!auth.Authorized) {
|
|
5249
6267
|
return {
|
|
5250
6268
|
Success: false,
|
|
5251
|
-
Message:
|
|
5252
|
-
? this.notAuthorizedForCompanyIntegrationMessage(ciID)
|
|
5253
|
-
: `Not authorized to access run '${runID}'`,
|
|
6269
|
+
Message: this.notAuthorizedForRunMessage(runID, auth.DeniedCompanyIntegrationID),
|
|
5254
6270
|
};
|
|
5255
6271
|
}
|
|
5256
6272
|
|
|
@@ -5284,14 +6300,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5284
6300
|
const snap = await reader.GetRun(runID);
|
|
5285
6301
|
if (!snap) return { Success: false, Message: `Run '${runID}' not found`, LatestSeq: sinceSeq ?? 0, IsInFlight: false };
|
|
5286
6302
|
|
|
5287
|
-
const
|
|
5288
|
-
if (!
|
|
5289
|
-
const ciID = snap.manifest.companyIntegrationID;
|
|
6303
|
+
const auth = await this.authorizeRunArtifact(snap, user, new Map<string, boolean>());
|
|
6304
|
+
if (!auth.Authorized) {
|
|
5290
6305
|
return {
|
|
5291
6306
|
Success: false,
|
|
5292
|
-
Message:
|
|
5293
|
-
? this.notAuthorizedForCompanyIntegrationMessage(ciID)
|
|
5294
|
-
: `Not authorized to access run '${runID}'`,
|
|
6307
|
+
Message: this.notAuthorizedForRunMessage(runID, auth.DeniedCompanyIntegrationID),
|
|
5295
6308
|
LatestSeq: sinceSeq ?? 0,
|
|
5296
6309
|
IsInFlight: false,
|
|
5297
6310
|
};
|
|
@@ -5329,6 +6342,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5329
6342
|
RunKind: s.manifest.runKind,
|
|
5330
6343
|
IntegrationID: s.manifest.integrationID,
|
|
5331
6344
|
CompanyIntegrationID: s.manifest.companyIntegrationID,
|
|
6345
|
+
CompanyIntegrationIDs: s.manifest.companyIntegrationIDs,
|
|
5332
6346
|
ObjectName: s.manifest.objectName,
|
|
5333
6347
|
TriggerType: s.manifest.triggerType,
|
|
5334
6348
|
StartedAt: s.manifest.startedAt,
|
|
@@ -5360,6 +6374,49 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5360
6374
|
|
|
5361
6375
|
// ── CONNECTOR CAPABILITIES ──────────────────────────────────────────
|
|
5362
6376
|
|
|
6377
|
+
/**
|
|
6378
|
+
* Returns the settings the connector bound to a CompanyIntegration declares in its
|
|
6379
|
+
* `SettingsSchema` — key, label, group, type, default, bounds and tier — so a client can
|
|
6380
|
+
* render the connection's settings form without knowing the connector. A connector that
|
|
6381
|
+
* declares none returns an empty list.
|
|
6382
|
+
*/
|
|
6383
|
+
@Query(() => ConnectorSettingsSchemaOutput)
|
|
6384
|
+
async IntegrationGetConnectorSettingsSchema(
|
|
6385
|
+
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
6386
|
+
@Ctx() ctx: AppContext
|
|
6387
|
+
): Promise<ConnectorSettingsSchemaOutput> {
|
|
6388
|
+
try {
|
|
6389
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
6390
|
+
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
6391
|
+
const { connector } = await this.resolveConnector(companyIntegrationID, user, provider);
|
|
6392
|
+
const schema = connector.SettingsSchema ?? [];
|
|
6393
|
+
return {
|
|
6394
|
+
Success: true,
|
|
6395
|
+
Message: `${schema.length} setting(s)`,
|
|
6396
|
+
Settings: schema.map(s => ({
|
|
6397
|
+
Key: s.Key,
|
|
6398
|
+
Label: s.Label,
|
|
6399
|
+
Group: s.Group,
|
|
6400
|
+
Type: s.Type,
|
|
6401
|
+
Options: s.Options,
|
|
6402
|
+
// Serialized because the value is genuinely polymorphic per setting and a
|
|
6403
|
+
// typed union here would be a schema change every time one is added. The
|
|
6404
|
+
// renderer already knows the Type, so it knows how to read this.
|
|
6405
|
+
DefaultJSON: s.Default === undefined ? undefined : JSON.stringify(s.Default),
|
|
6406
|
+
Min: s.Min,
|
|
6407
|
+
Max: s.Max,
|
|
6408
|
+
Tier: s.Tier,
|
|
6409
|
+
AppliesTo: s.AppliesTo,
|
|
6410
|
+
Description: s.Description,
|
|
6411
|
+
Sensitive: s.Sensitive,
|
|
6412
|
+
})),
|
|
6413
|
+
};
|
|
6414
|
+
} catch (e) {
|
|
6415
|
+
LogError(`IntegrationGetConnectorSettingsSchema error: ${e}`);
|
|
6416
|
+
return { Success: false, Message: this.formatError(e), Settings: [] };
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
|
|
5363
6420
|
/**
|
|
5364
6421
|
* Returns the CRUD capability flags for the connector bound to a CompanyIntegration.
|
|
5365
6422
|
* Use this to determine which operations (Create/Update/Delete/Search) are supported
|
|
@@ -5370,6 +6427,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5370
6427
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
5371
6428
|
@Ctx() ctx: AppContext
|
|
5372
6429
|
): Promise<ConnectorCapabilitiesOutput> {
|
|
6430
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
5373
6431
|
try {
|
|
5374
6432
|
const user = this.getAuthenticatedUser(ctx);
|
|
5375
6433
|
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
@@ -5388,6 +6446,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5388
6446
|
LogError(`IntegrationGetConnectorCapabilities error: ${e}`);
|
|
5389
6447
|
return { Success: false, Message: this.formatError(e) };
|
|
5390
6448
|
}
|
|
6449
|
+
});
|
|
5391
6450
|
}
|
|
5392
6451
|
|
|
5393
6452
|
// ── APPLY ALL BATCH ─────────────────────────────────────────────────
|
|
@@ -5412,20 +6471,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5412
6471
|
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
5413
6472
|
const validatedPlatform = this.validatePlatform(platform);
|
|
5414
6473
|
|
|
5415
|
-
|
|
5416
|
-
// mj sync push writes records via stored procedures which do NOT fire
|
|
5417
|
-
// BaseEntity change events, so the RunView cache is never auto-invalidated.
|
|
5418
|
-
// Explicitly clearing these entries ensures Config(true) re-queries the DB.
|
|
5419
|
-
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Objects');
|
|
5420
|
-
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Object Fields');
|
|
5421
|
-
|
|
5422
|
-
// Force-refresh integration metadata cache so IntrospectSchema
|
|
5423
|
-
// picks up any IntegrationObject/Field changes made via mj sync push
|
|
5424
|
-
await IntegrationEngine.Instance.Config(true, user);
|
|
6474
|
+
await this.refreshIntegrationCatalogCaches(user, provider);
|
|
5425
6475
|
|
|
5426
6476
|
// Phase 1: Build schema for each connector in parallel
|
|
5427
6477
|
const buildResults = await Promise.allSettled(
|
|
5428
|
-
|
|
6478
|
+
// One catalog scope per connection — the innermost wins, so each build resolves its own.
|
|
6479
|
+
input.Connectors.map((connInput) => WithCatalogScope(connInput.CompanyIntegrationID, async () => {
|
|
5429
6480
|
const { connector, companyIntegration } = await this.resolveConnector(connInput.CompanyIntegrationID, user, provider);
|
|
5430
6481
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
5431
6482
|
console.log(
|
|
@@ -5494,7 +6545,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5494
6545
|
// persisted rows instead.
|
|
5495
6546
|
//
|
|
5496
6547
|
// Persist is also skipped here — the Save hook already did it.
|
|
5497
|
-
sourceSchema = this.buildSourceSchemaFromPersistedRows(
|
|
6548
|
+
sourceSchema = this.buildSourceSchemaFromPersistedRows(
|
|
6549
|
+
companyIntegration.IntegrationID,
|
|
6550
|
+
undefined,
|
|
6551
|
+
undefined,
|
|
6552
|
+
await LoadScopedFieldsForBuild(companyIntegration.IntegrationID, user),
|
|
6553
|
+
);
|
|
5498
6554
|
if (sourceSchema.Objects.length === 0) {
|
|
5499
6555
|
// Defensive fallback: if the engine cache is empty (hook
|
|
5500
6556
|
// didn't run, or this is a direct-API caller that bypasses
|
|
@@ -5549,6 +6605,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5549
6605
|
for (let i = 0; i < resolvedNames.length; i++) {
|
|
5550
6606
|
fieldsByName.set(resolvedNames[i].toLowerCase(), orderedSources[i].Fields);
|
|
5551
6607
|
}
|
|
6608
|
+
// Mirror the filtered flow: nothing resolved means nothing to build.
|
|
6609
|
+
if (connInput.SourceObjects.length > 0 && resolvedNames.length === 0) {
|
|
6610
|
+
throw new Error(
|
|
6611
|
+
`No source objects resolved — none of the ${connInput.SourceObjects.length} selection(s) ` +
|
|
6612
|
+
`matched this connection's catalog or the shared catalog; nothing was built`
|
|
6613
|
+
);
|
|
6614
|
+
}
|
|
5552
6615
|
}
|
|
5553
6616
|
|
|
5554
6617
|
const objects = resolvedNames.map(name => {
|
|
@@ -5572,6 +6635,26 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5572
6635
|
}
|
|
5573
6636
|
|
|
5574
6637
|
// Inject post-restart pending work payload
|
|
6638
|
+
// everything.txt: "for the very first RSU of a connector, the sync afterwards
|
|
6639
|
+
// should be chosen to be full/incremental, but always full generally as the
|
|
6640
|
+
// default." The single-connector path has always done this; the batch path did
|
|
6641
|
+
// not, and its input default of false made the null-coalesce below dead. So a
|
|
6642
|
+
// re-apply that the caller left unspecified ran INCREMENTAL against objects
|
|
6643
|
+
// whose watermark had just been reset by the schema change.
|
|
6644
|
+
const priorMapsResult = await new RunView().RunView<{ ID: string }>({
|
|
6645
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
6646
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(connInput.CompanyIntegrationID, 'CompanyIntegrationID')}'`,
|
|
6647
|
+
Fields: ['ID'],
|
|
6648
|
+
MaxRows: 1,
|
|
6649
|
+
ResultType: 'simple',
|
|
6650
|
+
BypassCache: true,
|
|
6651
|
+
SkipTotalRowCount: true,
|
|
6652
|
+
}, user);
|
|
6653
|
+
const hadPriorMaps = priorMapsResult.Success && priorMapsResult.Results.length > 0;
|
|
6654
|
+
// One value for both ways the sync starts: the restart path queues it below, and
|
|
6655
|
+
// the skipRestart branch starts the sync itself from the build it is handed.
|
|
6656
|
+
const fullSync = input.FullSync ?? !hadPriorMaps;
|
|
6657
|
+
|
|
5575
6658
|
const pendingPayload: RSUPendingWork = {
|
|
5576
6659
|
CompanyIntegrationID: connInput.CompanyIntegrationID,
|
|
5577
6660
|
SourceObjectNames: resolvedNames,
|
|
@@ -5580,7 +6663,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5580
6663
|
CronExpression: connInput.CronExpression,
|
|
5581
6664
|
ScheduleTimezone: connInput.ScheduleTimezone,
|
|
5582
6665
|
StartSync: input.StartSync,
|
|
5583
|
-
FullSync:
|
|
6666
|
+
FullSync: fullSync,
|
|
5584
6667
|
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
5585
6668
|
// Unrecognized directions fall back to undefined = "use the entity map's own SyncDirection"
|
|
5586
6669
|
SyncDirection: input.SyncDirection && isValidSyncDirection(input.SyncDirection) ? input.SyncDirection : undefined,
|
|
@@ -5599,8 +6682,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5599
6682
|
objects,
|
|
5600
6683
|
schemaOutput,
|
|
5601
6684
|
rsuInput,
|
|
6685
|
+
// carried so the skipRestart branch can apply remove-as-disable
|
|
6686
|
+
resolvedNames,
|
|
6687
|
+
// carried so the skipRestart branch starts the sync the restart path queues
|
|
6688
|
+
fullSync,
|
|
5602
6689
|
};
|
|
5603
|
-
})
|
|
6690
|
+
}))
|
|
5604
6691
|
);
|
|
5605
6692
|
|
|
5606
6693
|
// Separate successes and failures
|
|
@@ -5608,6 +6695,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5608
6695
|
connInput: ApplyAllBatchConnectorInput;
|
|
5609
6696
|
connector: BaseIntegrationConnector;
|
|
5610
6697
|
companyIntegration: MJCompanyIntegrationEntity;
|
|
6698
|
+
resolvedNames: string[];
|
|
6699
|
+
fullSync: boolean;
|
|
5611
6700
|
schemaName: string;
|
|
5612
6701
|
objects: SchemaPreviewObjectInput[];
|
|
5613
6702
|
schemaOutput: SchemaBuilderOutput;
|
|
@@ -5652,6 +6741,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5652
6741
|
// Phase 3: Post-pipeline — create entity maps, field maps, schedules for each success
|
|
5653
6742
|
for (let i = 0; i < successfulBuilds.length; i++) {
|
|
5654
6743
|
const build = successfulBuilds[i];
|
|
6744
|
+
// Entity maps, the unselected-map pass and the first sync all read this connection's catalog.
|
|
6745
|
+
await WithCatalogScope(build.connInput.CompanyIntegrationID, async () => {
|
|
5655
6746
|
const pipelineResult = batchResult.Results[i];
|
|
5656
6747
|
const integrationName = build.companyIntegration.Integration;
|
|
5657
6748
|
|
|
@@ -5665,7 +6756,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5665
6756
|
});
|
|
5666
6757
|
// No pending-work cleanup needed — rows are registered only for
|
|
5667
6758
|
// migrations that succeeded.
|
|
5668
|
-
continue
|
|
6759
|
+
return; // stands in for `continue` — this body is the per-connection scope's arrow
|
|
5669
6760
|
}
|
|
5670
6761
|
|
|
5671
6762
|
const connResult: ApplyAllBatchConnectorResult = {
|
|
@@ -5686,6 +6777,20 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5686
6777
|
);
|
|
5687
6778
|
connResult.EntityMapsCreated = entityMapsCreated;
|
|
5688
6779
|
|
|
6780
|
+
// Remove-as-disable, same rule the single-connector path applies. Without this
|
|
6781
|
+
// the batch path silently ignored UnselectedAction: a user who deselected a
|
|
6782
|
+
// table and applied with skipRestart kept syncing it. Not reachable while the
|
|
6783
|
+
// surfaces used different mutations; it becomes reachable the moment one of
|
|
6784
|
+
// them moves onto the batch path, which is exactly what the shared adapter does.
|
|
6785
|
+
if ((input.UnselectedAction ?? 'disable') !== 'ignore') {
|
|
6786
|
+
const disabledObjects = await DisableUnselectedEntityMaps(
|
|
6787
|
+
build.connInput.CompanyIntegrationID, build.resolvedNames, user, provider
|
|
6788
|
+
);
|
|
6789
|
+
if (disabledObjects.length > 0) {
|
|
6790
|
+
console.log(`[IntegrationApplyAllBatch] Disabled ${disabledObjects.length} unselected entity map(s): ${disabledObjects.join(', ')}`);
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
|
|
5689
6794
|
const createdMapIDs = entityMapsCreated.map(em => em.EntityMapID).filter(Boolean);
|
|
5690
6795
|
const scopedMapIDs = input.SyncScope === 'all' ? undefined : createdMapIDs;
|
|
5691
6796
|
|
|
@@ -5698,7 +6803,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5698
6803
|
const shouldStartSync = input.StartSync !== false &&
|
|
5699
6804
|
(input.SyncScope === 'all' || createdMapIDs.length > 0);
|
|
5700
6805
|
const syncRunID = shouldStartSync
|
|
5701
|
-
? await this.startSyncAfterApply(build.connInput.CompanyIntegrationID, user, scopedMapIDs,
|
|
6806
|
+
? await this.startSyncAfterApply(build.connInput.CompanyIntegrationID, user, scopedMapIDs, build.fullSync)
|
|
5702
6807
|
: null;
|
|
5703
6808
|
if (syncRunID) connResult.SyncRunID = syncRunID;
|
|
5704
6809
|
|
|
@@ -5720,6 +6825,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5720
6825
|
}
|
|
5721
6826
|
|
|
5722
6827
|
connectorResults.push(connResult);
|
|
6828
|
+
});
|
|
5723
6829
|
}
|
|
5724
6830
|
|
|
5725
6831
|
const pipelineSteps = batchResult.Results[0]?.Steps.map((s: RSUPipelineStep) => ({
|
|
@@ -5766,6 +6872,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5766
6872
|
EntityName: 'MJ: Scheduled Job Types',
|
|
5767
6873
|
ExtraFilter: `DriverClass='IntegrationSyncScheduledJobDriver'`,
|
|
5768
6874
|
MaxRows: 1,
|
|
6875
|
+
SkipTotalRowCount: true,
|
|
5769
6876
|
ResultType: 'simple',
|
|
5770
6877
|
Fields: ['ID']
|
|
5771
6878
|
}, user);
|
|
@@ -5840,7 +6947,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5840
6947
|
// Step 2: Null out ScheduledJobRunID on CompanyIntegrationRuns (break FK before deleting job runs)
|
|
5841
6948
|
const ciRunsResult = await rv.RunView<MJCompanyIntegrationRunEntity>({
|
|
5842
6949
|
EntityName: 'MJ: Company Integration Runs',
|
|
5843
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
6950
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5844
6951
|
ResultType: 'entity_object'
|
|
5845
6952
|
}, sysUser);
|
|
5846
6953
|
if (ciRunsResult.Success) {
|
|
@@ -5855,7 +6962,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5855
6962
|
// Step 3: Delete scheduled job runs + jobs (reference CI via Configuration JSON)
|
|
5856
6963
|
const jobsResult = await rv.RunView<MJScheduledJobEntity>({
|
|
5857
6964
|
EntityName: 'MJ: Scheduled Jobs',
|
|
5858
|
-
ExtraFilter: `Configuration LIKE '%${companyIntegrationID}%'`,
|
|
6965
|
+
ExtraFilter: `Configuration LIKE '%${sqlUuid(companyIntegrationID, 'companyIntegrationID')}%'`,
|
|
5859
6966
|
ResultType: 'entity_object'
|
|
5860
6967
|
}, sysUser);
|
|
5861
6968
|
if (jobsResult.Success) {
|
|
@@ -5905,7 +7012,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5905
7012
|
// Step 4: Delete entity map children (field maps, watermarks, record maps)
|
|
5906
7013
|
const entityMapsResult = await rv.RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
5907
7014
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
5908
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
7015
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5909
7016
|
ResultType: 'entity_object'
|
|
5910
7017
|
}, sysUser);
|
|
5911
7018
|
const entityMaps = entityMapsResult.Success ? entityMapsResult.Results : [];
|
|
@@ -5937,7 +7044,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5937
7044
|
|
|
5938
7045
|
const rmResult = await rv.RunView<MJCompanyIntegrationRecordMapEntity>({
|
|
5939
7046
|
EntityName: 'MJ: Company Integration Record Maps',
|
|
5940
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
7047
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5941
7048
|
ResultType: 'entity_object'
|
|
5942
7049
|
}, sysUser);
|
|
5943
7050
|
if (rmResult.Success) {
|
|
@@ -5957,7 +7064,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5957
7064
|
// Step 6: Delete employee-company integration links
|
|
5958
7065
|
const empResult = await rv.RunView<MJEmployeeCompanyIntegrationEntity>({
|
|
5959
7066
|
EntityName: 'MJ: Employee Company Integrations',
|
|
5960
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
7067
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
5961
7068
|
ResultType: 'entity_object'
|
|
5962
7069
|
}, sysUser);
|
|
5963
7070
|
if (empResult.Success) {
|
|
@@ -5967,6 +7074,39 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5967
7074
|
}
|
|
5968
7075
|
}
|
|
5969
7076
|
|
|
7077
|
+
// Step 6b: the per-connection catalog. Its rows carry a plain FK to this connection and
|
|
7078
|
+
// nothing cascades, so without this the CompanyIntegration delete below violates the
|
|
7079
|
+
// constraint and the WHOLE transaction rolls back — "Transaction failed — all deletes
|
|
7080
|
+
// rolled back" for any connection that ever ran a discovery on the per-connection
|
|
7081
|
+
// catalog (sandbox 2026-09-11, MJ-CAT-18). Fields first: they reference the objects,
|
|
7082
|
+
// including each other's objects through RelatedCompanyIntegrationObjectID, and every
|
|
7083
|
+
// row that can point at this connection's objects is this connection's own. Guarded on
|
|
7084
|
+
// registration, so a workspace without the catalog migration deletes exactly as before.
|
|
7085
|
+
if (md.Entities.some(e => e.Name === 'MJ: Company Integration Objects')) {
|
|
7086
|
+
const cioResult = await rv.RunView<BaseEntity>({
|
|
7087
|
+
EntityName: 'MJ: Company Integration Objects',
|
|
7088
|
+
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
7089
|
+
ResultType: 'entity_object',
|
|
7090
|
+
}, sysUser);
|
|
7091
|
+
const perConnectionObjects = cioResult.Success ? cioResult.Results : [];
|
|
7092
|
+
if (perConnectionObjects.length > 0) {
|
|
7093
|
+
const objectIDs = perConnectionObjects.map(o => `'${String(o.Get('ID')).replace(/'/g, "''")}'`).join(',');
|
|
7094
|
+
const ciofResult = await rv.RunView<BaseEntity>({
|
|
7095
|
+
EntityName: 'MJ: Company Integration Object Fields',
|
|
7096
|
+
ExtraFilter: `CompanyIntegrationObjectID IN (${objectIDs})`,
|
|
7097
|
+
ResultType: 'entity_object',
|
|
7098
|
+
}, sysUser);
|
|
7099
|
+
for (const field of (ciofResult.Success ? ciofResult.Results : [])) {
|
|
7100
|
+
field.TransactionGroup = tg;
|
|
7101
|
+
await field.Delete();
|
|
7102
|
+
}
|
|
7103
|
+
for (const object of perConnectionObjects) {
|
|
7104
|
+
object.TransactionGroup = tg;
|
|
7105
|
+
await object.Delete();
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
|
|
5970
7110
|
// Step 7: Delete the CompanyIntegration itself FIRST, so its CredentialID FK reference
|
|
5971
7111
|
// is gone before we delete the Credential. (Deleting the Credential first violates the
|
|
5972
7112
|
// CompanyIntegration.CredentialID foreign key — the constraint is checked per-statement
|
|
@@ -6029,12 +7169,44 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6029
7169
|
@Arg("platform", { defaultValue: "sqlserver" }) platform: string,
|
|
6030
7170
|
@Arg("skipGitCommit", { defaultValue: false }) skipGitCommit: boolean,
|
|
6031
7171
|
@Arg("skipRestart", { defaultValue: false }) skipRestart: boolean,
|
|
6032
|
-
|
|
6033
|
-
|
|
7172
|
+
// CONTRACT: new arrivals are DISCOVERED, not adopted. everything.txt is explicit — "we
|
|
7173
|
+
// create entity maps for all objects that are determined as things that should be now
|
|
7174
|
+
// added but arent, we enable nothing (because the user needs to, after the refresh, then
|
|
7175
|
+
// go turn them on)".
|
|
7176
|
+
//
|
|
7177
|
+
// This defaulted TRUE for a while on the claim that a disabled new object "is absent from
|
|
7178
|
+
// IntrospectSchema — so it never reached key inference and never reached a migration".
|
|
7179
|
+
// That claim is wrong, and the code says so plainly:
|
|
7180
|
+
// - `CreateDisabled` governs ONLY the entity/field map Status and SyncEnabled
|
|
7181
|
+
// (index.ts:1833-1834, :1880). It never touches IntegrationObject.Status.
|
|
7182
|
+
// - New IntegrationObject rows are written Status='Active' unconditionally
|
|
7183
|
+
// (IntegrationSchemaSync.ts:852), so they are returned by GetActiveIntegrationObjects.
|
|
7184
|
+
// - Phase 4 builds over `evolveNames = continuingMaps + newObjects`, and newObjects is
|
|
7185
|
+
// included regardless of this flag — so the table IS created either way.
|
|
7186
|
+
// The only thing this flag decides is whether the new object starts SYNCING unasked.
|
|
7187
|
+
@Arg("autoEnableNewObjects", { defaultValue: false, description: 'newly-appeared objects get their entity maps created DISABLED — their tables are still created, they simply do not start syncing until the user enables them. Pass true to auto-enable instead.' }) autoEnableNewObjects: boolean,
|
|
7188
|
+
@Arg("autoEnableNewColumns", { defaultValue: false, description: 'newly-appeared COLUMNS on an enabled object get their field maps created DISABLED, matching autoEnableNewObjects. Pass true to auto-enable instead. NOTE: this governs the REFRESH only — a column discovered mid-SYNC is never auto-created; it is captured as a candidate and needs acceptance (Configuration.autoPromoteCustomColumns, default false).' }) autoEnableNewColumns: boolean,
|
|
6034
7189
|
@Arg("deactivateAbsent", { nullable: true, description: 'Deactivate IO/IOF absent from this re-discovery (default true — comprehensive refresh; gated on the connector\'s authoritative-discovery getter).' }) deactivateAbsent: boolean | undefined,
|
|
6035
7190
|
@Arg("cascadeRemoveDependents", { defaultValue: false, description: 'When a removed object has still-active dependents (DAG parent edges), also disable the transitive dependent closure ("force remove those too"). Default false: dependents stay active and each broken edge is surfaced as a warning.' }) cascadeRemoveDependents: boolean,
|
|
7191
|
+
// everything.txt: a custom column the source started sending is CAPTURED automatically but
|
|
7192
|
+
// materialising it "requires that the user accepts" it. So candidates are always REPORTED
|
|
7193
|
+
// (CustomColumnCandidates on the output) and this list is how they come back accepted.
|
|
7194
|
+
// Empty default = report and promote nothing, which is the contract.
|
|
7195
|
+
//
|
|
7196
|
+
// Accepted columns ride THIS run's migration rather than starting their own. Promoting in a
|
|
7197
|
+
// second batch would restart the workspace twice for one user action, and back-to-back RSU
|
|
7198
|
+
// restarts are their own incident class here.
|
|
7199
|
+
@Arg("acceptCustomColumns", () => [String], { defaultValue: [], description: 'Column names from a previous run\'s CustomColumnCandidates to materialise as part of THIS refresh — one migration, one restart. Default empty: candidates are reported and nothing is promoted.' }) acceptCustomColumns: string[],
|
|
6036
7200
|
@Ctx() ctx: AppContext
|
|
6037
7201
|
): Promise<SchemaEvolutionOutput> {
|
|
7202
|
+
return WithCatalogScope(companyIntegrationID, async () => {
|
|
7203
|
+
// Checked BEFORE the lock is taken, so a refusal owes no release.
|
|
7204
|
+
{
|
|
7205
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
7206
|
+
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true }) as unknown as IMetadataProvider;
|
|
7207
|
+
const paused = await this.describeIfPaused(companyIntegrationID, user, md);
|
|
7208
|
+
if (paused) return { Success: false, HasChanges: false, Message: `Schema evolution not started: ${paused}` };
|
|
7209
|
+
}
|
|
6038
7210
|
// sync lock: schema evolution rewrites the metadata, field maps and DDL a sync
|
|
6039
7211
|
// reads — data syncs (manual + scheduled) must not run while this is in flight.
|
|
6040
7212
|
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema evolution')) {
|
|
@@ -6046,7 +7218,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6046
7218
|
try {
|
|
6047
7219
|
const user = this.getAuthenticatedUser(ctx);
|
|
6048
7220
|
const validatedPlatform = this.validatePlatform(platform);
|
|
6049
|
-
const
|
|
7221
|
+
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
|
|
7222
|
+
const md = provider as unknown as IMetadataProvider;
|
|
6050
7223
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, md);
|
|
6051
7224
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
6052
7225
|
const rv = new RunView();
|
|
@@ -6077,7 +7250,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6077
7250
|
const activeNamesLower = new Set(activeIOs.map(o => o.Name.toLowerCase()));
|
|
6078
7251
|
const priorMapsResult = await rv.RunView<MJCompanyIntegrationEntityMapEntity>({
|
|
6079
7252
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
6080
|
-
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
7253
|
+
ExtraFilter: `CompanyIntegrationID='${sqlUuid(companyIntegrationID, 'companyIntegrationID')}'`,
|
|
6081
7254
|
ResultType: 'entity_object',
|
|
6082
7255
|
BypassCache: true, // the diff must read committed state
|
|
6083
7256
|
}, user);
|
|
@@ -6160,8 +7333,15 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6160
7333
|
}
|
|
6161
7334
|
|
|
6162
7335
|
// Source of truth for the evolution = the PERSISTED post-resolution rows (the refresh in
|
|
6163
|
-
// Phase 1 just wrote them via the overlay) — no duplicate vendor introspect.
|
|
6164
|
-
|
|
7336
|
+
// Phase 1 just wrote them via the overlay) — no duplicate vendor introspect. Phase 6 reads
|
|
7337
|
+
// the same field rows, so they are loaded once.
|
|
7338
|
+
const scopedFields = await LoadScopedFieldsForBuild(companyIntegration.IntegrationID, user);
|
|
7339
|
+
const sourceSchema = this.buildSourceSchemaFromPersistedRows(
|
|
7340
|
+
companyIntegration.IntegrationID,
|
|
7341
|
+
undefined,
|
|
7342
|
+
undefined,
|
|
7343
|
+
scopedFields,
|
|
7344
|
+
);
|
|
6165
7345
|
|
|
6166
7346
|
// Normalize names to match source schema casing
|
|
6167
7347
|
const nameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
|
|
@@ -6178,7 +7358,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6178
7358
|
};
|
|
6179
7359
|
|
|
6180
7360
|
// Build target configs and SchemaBuilder input with ExistingTables
|
|
6181
|
-
const
|
|
7361
|
+
const drops = NewTargetConfigDrops();
|
|
7362
|
+
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, validatedPlatform, connector, drops);
|
|
7363
|
+
// By here Phase 1 has persisted the re-resolved catalog and Phase 3 has disabled the removed
|
|
7364
|
+
// objects' maps, so "Nothing was applied" would be untrue. Failing is still right: Phase 6
|
|
7365
|
+
// would otherwise reconcile field maps against an empty resolution.
|
|
7366
|
+
AssertTargetConfigs(objects, targetConfigs, companyIntegration.Integration, drops,
|
|
7367
|
+
`The refreshed catalog had already been saved and the removed objects' entity maps disabled; ` +
|
|
7368
|
+
`no table was built and no migration ran.`);
|
|
6182
7369
|
|
|
6183
7370
|
const schemaInput: SchemaBuilderInput = {
|
|
6184
7371
|
SourceSchema: filteredSchema,
|
|
@@ -6186,7 +7373,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6186
7373
|
Platform: validatedPlatform,
|
|
6187
7374
|
MJVersion: process.env.MJ_VERSION ?? '5.11.0',
|
|
6188
7375
|
SourceType: companyIntegration.Integration,
|
|
6189
|
-
AdditionalSchemaInfoPath:
|
|
7376
|
+
AdditionalSchemaInfoPath: RuntimeSchemaManager.Instance.AdditionalSchemaInfoPath,
|
|
6190
7377
|
MigrationsDir: process.env.RSU_MIGRATIONS_PATH ?? 'migrations/rsu',
|
|
6191
7378
|
MetadataDir: process.env.RSU_METADATA_DIR ?? 'metadata',
|
|
6192
7379
|
ExistingTables: existingTables,
|
|
@@ -6197,6 +7384,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6197
7384
|
const builder = new SchemaBuilder();
|
|
6198
7385
|
const schemaOutput = builder.BuildSchema(schemaInput);
|
|
6199
7386
|
|
|
7387
|
+
// Declared rows this evolution leaves out (deactivated objects/fields). This path calls
|
|
7388
|
+
// SchemaBuilder directly rather than through buildSchemaForConnector, so it never carried
|
|
7389
|
+
// them — an operator whose selected table came back without a column had nothing to read.
|
|
7390
|
+
// Unshifted onto schemaOutput.Warnings, which is what every return below surfaces.
|
|
7391
|
+
const inactiveWarnings = this.collectInactiveRowWarnings(companyIntegration.IntegrationID, normalizedNames, scopedFields);
|
|
7392
|
+
if (inactiveWarnings.length > 0) schemaOutput.Warnings.unshift(...inactiveWarnings);
|
|
7393
|
+
|
|
6200
7394
|
if (schemaOutput.Errors.length > 0) {
|
|
6201
7395
|
return {
|
|
6202
7396
|
Success: false,
|
|
@@ -6227,6 +7421,36 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6227
7421
|
}
|
|
6228
7422
|
}
|
|
6229
7423
|
|
|
7424
|
+
// ── Phase 5b — MJ-RUN-37: a WATERMARK-FIELD swap is a change the DDL diff cannot see ──
|
|
7425
|
+
//
|
|
7426
|
+
// A connector upgrade can move an object's incremental cursor to a different source
|
|
7427
|
+
// column (`modified_at` → `last_updated`). Phase 1's persist already DETECTS that — the
|
|
7428
|
+
// overlay rewrites `IntegrationObject.IncrementalWatermarkField` and records it in the
|
|
7429
|
+
// merge log's `ChangedAttributes`. Nothing consumed it, and no other source of
|
|
7430
|
+
// `changedObjects` can: a cursor swap adds no column (so `diff.AddedColumns` and
|
|
7431
|
+
// `ModifiedColumns` are both empty) and touches no field map (so Phase 6 stays quiet).
|
|
7432
|
+
//
|
|
7433
|
+
// The object was therefore treated as unchanged, its Pull watermark survived, and the
|
|
7434
|
+
// next incremental sync applied the OLD column's stored value as a lower bound on the
|
|
7435
|
+
// NEW one. Where the new column sorts later, every row below that value is filtered out
|
|
7436
|
+
// AT THE SOURCE — permanently, and with no artifact: the source reports a total
|
|
7437
|
+
// consistent with the filter it was given, so fetched equals expected and the run
|
|
7438
|
+
// closes clean. Absence, not duplication, is what this produces.
|
|
7439
|
+
//
|
|
7440
|
+
// Scoped to CONTINUING maps deliberately. A new object has no watermark to invalidate
|
|
7441
|
+
// and a removed one has no sync left to protect; widening this to every persisted object
|
|
7442
|
+
// would inflate `ChangedObjects` (and `hasChanges`) with names Phase 7 cannot act on.
|
|
7443
|
+
const watermarkFieldChanged = ObjectsWithWatermarkFieldChange(
|
|
7444
|
+
refresh.PersistResult?.ObjectMergeLog ?? [],
|
|
7445
|
+
continuingMaps.map(m => m.ExternalObjectName ?? ''),
|
|
7446
|
+
);
|
|
7447
|
+
for (const name of watermarkFieldChanged) {
|
|
7448
|
+
if (!changedObjects.some(n => n.toLowerCase() === name.toLowerCase())) changedObjects.push(name);
|
|
7449
|
+
}
|
|
7450
|
+
if (watermarkFieldChanged.length > 0) {
|
|
7451
|
+
LogStatus(`[SchemaEvolution] IncrementalWatermarkField changed on ${watermarkFieldChanged.length} object(s) — resetting their Pull watermarks: ${watermarkFieldChanged.join(', ')}`);
|
|
7452
|
+
}
|
|
7453
|
+
|
|
6230
7454
|
// ── Phase 6 — reconcile CONTINUING maps' field maps to the resolution ──
|
|
6231
7455
|
// Added source fields get field maps (Active on enabled maps, Disabled on disabled
|
|
6232
7456
|
// maps); field maps whose source field vanished from the resolution are disabled.
|
|
@@ -6235,12 +7459,64 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6235
7459
|
for (const em of continuingMaps) {
|
|
6236
7460
|
const io = iosByLowerName.get((em.ExternalObjectName ?? '').toLowerCase());
|
|
6237
7461
|
if (!io) continue;
|
|
6238
|
-
const fmChanges = await this.reconcileFieldMapsForEntityMap(em, io.ID, user, md, autoEnableNewColumns);
|
|
7462
|
+
const fmChanges = await this.reconcileFieldMapsForEntityMap(em, io.ID, user, md, autoEnableNewColumns, scopedFields);
|
|
6239
7463
|
if ((fmChanges.Added > 0 || fmChanges.Disabled > 0) && em.ExternalObjectName && !changedObjects.includes(em.ExternalObjectName)) {
|
|
6240
7464
|
changedObjects.push(em.ExternalObjectName);
|
|
6241
7465
|
}
|
|
6242
7466
|
}
|
|
6243
7467
|
|
|
7468
|
+
// ── Phase 6b — RE-KEYED objects: the identity of every stored row just changed ──
|
|
7469
|
+
//
|
|
7470
|
+
// A connector upgrade can change an object's primary key (PheedLoop 1.4.6 makes
|
|
7471
|
+
// Attendees `code + eventCode`, because it is fetched once per event and an attendee at
|
|
7472
|
+
// two events returns twice with the same `code`). `ToExternalRecord` joins every
|
|
7473
|
+
// IsPrimaryKey field with '|' in Sequence order, so changing the key — its members OR
|
|
7474
|
+
// their order — re-identifies every record the source will send from now on. Nothing can
|
|
7475
|
+
// match a stored row again: the RecordMap is keyed `EntityID|ExternalID`, and
|
|
7476
|
+
// `MatchEngine.FindByKeyFields` queries the destination table by key fields that are
|
|
7477
|
+
// NULL on rows written before the change. Left alone, the next sync inserts the entire
|
|
7478
|
+
// source ALONGSIDE what is there.
|
|
7479
|
+
//
|
|
7480
|
+
// Compared against the key the TABLES were built with (the entity's registered fields),
|
|
7481
|
+
// not against the previous catalog — the built key is what the stored rows were actually
|
|
7482
|
+
// written under, and it is the only side that cannot drift from them.
|
|
7483
|
+
//
|
|
7484
|
+
// The catalog side reads the same field rows as the build and Phase 6. A cold
|
|
7485
|
+
// per-connection object answers `[]` from the engine, and an empty key against a real
|
|
7486
|
+
// built key reads as a re-key, which deletes the object's record maps and rows.
|
|
7487
|
+
const rekeyTargets: Array<{
|
|
7488
|
+
ExternalObjectName: string; EntityMapID: string; EntityID: string;
|
|
7489
|
+
SchemaName: string; BaseTable: string;
|
|
7490
|
+
}> = [];
|
|
7491
|
+
for (const em of continuingMaps) {
|
|
7492
|
+
const io = iosByLowerName.get((em.ExternalObjectName ?? '').toLowerCase());
|
|
7493
|
+
if (!io || !em.EntityID) continue;
|
|
7494
|
+
const entity = md.EntityByID(em.EntityID);
|
|
7495
|
+
if (!entity) continue;
|
|
7496
|
+
const builtKey = IdentityKeyFields(entity.Fields.map(ef => ({
|
|
7497
|
+
Name: ef.Name, IsPrimaryKey: ef.IsPrimaryKey, Sequence: ef.Sequence,
|
|
7498
|
+
})));
|
|
7499
|
+
const catalogKey = CatalogIdentityKey(
|
|
7500
|
+
FieldsForBuild(io.ID, scopedFields, IntegrationEngineBase.Instance)
|
|
7501
|
+
);
|
|
7502
|
+
if (!DecideRekeyed(builtKey, catalogKey)) continue;
|
|
7503
|
+
rekeyTargets.push({
|
|
7504
|
+
ExternalObjectName: em.ExternalObjectName as string,
|
|
7505
|
+
EntityMapID: em.ID,
|
|
7506
|
+
EntityID: em.EntityID,
|
|
7507
|
+
SchemaName: entity.SchemaName,
|
|
7508
|
+
BaseTable: entity.BaseTable,
|
|
7509
|
+
});
|
|
7510
|
+
// A re-key is a change even when no column moved — a pure reorder alters no DDL,
|
|
7511
|
+
// and without this the object would not reach the watermark reset below.
|
|
7512
|
+
if (em.ExternalObjectName && !changedObjects.includes(em.ExternalObjectName)) {
|
|
7513
|
+
changedObjects.push(em.ExternalObjectName);
|
|
7514
|
+
}
|
|
7515
|
+
}
|
|
7516
|
+
const rekeyedObjects = await ClearRekeyedObjectData(
|
|
7517
|
+
rekeyTargets, companyIntegrationID, user, provider
|
|
7518
|
+
);
|
|
7519
|
+
|
|
6244
7520
|
// ── Phase 7 — U10: reset the Pull watermark of every CHANGED object so the next sync
|
|
6245
7521
|
// full-fetches + backfills the schema change (content-hash bounds the rewrite cost).
|
|
6246
7522
|
const changedLower = new Set(changedObjects.map(n => n.toLowerCase()));
|
|
@@ -6267,6 +7543,46 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6267
7543
|
let pipelineSteps: RSUStepOutput[] | undefined;
|
|
6268
7544
|
let gitCommitSuccess: boolean | undefined;
|
|
6269
7545
|
let apiRestarted: boolean | undefined;
|
|
7546
|
+
// ── Overflow columns: always report, promote only what came back accepted. ──
|
|
7547
|
+
//
|
|
7548
|
+
// A refresh is where an operator asks "what has changed on the source?", and columns
|
|
7549
|
+
// the connector has been quietly capturing into the overflow JSON are part of that
|
|
7550
|
+
// answer. Reporting is free and read-only. Promoting is not, so it needs the explicit
|
|
7551
|
+
// acceptance everything.txt asks for.
|
|
7552
|
+
let customColumnCandidates: CustomColumnCandidate[] = [];
|
|
7553
|
+
let promotionInputs: RSUPipelineInput[] = [];
|
|
7554
|
+
let promotedColumns: PromotedColumn[] = [];
|
|
7555
|
+
try {
|
|
7556
|
+
const promoter = new IntegrationCustomColumnPromoter(user, md);
|
|
7557
|
+
const mapped = await this.getMappedEntityNames(companyIntegrationID, user);
|
|
7558
|
+
for (const en of mapped) {
|
|
7559
|
+
customColumnCandidates.push(...await promoter.ListCandidates(companyIntegrationID, en));
|
|
7560
|
+
}
|
|
7561
|
+
const accepted = new Set((acceptCustomColumns ?? []).map(c => c.toLowerCase()));
|
|
7562
|
+
if (accepted.size > 0) {
|
|
7563
|
+
// Only entities that actually own an accepted column — planning an entity with
|
|
7564
|
+
// nothing accepted would promote its OTHER candidates too, which is precisely
|
|
7565
|
+
// the un-asked-for materialisation the acceptance gate exists to prevent.
|
|
7566
|
+
const entitiesWithAccepted = Array.from(new Set(
|
|
7567
|
+
customColumnCandidates.filter(c => accepted.has(c.ColumnName.toLowerCase())).map(c => c.EntityName)
|
|
7568
|
+
));
|
|
7569
|
+
if (entitiesWithAccepted.length > 0) {
|
|
7570
|
+
const plan = await promoter.PlanPromotion(companyIntegrationID, entitiesWithAccepted);
|
|
7571
|
+
if (plan) {
|
|
7572
|
+
promotionInputs = plan.BatchInputs;
|
|
7573
|
+
promotedColumns = plan.Plans.flatMap(p => p.work.map(w => ({ EntityName: p.entityName, ColumnName: w.columnName })));
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
}
|
|
7577
|
+
} catch (candErr) {
|
|
7578
|
+
// Neither reporting nor promoting overflow columns is what a refresh is FOR. A
|
|
7579
|
+
// failure here must not fail the refresh — it costs the offer, not the evolution.
|
|
7580
|
+
LogError(`IntegrationSchemaEvolution: custom-column candidates unavailable — ${candErr}`);
|
|
7581
|
+
customColumnCandidates = [];
|
|
7582
|
+
promotionInputs = [];
|
|
7583
|
+
promotedColumns = [];
|
|
7584
|
+
}
|
|
7585
|
+
|
|
6270
7586
|
if (hasDDLChanges) {
|
|
6271
7587
|
const rsuInput = builder.BuildRSUInput(schemaOutput, schemaInput, {
|
|
6272
7588
|
SkipGitCommit: skipGitCommit,
|
|
@@ -6301,8 +7617,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6301
7617
|
rsuInput.ContextUser = user;
|
|
6302
7618
|
}
|
|
6303
7619
|
|
|
7620
|
+
// Retrying variant, for the same reason every other install path uses it: a refresh
|
|
7621
|
+
// is unattended and long, and a transient ExecuteMigration / RunCodeGen /
|
|
7622
|
+
// CompileTypeScript / RestartMJAPI failure would otherwise abandon it with the
|
|
7623
|
+
// metadata already re-resolved and the DDL not applied — the half-state an operator
|
|
7624
|
+
// then has to diagnose. Replay happens only while nothing was applied.
|
|
6304
7625
|
const rsm = RuntimeSchemaManager.Instance;
|
|
6305
|
-
|
|
7626
|
+
// ONE batch: the refresh's own DDL plus any accepted overflow columns. RSU
|
|
7627
|
+
// registers PendingWork from EVERY input, so the promoter's post-restart
|
|
7628
|
+
// completion survives being appended here rather than run as its own batch.
|
|
7629
|
+
const batchResult = await rsm.RunPipelineBatchWithRetry([rsuInput, ...promotionInputs]);
|
|
6306
7630
|
const pipelineResult = batchResult.Results[0];
|
|
6307
7631
|
pipelineSteps = pipelineResult?.Steps.map((s: RSUPipelineStep) => ({
|
|
6308
7632
|
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
@@ -6338,19 +7662,49 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6338
7662
|
}
|
|
6339
7663
|
}
|
|
6340
7664
|
}
|
|
7665
|
+
} else if (promotionInputs.length > 0) {
|
|
7666
|
+
// The source's own shape is unchanged, but the operator accepted overflow columns.
|
|
7667
|
+
// They still need a migration; there is simply no evolution DDL to share it with.
|
|
7668
|
+
// Retrying variant, as above: it replays only while nothing was applied.
|
|
7669
|
+
const rsm = RuntimeSchemaManager.Instance;
|
|
7670
|
+
const batchResult = await rsm.RunPipelineBatchWithRetry(promotionInputs);
|
|
7671
|
+
const first = batchResult.Results[0];
|
|
7672
|
+
pipelineSteps = first?.Steps.map((s: RSUPipelineStep) => ({
|
|
7673
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
7674
|
+
}));
|
|
7675
|
+
gitCommitSuccess = first?.GitCommitSuccess;
|
|
7676
|
+
apiRestarted = first?.APIRestarted;
|
|
7677
|
+
if (!first?.Success) {
|
|
7678
|
+
return {
|
|
7679
|
+
Success: false,
|
|
7680
|
+
Message: `Column promotion failed: ${first?.ErrorMessage ?? 'unknown error'}`,
|
|
7681
|
+
HasChanges: false,
|
|
7682
|
+
Steps: pipelineSteps,
|
|
7683
|
+
CustomColumnCandidates: customColumnCandidates.length > 0 ? customColumnCandidates : undefined,
|
|
7684
|
+
};
|
|
7685
|
+
}
|
|
6341
7686
|
}
|
|
6342
7687
|
|
|
7688
|
+
// Report only what is STILL outstanding: a column accepted this run is being created
|
|
7689
|
+
// right now, so leaving it in the offer would invite the operator to accept it twice.
|
|
7690
|
+
const acceptedNames = new Set(promotedColumns.map(c => c.ColumnName.toLowerCase()));
|
|
7691
|
+
const outstandingCandidates = customColumnCandidates.filter(c => !acceptedNames.has(c.ColumnName.toLowerCase()));
|
|
7692
|
+
|
|
6343
7693
|
const summary = [
|
|
6344
7694
|
newObjects.length > 0 ? `${newObjects.length} new object(s) (${autoEnableNewObjects ? 'enabled' : 'created disabled'})` : null,
|
|
6345
7695
|
removedObjects.length > 0 ? `${removedObjects.length} removed object(s) disabled` : null,
|
|
6346
7696
|
changedObjects.length > 0 ? `${changedObjects.length} changed object(s), ${addedColumns} column(s) added (${autoEnableNewColumns ? 'enabled' : 'created disabled'}), ${modifiedColumns} modified` : null,
|
|
6347
7697
|
watermarksReset.length > 0 ? `${watermarksReset.length} watermark(s) reset for backfill` : null,
|
|
7698
|
+
promotedColumns.length > 0 ? `${promotedColumns.length} accepted custom column(s) materialised` : null,
|
|
7699
|
+
outstandingCandidates.length > 0 ? `${outstandingCandidates.length} custom column(s) awaiting your acceptance` : null,
|
|
6348
7700
|
].filter(Boolean).join('; ');
|
|
6349
7701
|
|
|
6350
7702
|
return {
|
|
6351
7703
|
Success: true,
|
|
6352
7704
|
Message: `Schema evolution applied — ${summary || 'no per-object deltas'}`,
|
|
6353
7705
|
HasChanges: true,
|
|
7706
|
+
CustomColumnCandidates: outstandingCandidates.length > 0 ? outstandingCandidates : undefined,
|
|
7707
|
+
PromotedColumns: promotedColumns.length > 0 ? promotedColumns : undefined,
|
|
6354
7708
|
AddedColumns: addedColumns,
|
|
6355
7709
|
ModifiedColumns: modifiedColumns,
|
|
6356
7710
|
Steps: pipelineSteps,
|
|
@@ -6361,6 +7715,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6361
7715
|
CascadeRemovedObjects: cascadeRemoved.length > 0 ? cascadeRemoved : undefined,
|
|
6362
7716
|
ChangedObjects: changedObjects.length > 0 ? changedObjects : undefined,
|
|
6363
7717
|
WatermarksReset: watermarksReset.length > 0 ? watermarksReset : undefined,
|
|
7718
|
+
RekeyedObjects: rekeyedObjects.length > 0 ? rekeyedObjects : undefined,
|
|
6364
7719
|
Warnings: (dagRemovalWarnings.length + schemaOutput.Warnings.length) > 0
|
|
6365
7720
|
? [...dagRemovalWarnings, ...schemaOutput.Warnings]
|
|
6366
7721
|
: undefined,
|
|
@@ -6371,6 +7726,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6371
7726
|
} finally {
|
|
6372
7727
|
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
6373
7728
|
}
|
|
7729
|
+
});
|
|
6374
7730
|
}
|
|
6375
7731
|
|
|
6376
7732
|
/**
|
|
@@ -6390,17 +7746,22 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
6390
7746
|
* A field map whose source field is no longer Active in the resolution is DISABLED, never
|
|
6391
7747
|
* deleted — a later re-appearance re-enables it through the same reconciliation (the
|
|
6392
7748
|
* field-is-back branch above), so retiring and restoring a column are both non-destructive.
|
|
7749
|
+
*
|
|
7750
|
+
* Under a per-connection catalog the field rows come from `scopedFields`, never from the
|
|
7751
|
+
* engine: the engine answers `[]` for a cold per-connection object, and an empty field list here
|
|
7752
|
+
* disables every Active field map on the object.
|
|
6393
7753
|
*/
|
|
6394
7754
|
private async reconcileFieldMapsForEntityMap(
|
|
6395
7755
|
em: MJCompanyIntegrationEntityMapEntity,
|
|
6396
7756
|
integrationObjectID: string,
|
|
6397
7757
|
user: UserInfo,
|
|
6398
7758
|
md: IMetadataProvider,
|
|
6399
|
-
|
|
7759
|
+
// see decideFieldMapReconcile — an omitted argument must not silently adopt
|
|
7760
|
+
autoEnableNewColumns = false,
|
|
7761
|
+
scopedFields?: ScopedBuildFields | null,
|
|
6400
7762
|
): Promise<{ Added: number; Disabled: number }> {
|
|
6401
7763
|
const result = { Added: 0, Disabled: 0 };
|
|
6402
|
-
const activeFields = IntegrationEngineBase.Instance
|
|
6403
|
-
.GetIntegrationObjectFields(integrationObjectID)
|
|
7764
|
+
const activeFields = FieldsForBuild(integrationObjectID, scopedFields, IntegrationEngineBase.Instance)
|
|
6404
7765
|
.filter(f => f.Status === 'Active');
|
|
6405
7766
|
const mapEnabled = em.Status === 'Active' && em.SyncEnabled === true;
|
|
6406
7767
|
|