@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.4
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/agentSessions/SessionManager.d.ts +19 -5
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +27 -9
- package/dist/agentSessions/SessionManager.js.map +1 -1
- 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 +12 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +57 -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/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- 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.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +8 -7
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
- package/dist/generated/generated.d.ts +619 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3341 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +28 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +57 -8
- 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 +147 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +79 -15
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +472 -97
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/InactiveRowWarnings.d.ts +48 -0
- package/dist/integration/InactiveRowWarnings.d.ts.map +1 -0
- package/dist/integration/InactiveRowWarnings.js +57 -0
- package/dist/integration/InactiveRowWarnings.js.map +1 -0
- 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 +62 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +67 -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/AdhocQueryResolver.d.ts +4 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +20 -6
- package/dist/resolvers/AdhocQueryResolver.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/IdentityClaimRedemptionResolver.d.ts +37 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.d.ts.map +1 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js +220 -0
- package/dist/resolvers/IdentityClaimRedemptionResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +108 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +374 -87
- 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/resolvers/meetingRecordingRegistration.d.ts.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.js +3 -6
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -1
- 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 +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +84 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/CustomColumnPromoterOnePass.test.ts +387 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/InactiveRowWarnings.test.ts +87 -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__/ReactivateRefreshMessage.test.ts +85 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +147 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +55 -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/agentSessions/SessionManager.ts +27 -9
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +60 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +55 -36
- package/src/context.ts +26 -2
- package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +8 -7
- package/src/generated/generated.ts +2347 -10551
- package/src/generic/ResolverBase.ts +67 -8
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +165 -16
- package/src/integration/CustomColumnPromoter.ts +520 -98
- package/src/integration/InactiveRowWarnings.ts +85 -0
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +93 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/AdhocQueryResolver.ts +21 -6
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IdentityClaimRedemptionResolver.ts +184 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +390 -71
- 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/resolvers/meetingRecordingRegistration.ts +3 -7
- 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 +14 -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,18 @@ 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 { ComputeInactiveRowWarnings } from "../integration/InactiveRowWarnings.js";
|
|
60
|
+
import { BuildCreateConnectionMessage, BuildDetachedRefreshMessage, BuildReactivateMessage, BuildUpdateConnectionMessage } from "../integration/SchemaRefreshLaunch.js";
|
|
61
|
+
// Type-only: the registered runtime class for 'MJ: Company Integrations'. Lets the create path name the
|
|
62
|
+
// server subclass it actually gets back from GetEntityObject with a real type rather than a cast.
|
|
63
|
+
import type { MJCompanyIntegrationEntityServer } from "@memberjunction/core-entities-server";
|
|
58
64
|
import { AppContext } from "../types.js";
|
|
59
65
|
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
60
66
|
import { UserCache } from "@memberjunction/generic-database-provider";
|
|
@@ -569,6 +575,21 @@ class CreateConnectionInput {
|
|
|
569
575
|
@ObjectType()
|
|
570
576
|
class CreateConnectionPipelineSummary {
|
|
571
577
|
@Field() RunID: string;
|
|
578
|
+
/**
|
|
579
|
+
* True when the pipeline was launched detached (`awaitSchemaRefresh: false`) and is STILL RUNNING —
|
|
580
|
+
* every count below is a placeholder zero, not a result. Tail `RunID` via IntegrationTailRunEvents
|
|
581
|
+
* (or the IntegrationProgress subscription, kind='ConnectorCreation') for the real outcome.
|
|
582
|
+
*/
|
|
583
|
+
@Field() InProgress: boolean;
|
|
584
|
+
/**
|
|
585
|
+
* Whether the refresh pipeline itself succeeded. A pipeline that fails at ConnectionTest still
|
|
586
|
+
* RETURNS (it does not throw), with every count at zero — so counts alone cannot distinguish
|
|
587
|
+
* "found nothing to change" from "never got past the credential check". Always false while
|
|
588
|
+
* `InProgress` is true: a detached run's outcome is not known yet.
|
|
589
|
+
*/
|
|
590
|
+
@Field() Succeeded: boolean;
|
|
591
|
+
/** The pipeline's own failure reason when `Succeeded` is false and the run has finished. */
|
|
592
|
+
@Field({ nullable: true }) FailureMessage?: string;
|
|
572
593
|
@Field() ObjectsCreated: number;
|
|
573
594
|
@Field() ObjectsUpdated: number;
|
|
574
595
|
@Field() FieldsCreated: number;
|
|
@@ -613,6 +634,7 @@ class IntegrationSyncConfigInput {
|
|
|
613
634
|
@Field(() => Boolean, { nullable: true, description: '§4 cross-layer pipelining: a child map starts when ITS parents finish, not the whole layer.' }) CrossLayerPipeline?: boolean;
|
|
614
635
|
@Field(() => Boolean, { nullable: true, description: 'Merkle/partition hash-diff reconcile for watermark-less change detection (buffers the set in RAM).' }) PartitionReconcile?: boolean;
|
|
615
636
|
@Field(() => Int, { nullable: true, description: 'Time budget (ms) for stage-2 streaming field discovery before it stops and uses what it gathered.' }) DiscoveryTimeBudgetMs?: number;
|
|
637
|
+
@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
638
|
@Field(() => Int, { nullable: true, description: 'Batch size for stage-2 streaming field discovery (records per FetchChanges page). Default 500.' }) DiscoveryBatchSize?: number;
|
|
617
639
|
@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
640
|
@Field(() => Boolean, { nullable: true, description: '§7 Comprehensive refresh deactivates Declared objects/fields ABSENT from an AUTHORITATIVE discovery (reversible). Default false.' }) DeactivateAbsent?: boolean;
|
|
@@ -631,6 +653,7 @@ class IntegrationSyncConfigOutput {
|
|
|
631
653
|
@Field(() => Boolean, { nullable: true }) CrossLayerPipeline?: boolean;
|
|
632
654
|
@Field(() => Boolean, { nullable: true }) PartitionReconcile?: boolean;
|
|
633
655
|
@Field(() => Int, { nullable: true }) DiscoveryTimeBudgetMs?: number;
|
|
656
|
+
@Field(() => Int, { nullable: true }) FetchTimeoutMs?: number;
|
|
634
657
|
@Field(() => Int, { nullable: true }) DiscoveryBatchSize?: number;
|
|
635
658
|
@Field(() => Int, { nullable: true }) DiscoveryMaxRecords?: number;
|
|
636
659
|
@Field(() => Boolean, { nullable: true }) DeactivateAbsent?: boolean;
|
|
@@ -946,7 +969,7 @@ class IntegrationRunEventsOutput {
|
|
|
946
969
|
@Field() IsInFlight: boolean;
|
|
947
970
|
}
|
|
948
971
|
|
|
949
|
-
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.
|
|
972
|
+
// Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgressAsync()
|
|
950
973
|
|
|
951
974
|
@ObjectType()
|
|
952
975
|
class ConnectionSummaryOutput {
|
|
@@ -2316,6 +2339,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2316
2339
|
private buildSourceSchemaFromPersistedRows(
|
|
2317
2340
|
integrationID: string,
|
|
2318
2341
|
requestedNames?: string[],
|
|
2342
|
+
warningsOut?: string[],
|
|
2319
2343
|
): SourceSchemaInfo {
|
|
2320
2344
|
const engine = IntegrationEngineBase.Instance;
|
|
2321
2345
|
// ACTIVE-only materialization: an object/field a given tenant doesn't expose is marked
|
|
@@ -2334,10 +2358,15 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2334
2358
|
for (const io of ios) ioByID.set(io.ID, io.Name);
|
|
2335
2359
|
|
|
2336
2360
|
const result: SourceSchemaInfo = { Objects: [] };
|
|
2361
|
+
const fieldsByObjectName: Record<string, Array<{ Name: string; Status: string | null }>> = {};
|
|
2337
2362
|
for (const io of ios) {
|
|
2338
2363
|
if (filter && !filter.has(io.Name.toLowerCase())) continue;
|
|
2339
2364
|
// Active fields only — an inactive (source-absent / deactivated) field is not materialized.
|
|
2340
|
-
const
|
|
2365
|
+
const allFields = engine.GetIntegrationObjectFields(io.ID);
|
|
2366
|
+
const iofs = allFields.filter(iof => iof.Status === 'Active');
|
|
2367
|
+
// Remember what this object declared, active or not — the caller's warning collector
|
|
2368
|
+
// turns the difference into the one message that explains a column that never appeared.
|
|
2369
|
+
if (warningsOut) fieldsByObjectName[io.Name] = allFields.map(f => ({ Name: f.Name, Status: f.Status }));
|
|
2341
2370
|
|
|
2342
2371
|
const fields = iofs.map(iof => {
|
|
2343
2372
|
const targetIOName = iof.RelatedIntegrationObjectID
|
|
@@ -2378,6 +2407,18 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2378
2407
|
IncrementalWatermarkField: io.IncrementalWatermarkField ?? undefined,
|
|
2379
2408
|
});
|
|
2380
2409
|
}
|
|
2410
|
+
|
|
2411
|
+
// Declared-but-inactive rows this rebuild left out. The caller decides what to do with the
|
|
2412
|
+
// strings (the apply path puts them on its Warnings); computing them costs nothing when no
|
|
2413
|
+
// collector was passed, since the field lists above are only gathered then.
|
|
2414
|
+
if (warningsOut) {
|
|
2415
|
+
warningsOut.push(...ComputeInactiveRowWarnings({
|
|
2416
|
+
RequestedNames: requestedNames ?? null,
|
|
2417
|
+
AllObjects: engine.GetIntegrationObjectsByIntegrationID(integrationID)
|
|
2418
|
+
.map(io => ({ Name: io.Name, Status: io.Status })),
|
|
2419
|
+
FieldsByObjectName: fieldsByObjectName,
|
|
2420
|
+
}));
|
|
2421
|
+
}
|
|
2381
2422
|
return result;
|
|
2382
2423
|
}
|
|
2383
2424
|
|
|
@@ -2386,9 +2427,42 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2386
2427
|
user: UserInfo,
|
|
2387
2428
|
provider: IMetadataProvider,
|
|
2388
2429
|
universalPKConvention?: string,
|
|
2430
|
+
runID?: string,
|
|
2431
|
+
): Promise<CreateConnectionPipelineSummary> {
|
|
2432
|
+
// Sync lock (#3656). This pipeline rewrites the very IO/IOF rows, field
|
|
2433
|
+
// maps and DDL a sync reads, so it must not overlap a sync or another
|
|
2434
|
+
// maintenance operation for the same connection. The two other
|
|
2435
|
+
// pipeline call sites — IntegrationRefreshConnectorSchema and
|
|
2436
|
+
// IntegrationSchemaEvolution — already take this lock inline; this is
|
|
2437
|
+
// the third and fourth (create/update/reactivate) finally taking it too.
|
|
2438
|
+
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema refresh')) {
|
|
2439
|
+
throw new Error(
|
|
2440
|
+
'Schema refresh not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.'
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
try {
|
|
2444
|
+
return await this.runSchemaRefreshPipelineLocked(companyIntegrationID, user, provider, universalPKConvention, runID);
|
|
2445
|
+
} finally {
|
|
2446
|
+
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/** The body of {@link runSchemaRefreshPipeline}, run with the maintenance lock held. */
|
|
2451
|
+
private async runSchemaRefreshPipelineLocked(
|
|
2452
|
+
companyIntegrationID: string,
|
|
2453
|
+
user: UserInfo,
|
|
2454
|
+
provider: IMetadataProvider,
|
|
2455
|
+
universalPKConvention?: string,
|
|
2456
|
+
runID?: string,
|
|
2389
2457
|
): Promise<CreateConnectionPipelineSummary> {
|
|
2390
2458
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
|
|
2391
2459
|
const pipeline = new IntegrationConnectorCreationPipeline();
|
|
2460
|
+
// SoftPKClassifier's LLM tier (universal → naming → statistical → LLM →
|
|
2461
|
+
// synthetic) only runs when this callback is supplied. It used to be
|
|
2462
|
+
// wired by the CompanyIntegration save hook; with that hook gone
|
|
2463
|
+
// (#3738) it is wired here, so the create/update/reactivate paths keep
|
|
2464
|
+
// the same PK-inference quality they had before.
|
|
2465
|
+
const llmInference = await buildIntegrationLLMPKCallback(user);
|
|
2392
2466
|
const runOpts = {
|
|
2393
2467
|
Connector: connector,
|
|
2394
2468
|
CompanyIntegration: companyIntegration,
|
|
@@ -2397,6 +2471,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2397
2471
|
UniversalPKConvention: universalPKConvention || undefined,
|
|
2398
2472
|
ConsoleMirror: true,
|
|
2399
2473
|
TriggerType: 'Manual' as const,
|
|
2474
|
+
LLMInference: llmInference ?? undefined,
|
|
2475
|
+
// Caller-supplied runID: lets the detached path hand a tailable ID back to the client
|
|
2476
|
+
// BEFORE the pipeline has done any work. Omitted ⇒ the pipeline generates its own.
|
|
2477
|
+
RunID: runID,
|
|
2400
2478
|
};
|
|
2401
2479
|
const result = await pipeline.Run(runOpts as unknown as Parameters<typeof pipeline.Run>[0]);
|
|
2402
2480
|
|
|
@@ -2407,6 +2485,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2407
2485
|
|
|
2408
2486
|
return {
|
|
2409
2487
|
RunID: result.RunID,
|
|
2488
|
+
InProgress: false,
|
|
2489
|
+
Succeeded: result.Success,
|
|
2490
|
+
FailureMessage: result.FailureMessage,
|
|
2410
2491
|
ObjectsCreated: result.PersistResult?.ObjectsCreated ?? 0,
|
|
2411
2492
|
ObjectsUpdated: result.PersistResult?.ObjectsUpdated ?? 0,
|
|
2412
2493
|
FieldsCreated: result.PersistResult?.FieldsCreated ?? 0,
|
|
@@ -2423,6 +2504,94 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2423
2504
|
};
|
|
2424
2505
|
}
|
|
2425
2506
|
|
|
2507
|
+
/**
|
|
2508
|
+
* Launches the schema-refresh pipeline WITHOUT awaiting it and returns a summary carrying the
|
|
2509
|
+
* tailable `RunID` and `InProgress: true`.
|
|
2510
|
+
*
|
|
2511
|
+
* Why: the pipeline is a live vendor introspect — minutes on a large catalog (HubSpot: 130
|
|
2512
|
+
* objects). Awaiting it inline holds the create/update mutation open for that entire time, so the
|
|
2513
|
+
* connection wizard's Finish button sits on an indeterminate spinner with no way to tell progress
|
|
2514
|
+
* from a hang. The pipeline already writes a complete, durable, per-run event stream keyed by its
|
|
2515
|
+
* runID; the only thing missing was handing that ID to the caller BEFORE the work starts. Hence
|
|
2516
|
+
* the caller-supplied runID.
|
|
2517
|
+
*
|
|
2518
|
+
* Trade-off the caller opts into: the mutation no longer reports what the refresh found (counts
|
|
2519
|
+
* are placeholder zeros) and a refresh failure no longer surfaces in the mutation's Message — both
|
|
2520
|
+
* live on the run stream instead. That is why `awaitSchemaRefresh` defaults to true.
|
|
2521
|
+
*
|
|
2522
|
+
* ON CREATE this is the only refresh that runs. #3738 removed the activation hook that used to
|
|
2523
|
+
* fire the same pipeline inside `Save()`, awaited — which had made the create mutation pay a full
|
|
2524
|
+
* introspect before ever reaching this method, and had run it BEFORE the connection test, so a
|
|
2525
|
+
* rejected connection was rolled back with its discovered schema already written.
|
|
2526
|
+
*
|
|
2527
|
+
* Belt and braces: if the launch below IS coalesced onto some other run for this connection (a
|
|
2528
|
+
* concurrent refresh, a repeat click), the pipeline publishes a terminal alias run under `runID`
|
|
2529
|
+
* naming the run that served it — so the ID handed back here is tailable in every case.
|
|
2530
|
+
*/
|
|
2531
|
+
private startSchemaRefreshPipelineDetached(
|
|
2532
|
+
companyIntegrationID: string,
|
|
2533
|
+
user: UserInfo,
|
|
2534
|
+
provider: IMetadataProvider,
|
|
2535
|
+
universalPKConvention?: string,
|
|
2536
|
+
): CreateConnectionPipelineSummary {
|
|
2537
|
+
const runID = IntegrationProgressEmitter.newRunID('connector');
|
|
2538
|
+
// Deliberately not awaited. Once the pipeline is running it records every outcome on this
|
|
2539
|
+
// run's artifact stream under `runID` itself — but it can also throw BEFORE it ever
|
|
2540
|
+
// constructs its emitter (connector-driver resolution, a missing Integration row), and in
|
|
2541
|
+
// the blocking path that throw surfaced in the mutation's Message. Detached, the caller has
|
|
2542
|
+
// already been handed `runID` and has nothing but the stream to watch, so a pre-emitter
|
|
2543
|
+
// throw must be published onto that stream — otherwise the run never appears at all and a
|
|
2544
|
+
// tailer waits forever on a run ID that will never produce an event.
|
|
2545
|
+
void this.runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention, runID)
|
|
2546
|
+
.catch(err => this.publishDetachedLaunchFailure(runID, companyIntegrationID, err));
|
|
2547
|
+
return {
|
|
2548
|
+
RunID: runID,
|
|
2549
|
+
InProgress: true,
|
|
2550
|
+
// Not known yet — the run has only just been launched.
|
|
2551
|
+
Succeeded: false,
|
|
2552
|
+
ObjectsCreated: 0,
|
|
2553
|
+
ObjectsUpdated: 0,
|
|
2554
|
+
FieldsCreated: 0,
|
|
2555
|
+
FieldsUpdated: 0,
|
|
2556
|
+
UnresolvedObjects: [],
|
|
2557
|
+
PKVerdicts: [],
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/**
|
|
2562
|
+
* Terminates a detached run that failed BEFORE the pipeline could open its own artifact stream.
|
|
2563
|
+
*
|
|
2564
|
+
* The pipeline constructs its emitter inside `Run()`, so anything that throws on the way there —
|
|
2565
|
+
* connector-driver resolution (`No connector registered for driver class "X"`), a missing
|
|
2566
|
+
* Integration row, a CompanyIntegration that vanished — produces no run directory at all. In the
|
|
2567
|
+
* blocking path that throw surfaced in the mutation's Message. Detached, the caller has already
|
|
2568
|
+
* been handed the run ID and has only the stream to watch, so the failure has to be published
|
|
2569
|
+
* there or the run is invisible forever.
|
|
2570
|
+
*
|
|
2571
|
+
* Only ever called on the rejection path: if the pipeline got far enough to build its own emitter
|
|
2572
|
+
* it terminates its own run, and a pipeline that completed never reaches here.
|
|
2573
|
+
*/
|
|
2574
|
+
private publishDetachedLaunchFailure(runID: string, companyIntegrationID: string, err: unknown): void {
|
|
2575
|
+
const message = this.formatError(err);
|
|
2576
|
+
LogError(`Detached schema refresh (run ${runID}) failed: ${message}`);
|
|
2577
|
+
try {
|
|
2578
|
+
const emitter = new IntegrationProgressEmitter({
|
|
2579
|
+
runID,
|
|
2580
|
+
runKind: 'ConnectorCreation',
|
|
2581
|
+
companyIntegrationID,
|
|
2582
|
+
triggerType: 'Manual',
|
|
2583
|
+
startedAt: new Date().toISOString(),
|
|
2584
|
+
});
|
|
2585
|
+
emitter.runStart('Detached schema refresh launch');
|
|
2586
|
+
emitter.stageError('Launch', message, { code: 'schema-refresh-launch-failed' });
|
|
2587
|
+
void emitter.fail(`Schema refresh could not start: ${message}`, 'schema-refresh-launch-failed')
|
|
2588
|
+
.catch(e => LogError(`Detached schema refresh (run ${runID}): failure artifact write failed — ${e}`));
|
|
2589
|
+
} catch (e) {
|
|
2590
|
+
// Progress reporting must never mask the original failure, which is already logged above.
|
|
2591
|
+
LogError(`Detached schema refresh (run ${runID}): could not open failure artifact — ${e}`);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2426
2595
|
/**
|
|
2427
2596
|
* Rolls back a freshly created connection by deleting both the CompanyIntegration and Credential records.
|
|
2428
2597
|
*/
|
|
@@ -2594,6 +2763,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2594
2763
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2595
2764
|
@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
2765
|
@Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot). Improves SoftPKClassifier convergence." }) universalPKConvention: string | undefined,
|
|
2766
|
+
@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
2767
|
@Ctx() ctx: AppContext
|
|
2598
2768
|
): Promise<CreateConnectionOutput> {
|
|
2599
2769
|
try {
|
|
@@ -2616,16 +2786,25 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2616
2786
|
const credentialID = credential.ID;
|
|
2617
2787
|
|
|
2618
2788
|
// 2. Create CompanyIntegration linked to the Credential
|
|
2619
|
-
const ci = await md.GetEntityObject<
|
|
2789
|
+
const ci = await md.GetEntityObject<MJCompanyIntegrationEntityServer>('MJ: Company Integrations', user);
|
|
2620
2790
|
ci.NewRecord();
|
|
2621
2791
|
ci.IntegrationID = input.IntegrationID;
|
|
2622
2792
|
ci.CompanyID = input.CompanyID;
|
|
2623
2793
|
ci.CredentialID = credentialID;
|
|
2624
|
-
|
|
2794
|
+
// Created INACTIVE on purpose (#3738). A connection is not marked
|
|
2795
|
+
// active until its credential has had a chance to prove itself in
|
|
2796
|
+
// step 3 below — so nothing downstream ever observes an active row
|
|
2797
|
+
// backed by a password that is about to be rejected and rolled back.
|
|
2798
|
+
ci.IsActive = false;
|
|
2625
2799
|
ci.Name = input.CredentialName; // Name is required on CompanyIntegration
|
|
2626
2800
|
if (input.ExternalSystemID) ci.ExternalSystemID = input.ExternalSystemID;
|
|
2627
2801
|
if (input.Configuration) ci.Configuration = input.Configuration;
|
|
2628
2802
|
|
|
2803
|
+
// Whether the refresh runs detached (this resolver owns it and returns a tailable RunID)
|
|
2804
|
+
// or inline below. No save-side refresh exists to coordinate with any more — #3738 removed
|
|
2805
|
+
// the activation hook, so every refresh on this path is one this method asked for.
|
|
2806
|
+
const ownSchemaRefresh = runSchemaRefresh && !awaitSchemaRefresh;
|
|
2807
|
+
|
|
2629
2808
|
const saved = await ci.Save();
|
|
2630
2809
|
if (!saved) {
|
|
2631
2810
|
const validationErrors = ci.LatestResult?.Message || 'Unknown validation error';
|
|
@@ -2650,12 +2829,27 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2650
2829
|
testMessage = testResult.Message;
|
|
2651
2830
|
}
|
|
2652
2831
|
|
|
2832
|
+
// 3b. Credential has proven itself (or the caller declined a test,
|
|
2833
|
+
// which is the pre-existing default) — activate. Ordering matters:
|
|
2834
|
+
// this save is what makes the connection usable, and it must not
|
|
2835
|
+
// happen while the credential is still unverified.
|
|
2836
|
+
ci.IsActive = true;
|
|
2837
|
+
if (!await ci.Save()) {
|
|
2838
|
+
await this.rollbackCreatedConnection(ci, credential);
|
|
2839
|
+
return {
|
|
2840
|
+
Success: false,
|
|
2841
|
+
Message: `Failed to activate CompanyIntegration: ${ci.LatestResult?.Message || 'Unknown error'}`,
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2653
2845
|
// 4. Auto-run schema refresh pipeline (intermittent server-side period).
|
|
2654
2846
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2655
2847
|
// caller also asked for a test. The wizard may have tested separately
|
|
2656
2848
|
// and just be hitting Create to save.
|
|
2657
2849
|
let schemaRefreshSummary: CreateConnectionPipelineSummary | undefined;
|
|
2658
|
-
if (
|
|
2850
|
+
if (ownSchemaRefresh) {
|
|
2851
|
+
schemaRefreshSummary = this.startSchemaRefreshPipelineDetached(ci.ID, user, md, universalPKConvention);
|
|
2852
|
+
} else if (runSchemaRefresh) {
|
|
2659
2853
|
try {
|
|
2660
2854
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2661
2855
|
ci.ID, user, md, universalPKConvention
|
|
@@ -2671,9 +2865,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2671
2865
|
if (testConnection || schemaRefreshSummary) {
|
|
2672
2866
|
return {
|
|
2673
2867
|
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',
|
|
2868
|
+
Message: BuildCreateConnectionMessage(testConnection, schemaRefreshSummary),
|
|
2677
2869
|
CompanyIntegrationID: ci.ID,
|
|
2678
2870
|
CredentialID: credentialID,
|
|
2679
2871
|
ConnectionTestSuccess: testPassed,
|
|
@@ -2706,6 +2898,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2706
2898
|
@Arg("testConnection", () => Boolean, { defaultValue: false }) testConnection: boolean,
|
|
2707
2899
|
@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
2900
|
@Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot)" }) universalPKConvention: string | undefined,
|
|
2901
|
+
@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
2902
|
@Ctx() ctx: AppContext
|
|
2710
2903
|
): Promise<MutationResultOutput> {
|
|
2711
2904
|
try {
|
|
@@ -2752,15 +2945,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2752
2945
|
// Fires whenever runSchemaRefresh=true, regardless of whether the
|
|
2753
2946
|
// caller also asked for a test — the wizard may have tested separately
|
|
2754
2947
|
// already and is just hitting Update to save edits.
|
|
2948
|
+
if (runSchemaRefresh && !awaitSchemaRefresh) {
|
|
2949
|
+
const detached = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md, universalPKConvention);
|
|
2950
|
+
return { Success: true, Message: BuildDetachedRefreshMessage(detached.RunID) };
|
|
2951
|
+
}
|
|
2755
2952
|
if (runSchemaRefresh) {
|
|
2756
2953
|
try {
|
|
2757
2954
|
const refreshResult = await this.runSchemaRefreshPipeline(
|
|
2758
2955
|
companyIntegrationID, user, md, universalPKConvention
|
|
2759
2956
|
);
|
|
2760
|
-
return {
|
|
2761
|
-
Success: true,
|
|
2762
|
-
Message: `Updated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
|
|
2763
|
-
};
|
|
2957
|
+
return { Success: true, Message: BuildUpdateConnectionMessage(refreshResult) };
|
|
2764
2958
|
} catch (refreshErr) {
|
|
2765
2959
|
LogError(`IntegrationUpdateConnection: pipeline error — ${refreshErr}`);
|
|
2766
2960
|
return { Success: true, Message: `Updated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
@@ -2803,6 +2997,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2803
2997
|
set('crossLayerPipeline', config.CrossLayerPipeline);
|
|
2804
2998
|
set('partitionReconcile', config.PartitionReconcile);
|
|
2805
2999
|
set('discoveryTimeBudgetMs', config.DiscoveryTimeBudgetMs);
|
|
3000
|
+
set('fetchTimeoutMs', config.FetchTimeoutMs);
|
|
2806
3001
|
set('discoveryBatchSize', config.DiscoveryBatchSize);
|
|
2807
3002
|
set('discoveryMaxRecords', config.DiscoveryMaxRecords);
|
|
2808
3003
|
set('deactivateAbsent', config.DeactivateAbsent);
|
|
@@ -2858,6 +3053,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2858
3053
|
CrossLayerPipeline: bool(cfg.crossLayerPipeline),
|
|
2859
3054
|
PartitionReconcile: bool(cfg.partitionReconcile),
|
|
2860
3055
|
DiscoveryTimeBudgetMs: num(cfg.discoveryTimeBudgetMs),
|
|
3056
|
+
FetchTimeoutMs: num(cfg.fetchTimeoutMs),
|
|
2861
3057
|
DiscoveryBatchSize: num(cfg.discoveryBatchSize),
|
|
2862
3058
|
DiscoveryMaxRecords: num(cfg.discoveryMaxRecords),
|
|
2863
3059
|
DeactivateAbsent: bool(cfg.deactivateAbsent),
|
|
@@ -2967,10 +3163,49 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2967
3163
|
|
|
2968
3164
|
/**
|
|
2969
3165
|
* Reactivates a previously deactivated CompanyIntegration by setting IsActive=true.
|
|
3166
|
+
*
|
|
3167
|
+
* Before #3738 this mutation inherited a full schema refresh from the
|
|
3168
|
+
* CompanyIntegration save hook, with no way to decline it. The refresh is
|
|
3169
|
+
* now this resolver's own, explicit step — visible in the API, suppressible
|
|
3170
|
+
* with `runSchemaRefresh: false`, and (since it gained `awaitSchemaRefresh`)
|
|
3171
|
+
* no longer something the caller has to sit through.
|
|
3172
|
+
*
|
|
3173
|
+
* WHY REACTIVATION NO LONGER SCANS THE SOURCE BY DEFAULT. Resuming a
|
|
3174
|
+
* connection and rescanning its schema are separate decisions that this
|
|
3175
|
+
* mutation used to fuse. A one-click resume would spend minutes of a
|
|
3176
|
+
* vendor's rate budget on an introspect nobody asked for, and the catalog is
|
|
3177
|
+
* usually exactly as current as it was when the connection was paused.
|
|
3178
|
+
* `IntegrationRefreshConnectorSchema` is the operation for "rescan now", and
|
|
3179
|
+
* a caller that genuinely wants both passes `runSchemaRefresh: true`.
|
|
3180
|
+
*
|
|
3181
|
+
* WHY THAT REFRESH, WHEN REQUESTED, DEFAULTS TO DETACHED AND ITS SIBLINGS DO
|
|
3182
|
+
* NOT. Create and Update default to `awaitSchemaRefresh: true` because the
|
|
3183
|
+
* caller is in a wizard, waiting on a form, and the counts ARE the answer
|
|
3184
|
+
* they asked for. Reactivate is a one-click toggle on a connection row, and
|
|
3185
|
+
* — unlike its siblings — its entire durable effect is already committed by
|
|
3186
|
+
* the time any refresh begins: the `Save()` above returned. Blocking the
|
|
3187
|
+
* response for the minutes a live introspect takes cannot make that
|
|
3188
|
+
* reactivation any more true; it can only misreport it.
|
|
3189
|
+
*
|
|
3190
|
+
* And it did. The transport, not this resolver, is what breaks: a held
|
|
3191
|
+
* request dies at the load balancer's fixed ceiling, which the app cannot
|
|
3192
|
+
* configure and which differs by front end, and the client then shows a
|
|
3193
|
+
* resumed connection as a failure. Note the shape of that bug — the two failure paths BELOW are both
|
|
3194
|
+
* handled correctly, returning `Success: true` with the refresh problem
|
|
3195
|
+
* appended, precisely because reactivation already happened. The gateway is
|
|
3196
|
+
* the one caller of this mutation that never reaches them.
|
|
3197
|
+
*
|
|
3198
|
+
* So the default here optimises for the operation actually being requested
|
|
3199
|
+
* (resume this connection) rather than for the operation that happens to be
|
|
3200
|
+
* expensive (rescan the source). The refresh still runs — detached, on a
|
|
3201
|
+
* durable, tailable run stream — and `awaitSchemaRefresh: true` restores the
|
|
3202
|
+
* blocking behaviour for a caller that genuinely wants the counts inline.
|
|
2970
3203
|
*/
|
|
2971
3204
|
@Mutation(() => MutationResultOutput)
|
|
2972
3205
|
async IntegrationReactivateConnection(
|
|
2973
3206
|
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
3207
|
+
@Arg("runSchemaRefresh", () => Boolean, { defaultValue: false, description: "When false (default), reactivation ONLY reactivates — no live scan of the source. Resuming a connection and rescanning its schema are separate decisions, and a resume should not spend minutes of a vendor's rate budget nobody asked for; run IntegrationRefreshConnectorSchema when the catalog actually needs refreshing. Pass true to re-run IntegrationConnectorCreationPipeline as part of the reactivation." }) runSchemaRefresh: boolean,
|
|
3208
|
+
@Arg("awaitSchemaRefresh", () => Boolean, { defaultValue: false, description: "When false (default), the schema refresh is launched detached and this mutation returns as soon as the connection is active, naming the run to tail. Reactivation is already committed at that point, so blocking on a minutes-long live introspect can only delay — or, at the load balancer's fixed request ceiling, misreport — an operation that has already succeeded. Pass true to block until the refresh finishes and get its counts in the message." }) awaitSchemaRefresh: boolean,
|
|
2974
3209
|
@Ctx() ctx: AppContext
|
|
2975
3210
|
): Promise<MutationResultOutput> {
|
|
2976
3211
|
try {
|
|
@@ -2981,7 +3216,26 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
2981
3216
|
if (!loaded) return { Success: false, Message: 'CompanyIntegration not found' };
|
|
2982
3217
|
ci.IsActive = true;
|
|
2983
3218
|
if (!await ci.Save()) return { Success: false, Message: `Failed to reactivate: ${ci.LatestResult?.Message ?? 'Unknown error'}` };
|
|
2984
|
-
|
|
3219
|
+
|
|
3220
|
+
if (runSchemaRefresh && !awaitSchemaRefresh) {
|
|
3221
|
+
const detached = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md);
|
|
3222
|
+
return { Success: true, Message: BuildReactivateMessage(detached) };
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
if (runSchemaRefresh) {
|
|
3226
|
+
try {
|
|
3227
|
+
const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md);
|
|
3228
|
+
return { Success: true, Message: BuildReactivateMessage(refreshResult) };
|
|
3229
|
+
} catch (refreshErr) {
|
|
3230
|
+
// Refresh failure does NOT undo the reactivation — the
|
|
3231
|
+
// connection is active, only discovery failed, and the
|
|
3232
|
+
// operator can re-run IntegrationRefreshConnectorSchema.
|
|
3233
|
+
LogError(`IntegrationReactivateConnection: pipeline error — ${refreshErr}`);
|
|
3234
|
+
return { Success: true, Message: `Reactivated (schema refresh failed: ${this.formatError(refreshErr)})` };
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return { Success: true, Message: BuildReactivateMessage(undefined) };
|
|
2985
3239
|
} catch (e) {
|
|
2986
3240
|
LogError(`IntegrationReactivateConnection error: ${e}`);
|
|
2987
3241
|
return { Success: false, Message: this.formatError(e) };
|
|
@@ -3138,15 +3392,22 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3138
3392
|
// the Phase 0 v5.39.x Save hook. Fall back to live IntrospectSchema only
|
|
3139
3393
|
// for direct-API callers bypassing the wizard (empty IO cache).
|
|
3140
3394
|
const requestedNames = new Set(objects.map(o => o.SourceObjectName));
|
|
3395
|
+
// Declared rows this apply will leave out (deactivated objects/fields) — surfaced on the
|
|
3396
|
+
// mutation's Warnings so a column that never appears has a stated reason.
|
|
3397
|
+
const inactiveWarnings: string[] = [];
|
|
3141
3398
|
let sourceSchema: SourceSchemaInfo = this.buildSourceSchemaFromPersistedRows(
|
|
3142
3399
|
companyIntegration.IntegrationID,
|
|
3143
3400
|
Array.from(requestedNames),
|
|
3401
|
+
inactiveWarnings,
|
|
3144
3402
|
);
|
|
3145
3403
|
if (sourceSchema.Objects.length === 0) {
|
|
3146
3404
|
LogError(`[IntegrationApplySchema] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
|
|
3147
3405
|
const introspect = connector.IntrospectSchema.bind(connector) as
|
|
3148
3406
|
(ci: unknown, u: unknown) => Promise<SourceSchemaInfo>;
|
|
3149
3407
|
sourceSchema = await introspect(companyIntegration, user);
|
|
3408
|
+
// The persisted rows are not what gets materialized on this path, so anything they
|
|
3409
|
+
// said about deactivated rows describes a schema this apply is not using.
|
|
3410
|
+
inactiveWarnings.length = 0;
|
|
3150
3411
|
} else {
|
|
3151
3412
|
console.log(
|
|
3152
3413
|
`[IntegrationApplySchema] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
|
|
@@ -3181,6 +3442,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3181
3442
|
SkipGitCommit: skipGitCommit,
|
|
3182
3443
|
SkipRestart: skipRestart,
|
|
3183
3444
|
});
|
|
3445
|
+
if (inactiveWarnings.length > 0) SchemaOutput.Warnings.unshift(...inactiveWarnings);
|
|
3184
3446
|
|
|
3185
3447
|
return {
|
|
3186
3448
|
Success: PipelineResult.Success,
|
|
@@ -3398,12 +3660,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3398
3660
|
);
|
|
3399
3661
|
|
|
3400
3662
|
// 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).
|
|
3663
|
+
// Build per-object field map for the pending payload (null = all fields).
|
|
3407
3664
|
// resolved.sourceObjects is order-aligned with resolvedNames after the
|
|
3408
3665
|
// resolveSourceObjectsToNames refactor — pair them directly instead
|
|
3409
3666
|
// of looking up by ID (which broke for name-only selections).
|
|
@@ -3426,7 +3683,7 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3426
3683
|
const hadPriorMaps = priorMapsResult.Success && priorMapsResult.Results.length > 0;
|
|
3427
3684
|
const effectiveFullSync = input.FullSync ?? !hadPriorMaps;
|
|
3428
3685
|
|
|
3429
|
-
const pendingPayload = {
|
|
3686
|
+
const pendingPayload: RSUPendingWork = {
|
|
3430
3687
|
CompanyIntegrationID: input.CompanyIntegrationID,
|
|
3431
3688
|
SourceObjectNames: resolvedNames,
|
|
3432
3689
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -3435,13 +3692,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3435
3692
|
ScheduleTimezone: input.ScheduleTimezone,
|
|
3436
3693
|
StartSync: input.StartSync,
|
|
3437
3694
|
FullSync: effectiveFullSync,
|
|
3438
|
-
SyncScope: input.SyncScope
|
|
3695
|
+
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
3439
3696
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
3440
3697
|
CreatedAt: new Date().toISOString(),
|
|
3441
3698
|
};
|
|
3442
|
-
rsuInput.
|
|
3443
|
-
|
|
3444
|
-
];
|
|
3699
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
3700
|
+
rsuInput.ContextUser = user;
|
|
3445
3701
|
|
|
3446
3702
|
// Step 5: Run pipeline (restart kills process at the end).
|
|
3447
3703
|
// Retrying variant — an install should not die because the migration hit a dropped
|
|
@@ -3454,9 +3710,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3454
3710
|
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
3455
3711
|
}));
|
|
3456
3712
|
|
|
3457
|
-
//
|
|
3713
|
+
// Pending work is registered only for migrations that succeeded, so a
|
|
3714
|
+
// failed pipeline has nothing to clean up.
|
|
3458
3715
|
if (!migrationSucceeded) {
|
|
3459
|
-
try { (await import('node:fs')).unlinkSync(pendingFilePath); } catch { /* may not exist */ }
|
|
3460
3716
|
return {
|
|
3461
3717
|
Success: false,
|
|
3462
3718
|
Message: `Pipeline failed: ${batchResult.Results[0]?.ErrorMessage ?? 'unknown error'}`,
|
|
@@ -3514,7 +3770,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3514
3770
|
}
|
|
3515
3771
|
}
|
|
3516
3772
|
|
|
3517
|
-
|
|
3773
|
+
// The work just happened inline (no restart), so close out its durable rows.
|
|
3774
|
+
for (const pendingWorkID of batchResult.Results[0]?.PendingWorkIDs ?? []) {
|
|
3775
|
+
await rsm.CompletePendingWork(pendingWorkID, user);
|
|
3776
|
+
}
|
|
3518
3777
|
|
|
3519
3778
|
return {
|
|
3520
3779
|
Success: true,
|
|
@@ -3892,6 +4151,9 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3892
4151
|
// Intacct AND silently dropped selections when the second pass returned
|
|
3893
4152
|
// fewer objects than the first (rate limits, transient errors).
|
|
3894
4153
|
let sourceSchema: SourceSchemaInfo;
|
|
4154
|
+
// Declared rows this apply will silently leave out (deactivated objects/fields). Collected
|
|
4155
|
+
// during the rebuild and surfaced on the apply's Warnings, alongside the schema-limit ones.
|
|
4156
|
+
const inactiveWarnings: string[] = [];
|
|
3895
4157
|
if (prefetchedSourceSchema) {
|
|
3896
4158
|
sourceSchema = prefetchedSourceSchema;
|
|
3897
4159
|
} else {
|
|
@@ -3899,12 +4161,16 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3899
4161
|
sourceSchema = this.buildSourceSchemaFromPersistedRows(
|
|
3900
4162
|
companyIntegration.IntegrationID,
|
|
3901
4163
|
requestedNamesForReuse,
|
|
4164
|
+
inactiveWarnings,
|
|
3902
4165
|
);
|
|
3903
4166
|
if (sourceSchema.Objects.length === 0) {
|
|
3904
4167
|
LogError(`[buildSchemaForConnector] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
|
|
3905
4168
|
const introspect = connector.IntrospectSchema.bind(connector) as
|
|
3906
4169
|
(ci: unknown, u: unknown) => Promise<SourceSchemaInfo>;
|
|
3907
4170
|
sourceSchema = await introspect(companyIntegration, user);
|
|
4171
|
+
// The persisted rows are not what gets materialized on this path, so anything they
|
|
4172
|
+
// said about deactivated rows describes a schema this apply is not using.
|
|
4173
|
+
inactiveWarnings.length = 0;
|
|
3908
4174
|
} else {
|
|
3909
4175
|
console.log(
|
|
3910
4176
|
`[buildSchemaForConnector] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
|
|
@@ -3947,8 +4213,10 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
3947
4213
|
|
|
3948
4214
|
const builder = new SchemaBuilder();
|
|
3949
4215
|
const schemaOutput = builder.BuildSchema(input);
|
|
3950
|
-
// Surface the auto-disabled over-wide objects (if any) in the apply's Warnings → CLI output
|
|
4216
|
+
// Surface the auto-disabled over-wide objects (if any) in the apply's Warnings → CLI output,
|
|
4217
|
+
// together with the declared-but-deactivated rows this apply left out.
|
|
3951
4218
|
if (limitWarnings.length > 0) schemaOutput.Warnings.unshift(...limitWarnings);
|
|
4219
|
+
if (inactiveWarnings.length > 0) schemaOutput.Warnings.unshift(...inactiveWarnings);
|
|
3952
4220
|
|
|
3953
4221
|
if (schemaOutput.Errors.length > 0) {
|
|
3954
4222
|
throw new Error(`Schema generation failed: ${schemaOutput.Errors.join('; ')}`);
|
|
@@ -4103,6 +4371,60 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4103
4371
|
}
|
|
4104
4372
|
}
|
|
4105
4373
|
|
|
4374
|
+
/**
|
|
4375
|
+
* Enqueue a sync for worker execution (PR 1 item 8) instead of running it in this
|
|
4376
|
+
* process. Creates the run row with `Status='Queued'` and returns its ID immediately —
|
|
4377
|
+
* so the caller gets a real, trackable RunID with no fire-and-forget polling window,
|
|
4378
|
+
* and the sync survives this process going away before it starts.
|
|
4379
|
+
*
|
|
4380
|
+
* Requires a process running the integration sync worker (`integrationSyncWorker.enabled`);
|
|
4381
|
+
* without one, rows accumulate in the queue unexecuted.
|
|
4382
|
+
*/
|
|
4383
|
+
@Mutation(() => StartSyncOutput)
|
|
4384
|
+
@RequireSystemUser()
|
|
4385
|
+
async IntegrationEnqueueSync(
|
|
4386
|
+
@Arg("companyIntegrationID") companyIntegrationID: string,
|
|
4387
|
+
@Arg("fullSync", () => Boolean, { defaultValue: false, description: 'If true, ignores watermarks and re-fetches all records from the source' }) fullSync: boolean,
|
|
4388
|
+
@Arg("entityMapIDs", () => [String], { nullable: true, description: 'Optional: sync only these entity maps. If omitted, syncs all maps for the connector.' }) entityMapIDs: string[],
|
|
4389
|
+
@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,
|
|
4390
|
+
@Ctx() ctx: AppContext
|
|
4391
|
+
): Promise<StartSyncOutput> {
|
|
4392
|
+
try {
|
|
4393
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4394
|
+
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
4395
|
+
await IntegrationEngine.Instance.Config(false, user, provider);
|
|
4396
|
+
|
|
4397
|
+
// Same upfront gates as IntegrationStartSync — refusing here beats queueing work
|
|
4398
|
+
// that the engine will only refuse later, in another process, out of the caller's sight.
|
|
4399
|
+
const ciCheck = await provider.GetEntityObject<MJCompanyIntegrationEntity>('MJ: Company Integrations', user);
|
|
4400
|
+
if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
|
|
4401
|
+
return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not enqueued' };
|
|
4402
|
+
}
|
|
4403
|
+
const maintenance = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
4404
|
+
if (maintenance) {
|
|
4405
|
+
return { Success: false, Message: `Sync not enqueued: ${maintenance.Reason} is in progress for this connection (since ${maintenance.AcquiredAt.toISOString()}). Retry after it completes.` };
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
const syncOptions: IntegrationSyncOptions = {};
|
|
4409
|
+
if (fullSync) syncOptions.FullSync = true;
|
|
4410
|
+
if (entityMapIDs?.length) syncOptions.EntityMapIDs = entityMapIDs;
|
|
4411
|
+
if (syncDirection) syncOptions.SyncDirection = syncDirection;
|
|
4412
|
+
|
|
4413
|
+
const runID = await IntegrationEngine.Instance.EnqueueSync(
|
|
4414
|
+
companyIntegrationID,
|
|
4415
|
+
user,
|
|
4416
|
+
'Manual',
|
|
4417
|
+
Object.keys(syncOptions).length > 0 ? syncOptions : undefined,
|
|
4418
|
+
provider
|
|
4419
|
+
);
|
|
4420
|
+
|
|
4421
|
+
return { Success: true, Message: 'Sync enqueued', RunID: runID };
|
|
4422
|
+
} catch (e) {
|
|
4423
|
+
LogError(`IntegrationEnqueueSync error: ${e}`);
|
|
4424
|
+
return { Success: false, Message: this.formatError(e) };
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4106
4428
|
/**
|
|
4107
4429
|
* Cancels a running sync by marking its status as Cancelled.
|
|
4108
4430
|
*/
|
|
@@ -4112,10 +4434,13 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4112
4434
|
@Ctx() ctx: AppContext
|
|
4113
4435
|
): Promise<MutationResultOutput> {
|
|
4114
4436
|
try {
|
|
4115
|
-
this.getAuthenticatedUser(ctx);
|
|
4437
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
4438
|
+
const provider = GetReadWriteProvider(ctx.providers) as unknown as IMetadataProvider;
|
|
4116
4439
|
|
|
4117
|
-
//
|
|
4118
|
-
|
|
4440
|
+
// DB-backed cancel (durable runs): stamps CancelRequestedAt on the live run row, so
|
|
4441
|
+
// the request reaches the owning process wherever it is — this server, another API
|
|
4442
|
+
// node, or a worker — via its heartbeat/boundary checks. No in-memory signal.
|
|
4443
|
+
const cancelled = await IntegrationEngine.CancelSyncAsync(companyIntegrationID, user, provider);
|
|
4119
4444
|
if (!cancelled) {
|
|
4120
4445
|
return { Success: false, Message: 'No active sync found for this connector' };
|
|
4121
4446
|
}
|
|
@@ -4712,8 +5037,11 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
4712
5037
|
@Ctx() ctx: AppContext
|
|
4713
5038
|
): Promise<OperationProgressOutput> {
|
|
4714
5039
|
try {
|
|
4715
|
-
this.getAuthenticatedUser(ctx);
|
|
4716
|
-
const
|
|
5040
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
5041
|
+
const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true }) as unknown as IMetadataProvider;
|
|
5042
|
+
// DB-backed progress (durable runs): reads ProgressJSON from the live run row, so
|
|
5043
|
+
// progress is visible from ANY process — not just the one executing the sync.
|
|
5044
|
+
const syncProgress = await IntegrationEngine.GetSyncProgressAsync(companyIntegrationID, user, provider);
|
|
4717
5045
|
if (syncProgress) {
|
|
4718
5046
|
return {
|
|
4719
5047
|
Success: true,
|
|
@@ -5236,18 +5564,14 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5236
5564
|
connInput.CompanyIntegrationID, objects, validatedPlatform, user, skipGitCommit, skipRestart, provider, sourceSchema
|
|
5237
5565
|
);
|
|
5238
5566
|
|
|
5239
|
-
// Build per-object field map for pending
|
|
5567
|
+
// Build per-object field map for the pending payload
|
|
5240
5568
|
const sourceObjectFields: Record<string, string[] | null> = {};
|
|
5241
5569
|
for (const name of resolvedNames) {
|
|
5242
5570
|
sourceObjectFields[name] = fieldsByName.get(name.toLowerCase()) ?? null;
|
|
5243
5571
|
}
|
|
5244
5572
|
|
|
5245
5573
|
// 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 = {
|
|
5574
|
+
const pendingPayload: RSUPendingWork = {
|
|
5251
5575
|
CompanyIntegrationID: connInput.CompanyIntegrationID,
|
|
5252
5576
|
SourceObjectNames: resolvedNames,
|
|
5253
5577
|
SourceObjectFields: sourceObjectFields,
|
|
@@ -5256,15 +5580,15 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5256
5580
|
ScheduleTimezone: connInput.ScheduleTimezone,
|
|
5257
5581
|
StartSync: input.StartSync,
|
|
5258
5582
|
FullSync: input.FullSync ?? false,
|
|
5259
|
-
SyncScope: input.SyncScope
|
|
5260
|
-
|
|
5261
|
-
|
|
5583
|
+
SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
|
|
5584
|
+
// Unrecognized directions fall back to undefined = "use the entity map's own SyncDirection"
|
|
5585
|
+
SyncDirection: input.SyncDirection && isValidSyncDirection(input.SyncDirection) ? input.SyncDirection : undefined,
|
|
5586
|
+
ScheduleSyncDirection: input.ScheduleSyncDirection && isValidSyncDirection(input.ScheduleSyncDirection) ? input.ScheduleSyncDirection : undefined,
|
|
5262
5587
|
UnselectedAction: (input.UnselectedAction ?? 'disable') as 'disable' | 'ignore',
|
|
5263
5588
|
CreatedAt: new Date().toISOString(),
|
|
5264
5589
|
};
|
|
5265
|
-
rsuInput.
|
|
5266
|
-
|
|
5267
|
-
];
|
|
5590
|
+
rsuInput.PendingWork = [pendingPayload];
|
|
5591
|
+
rsuInput.ContextUser = user;
|
|
5268
5592
|
|
|
5269
5593
|
return {
|
|
5270
5594
|
connInput,
|
|
@@ -5274,7 +5598,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5274
5598
|
objects,
|
|
5275
5599
|
schemaOutput,
|
|
5276
5600
|
rsuInput,
|
|
5277
|
-
pendingFilePath,
|
|
5278
5601
|
};
|
|
5279
5602
|
})
|
|
5280
5603
|
);
|
|
@@ -5288,7 +5611,6 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5288
5611
|
objects: SchemaPreviewObjectInput[];
|
|
5289
5612
|
schemaOutput: SchemaBuilderOutput;
|
|
5290
5613
|
rsuInput: RSUPipelineInput;
|
|
5291
|
-
pendingFilePath: string;
|
|
5292
5614
|
}> = [];
|
|
5293
5615
|
const connectorResults: ApplyAllBatchConnectorResult[] = [];
|
|
5294
5616
|
|
|
@@ -5340,8 +5662,8 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5340
5662
|
Message: pipelineResult?.ErrorMessage ?? 'Pipeline failed',
|
|
5341
5663
|
Warnings: build.schemaOutput.Warnings.length > 0 ? build.schemaOutput.Warnings : undefined,
|
|
5342
5664
|
});
|
|
5343
|
-
//
|
|
5344
|
-
|
|
5665
|
+
// No pending-work cleanup needed — rows are registered only for
|
|
5666
|
+
// migrations that succeeded.
|
|
5345
5667
|
continue;
|
|
5346
5668
|
}
|
|
5347
5669
|
|
|
@@ -5388,10 +5710,12 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5388
5710
|
if (scheduleResult) connResult.ScheduledJobID = scheduleResult;
|
|
5389
5711
|
}
|
|
5390
5712
|
|
|
5391
|
-
//
|
|
5392
|
-
|
|
5713
|
+
// The work just happened inline (no restart), so close out its durable rows.
|
|
5714
|
+
for (const pendingWorkID of pipelineResult.PendingWorkIDs ?? []) {
|
|
5715
|
+
await rsm.CompletePendingWork(pendingWorkID, user);
|
|
5716
|
+
}
|
|
5393
5717
|
|
|
5394
|
-
connResult.Message
|
|
5718
|
+
connResult.Message =`Applied ${build.objects.length} object(s) — ${entityMapsCreated.length} entity maps created${syncRunID ? ', sync started' : ''}`;
|
|
5395
5719
|
}
|
|
5396
5720
|
|
|
5397
5721
|
connectorResults.push(connResult);
|
|
@@ -5958,26 +6282,21 @@ export class IntegrationDiscoveryResolver extends ResolverBase {
|
|
|
5958
6282
|
// ApplyAll uses. default: created DISABLED (user enables after the refresh);
|
|
5959
6283
|
// autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
|
|
5960
6284
|
if (newObjects.length > 0 && !skipRestart) {
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
|
|
5964
|
-
rsuInput.PostRestartFiles = [
|
|
5965
|
-
...(rsuInput.PostRestartFiles ?? []),
|
|
6285
|
+
rsuInput.PendingWork = [
|
|
6286
|
+
...(rsuInput.PendingWork ?? []),
|
|
5966
6287
|
{
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
CreateDisabled: !autoEnableNewObjects,
|
|
5977
|
-
CreatedAt: new Date().toISOString(),
|
|
5978
|
-
}, null, 2),
|
|
6288
|
+
CompanyIntegrationID: companyIntegrationID,
|
|
6289
|
+
SourceObjectNames: newObjects,
|
|
6290
|
+
SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
|
|
6291
|
+
SchemaName: schemaName,
|
|
6292
|
+
StartSync: false,
|
|
6293
|
+
SyncScope: 'created',
|
|
6294
|
+
UnselectedAction: 'ignore',
|
|
6295
|
+
CreateDisabled: !autoEnableNewObjects,
|
|
6296
|
+
CreatedAt: new Date().toISOString(),
|
|
5979
6297
|
},
|
|
5980
6298
|
];
|
|
6299
|
+
rsuInput.ContextUser = user;
|
|
5981
6300
|
}
|
|
5982
6301
|
|
|
5983
6302
|
const rsm = RuntimeSchemaManager.Instance;
|