@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType, Int, Float } from "type-graphql";
|
|
2
2
|
import { CompositeKey, DatabaseProviderBase, EntityInfo, LocalCacheManager, Metadata, RunView, UserInfo, LogError, LogStatus, IMetadataProvider, TransactionGroupBase } from "@memberjunction/core";
|
|
3
3
|
import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
|
|
4
|
+
import { NoLog } from "../logging/NoLog.js";
|
|
4
5
|
import { UUIDsEqual } from "@memberjunction/global";
|
|
5
6
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
6
7
|
import {
|
|
@@ -38,6 +39,7 @@ import type {
|
|
|
38
39
|
GenerateIntegrationActionResult
|
|
39
40
|
} from "@memberjunction/integration-engine";
|
|
40
41
|
import { IntegrationEngineBase } from "@memberjunction/integration-engine-base";
|
|
42
|
+
import { buildIntegrationLLMPKCallback } from "@memberjunction/core-entities-server";
|
|
41
43
|
import {
|
|
42
44
|
SchemaBuilder,
|
|
43
45
|
TypeMapper,
|
|
@@ -47,16 +49,20 @@ import {
|
|
|
47
49
|
ExistingTableInfo,
|
|
48
50
|
SchemaEvolution
|
|
49
51
|
} from "@memberjunction/integration-schema-builder";
|
|
50
|
-
import { RuntimeSchemaManager, type RSUPipelineStep, type RSUPipelineInput } from "@memberjunction/schema-engine";
|
|
52
|
+
import { RuntimeSchemaManager, type RSUPipelineStep, type RSUPipelineInput, type RSUPendingWork } from "@memberjunction/schema-engine";
|
|
51
53
|
import type { SchemaBuilderOutput } from "@memberjunction/integration-schema-builder";
|
|
52
|
-
import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
54
|
+
import { IntegrationProgressEmitter, IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
53
55
|
import type { IntegrationRunSnapshot, IntegrationRunKind } from "@memberjunction/integration-progress-artifacts";
|
|
54
56
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
55
57
|
import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
|
|
56
58
|
import { ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
|
|
59
|
+
import { BuildCreateConnectionMessage, BuildDetachedRefreshMessage, BuildUpdateConnectionMessage } from "../integration/SchemaRefreshLaunch.js";
|
|
60
|
+
// Type-only: the registered runtime class for 'MJ: Company Integrations'. Lets the create path name the
|
|
61
|
+
// server subclass it actually gets back from GetEntityObject with a real type rather than a cast.
|
|
62
|
+
import type { MJCompanyIntegrationEntityServer } from "@memberjunction/core-entities-server";
|
|
57
63
|
import { AppContext } from "../types.js";
|
|
58
64
|
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
59
|
-
import { UserCache } from "@memberjunction/
|
|
65
|
+
import { UserCache } from "@memberjunction/generic-database-provider";
|
|
60
66
|
|
|
61
67
|
// ─── RSU Pipeline Output Types ──────────────────────────────────────────────
|
|
62
68
|
|
|
@@ -551,7 +557,16 @@ class CreateConnectionInput {
|
|
|
551
557
|
@Field() CompanyID: string;
|
|
552
558
|
@Field() CredentialTypeID: string;
|
|
553
559
|
@Field() CredentialName: string;
|
|
554
|
-
|
|
560
|
+
/**
|
|
561
|
+
* The credential payload being connected, as a JSON string.
|
|
562
|
+
*
|
|
563
|
+
* `@NoLog` is load-bearing here, not decorative. This field does NOT map to an
|
|
564
|
+
* entity column — the resolver assigns it onto `MJ: Credentials`.`Values`
|
|
565
|
+
* (which IS `Encrypt=true`) in procedural code further down. The metadata-driven
|
|
566
|
+
* half of the redactor keys off entity columns, so it cannot see this field, and
|
|
567
|
+
* without the mark the variables log emits the credential in plaintext.
|
|
568
|
+
*/
|
|
569
|
+
@Field() @NoLog CredentialValues: string;
|
|
555
570
|
@Field({ nullable: true }) ExternalSystemID?: string;
|
|
556
571
|
@Field({ nullable: true }) Configuration?: string;
|
|
557
572
|
}
|
|
@@ -559,6 +574,21 @@ class CreateConnectionInput {
|
|
|
559
574
|
@ObjectType()
|
|
560
575
|
class CreateConnectionPipelineSummary {
|
|
561
576
|
@Field() RunID: string;
|
|
577
|
+
/**
|
|
578
|
+
* True when the pipeline was launched detached (`awaitSchemaRefresh: false`) and is STILL RUNNING —
|
|
579
|
+
* every count below is a placeholder zero, not a result. Tail `RunID` via IntegrationTailRunEvents
|
|
580
|
+
* (or the IntegrationProgress subscription, kind='ConnectorCreation') for the real outcome.
|
|
581
|
+
*/
|
|
582
|
+
@Field() InProgress: boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Whether the refresh pipeline itself succeeded. A pipeline that fails at ConnectionTest still
|
|
585
|
+
* RETURNS (it does not throw), with every count at zero — so counts alone cannot distinguish
|
|
586
|
+
* "found nothing to change" from "never got past the credential check". Always false while
|
|
587
|
+
* `InProgress` is true: a detached run's outcome is not known yet.
|
|
588
|
+
*/
|
|
589
|
+
@Field() Succeeded: boolean;
|
|
590
|
+
/** The pipeline's own failure reason when `Succeeded` is false and the run has finished. */
|
|
591
|
+
@Field({ nullable: true }) FailureMessage?: string;
|
|
562
592
|
@Field() ObjectsCreated: number;
|
|
563
593
|
@Field() ObjectsUpdated: number;
|
|
564
594
|
@Field() FieldsCreated: number;
|
|
@@ -603,6 +633,7 @@ class IntegrationSyncConfigInput {
|
|
|
603
633
|
@Field(() => Boolean, { nullable: true, description: '§4 cross-layer pipelining: a child map starts when ITS parents finish, not the whole layer.' }) CrossLayerPipeline?: boolean;
|
|
604
634
|
@Field(() => Boolean, { nullable: true, description: 'Merkle/partition hash-diff reconcile for watermark-less change detection (buffers the set in RAM).' }) PartitionReconcile?: boolean;
|
|
605
635
|
@Field(() => Int, { nullable: true, description: 'Time budget (ms) for stage-2 streaming field discovery before it stops and uses what it gathered.' }) DiscoveryTimeBudgetMs?: number;
|
|
636
|
+
@Field(() => Int, { nullable: true, description: 'Per-page FetchChanges timeout (ms). Raise it for a connector that fans out one request per parent record, whose page time scales with BatchSize. Wins over the connector\'s own FetchChangesTimeoutMs; unset = the connector\'s value, else the framework default (30000).' }) FetchTimeoutMs?: number;
|
|
606
637
|
@Field(() => Int, { nullable: true, description: 'Batch size for stage-2 streaming field discovery (records per FetchChanges page). Default 500.' }) DiscoveryBatchSize?: number;
|
|
607
638
|
@Field(() => Int, { nullable: true, description: 'Max records sampled in stage-2 streaming field discovery (a column corpus + PK guess; NOT a full scan). Default 500.' }) DiscoveryMaxRecords?: number;
|
|
608
639
|
@Field(() => Boolean, { nullable: true, description: '§7 Comprehensive refresh deactivates Declared objects/fields ABSENT from an AUTHORITATIVE discovery (reversible). Default false.' }) DeactivateAbsent?: boolean;
|
|
@@ -621,6 +652,7 @@ class IntegrationSyncConfigOutput {
|
|
|
621
652
|
@Field(() => Boolean, { nullable: true }) CrossLayerPipeline?: boolean;
|
|
622
653
|
@Field(() => Boolean, { nullable: true }) PartitionReconcile?: boolean;
|
|
623
654
|
@Field(() => Int, { nullable: true }) DiscoveryTimeBudgetMs?: number;
|
|
655
|
+
@Field(() => Int, { nullable: true }) FetchTimeoutMs?: number;
|
|
624
656
|
@Field(() => Int, { nullable: true }) DiscoveryBatchSize?: number;
|
|
625
657
|
@Field(() => Int, { nullable: true }) DiscoveryMaxRecords?: number;
|
|
626
658
|
@Field(() => Boolean, { nullable: true }) DeactivateAbsent?: boolean;
|
|
@@ -936,7 +968,7 @@ class IntegrationRunEventsOutput {
|
|
|
936
968
|
@Field() IsInFlight: boolean;
|
|
937
969
|
}
|
|
938
970
|
|
|
939
|
-
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.
|
|
971
|
+
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgressAsync()
|
|
940
972
|
|
|
941
973
|
@ObjectType()
|
|
942
974
|
class ConnectionSummaryOutput {
|
|
@@ -2376,9 +2408,42 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2376
2408
|
user: UserInfo,
|
|
2377
2409
|
provider: IMetadataProvider,
|
|
2378
2410
|
universalPKConvention?: string,
|
|
2411
|
+
runID?: string,
|
|
2412
|
+
): Promise<CreateConnectionPipelineSummary> {
|
|
2413
|
+
// Sync lock (#3656). This pipeline rewrites the very IO/IOF rows, field
|
|
2414
|
+
// maps and DDL a sync reads, so it must not overlap a sync or another
|
|
2415
|
+
// maintenance operation for the same connection. The two other
|
|
2416
|
+
// pipeline call sites — IntegrationRefreshConnectorSchema and
|
|
2417
|
+
// IntegrationSchemaEvolution — already take this lock inline; this is
|
|
2418
|
+
// the third and fourth (create/update/reactivate) finally taking it too.
|
|
2419
|
+
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema refresh')) {
|
|
2420
|
+
throw new Error(
|
|
2421
|
+
'Schema refresh not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.'
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
try {
|
|
2425
|
+
return await this.runSchemaRefreshPipelineLocked(companyIntegrationID, user, provider, universalPKConvention, runID);
|
|
2426
|
+
} finally {
|
|
2427
|
+
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/** The body of {@link runSchemaRefreshPipeline}, run with the maintenance lock held. */
|
|
2432
|
+
private async runSchemaRefreshPipelineLocked(
|
|
2433
|
+
companyIntegrationID: string,
|
|
2434
|
+
user: UserInfo,
|
|
2435
|
+
provider: IMetadataProvider,
|
|
2436
|
+
universalPKConvention?: string,
|
|
2437
|
+
runID?: string,
|
|
2379
2438
|
): Promise<CreateConnectionPipelineSummary> {
|
|
2380
2439
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
|
|
2381
2440
|
const pipeline = new IntegrationConnectorCreationPipeline();
|
|
2441
|
+
// SoftPKClassifier's LLM tier (universal → naming → statistical → LLM →
|
|
2442
|
+
// synthetic) only runs when this callback is supplied. It used to be
|
|
2443
|
+
// wired by the CompanyIntegration save hook; with that hook gone
|
|
2444
|
+
// (#3738) it is wired here, so the create/update/reactivate paths keep
|
|
2445
|
+
// the same PK-inference quality they had before.
|
|
2446
|
+
const llmInference = await buildIntegrationLLMPKCallback(user);
|
|
2382
2447
|
const runOpts = {
|
|
2383
2448
|
Connector: connector,
|
|
2384
2449
|
CompanyIntegration: companyIntegration,
|
|
@@ -2387,6 +2452,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2387
2452
|
UniversalPKConvention: universalPKConvention || undefined,
|
|
2388
2453
|
ConsoleMirror: true,
|
|
2389
2454
|
TriggerType: 'Manual' as const,
|
|
2455
|
+
LLMInference: llmInference ?? undefined,
|
|
2456
|
+
// Caller-supplied runID: lets the detached path hand a tailable ID back to the client
|
|
2457
|
+
// BEFORE the pipeline has done any work. Omitted ⇒ the pipeline generates its own.
|
|
2458
|
+
RunID: runID,
|
|
2390
2459
|
};
|
|
2391
2460
|
const result = await pipeline.Run(runOpts as unknown as Parameters<typeof pipeline.Run>[0]);
|
|
2392
2461
|
|
|
@@ -2397,6 +2466,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2397
2466
|
|
|
2398
2467
|
return {
|
|
2399
2468
|
RunID: result.RunID,
|
|
2469
|
+
InProgress: false,
|
|
2470
|
+
Succeeded: result.Success,
|
|
2471
|
+
FailureMessage: result.FailureMessage,
|
|
2400
2472
|
ObjectsCreated: result.PersistResult?.ObjectsCreated ?? 0,
|
|
2401
2473
|
ObjectsUpdated: result.PersistResult?.ObjectsUpdated ?? 0,
|
|
2402
2474
|
FieldsCreated: result.PersistResult?.FieldsCreated ?? 0,
|
|
@@ -2413,6 +2485,94 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2413
2485
|
};
|
|
2414
2486
|
}
|
|
2415
2487
|
|
|
2488
|
+
/**
|
|
2489
|
+
* Launches the schema-refresh pipeline WITHOUT awaiting it and returns a summary carrying the
|
|
2490
|
+
* tailable `RunID` and `InProgress: true`.
|
|
2491
|
+
*
|
|
2492
|
+
* Why: the pipeline is a live vendor introspect — minutes on a large catalog (HubSpot: 130
|
|
2493
|
+
* objects). Awaiting it inline holds the create/update mutation open for that entire time, so the
|
|
2494
|
+
* connection wizard's Finish button sits on an indeterminate spinner with no way to tell progress
|
|
2495
|
+
* from a hang. The pipeline already writes a complete, durable, per-run event stream keyed by its
|
|
2496
|
+
* runID; the only thing missing was handing that ID to the caller BEFORE the work starts. Hence
|
|
2497
|
+
* the caller-supplied runID.
|
|
2498
|
+
*
|
|
2499
|
+
* Trade-off the caller opts into: the mutation no longer reports what the refresh found (counts
|
|
2500
|
+
* are placeholder zeros) and a refresh failure no longer surfaces in the mutation's Message — both
|
|
2501
|
+
* live on the run stream instead. That is why `awaitSchemaRefresh` defaults to true.
|
|
2502
|
+
*
|
|
2503
|
+
* ON CREATE this is the only refresh that runs. #3738 removed the activation hook that used to
|
|
2504
|
+
* fire the same pipeline inside `Save()`, awaited — which had made the create mutation pay a full
|
|
2505
|
+
* introspect before ever reaching this method, and had run it BEFORE the connection test, so a
|
|
2506
|
+
* rejected connection was rolled back with its discovered schema already written.
|
|
2507
|
+
*
|
|
2508
|
+
* Belt and braces: if the launch below IS coalesced onto some other run for this connection (a
|
|
2509
|
+
* concurrent refresh, a repeat click), the pipeline publishes a terminal alias run under `runID`
|
|
2510
|
+
* naming the run that served it — so the ID handed back here is tailable in every case.
|
|
2511
|
+
*/
|
|
2512
|
+
private startSchemaRefreshPipelineDetached(
|
|
2513
|
+
companyIntegrationID: string,
|
|
2514
|
+
user: UserInfo,
|
|
2515
|
+
provider: IMetadataProvider,
|
|
2516
|
+
universalPKConvention?: string,
|
|
2517
|
+
): CreateConnectionPipelineSummary {
|
|
2518
|
+
const runID = IntegrationProgressEmitter.newRunID('connector');
|
|
2519
|
+
// Deliberately not awaited. Once the pipeline is running it records every outcome on this
|
|
2520
|
+
// run's artifact stream under `runID` itself — but it can also throw BEFORE it ever
|
|
2521
|
+
// constructs its emitter (connector-driver resolution, a missing Integration row), and in
|
|
2522
|
+
// the blocking path that throw surfaced in the mutation's Message. Detached, the caller has
|
|
2523
|
+
// already been handed `runID` and has nothing but the stream to watch, so a pre-emitter
|
|
2524
|
+
// throw must be published onto that stream — otherwise the run never appears at all and a
|
|
2525
|
+
// tailer waits forever on a run ID that will never produce an event.
|
|
2526
|
+
void this.runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention, runID)
|
|
2527
|
+
.catch(err => this.publishDetachedLaunchFailure(runID, companyIntegrationID, err));
|
|
2528
|
+
return {
|
|
2529
|
+
RunID: runID,
|
|
2530
|
+
InProgress: true,
|
|
2531
|
+
// Not known yet — the run has only just been launched.
|
|
2532
|
+
Succeeded: false,
|
|
2533
|
+
ObjectsCreated: 0,
|
|
2534
|
+
ObjectsUpdated: 0,
|
|
2535
|
+
FieldsCreated: 0,
|
|
2536
|
+
FieldsUpdated: 0,
|
|
2537
|
+
UnresolvedObjects: [],
|
|
2538
|
+
PKVerdicts: [],
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
/**
|
|
2543
|
+
* Terminates a detached run that failed BEFORE the pipeline could open its own artifact stream.
|
|
2544
|
+
*
|
|
2545
|
+
* The pipeline constructs its emitter inside `Run()`, so anything that throws on the way there —
|
|
2546
|
+
* connector-driver resolution (`No connector registered for driver class "X"`), a missing
|
|
2547
|
+
* Integration row, a CompanyIntegration that vanished — produces no run directory at all. In the
|
|
2548
|
+
* blocking path that throw surfaced in the mutation's Message. Detached, the caller has already
|
|
2549
|
+
* been handed the run ID and has only the stream to watch, so the failure has to be published
|
|
2550
|
+
* there or the run is invisible forever.
|
|
2551
|
+
*
|
|
2552
|
+
* Only ever called on the rejection path: if the pipeline got far enough to build its own emitter
|
|
2553
|
+
* it terminates its own run, and a pipeline that completed never reaches here.
|
|
2554
|
+
*/
|
|
2555
|
+
private publishDetachedLaunchFailure(runID: string, companyIntegrationID: string, err: unknown): void {
|
|
2556
|
+
const message = this.formatError(err);
|
|
2557
|
+
LogError(`Detached schema refresh (run ${runID}) failed: ${message}`);
|
|
2558
|
+
try {
|
|
2559
|
+
const emitter = new IntegrationProgressEmitter({
|
|
2560
|
+
runID,
|
|
2561
|
+
runKind: 'ConnectorCreation',
|
|
2562
|
+
companyIntegrationID,
|
|
2563
|
+
triggerType: 'Manual',
|
|
2564
|
+
startedAt: new Date().toISOString(),
|
|
2565
|
+
});
|
|
2566
|
+
emitter.runStart('Detached schema refresh launch');
|
|
2567
|
+
emitter.stageError('Launch', message, { code: 'schema-refresh-launch-failed' });
|
|
2568
|
+
void emitter.fail(`Schema refresh could not start: ${message}`, 'schema-refresh-launch-failed')
|
|
2569
|
+
.catch(e => LogError(`Detached schema refresh (run ${runID}): failure artifact write failed — ${e}`));
|
|
2570
|
+
} catch (e) {
|
|
2571
|
+
// Progress reporting must never mask the original failure, which is already logged above.
|
|
2572
|
+
LogError(`Detached schema refresh (run ${runID}): could not open failure artifact — ${e}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2416
2576
|
/**
|
|
2417
2577
|
* Rolls back a freshly created connection by deleting both the CompanyIntegration and Credential records.
|
|
2418
2578
|
*/
|
|
@@ -2584,6 +2744,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2584
2744
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2585
2745
|
@Arg("runSchemaRefresh", () => Boolean, { defaultValue: true, description: "When true (default) and TestConnection succeeds, automatically runs IntegrationConnectorCreationPipeline (live introspect → persist Declared/Discovered/Custom → SoftPKClassifier). The intermittent server-side work the wizard's Forward step represents." }) runSchemaRefresh: boolean,
|
|
2586
2746
|
@Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot). Improves SoftPKClassifier convergence." }) universalPKConvention: string | undefined,
|
|
2747
|
+
@Arg("awaitSchemaRefresh", () => Boolean, { defaultValue: true, description: "When false, the schema refresh is launched detached and this mutation returns immediately with SchemaRefresh.RunID + InProgress=true — tail that run instead of blocking on a minutes-long live introspect. The introspect happens exactly once either way, and always AFTER the connection test, so a failed test rolls back a connection that left no discovered schema behind. Default true preserves the blocking behaviour (counts returned inline)." }) awaitSchemaRefresh: boolean,
|
|
2587
2748
|
@Ctx() ctx: AppContext
|
|
2588
2749
|
): Promise<CreateConnectionOutput> {
|
|
2589
2750
|
try {
|
|
@@ -2606,16 +2767,25 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2606
2767
|
const credentialID = credential.ID;
|
|
2607
2768
|
|
|
2608
2769
|
// 2. Create CompanyIntegration linked to the Credential
|
|
2609
|
-
const ci = await md.GetEntityObject<
|
|
2770
|
+
const ci = await md.GetEntityObject<MJCompanyIntegrationEntityServer>('MJ: Company Integrations', user);
|
|
2610
2771
|
ci.NewRecord();
|
|
2611
2772
|
ci.IntegrationID = input.IntegrationID;
|
|
2612
2773
|
ci.CompanyID = input.CompanyID;
|
|
2613
2774
|
ci.CredentialID = credentialID;
|
|
2614
|
-
|
|
2775
|
+
// Created INACTIVE on purpose (#3738). A connection is not marked
|
|
2776
|
+
// active until its credential has had a chance to prove itself in
|
|
2777
|
+
// step 3 below — so nothing downstream ever observes an active row
|
|
2778
|
+
// backed by a password that is about to be rejected and rolled back.
|
|
2779
|
+
ci.IsActive = false;
|
|
2615
2780
|
ci.Name = input.CredentialName; // Name is required on CompanyIntegration
|
|
2616
2781
|
if (input.ExternalSystemID) ci.ExternalSystemID = input.ExternalSystemID;
|
|
2617
2782
|
if (input.Configuration) ci.Configuration = input.Configuration;
|
|
2618
2783
|
|
|
2784
|
+
// Whether the refresh runs detached (this resolver owns it and returns a tailable RunID)
|
|
2785
|
+
// or inline below. No save-side refresh exists to coordinate with any more — #3738 removed
|
|
2786
|
+
// the activation hook, so every refresh on this path is one this method asked for.
|
|
2787
|
+
const ownSchemaRefresh = runSchemaRefresh && !awaitSchemaRefresh;
|
|
2788
|
+
|
|
2619
2789
|
const saved = await ci.Save();
|
|
2620
2790
|
if (!saved) {
|
|
2621
2791
|
const validationErrors = ci.LatestResult?.Message || 'Unknown validation error';
|
|
@@ -2640,12 +2810,27 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2640
2810
|
testMessage = testResult.Message;
|
|
2641
2811
|
}
|
|
2642
2812
|
|
|
2813
|
+
// 3b. Credential has proven itself (or the caller declined a test,
|
|
2814
|
+
// which is the pre-existing default) — activate. Ordering matters:
|
|
2815
|
+
// this save is what makes the connection usable, and it must not
|
|
2816
|
+
// happen while the credential is still unverified.
|
|
2817
|
+
ci.IsActive = true;
|
|
2818
|
+
if (!await ci.Save()) {
|
|
2819
|
+
await this.rollbackCreatedConnection(ci, credential);
|
|
2820
|
+
return {
|
|
2821
|
+
Success: false,
|
|
2822
|
+
Message: `Failed to activate CompanyIntegration: ${ci.LatestResult?.Message || 'Unknown error'}`,
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2643
2826
|
// 4. Auto-run schema refresh pipeline (intermittent server-side period).
|
|
2644
2827
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2645
2828
|
// caller also asked for a test. The wizard may have tested separately
|
|
2646
2829
|
// and just be hitting Create to save.
|
|
2647
2830
|
let schemaRefreshSummary: CreateConnectionPipelineSummary | undefined;
|
|
2648
|
-
if (
|
|
2831
|
+
if (ownSchemaRefresh) {
|
|
2832
|
+
schemaRefreshSummary = this.startSchemaRefreshPipelineDetached(ci.ID, user, md, universalPKConvention);
|
|
2833
|
+
} else if (runSchemaRefresh) {
|
|
2649
2834
|
try {
|
|
2650
2835
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2651
2836
|
ci.ID, user, md, universalPKConvention
|
|
@@ -2661,9 +2846,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2661
2846
|
if (testConnection || schemaRefreshSummary) {
|
|
2662
2847
|
return {
|
|
2663
2848
|
Success: true,
|
|
2664
|
-
Message: schemaRefreshSummary
|
|
2665
|
-
? `Connection created${testConnection ? ', test passed' : ''}, schema refresh: ${schemaRefreshSummary.ObjectsCreated} created, ${schemaRefreshSummary.ObjectsUpdated} updated, ${schemaRefreshSummary.UnresolvedObjects.length} PK-unresolved`
|
|
2666
|
-
: 'Connection created and test passed',
|
|
2849
|
+
Message: BuildCreateConnectionMessage(testConnection, schemaRefreshSummary),
|
|
2667
2850
|
CompanyIntegrationID: ci.ID,
|
|
2668
2851
|
CredentialID: credentialID,
|
|
2669
2852
|
ConnectionTestSuccess: testPassed,
|
|
@@ -2696,6 +2879,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2696
2879
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2697
2880
|
@Arg("runSchemaRefresh", () => Boolean, { defaultValue: true, description: "When true (default) and TestConnection succeeds, automatically runs IntegrationConnectorCreationPipeline. Same intermittent server-side step as the create flow." }) runSchemaRefresh: boolean,
|
|
2698
2881
|
@Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot)" }) universalPKConvention: string | undefined,
|
|
2882
|
+
@Arg("awaitSchemaRefresh", () => Boolean, { defaultValue: true, description: "When false, the schema refresh is launched detached and this mutation returns immediately with the run ID to tail instead of blocking on a minutes-long live introspect. Default true preserves the blocking behaviour." }) awaitSchemaRefresh: boolean,
|
|
2699
2883
|
@Ctx() ctx: AppContext
|
|
2700
2884
|
): Promise<MutationResultOutput> {
|
|
2701
2885
|
try {
|
|
@@ -2742,15 +2926,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2742
2926
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2743
2927
|
// caller also asked for a test — the wizard may have tested separately
|
|
2744
2928
|
// already and is just hitting Update to save edits.
|
|
2929
|
+
if (runSchemaRefresh && !awaitSchemaRefresh) {
|
|
2930
|
+
const detached = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md, universalPKConvention);
|
|
2931
|
+
return { Success: true, Message: BuildDetachedRefreshMessage(detached.RunID) };
|
|
2932
|
+
}
|
|
2745
2933
|
if (runSchemaRefresh) {
|
|
2746
2934
|
try {
|
|
2747
2935
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2748
2936
|
companyIntegrationID, user, md, universalPKConvention
|
|
2749
2937
|
);
|
|
2750
|
-
return {
|
|
2751
|
-
Success: true,
|
|
2752
|
-
Message: `Updated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
|
|
2753
|
-
};
|
|
2938
|
+
return { Success: true, Message: BuildUpdateConnectionMessage(refreshResult) };
|
|
2754
2939
|
} catch (refreshErr) {
|
|
2755
2940
|
LogError(`IntegrationUpdateConnection: pipeline error — ${refreshErr}`);
|
|
2756
2941
|
return { Success: true, Message: `Updated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
@@ -2793,6 +2978,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2793
2978
|
set('crossLayerPipeline', config.CrossLayerPipeline);
|
|
2794
2979
|
set('partitionReconcile', config.PartitionReconcile);
|
|
2795
2980
|
set('discoveryTimeBudgetMs', config.DiscoveryTimeBudgetMs);
|
|
2981
|
+
set('fetchTimeoutMs', config.FetchTimeoutMs);
|
|
2796
2982
|
set('discoveryBatchSize', config.DiscoveryBatchSize);
|
|
2797
2983
|
set('discoveryMaxRecords', config.DiscoveryMaxRecords);
|
|
2798
2984
|
set('deactivateAbsent', config.DeactivateAbsent);
|
|
@@ -2848,6 +3034,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2848
3034
|
CrossLayerPipeline: bool(cfg.crossLayerPipeline),
|
|
2849
3035
|
PartitionReconcile: bool(cfg.partitionReconcile),
|
|
2850
3036
|
DiscoveryTimeBudgetMs: num(cfg.discoveryTimeBudgetMs),
|
|
3037
|
+
FetchTimeoutMs: num(cfg.fetchTimeoutMs),
|
|
2851
3038
|
DiscoveryBatchSize: num(cfg.discoveryBatchSize),
|
|
2852
3039
|
DiscoveryMaxRecords: num(cfg.discoveryMaxRecords),
|
|
2853
3040
|
DeactivateAbsent: bool(cfg.deactivateAbsent),
|
|
@@ -2957,10 +3144,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2957
3144
|
|
|
2958
3145
|
/**
|
|
2959
3146
|
* Reactivates a previously deactivated CompanyIntegration by setting IsActive=true.
|
|
3147
|
+
*
|
|
3148
|
+
* Before #3738 this mutation inherited a full schema refresh from the
|
|
3149
|
+
* CompanyIntegration save hook, with no way to decline it. The refresh is
|
|
3150
|
+
* now this resolver's own, explicit step — same default behaviour, but
|
|
3151
|
+
* visible in the API and suppressible with `runSchemaRefresh: false`.
|
|
2960
3152
|
*/
|
|
2961
3153
|
@Mutation(() => MutationResultOutput)
|
|
2962
3154
|
async IntegrationReactivateConnection(
|
|
2963
3155
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
3156
|
+
@Arg("runSchemaRefresh", () => Boolean, { defaultValue: true, description: "When true (default), re-runs IntegrationConnectorCreationPipeline after reactivating so the catalog reflects the source as it is now. Pass false to reactivate without a live scan — useful when the catalog is known-current, or when the source is large enough that discovery should be scheduled separately." }) runSchemaRefresh: boolean,
|
|
2964
3157
|
@Ctx() ctx: AppContext
|
|
2965
3158
|
): Promise<MutationResultOutput> {
|
|
2966
3159
|
try {
|
|
@@ -2971,6 +3164,23 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2971
3164
|
if (!loaded) return { Success: false, Message: 'CompanyIntegration not found' };
|
|
2972
3165
|
ci.IsActive = true;
|
|
2973
3166
|
if (!await ci.Save()) return { Success: false, Message: `Failed to reactivate: ${ci.LatestResult?.Message ?? 'Unknown error'}` };
|
|
3167
|
+
|
|
3168
|
+
if (runSchemaRefresh) {
|
|
3169
|
+
try {
|
|
3170
|
+
const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md);
|
|
3171
|
+
return {
|
|
3172
|
+
Success: true,
|
|
3173
|
+
Message: `Reactivated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
|
|
3174
|
+
};
|
|
3175
|
+
} catch (refreshErr) {
|
|
3176
|
+
// Refresh failure does NOT undo the reactivation — the
|
|
3177
|
+
// connection is active, only discovery failed, and the
|
|
3178
|
+
// operator can re-run IntegrationRefreshConnectorSchema.
|
|
3179
|
+
LogError(`IntegrationReactivateConnection: pipeline error — ${refreshErr}`);
|
|
3180
|
+
return { Success: true, Message: `Reactivated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
|
|
2974
3184
|
return { Success: true, Message: 'Reactivated' };
|
|
2975
3185
|
} catch (e) {
|
|
2976
3186
|
LogError(`IntegrationReactivateConnection error: ${e}`);
|
|
@@ -3388,12 +3598,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3388
3598
|
);
|
|
3389
3599
|
|
|
3390
3600
|
// Step 4: Inject integration post-restart payload into RSU input.
|
|
3391
|
-
|
|
3392
|
-
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
3393
|
-
const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
|
|
3394
|
-
const pendingFilePath = join(pendingWorkDir, `${Date.now()}.json`);
|
|
3395
|
-
|
|
3396
|
-
// Build per-object field map for pending file (null = all fields).
|
|
3601
|
+
// Build per-object field map for the pending payload (null = all fields).
|
|
3397
3602
|
// resolved.sourceObjects is order-aligned with resolvedNames after the
|
|
3398
3603
|
// resolveSourceObjectsToNames refactor — pair them directly instead
|
|
3399
3604
|
// of looking up by ID (which broke for name-only selections).
|
|
@@ -3416,7 +3621,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3416
3621
|
const hadPriorMaps = priorMapsResult.Success && priorMapsResult.Results.length > 0;
|
|
3417
3622
|
const effectiveFullSync = input.FullSync ?? !hadPriorMaps;
|
|
3418
3623
|
|
|
3419
|
-
const pendingPayload = {
|
|
3624
|
+
const pendingPayload: RSUPendingWork = {
|
|
3420
3625
|
CompanyIntegrationID: input.CompanyIntegrationID,
|
|
3421
3626
|
SourceObjectNames: resolvedNames,
|
|
3422
3627
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -3425,13 +3630,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3425
3630
|
ScheduleTimezone: input.ScheduleTimezone,
|
|
3426
3631
|
StartSync: input.StartSync,
|
|
3427
3632
|
FullSync: effectiveFullSync,
|
|
3428
|
-
SyncScope: input.SyncScope
|
|
3633
|
+
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
3429
3634
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
3430
3635
|
CreatedAt: new Date().toISOString(),
|
|
3431
3636
|
};
|
|
3432
|
-
rsuInput.
|
|
3433
|
-
|
|
3434
|
-
];
|
|
3637
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
3638
|
+
rsuInput.ContextUser = user;
|
|
3435
3639
|
|
|
3436
3640
|
// Step 5: Run pipeline (restart kills process at the end).
|
|
3437
3641
|
// Retrying variant — an install should not die because the migration hit a dropped
|
|
@@ -3444,9 +3648,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3444
3648
|
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
3445
3649
|
}));
|
|
3446
3650
|
|
|
3447
|
-
//
|
|
3651
|
+
// Pending work is registered only for migrations that succeeded, so a
|
|
3652
|
+
// failed pipeline has nothing to clean up.
|
|
3448
3653
|
if (!migrationSucceeded) {
|
|
3449
|
-
try { (await import('node:fs')).unlinkSync(pendingFilePath); } catch { /* may not exist */ }
|
|
3450
3654
|
return {
|
|
3451
3655
|
Success: false,
|
|
3452
3656
|
Message: `Pipeline failed: ${batchResult.Results[0]?.ErrorMessage ?? 'unknown error'}`,
|
|
@@ -3504,7 +3708,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3504
3708
|
}
|
|
3505
3709
|
}
|
|
3506
3710
|
|
|
3507
|
-
|
|
3711
|
+
// The work just happened inline (no restart), so close out its durable rows.
|
|
3712
|
+
for (const pendingWorkID of batchResult.Results[0]?.PendingWorkIDs ?? []) {
|
|
3713
|
+
await rsm.CompletePendingWork(pendingWorkID, user);
|
|
3714
|
+
}
|
|
3508
3715
|
|
|
3509
3716
|
return {
|
|
3510
3717
|
Success: true,
|
|
@@ -4093,6 +4300,60 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4093
4300
|
}
|
|
4094
4301
|
}
|
|
4095
4302
|
|
|
4303
|
+
/**
|
|
4304
|
+
* Enqueue a sync for worker execution (PR 1 item 8) instead of running it in this
|
|
4305
|
+
* process. Creates the run row with `Status='Queued'` and returns its ID immediately —
|
|
4306
|
+
* so the caller gets a real, trackable RunID with no fire-and-forget polling window,
|
|
4307
|
+
* and the sync survives this process going away before it starts.
|
|
4308
|
+
*
|
|
4309
|
+
* Requires a process running the integration sync worker (`integrationSyncWorker.enabled`);
|
|
4310
|
+
* without one, rows accumulate in the queue unexecuted.
|
|
4311
|
+
*/
|
|
4312
|
+
@Mutation(() => StartSyncOutput)
|
|
4313
|
+
@RequireSystemUser()
|
|
4314
|
+
async IntegrationEnqueueSync(
|
|
4315
|
+
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
4316
|
+
@Arg("fullSync", () => Boolean, { defaultValue: false, description: 'If true, ignores watermarks and re-fetches all records from the source' }) fullSync: boolean,
|
|
4317
|
+
@Arg("entityMapIDs", () => [String], { nullable: true, description: 'Optional: sync only these entity maps. If omitted, syncs all maps for the connector.' }) entityMapIDs: string[],
|
|
4318
|
+
@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,
|
|
4319
|
+
@Ctx() ctx: AppContext
|
|
4320
|
+
): Promise<StartSyncOutput> {
|
|
4321
|
+
try {
|
|
4322
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4323
|
+
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
4324
|
+
await IntegrationEngine.Instance.Config(false, user, provider);
|
|
4325
|
+
|
|
4326
|
+
// Same upfront gates as IntegrationStartSync — refusing here beats queueing work
|
|
4327
|
+
// that the engine will only refuse later, in another process, out of the caller's sight.
|
|
4328
|
+
const ciCheck = await provider.GetEntityObject<MJCompanyIntegrationEntity>('MJ: Company Integrations', user);
|
|
4329
|
+
if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
|
|
4330
|
+
return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not enqueued' };
|
|
4331
|
+
}
|
|
4332
|
+
const maintenance = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
4333
|
+
if (maintenance) {
|
|
4334
|
+
return { Success: false, Message: `Sync not enqueued: ${maintenance.Reason} is in progress for this connection (since ${maintenance.AcquiredAt.toISOString()}). Retry after it completes.` };
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
const syncOptions: IntegrationSyncOptions = {};
|
|
4338
|
+
if (fullSync) syncOptions.FullSync = true;
|
|
4339
|
+
if (entityMapIDs?.length) syncOptions.EntityMapIDs = entityMapIDs;
|
|
4340
|
+
if (syncDirection) syncOptions.SyncDirection = syncDirection;
|
|
4341
|
+
|
|
4342
|
+
const runID = await IntegrationEngine.Instance.EnqueueSync(
|
|
4343
|
+
companyIntegrationID,
|
|
4344
|
+
user,
|
|
4345
|
+
'Manual',
|
|
4346
|
+
Object.keys(syncOptions).length > 0 ? syncOptions : undefined,
|
|
4347
|
+
provider
|
|
4348
|
+
);
|
|
4349
|
+
|
|
4350
|
+
return { Success: true, Message: 'Sync enqueued', RunID: runID };
|
|
4351
|
+
} catch (e) {
|
|
4352
|
+
LogError(`IntegrationEnqueueSync error: ${e}`);
|
|
4353
|
+
return { Success: false, Message: this.formatError(e) };
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4096
4357
|
/**
|
|
4097
4358
|
* Cancels a running sync by marking its status as Cancelled.
|
|
4098
4359
|
*/
|
|
@@ -4102,10 +4363,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4102
4363
|
@Ctx() ctx: AppContext
|
|
4103
4364
|
): Promise<MutationResultOutput> {
|
|
4104
4365
|
try {
|
|
4105
|
-
this.getAuthenticatedUser(ctx);
|
|
4366
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4367
|
+
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
4106
4368
|
|
|
4107
|
-
//
|
|
4108
|
-
|
|
4369
|
+
// DB-backed cancel (durable runs): stamps CancelRequestedAt on the live run row, so
|
|
4370
|
+
// the request reaches the owning process wherever it is — this server, another API
|
|
4371
|
+
// node, or a worker — via its heartbeat/boundary checks. No in-memory signal.
|
|
4372
|
+
const cancelled = await IntegrationEngine.CancelSyncAsync(companyIntegrationID, user, provider);
|
|
4109
4373
|
if (!cancelled) {
|
|
4110
4374
|
return { Success: false, Message: 'No active sync found for this connector' };
|
|
4111
4375
|
}
|
|
@@ -4702,8 +4966,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4702
4966
|
@Ctx() ctx: AppContext
|
|
4703
4967
|
): Promise<OperationProgressOutput> {
|
|
4704
4968
|
try {
|
|
4705
|
-
this.getAuthenticatedUser(ctx);
|
|
4706
|
-
const
|
|
4969
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4970
|
+
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
4971
|
+
// DB-backed progress (durable runs): reads ProgressJSON from the live run row, so
|
|
4972
|
+
// progress is visible from ANY process — not just the one executing the sync.
|
|
4973
|
+
const syncProgress = await IntegrationEngine.GetSyncProgressAsync(companyIntegrationID, user, provider);
|
|
4707
4974
|
if (syncProgress) {
|
|
4708
4975
|
return {
|
|
4709
4976
|
Success: true,
|
|
@@ -5226,18 +5493,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5226
5493
|
connInput.CompanyIntegrationID, objects, validatedPlatform, user, skipGitCommit, skipRestart, provider, sourceSchema
|
|
5227
5494
|
);
|
|
5228
5495
|
|
|
5229
|
-
// Build per-object field map for pending
|
|
5496
|
+
// Build per-object field map for the pending payload
|
|
5230
5497
|
const sourceObjectFields: Record<string, string[] | null> = {};
|
|
5231
5498
|
for (const name of resolvedNames) {
|
|
5232
5499
|
sourceObjectFields[name] = fieldsByName.get(name.toLowerCase()) ?? null;
|
|
5233
5500
|
}
|
|
5234
5501
|
|
|
5235
5502
|
// Inject post-restart pending work payload
|
|
5236
|
-
const
|
|
5237
|
-
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
5238
|
-
const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
|
|
5239
|
-
const pendingFilePath = join(pendingWorkDir, `${Date.now()}_${connInput.CompanyIntegrationID}.json`);
|
|
5240
|
-
const pendingPayload = {
|
|
5503
|
+
const pendingPayload: RSUPendingWork = {
|
|
5241
5504
|
CompanyIntegrationID: connInput.CompanyIntegrationID,
|
|
5242
5505
|
SourceObjectNames: resolvedNames,
|
|
5243
5506
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -5246,15 +5509,15 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5246
5509
|
ScheduleTimezone: connInput.ScheduleTimezone,
|
|
5247
5510
|
StartSync: input.StartSync,
|
|
5248
5511
|
FullSync: input.FullSync ?? false,
|
|
5249
|
-
SyncScope: input.SyncScope
|
|
5250
|
-
|
|
5251
|
-
|
|
5512
|
+
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
5513
|
+
// Unrecognized directions fall back to undefined = "use the entity map's own SyncDirection"
|
|
5514
|
+
SyncDirection: input.SyncDirection && isValidSyncDirection(input.SyncDirection) ? input.SyncDirection : undefined,
|
|
5515
|
+
ScheduleSyncDirection: input.ScheduleSyncDirection && isValidSyncDirection(input.ScheduleSyncDirection) ? input.ScheduleSyncDirection : undefined,
|
|
5252
5516
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
5253
5517
|
CreatedAt: new Date().toISOString(),
|
|
5254
5518
|
};
|
|
5255
|
-
rsuInput.
|
|
5256
|
-
|
|
5257
|
-
];
|
|
5519
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
5520
|
+
rsuInput.ContextUser = user;
|
|
5258
5521
|
|
|
5259
5522
|
return {
|
|
5260
5523
|
connInput,
|
|
@@ -5264,7 +5527,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5264
5527
|
objects,
|
|
5265
5528
|
schemaOutput,
|
|
5266
5529
|
rsuInput,
|
|
5267
|
-
pendingFilePath,
|
|
5268
5530
|
};
|
|
5269
5531
|
})
|
|
5270
5532
|
);
|
|
@@ -5278,7 +5540,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5278
5540
|
objects: SchemaPreviewObjectInput[];
|
|
5279
5541
|
schemaOutput: SchemaBuilderOutput;
|
|
5280
5542
|
rsuInput: RSUPipelineInput;
|
|
5281
|
-
pendingFilePath: string;
|
|
5282
5543
|
}> = [];
|
|
5283
5544
|
const connectorResults: ApplyAllBatchConnectorResult[] = [];
|
|
5284
5545
|
|
|
@@ -5330,8 +5591,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5330
5591
|
Message: pipelineResult?.ErrorMessage ?? 'Pipeline failed',
|
|
5331
5592
|
Warnings: build.schemaOutput.Warnings.length > 0 ? build.schemaOutput.Warnings : undefined,
|
|
5332
5593
|
});
|
|
5333
|
-
//
|
|
5334
|
-
|
|
5594
|
+
// No pending-work cleanup needed — rows are registered only for
|
|
5595
|
+
// migrations that succeeded.
|
|
5335
5596
|
continue;
|
|
5336
5597
|
}
|
|
5337
5598
|
|
|
@@ -5378,10 +5639,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5378
5639
|
if (scheduleResult) connResult.ScheduledJobID = scheduleResult;
|
|
5379
5640
|
}
|
|
5380
5641
|
|
|
5381
|
-
//
|
|
5382
|
-
|
|
5642
|
+
// The work just happened inline (no restart), so close out its durable rows.
|
|
5643
|
+
for (const pendingWorkID of pipelineResult.PendingWorkIDs ?? []) {
|
|
5644
|
+
await rsm.CompletePendingWork(pendingWorkID, user);
|
|
5645
|
+
}
|
|
5383
5646
|
|
|
5384
|
-
connResult.Message
|
|
5647
|
+
connResult.Message =`Applied ${build.objects.length} object(s) — ${entityMapsCreated.length} entity maps created${syncRunID ? ', sync started' : ''}`;
|
|
5385
5648
|
}
|
|
5386
5649
|
|
|
5387
5650
|
connectorResults.push(connResult);
|
|
@@ -5948,26 +6211,21 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5948
6211
|
// ApplyAll uses. default: created DISABLED (user enables after the refresh);
|
|
5949
6212
|
// autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
|
|
5950
6213
|
if (newObjects.length > 0 && !skipRestart) {
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
|
|
5954
|
-
rsuInput.PostRestartFiles = [
|
|
5955
|
-
...(rsuInput.PostRestartFiles ?? []),
|
|
6214
|
+
rsuInput.PendingWork = [
|
|
6215
|
+
...(rsuInput.PendingWork ?? []),
|
|
5956
6216
|
{
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
CreateDisabled: !autoEnableNewObjects,
|
|
5967
|
-
CreatedAt: new Date().toISOString(),
|
|
5968
|
-
}, null, 2),
|
|
6217
|
+
CompanyIntegrationID: companyIntegrationID,
|
|
6218
|
+
SourceObjectNames: newObjects,
|
|
6219
|
+
SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
|
|
6220
|
+
SchemaName: schemaName,
|
|
6221
|
+
StartSync: false,
|
|
6222
|
+
SyncScope: 'created',
|
|
6223
|
+
UnselectedAction: 'ignore',
|
|
6224
|
+
CreateDisabled: !autoEnableNewObjects,
|
|
6225
|
+
CreatedAt: new Date().toISOString(),
|
|
5969
6226
|
},
|
|
5970
6227
|
];
|
|
6228
|
+
rsuInput.ContextUser = user;
|
|
5971
6229
|
}
|
|
5972
6230
|
|
|
5973
6231
|
const rsm = RuntimeSchemaManager.Instance;
|