@memberjunction/server 6.1.0-edge.2 → 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/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +56 -13
- 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/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/generated/generated.d.ts +501 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2711 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +7 -4
- 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -14
- 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/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/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 +76 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
- package/dist/resolvers/IntegrationDiscoveryResolver.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/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/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/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 +129 -43
- package/dist/rest/OAuthCallbackHandler.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/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/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -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/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__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +59 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/config.ts +55 -36
- package/src/generated/generated.ts +1911 -10551
- package/src/generic/ResolverBase.ts +11 -4
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +108 -15
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- 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 +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/types.ts +6 -0
|
@@ -39,6 +39,7 @@ import type {
|
|
|
39
39
|
GenerateIntegrationActionResult
|
|
40
40
|
} from "@memberjunction/integration-engine";
|
|
41
41
|
import { IntegrationEngineBase } from "@memberjunction/integration-engine-base";
|
|
42
|
+
import { buildIntegrationLLMPKCallback } from "@memberjunction/core-entities-server";
|
|
42
43
|
import {
|
|
43
44
|
SchemaBuilder,
|
|
44
45
|
TypeMapper,
|
|
@@ -48,13 +49,17 @@ import {
|
|
|
48
49
|
ExistingTableInfo,
|
|
49
50
|
SchemaEvolution
|
|
50
51
|
} from "@memberjunction/integration-schema-builder";
|
|
51
|
-
import { RuntimeSchemaManager, type RSUPipelineStep, type RSUPipelineInput } from "@memberjunction/schema-engine";
|
|
52
|
+
import { RuntimeSchemaManager, type RSUPipelineStep, type RSUPipelineInput, type RSUPendingWork } from "@memberjunction/schema-engine";
|
|
52
53
|
import type { SchemaBuilderOutput } from "@memberjunction/integration-schema-builder";
|
|
53
|
-
import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
54
|
+
import { IntegrationProgressEmitter, IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
54
55
|
import type { IntegrationRunSnapshot, IntegrationRunKind } from "@memberjunction/integration-progress-artifacts";
|
|
55
56
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
56
57
|
import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
|
|
57
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";
|
|
58
63
|
import { AppContext } from "../types.js";
|
|
59
64
|
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
60
65
|
import { UserCache } from "@memberjunction/generic-database-provider";
|
|
@@ -569,6 +574,21 @@ class CreateConnectionInput {
|
|
|
569
574
|
@ObjectType()
|
|
570
575
|
class CreateConnectionPipelineSummary {
|
|
571
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;
|
|
572
592
|
@Field() ObjectsCreated: number;
|
|
573
593
|
@Field() ObjectsUpdated: number;
|
|
574
594
|
@Field() FieldsCreated: number;
|
|
@@ -613,6 +633,7 @@ class IntegrationSyncConfigInput {
|
|
|
613
633
|
@Field(() => Boolean, { nullable: true, description: '§4 cross-layer pipelining: a child map starts when ITS parents finish, not the whole layer.' }) CrossLayerPipeline?: boolean;
|
|
614
634
|
@Field(() => Boolean, { nullable: true, description: 'Merkle/partition hash-diff reconcile for watermark-less change detection (buffers the set in RAM).' }) PartitionReconcile?: boolean;
|
|
615
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;
|
|
616
637
|
@Field(() => Int, { nullable: true, description: 'Batch size for stage-2 streaming field discovery (records per FetchChanges page). Default 500.' }) DiscoveryBatchSize?: number;
|
|
617
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;
|
|
618
639
|
@Field(() => Boolean, { nullable: true, description: '§7 Comprehensive refresh deactivates Declared objects/fields ABSENT from an AUTHORITATIVE discovery (reversible). Default false.' }) DeactivateAbsent?: boolean;
|
|
@@ -631,6 +652,7 @@ class IntegrationSyncConfigOutput {
|
|
|
631
652
|
@Field(() => Boolean, { nullable: true }) CrossLayerPipeline?: boolean;
|
|
632
653
|
@Field(() => Boolean, { nullable: true }) PartitionReconcile?: boolean;
|
|
633
654
|
@Field(() => Int, { nullable: true }) DiscoveryTimeBudgetMs?: number;
|
|
655
|
+
@Field(() => Int, { nullable: true }) FetchTimeoutMs?: number;
|
|
634
656
|
@Field(() => Int, { nullable: true }) DiscoveryBatchSize?: number;
|
|
635
657
|
@Field(() => Int, { nullable: true }) DiscoveryMaxRecords?: number;
|
|
636
658
|
@Field(() => Boolean, { nullable: true }) DeactivateAbsent?: boolean;
|
|
@@ -946,7 +968,7 @@ class IntegrationRunEventsOutput {
|
|
|
946
968
|
@Field() IsInFlight: boolean;
|
|
947
969
|
}
|
|
948
970
|
|
|
949
|
-
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.
|
|
971
|
+
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgressAsync()
|
|
950
972
|
|
|
951
973
|
@ObjectType()
|
|
952
974
|
class ConnectionSummaryOutput {
|
|
@@ -2386,9 +2408,42 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2386
2408
|
user: UserInfo,
|
|
2387
2409
|
provider: IMetadataProvider,
|
|
2388
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,
|
|
2389
2438
|
): Promise<CreateConnectionPipelineSummary> {
|
|
2390
2439
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
|
|
2391
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);
|
|
2392
2447
|
const runOpts = {
|
|
2393
2448
|
Connector: connector,
|
|
2394
2449
|
CompanyIntegration: companyIntegration,
|
|
@@ -2397,6 +2452,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2397
2452
|
UniversalPKConvention: universalPKConvention || undefined,
|
|
2398
2453
|
ConsoleMirror: true,
|
|
2399
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,
|
|
2400
2459
|
};
|
|
2401
2460
|
const result = await pipeline.Run(runOpts as unknown as Parameters<typeof pipeline.Run>[0]);
|
|
2402
2461
|
|
|
@@ -2407,6 +2466,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2407
2466
|
|
|
2408
2467
|
return {
|
|
2409
2468
|
RunID: result.RunID,
|
|
2469
|
+
InProgress: false,
|
|
2470
|
+
Succeeded: result.Success,
|
|
2471
|
+
FailureMessage: result.FailureMessage,
|
|
2410
2472
|
ObjectsCreated: result.PersistResult?.ObjectsCreated ?? 0,
|
|
2411
2473
|
ObjectsUpdated: result.PersistResult?.ObjectsUpdated ?? 0,
|
|
2412
2474
|
FieldsCreated: result.PersistResult?.FieldsCreated ?? 0,
|
|
@@ -2423,6 +2485,94 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2423
2485
|
};
|
|
2424
2486
|
}
|
|
2425
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
|
+
|
|
2426
2576
|
/**
|
|
2427
2577
|
* Rolls back a freshly created connection by deleting both the CompanyIntegration and Credential records.
|
|
2428
2578
|
*/
|
|
@@ -2594,6 +2744,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2594
2744
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2595
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,
|
|
2596
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,
|
|
2597
2748
|
@Ctx() ctx: AppContext
|
|
2598
2749
|
): Promise<CreateConnectionOutput> {
|
|
2599
2750
|
try {
|
|
@@ -2616,16 +2767,25 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2616
2767
|
const credentialID = credential.ID;
|
|
2617
2768
|
|
|
2618
2769
|
// 2. Create CompanyIntegration linked to the Credential
|
|
2619
|
-
const ci = await md.GetEntityObject<
|
|
2770
|
+
const ci = await md.GetEntityObject<MJCompanyIntegrationEntityServer>('MJ: Company Integrations', user);
|
|
2620
2771
|
ci.NewRecord();
|
|
2621
2772
|
ci.IntegrationID = input.IntegrationID;
|
|
2622
2773
|
ci.CompanyID = input.CompanyID;
|
|
2623
2774
|
ci.CredentialID = credentialID;
|
|
2624
|
-
|
|
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;
|
|
2625
2780
|
ci.Name = input.CredentialName; // Name is required on CompanyIntegration
|
|
2626
2781
|
if (input.ExternalSystemID) ci.ExternalSystemID = input.ExternalSystemID;
|
|
2627
2782
|
if (input.Configuration) ci.Configuration = input.Configuration;
|
|
2628
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
|
+
|
|
2629
2789
|
const saved = await ci.Save();
|
|
2630
2790
|
if (!saved) {
|
|
2631
2791
|
const validationErrors = ci.LatestResult?.Message || 'Unknown validation error';
|
|
@@ -2650,12 +2810,27 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2650
2810
|
testMessage = testResult.Message;
|
|
2651
2811
|
}
|
|
2652
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
|
+
|
|
2653
2826
|
// 4. Auto-run schema refresh pipeline (intermittent server-side period).
|
|
2654
2827
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2655
2828
|
// caller also asked for a test. The wizard may have tested separately
|
|
2656
2829
|
// and just be hitting Create to save.
|
|
2657
2830
|
let schemaRefreshSummary: CreateConnectionPipelineSummary | undefined;
|
|
2658
|
-
if (
|
|
2831
|
+
if (ownSchemaRefresh) {
|
|
2832
|
+
schemaRefreshSummary = this.startSchemaRefreshPipelineDetached(ci.ID, user, md, universalPKConvention);
|
|
2833
|
+
} else if (runSchemaRefresh) {
|
|
2659
2834
|
try {
|
|
2660
2835
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2661
2836
|
ci.ID, user, md, universalPKConvention
|
|
@@ -2671,9 +2846,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2671
2846
|
if (testConnection || schemaRefreshSummary) {
|
|
2672
2847
|
return {
|
|
2673
2848
|
Success: true,
|
|
2674
|
-
Message: schemaRefreshSummary
|
|
2675
|
-
? `Connection created${testConnection ? ', test passed' : ''}, schema refresh: ${schemaRefreshSummary.ObjectsCreated} created, ${schemaRefreshSummary.ObjectsUpdated} updated, ${schemaRefreshSummary.UnresolvedObjects.length} PK-unresolved`
|
|
2676
|
-
: 'Connection created and test passed',
|
|
2849
|
+
Message: BuildCreateConnectionMessage(testConnection, schemaRefreshSummary),
|
|
2677
2850
|
CompanyIntegrationID: ci.ID,
|
|
2678
2851
|
CredentialID: credentialID,
|
|
2679
2852
|
ConnectionTestSuccess: testPassed,
|
|
@@ -2706,6 +2879,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2706
2879
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2707
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,
|
|
2708
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,
|
|
2709
2883
|
@Ctx() ctx: AppContext
|
|
2710
2884
|
): Promise<MutationResultOutput> {
|
|
2711
2885
|
try {
|
|
@@ -2752,15 +2926,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2752
2926
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2753
2927
|
// caller also asked for a test — the wizard may have tested separately
|
|
2754
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
|
+
}
|
|
2755
2933
|
if (runSchemaRefresh) {
|
|
2756
2934
|
try {
|
|
2757
2935
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2758
2936
|
companyIntegrationID, user, md, universalPKConvention
|
|
2759
2937
|
);
|
|
2760
|
-
return {
|
|
2761
|
-
Success: true,
|
|
2762
|
-
Message: `Updated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
|
|
2763
|
-
};
|
|
2938
|
+
return { Success: true, Message: BuildUpdateConnectionMessage(refreshResult) };
|
|
2764
2939
|
} catch (refreshErr) {
|
|
2765
2940
|
LogError(`IntegrationUpdateConnection: pipeline error — ${refreshErr}`);
|
|
2766
2941
|
return { Success: true, Message: `Updated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
@@ -2803,6 +2978,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2803
2978
|
set('crossLayerPipeline', config.CrossLayerPipeline);
|
|
2804
2979
|
set('partitionReconcile', config.PartitionReconcile);
|
|
2805
2980
|
set('discoveryTimeBudgetMs', config.DiscoveryTimeBudgetMs);
|
|
2981
|
+
set('fetchTimeoutMs', config.FetchTimeoutMs);
|
|
2806
2982
|
set('discoveryBatchSize', config.DiscoveryBatchSize);
|
|
2807
2983
|
set('discoveryMaxRecords', config.DiscoveryMaxRecords);
|
|
2808
2984
|
set('deactivateAbsent', config.DeactivateAbsent);
|
|
@@ -2858,6 +3034,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2858
3034
|
CrossLayerPipeline: bool(cfg.crossLayerPipeline),
|
|
2859
3035
|
PartitionReconcile: bool(cfg.partitionReconcile),
|
|
2860
3036
|
DiscoveryTimeBudgetMs: num(cfg.discoveryTimeBudgetMs),
|
|
3037
|
+
FetchTimeoutMs: num(cfg.fetchTimeoutMs),
|
|
2861
3038
|
DiscoveryBatchSize: num(cfg.discoveryBatchSize),
|
|
2862
3039
|
DiscoveryMaxRecords: num(cfg.discoveryMaxRecords),
|
|
2863
3040
|
DeactivateAbsent: bool(cfg.deactivateAbsent),
|
|
@@ -2967,10 +3144,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2967
3144
|
|
|
2968
3145
|
/**
|
|
2969
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`.
|
|
2970
3152
|
*/
|
|
2971
3153
|
@Mutation(() => MutationResultOutput)
|
|
2972
3154
|
async IntegrationReactivateConnection(
|
|
2973
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,
|
|
2974
3157
|
@Ctx() ctx: AppContext
|
|
2975
3158
|
): Promise<MutationResultOutput> {
|
|
2976
3159
|
try {
|
|
@@ -2981,6 +3164,23 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2981
3164
|
if (!loaded) return { Success: false, Message: 'CompanyIntegration not found' };
|
|
2982
3165
|
ci.IsActive = true;
|
|
2983
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
|
+
|
|
2984
3184
|
return { Success: true, Message: 'Reactivated' };
|
|
2985
3185
|
} catch (e) {
|
|
2986
3186
|
LogError(`IntegrationReactivateConnection error: ${e}`);
|
|
@@ -3398,12 +3598,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3398
3598
|
);
|
|
3399
3599
|
|
|
3400
3600
|
// Step 4: Inject integration post-restart payload into RSU input.
|
|
3401
|
-
|
|
3402
|
-
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
3403
|
-
const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
|
|
3404
|
-
const pendingFilePath = join(pendingWorkDir, `${Date.now()}.json`);
|
|
3405
|
-
|
|
3406
|
-
// Build per-object field map for pending file (null = all fields).
|
|
3601
|
+
// Build per-object field map for the pending payload (null = all fields).
|
|
3407
3602
|
// resolved.sourceObjects is order-aligned with resolvedNames after the
|
|
3408
3603
|
// resolveSourceObjectsToNames refactor — pair them directly instead
|
|
3409
3604
|
// of looking up by ID (which broke for name-only selections).
|
|
@@ -3426,7 +3621,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3426
3621
|
const hadPriorMaps = priorMapsResult.Success && priorMapsResult.Results.length > 0;
|
|
3427
3622
|
const effectiveFullSync = input.FullSync ?? !hadPriorMaps;
|
|
3428
3623
|
|
|
3429
|
-
const pendingPayload = {
|
|
3624
|
+
const pendingPayload: RSUPendingWork = {
|
|
3430
3625
|
CompanyIntegrationID: input.CompanyIntegrationID,
|
|
3431
3626
|
SourceObjectNames: resolvedNames,
|
|
3432
3627
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -3435,13 +3630,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3435
3630
|
ScheduleTimezone: input.ScheduleTimezone,
|
|
3436
3631
|
StartSync: input.StartSync,
|
|
3437
3632
|
FullSync: effectiveFullSync,
|
|
3438
|
-
SyncScope: input.SyncScope
|
|
3633
|
+
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
3439
3634
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
3440
3635
|
CreatedAt: new Date().toISOString(),
|
|
3441
3636
|
};
|
|
3442
|
-
rsuInput.
|
|
3443
|
-
|
|
3444
|
-
];
|
|
3637
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
3638
|
+
rsuInput.ContextUser = user;
|
|
3445
3639
|
|
|
3446
3640
|
// Step 5: Run pipeline (restart kills process at the end).
|
|
3447
3641
|
// Retrying variant — an install should not die because the migration hit a dropped
|
|
@@ -3454,9 +3648,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3454
3648
|
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
3455
3649
|
}));
|
|
3456
3650
|
|
|
3457
|
-
//
|
|
3651
|
+
// Pending work is registered only for migrations that succeeded, so a
|
|
3652
|
+
// failed pipeline has nothing to clean up.
|
|
3458
3653
|
if (!migrationSucceeded) {
|
|
3459
|
-
try { (await import('node:fs')).unlinkSync(pendingFilePath); } catch { /* may not exist */ }
|
|
3460
3654
|
return {
|
|
3461
3655
|
Success: false,
|
|
3462
3656
|
Message: `Pipeline failed: ${batchResult.Results[0]?.ErrorMessage ?? 'unknown error'}`,
|
|
@@ -3514,7 +3708,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3514
3708
|
}
|
|
3515
3709
|
}
|
|
3516
3710
|
|
|
3517
|
-
|
|
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
|
+
}
|
|
3518
3715
|
|
|
3519
3716
|
return {
|
|
3520
3717
|
Success: true,
|
|
@@ -4103,6 +4300,60 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4103
4300
|
}
|
|
4104
4301
|
}
|
|
4105
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
|
+
|
|
4106
4357
|
/**
|
|
4107
4358
|
* Cancels a running sync by marking its status as Cancelled.
|
|
4108
4359
|
*/
|
|
@@ -4112,10 +4363,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4112
4363
|
@Ctx() ctx: AppContext
|
|
4113
4364
|
): Promise<MutationResultOutput> {
|
|
4114
4365
|
try {
|
|
4115
|
-
this.getAuthenticatedUser(ctx);
|
|
4366
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4367
|
+
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
4116
4368
|
|
|
4117
|
-
//
|
|
4118
|
-
|
|
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);
|
|
4119
4373
|
if (!cancelled) {
|
|
4120
4374
|
return { Success: false, Message: 'No active sync found for this connector' };
|
|
4121
4375
|
}
|
|
@@ -4712,8 +4966,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4712
4966
|
@Ctx() ctx: AppContext
|
|
4713
4967
|
): Promise<OperationProgressOutput> {
|
|
4714
4968
|
try {
|
|
4715
|
-
this.getAuthenticatedUser(ctx);
|
|
4716
|
-
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);
|
|
4717
4974
|
if (syncProgress) {
|
|
4718
4975
|
return {
|
|
4719
4976
|
Success: true,
|
|
@@ -5236,18 +5493,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5236
5493
|
connInput.CompanyIntegrationID, objects, validatedPlatform, user, skipGitCommit, skipRestart, provider, sourceSchema
|
|
5237
5494
|
);
|
|
5238
5495
|
|
|
5239
|
-
// Build per-object field map for pending
|
|
5496
|
+
// Build per-object field map for the pending payload
|
|
5240
5497
|
const sourceObjectFields: Record<string, string[] | null> = {};
|
|
5241
5498
|
for (const name of resolvedNames) {
|
|
5242
5499
|
sourceObjectFields[name] = fieldsByName.get(name.toLowerCase()) ?? null;
|
|
5243
5500
|
}
|
|
5244
5501
|
|
|
5245
5502
|
// Inject post-restart pending work payload
|
|
5246
|
-
const
|
|
5247
|
-
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
5248
|
-
const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
|
|
5249
|
-
const pendingFilePath = join(pendingWorkDir, `${Date.now()}_${connInput.CompanyIntegrationID}.json`);
|
|
5250
|
-
const pendingPayload = {
|
|
5503
|
+
const pendingPayload: RSUPendingWork = {
|
|
5251
5504
|
CompanyIntegrationID: connInput.CompanyIntegrationID,
|
|
5252
5505
|
SourceObjectNames: resolvedNames,
|
|
5253
5506
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -5256,15 +5509,15 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5256
5509
|
ScheduleTimezone: connInput.ScheduleTimezone,
|
|
5257
5510
|
StartSync: input.StartSync,
|
|
5258
5511
|
FullSync: input.FullSync ?? false,
|
|
5259
|
-
SyncScope: input.SyncScope
|
|
5260
|
-
|
|
5261
|
-
|
|
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,
|
|
5262
5516
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
5263
5517
|
CreatedAt: new Date().toISOString(),
|
|
5264
5518
|
};
|
|
5265
|
-
rsuInput.
|
|
5266
|
-
|
|
5267
|
-
];
|
|
5519
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
5520
|
+
rsuInput.ContextUser = user;
|
|
5268
5521
|
|
|
5269
5522
|
return {
|
|
5270
5523
|
connInput,
|
|
@@ -5274,7 +5527,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5274
5527
|
objects,
|
|
5275
5528
|
schemaOutput,
|
|
5276
5529
|
rsuInput,
|
|
5277
|
-
pendingFilePath,
|
|
5278
5530
|
};
|
|
5279
5531
|
})
|
|
5280
5532
|
);
|
|
@@ -5288,7 +5540,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5288
5540
|
objects: SchemaPreviewObjectInput[];
|
|
5289
5541
|
schemaOutput: SchemaBuilderOutput;
|
|
5290
5542
|
rsuInput: RSUPipelineInput;
|
|
5291
|
-
pendingFilePath: string;
|
|
5292
5543
|
}> = [];
|
|
5293
5544
|
const connectorResults: ApplyAllBatchConnectorResult[] = [];
|
|
5294
5545
|
|
|
@@ -5340,8 +5591,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5340
5591
|
Message: pipelineResult?.ErrorMessage ?? 'Pipeline failed',
|
|
5341
5592
|
Warnings: build.schemaOutput.Warnings.length > 0 ? build.schemaOutput.Warnings : undefined,
|
|
5342
5593
|
});
|
|
5343
|
-
//
|
|
5344
|
-
|
|
5594
|
+
// No pending-work cleanup needed — rows are registered only for
|
|
5595
|
+
// migrations that succeeded.
|
|
5345
5596
|
continue;
|
|
5346
5597
|
}
|
|
5347
5598
|
|
|
@@ -5388,10 +5639,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5388
5639
|
if (scheduleResult) connResult.ScheduledJobID = scheduleResult;
|
|
5389
5640
|
}
|
|
5390
5641
|
|
|
5391
|
-
//
|
|
5392
|
-
|
|
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
|
+
}
|
|
5393
5646
|
|
|
5394
|
-
connResult.Message
|
|
5647
|
+
connResult.Message =`Applied ${build.objects.length} object(s) — ${entityMapsCreated.length} entity maps created${syncRunID ? ', sync started' : ''}`;
|
|
5395
5648
|
}
|
|
5396
5649
|
|
|
5397
5650
|
connectorResults.push(connResult);
|
|
@@ -5958,26 +6211,21 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5958
6211
|
// ApplyAll uses. default: created DISABLED (user enables after the refresh);
|
|
5959
6212
|
// autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
|
|
5960
6213
|
if (newObjects.length > 0 && !skipRestart) {
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
|
|
5964
|
-
rsuInput.PostRestartFiles = [
|
|
5965
|
-
...(rsuInput.PostRestartFiles ?? []),
|
|
6214
|
+
rsuInput.PendingWork = [
|
|
6215
|
+
...(rsuInput.PendingWork ?? []),
|
|
5966
6216
|
{
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
CreateDisabled: !autoEnableNewObjects,
|
|
5977
|
-
CreatedAt: new Date().toISOString(),
|
|
5978
|
-
}, 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(),
|
|
5979
6226
|
},
|
|
5980
6227
|
];
|
|
6228
|
+
rsuInput.ContextUser = user;
|
|
5981
6229
|
}
|
|
5982
6230
|
|
|
5983
6231
|
const rsm = RuntimeSchemaManager.Instance;
|