@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.
Files changed (162) hide show
  1. package/LICENSE +180 -4
  2. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  3. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  4. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  5. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  6. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  7. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderEngine.js +245 -0
  9. package/dist/auth/AuthProviderEngine.js.map +1 -0
  10. package/dist/auth/index.d.ts +11 -0
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js +56 -13
  13. package/dist/auth/index.js.map +1 -1
  14. package/dist/auth/initializeProviders.d.ts +64 -1
  15. package/dist/auth/initializeProviders.d.ts.map +1 -1
  16. package/dist/auth/initializeProviders.js +117 -5
  17. package/dist/auth/initializeProviders.js.map +1 -1
  18. package/dist/config.d.ts +89 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +52 -35
  21. package/dist/config.js.map +1 -1
  22. package/dist/generated/generated.d.ts +501 -1597
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +2711 -17021
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +1 -1
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +7 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +31 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +99 -14
  37. package/dist/index.js.map +1 -1
  38. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  39. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  40. package/dist/integration/RSUProgressBridge.js +230 -0
  41. package/dist/integration/RSUProgressBridge.js.map +1 -0
  42. package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
  43. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  44. package/dist/integration/SchemaRefreshLaunch.js +50 -0
  45. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  46. package/dist/providerConfigUnits.d.ts +20 -0
  47. package/dist/providerConfigUnits.d.ts.map +1 -0
  48. package/dist/providerConfigUnits.js +22 -0
  49. package/dist/providerConfigUnits.js.map +1 -0
  50. package/dist/resolvers/FileResolver.d.ts +34 -4
  51. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  52. package/dist/resolvers/FileResolver.js +218 -28
  53. package/dist/resolvers/FileResolver.js.map +1 -1
  54. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +76 -3
  55. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  56. package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
  57. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  58. package/dist/resolvers/QueryResolver.d.ts +5 -4
  59. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  60. package/dist/resolvers/QueryResolver.js +29 -14
  61. package/dist/resolvers/QueryResolver.js.map +1 -1
  62. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  63. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  64. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  65. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  66. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  67. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  68. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  69. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  70. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  71. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  72. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  73. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  74. package/dist/resolvers/UserResolver.d.ts +19 -17
  75. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  76. package/dist/resolvers/UserResolver.js +66 -67
  77. package/dist/resolvers/UserResolver.js.map +1 -1
  78. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  79. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  80. package/dist/resolvers/currentUserRoles.js +36 -0
  81. package/dist/resolvers/currentUserRoles.js.map +1 -0
  82. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  83. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  84. package/dist/rest/MediaAccessKeys.js +42 -1
  85. package/dist/rest/MediaAccessKeys.js.map +1 -1
  86. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  87. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  88. package/dist/rest/MediaStreamHandler.js +83 -2
  89. package/dist/rest/MediaStreamHandler.js.map +1 -1
  90. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  91. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  92. package/dist/rest/OAuthCallbackHandler.js +129 -43
  93. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  94. package/dist/rest/UploadTokenManager.d.ts +90 -0
  95. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  96. package/dist/rest/UploadTokenManager.js +156 -0
  97. package/dist/rest/UploadTokenManager.js.map +1 -0
  98. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  99. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  100. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  101. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  102. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  103. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  104. package/dist/services/StartTaskGraphDispatcher.js +21 -1
  105. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  106. package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
  107. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  108. package/dist/services/TaskGraphAgentRunner.js +23 -0
  109. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  110. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  111. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  112. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  113. package/dist/types.d.ts +6 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +91 -91
  117. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  118. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  119. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  120. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  121. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  122. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  123. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  124. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  125. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  126. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  127. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  128. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  129. package/src/__tests__/config-units.test.ts +74 -0
  130. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  131. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  132. package/src/__tests__/newUsers.test.ts +726 -0
  133. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  134. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  135. package/src/auth/AuthProviderEngine.ts +253 -0
  136. package/src/auth/index.ts +59 -15
  137. package/src/auth/initializeProviders.ts +128 -7
  138. package/src/config.ts +55 -36
  139. package/src/generated/generated.ts +1911 -10551
  140. package/src/generic/ResolverBase.ts +11 -4
  141. package/src/generic/RunViewResolver.ts +31 -0
  142. package/src/index.ts +108 -15
  143. package/src/integration/RSUProgressBridge.ts +248 -0
  144. package/src/integration/SchemaRefreshLaunch.ts +77 -0
  145. package/src/providerConfigUnits.ts +21 -0
  146. package/src/resolvers/FileResolver.ts +200 -40
  147. package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
  148. package/src/resolvers/QueryResolver.ts +25 -11
  149. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  150. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  151. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  152. package/src/resolvers/UserResolver.ts +99 -60
  153. package/src/resolvers/currentUserRoles.ts +47 -0
  154. package/src/rest/MediaAccessKeys.ts +47 -1
  155. package/src/rest/MediaStreamHandler.ts +98 -3
  156. package/src/rest/OAuthCallbackHandler.ts +149 -41
  157. package/src/rest/UploadTokenManager.ts +211 -0
  158. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  159. package/src/services/StartTaskGraphDispatcher.ts +21 -1
  160. package/src/services/TaskGraphAgentRunner.ts +23 -0
  161. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  162. package/src/types.ts +6 -0
@@ -19,12 +19,14 @@ import { UUIDsEqual } from "@memberjunction/global";
19
19
  import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
20
20
  import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync, decideSchemaLimitViolations, IntegrationConnectorCreationPipeline, IntegrationActionGenerator } from "@memberjunction/integration-engine";
21
21
  import { IntegrationEngineBase } from "@memberjunction/integration-engine-base";
22
+ import { buildIntegrationLLMPKCallback } from "@memberjunction/core-entities-server";
22
23
  import { SchemaBuilder, TypeMapper, SchemaEvolution } from "@memberjunction/integration-schema-builder";
23
24
  import { RuntimeSchemaManager } from "@memberjunction/schema-engine";
24
- import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
25
+ import { IntegrationProgressEmitter, IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
25
26
  import { ResolverBase } from "../generic/ResolverBase.js";
26
27
  import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
27
28
  import { ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
29
+ import { BuildCreateConnectionMessage, BuildDetachedRefreshMessage, BuildUpdateConnectionMessage } from "../integration/SchemaRefreshLaunch.js";
28
30
  import { RequireSystemUser } from "../directives/RequireSystemUser.js";
29
31
  import { UserCache } from "@memberjunction/generic-database-provider";
30
32
  // ─── RSU Pipeline Output Types ──────────────────────────────────────────────
@@ -1074,6 +1076,18 @@ __decorate([
1074
1076
  Field(),
1075
1077
  __metadata("design:type", String)
1076
1078
  ], CreateConnectionPipelineSummary.prototype, "RunID", void 0);
1079
+ __decorate([
1080
+ Field(),
1081
+ __metadata("design:type", Boolean)
1082
+ ], CreateConnectionPipelineSummary.prototype, "InProgress", void 0);
1083
+ __decorate([
1084
+ Field(),
1085
+ __metadata("design:type", Boolean)
1086
+ ], CreateConnectionPipelineSummary.prototype, "Succeeded", void 0);
1087
+ __decorate([
1088
+ Field({ nullable: true }),
1089
+ __metadata("design:type", String)
1090
+ ], CreateConnectionPipelineSummary.prototype, "FailureMessage", void 0);
1077
1091
  __decorate([
1078
1092
  Field(),
1079
1093
  __metadata("design:type", Number)
@@ -1181,6 +1195,10 @@ __decorate([
1181
1195
  Field(() => Int, { nullable: true, description: 'Time budget (ms) for stage-2 streaming field discovery before it stops and uses what it gathered.' }),
1182
1196
  __metadata("design:type", Number)
1183
1197
  ], IntegrationSyncConfigInput.prototype, "DiscoveryTimeBudgetMs", void 0);
1198
+ __decorate([
1199
+ 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).' }),
1200
+ __metadata("design:type", Number)
1201
+ ], IntegrationSyncConfigInput.prototype, "FetchTimeoutMs", void 0);
1184
1202
  __decorate([
1185
1203
  Field(() => Int, { nullable: true, description: 'Batch size for stage-2 streaming field discovery (records per FetchChanges page). Default 500.' }),
1186
1204
  __metadata("design:type", Number)
@@ -1234,6 +1252,10 @@ __decorate([
1234
1252
  Field(() => Int, { nullable: true }),
1235
1253
  __metadata("design:type", Number)
1236
1254
  ], IntegrationSyncConfigOutput.prototype, "DiscoveryTimeBudgetMs", void 0);
1255
+ __decorate([
1256
+ Field(() => Int, { nullable: true }),
1257
+ __metadata("design:type", Number)
1258
+ ], IntegrationSyncConfigOutput.prototype, "FetchTimeoutMs", void 0);
1237
1259
  __decorate([
1238
1260
  Field(() => Int, { nullable: true }),
1239
1261
  __metadata("design:type", Number)
@@ -2083,7 +2105,7 @@ __decorate([
2083
2105
  IntegrationRunEventsOutput = __decorate([
2084
2106
  ObjectType()
2085
2107
  ], IntegrationRunEventsOutput);
2086
- // Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgress()
2108
+ // Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgressAsync()
2087
2109
  let ConnectionSummaryOutput = class ConnectionSummaryOutput {
2088
2110
  };
2089
2111
  __decorate([
@@ -3372,9 +3394,33 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3372
3394
  }
3373
3395
  return result;
3374
3396
  }
3375
- async runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention) {
3397
+ async runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention, runID) {
3398
+ // Sync lock (#3656). This pipeline rewrites the very IO/IOF rows, field
3399
+ // maps and DDL a sync reads, so it must not overlap a sync or another
3400
+ // maintenance operation for the same connection. The two other
3401
+ // pipeline call sites — IntegrationRefreshConnectorSchema and
3402
+ // IntegrationSchemaEvolution — already take this lock inline; this is
3403
+ // the third and fourth (create/update/reactivate) finally taking it too.
3404
+ if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema refresh')) {
3405
+ throw new Error('Schema refresh not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.');
3406
+ }
3407
+ try {
3408
+ return await this.runSchemaRefreshPipelineLocked(companyIntegrationID, user, provider, universalPKConvention, runID);
3409
+ }
3410
+ finally {
3411
+ IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
3412
+ }
3413
+ }
3414
+ /** The body of {@link runSchemaRefreshPipeline}, run with the maintenance lock held. */
3415
+ async runSchemaRefreshPipelineLocked(companyIntegrationID, user, provider, universalPKConvention, runID) {
3376
3416
  const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
3377
3417
  const pipeline = new IntegrationConnectorCreationPipeline();
3418
+ // SoftPKClassifier's LLM tier (universal → naming → statistical → LLM →
3419
+ // synthetic) only runs when this callback is supplied. It used to be
3420
+ // wired by the CompanyIntegration save hook; with that hook gone
3421
+ // (#3738) it is wired here, so the create/update/reactivate paths keep
3422
+ // the same PK-inference quality they had before.
3423
+ const llmInference = await buildIntegrationLLMPKCallback(user);
3378
3424
  const runOpts = {
3379
3425
  Connector: connector,
3380
3426
  CompanyIntegration: companyIntegration,
@@ -3383,6 +3429,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3383
3429
  UniversalPKConvention: universalPKConvention || undefined,
3384
3430
  ConsoleMirror: true,
3385
3431
  TriggerType: 'Manual',
3432
+ LLMInference: llmInference ?? undefined,
3433
+ // Caller-supplied runID: lets the detached path hand a tailable ID back to the client
3434
+ // BEFORE the pipeline has done any work. Omitted ⇒ the pipeline generates its own.
3435
+ RunID: runID,
3386
3436
  };
3387
3437
  const result = await pipeline.Run(runOpts);
3388
3438
  // Refresh in-memory caches so downstream queries (object picker,
@@ -3391,6 +3441,9 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3391
3441
  await IntegrationEngine.Instance.Config(true, user, provider);
3392
3442
  return {
3393
3443
  RunID: result.RunID,
3444
+ InProgress: false,
3445
+ Succeeded: result.Success,
3446
+ FailureMessage: result.FailureMessage,
3394
3447
  ObjectsCreated: result.PersistResult?.ObjectsCreated ?? 0,
3395
3448
  ObjectsUpdated: result.PersistResult?.ObjectsUpdated ?? 0,
3396
3449
  FieldsCreated: result.PersistResult?.FieldsCreated ?? 0,
@@ -3406,6 +3459,88 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3406
3459
  })),
3407
3460
  };
3408
3461
  }
3462
+ /**
3463
+ * Launches the schema-refresh pipeline WITHOUT awaiting it and returns a summary carrying the
3464
+ * tailable `RunID` and `InProgress: true`.
3465
+ *
3466
+ * Why: the pipeline is a live vendor introspect — minutes on a large catalog (HubSpot: 130
3467
+ * objects). Awaiting it inline holds the create/update mutation open for that entire time, so the
3468
+ * connection wizard's Finish button sits on an indeterminate spinner with no way to tell progress
3469
+ * from a hang. The pipeline already writes a complete, durable, per-run event stream keyed by its
3470
+ * runID; the only thing missing was handing that ID to the caller BEFORE the work starts. Hence
3471
+ * the caller-supplied runID.
3472
+ *
3473
+ * Trade-off the caller opts into: the mutation no longer reports what the refresh found (counts
3474
+ * are placeholder zeros) and a refresh failure no longer surfaces in the mutation's Message — both
3475
+ * live on the run stream instead. That is why `awaitSchemaRefresh` defaults to true.
3476
+ *
3477
+ * ON CREATE this is the only refresh that runs. #3738 removed the activation hook that used to
3478
+ * fire the same pipeline inside `Save()`, awaited — which had made the create mutation pay a full
3479
+ * introspect before ever reaching this method, and had run it BEFORE the connection test, so a
3480
+ * rejected connection was rolled back with its discovered schema already written.
3481
+ *
3482
+ * Belt and braces: if the launch below IS coalesced onto some other run for this connection (a
3483
+ * concurrent refresh, a repeat click), the pipeline publishes a terminal alias run under `runID`
3484
+ * naming the run that served it — so the ID handed back here is tailable in every case.
3485
+ */
3486
+ startSchemaRefreshPipelineDetached(companyIntegrationID, user, provider, universalPKConvention) {
3487
+ const runID = IntegrationProgressEmitter.newRunID('connector');
3488
+ // Deliberately not awaited. Once the pipeline is running it records every outcome on this
3489
+ // run's artifact stream under `runID` itself — but it can also throw BEFORE it ever
3490
+ // constructs its emitter (connector-driver resolution, a missing Integration row), and in
3491
+ // the blocking path that throw surfaced in the mutation's Message. Detached, the caller has
3492
+ // already been handed `runID` and has nothing but the stream to watch, so a pre-emitter
3493
+ // throw must be published onto that stream — otherwise the run never appears at all and a
3494
+ // tailer waits forever on a run ID that will never produce an event.
3495
+ void this.runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention, runID)
3496
+ .catch(err => this.publishDetachedLaunchFailure(runID, companyIntegrationID, err));
3497
+ return {
3498
+ RunID: runID,
3499
+ InProgress: true,
3500
+ // Not known yet — the run has only just been launched.
3501
+ Succeeded: false,
3502
+ ObjectsCreated: 0,
3503
+ ObjectsUpdated: 0,
3504
+ FieldsCreated: 0,
3505
+ FieldsUpdated: 0,
3506
+ UnresolvedObjects: [],
3507
+ PKVerdicts: [],
3508
+ };
3509
+ }
3510
+ /**
3511
+ * Terminates a detached run that failed BEFORE the pipeline could open its own artifact stream.
3512
+ *
3513
+ * The pipeline constructs its emitter inside `Run()`, so anything that throws on the way there —
3514
+ * connector-driver resolution (`No connector registered for driver class "X"`), a missing
3515
+ * Integration row, a CompanyIntegration that vanished — produces no run directory at all. In the
3516
+ * blocking path that throw surfaced in the mutation's Message. Detached, the caller has already
3517
+ * been handed the run ID and has only the stream to watch, so the failure has to be published
3518
+ * there or the run is invisible forever.
3519
+ *
3520
+ * Only ever called on the rejection path: if the pipeline got far enough to build its own emitter
3521
+ * it terminates its own run, and a pipeline that completed never reaches here.
3522
+ */
3523
+ publishDetachedLaunchFailure(runID, companyIntegrationID, err) {
3524
+ const message = this.formatError(err);
3525
+ LogError(`Detached schema refresh (run ${runID}) failed: ${message}`);
3526
+ try {
3527
+ const emitter = new IntegrationProgressEmitter({
3528
+ runID,
3529
+ runKind: 'ConnectorCreation',
3530
+ companyIntegrationID,
3531
+ triggerType: 'Manual',
3532
+ startedAt: new Date().toISOString(),
3533
+ });
3534
+ emitter.runStart('Detached schema refresh launch');
3535
+ emitter.stageError('Launch', message, { code: 'schema-refresh-launch-failed' });
3536
+ void emitter.fail(`Schema refresh could not start: ${message}`, 'schema-refresh-launch-failed')
3537
+ .catch(e => LogError(`Detached schema refresh (run ${runID}): failure artifact write failed — ${e}`));
3538
+ }
3539
+ catch (e) {
3540
+ // Progress reporting must never mask the original failure, which is already logged above.
3541
+ LogError(`Detached schema refresh (run ${runID}): could not open failure artifact — ${e}`);
3542
+ }
3543
+ }
3409
3544
  /**
3410
3545
  * Rolls back a freshly created connection by deleting both the CompanyIntegration and Credential records.
3411
3546
  */
@@ -3556,7 +3691,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3556
3691
  /**
3557
3692
  * Creates a CompanyIntegration with a linked Credential entity for encrypted credential storage.
3558
3693
  */
3559
- async IntegrationCreateConnection(input, testConnection, runSchemaRefresh, universalPKConvention, ctx) {
3694
+ async IntegrationCreateConnection(input, testConnection, runSchemaRefresh, universalPKConvention, awaitSchemaRefresh, ctx) {
3560
3695
  try {
3561
3696
  const user = this.getAuthenticatedUser(ctx);
3562
3697
  const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
@@ -3579,12 +3714,20 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3579
3714
  ci.IntegrationID = input.IntegrationID;
3580
3715
  ci.CompanyID = input.CompanyID;
3581
3716
  ci.CredentialID = credentialID;
3582
- ci.IsActive = true;
3717
+ // Created INACTIVE on purpose (#3738). A connection is not marked
3718
+ // active until its credential has had a chance to prove itself in
3719
+ // step 3 below — so nothing downstream ever observes an active row
3720
+ // backed by a password that is about to be rejected and rolled back.
3721
+ ci.IsActive = false;
3583
3722
  ci.Name = input.CredentialName; // Name is required on CompanyIntegration
3584
3723
  if (input.ExternalSystemID)
3585
3724
  ci.ExternalSystemID = input.ExternalSystemID;
3586
3725
  if (input.Configuration)
3587
3726
  ci.Configuration = input.Configuration;
3727
+ // Whether the refresh runs detached (this resolver owns it and returns a tailable RunID)
3728
+ // or inline below. No save-side refresh exists to coordinate with any more — #3738 removed
3729
+ // the activation hook, so every refresh on this path is one this method asked for.
3730
+ const ownSchemaRefresh = runSchemaRefresh && !awaitSchemaRefresh;
3588
3731
  const saved = await ci.Save();
3589
3732
  if (!saved) {
3590
3733
  const validationErrors = ci.LatestResult?.Message || 'Unknown validation error';
@@ -3607,12 +3750,27 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3607
3750
  testPassed = true;
3608
3751
  testMessage = testResult.Message;
3609
3752
  }
3753
+ // 3b. Credential has proven itself (or the caller declined a test,
3754
+ // which is the pre-existing default) — activate. Ordering matters:
3755
+ // this save is what makes the connection usable, and it must not
3756
+ // happen while the credential is still unverified.
3757
+ ci.IsActive = true;
3758
+ if (!await ci.Save()) {
3759
+ await this.rollbackCreatedConnection(ci, credential);
3760
+ return {
3761
+ Success: false,
3762
+ Message: `Failed to activate CompanyIntegration: ${ci.LatestResult?.Message || 'Unknown error'}`,
3763
+ };
3764
+ }
3610
3765
  // 4. Auto-run schema refresh pipeline (intermittent server-side period).
3611
3766
  // Fires whenever runSchemaRefresh=true, regardless of whether the
3612
3767
  // caller also asked for a test. The wizard may have tested separately
3613
3768
  // and just be hitting Create to save.
3614
3769
  let schemaRefreshSummary;
3615
- if (runSchemaRefresh) {
3770
+ if (ownSchemaRefresh) {
3771
+ schemaRefreshSummary = this.startSchemaRefreshPipelineDetached(ci.ID, user, md, universalPKConvention);
3772
+ }
3773
+ else if (runSchemaRefresh) {
3616
3774
  try {
3617
3775
  const refreshResult = await this.runSchemaRefreshPipeline(ci.ID, user, md, universalPKConvention);
3618
3776
  schemaRefreshSummary = refreshResult;
@@ -3626,9 +3784,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3626
3784
  if (testConnection || schemaRefreshSummary) {
3627
3785
  return {
3628
3786
  Success: true,
3629
- Message: schemaRefreshSummary
3630
- ? `Connection created${testConnection ? ', test passed' : ''}, schema refresh: ${schemaRefreshSummary.ObjectsCreated} created, ${schemaRefreshSummary.ObjectsUpdated} updated, ${schemaRefreshSummary.UnresolvedObjects.length} PK-unresolved`
3631
- : 'Connection created and test passed',
3787
+ Message: BuildCreateConnectionMessage(testConnection, schemaRefreshSummary),
3632
3788
  CompanyIntegrationID: ci.ID,
3633
3789
  CredentialID: credentialID,
3634
3790
  ConnectionTestSuccess: testPassed,
@@ -3651,7 +3807,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3651
3807
  /**
3652
3808
  * Updates credential values and/or configuration on an existing CompanyIntegration.
3653
3809
  */
3654
- async IntegrationUpdateConnection(companyIntegrationID, credentialValues, configuration, externalSystemID, testConnection, runSchemaRefresh, universalPKConvention, ctx) {
3810
+ async IntegrationUpdateConnection(companyIntegrationID, credentialValues, configuration, externalSystemID, testConnection, runSchemaRefresh, universalPKConvention, awaitSchemaRefresh, ctx) {
3655
3811
  try {
3656
3812
  const user = this.getAuthenticatedUser(ctx);
3657
3813
  const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
@@ -3700,13 +3856,14 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3700
3856
  // Fires whenever runSchemaRefresh=true, regardless of whether the
3701
3857
  // caller also asked for a test — the wizard may have tested separately
3702
3858
  // already and is just hitting Update to save edits.
3859
+ if (runSchemaRefresh && !awaitSchemaRefresh) {
3860
+ const detached = this.startSchemaRefreshPipelineDetached(companyIntegrationID, user, md, universalPKConvention);
3861
+ return { Success: true, Message: BuildDetachedRefreshMessage(detached.RunID) };
3862
+ }
3703
3863
  if (runSchemaRefresh) {
3704
3864
  try {
3705
3865
  const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md, universalPKConvention);
3706
- return {
3707
- Success: true,
3708
- Message: `Updated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
3709
- };
3866
+ return { Success: true, Message: BuildUpdateConnectionMessage(refreshResult) };
3710
3867
  }
3711
3868
  catch (refreshErr) {
3712
3869
  LogError(`IntegrationUpdateConnection: pipeline error — ${refreshErr}`);
@@ -3751,6 +3908,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3751
3908
  set('crossLayerPipeline', config.CrossLayerPipeline);
3752
3909
  set('partitionReconcile', config.PartitionReconcile);
3753
3910
  set('discoveryTimeBudgetMs', config.DiscoveryTimeBudgetMs);
3911
+ set('fetchTimeoutMs', config.FetchTimeoutMs);
3754
3912
  set('discoveryBatchSize', config.DiscoveryBatchSize);
3755
3913
  set('discoveryMaxRecords', config.DiscoveryMaxRecords);
3756
3914
  set('deactivateAbsent', config.DeactivateAbsent);
@@ -3812,6 +3970,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3812
3970
  CrossLayerPipeline: bool(cfg.crossLayerPipeline),
3813
3971
  PartitionReconcile: bool(cfg.partitionReconcile),
3814
3972
  DiscoveryTimeBudgetMs: num(cfg.discoveryTimeBudgetMs),
3973
+ FetchTimeoutMs: num(cfg.fetchTimeoutMs),
3815
3974
  DiscoveryBatchSize: num(cfg.discoveryBatchSize),
3816
3975
  DiscoveryMaxRecords: num(cfg.discoveryMaxRecords),
3817
3976
  DeactivateAbsent: bool(cfg.deactivateAbsent),
@@ -3907,8 +4066,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3907
4066
  }
3908
4067
  /**
3909
4068
  * Reactivates a previously deactivated CompanyIntegration by setting IsActive=true.
4069
+ *
4070
+ * Before #3738 this mutation inherited a full schema refresh from the
4071
+ * CompanyIntegration save hook, with no way to decline it. The refresh is
4072
+ * now this resolver's own, explicit step — same default behaviour, but
4073
+ * visible in the API and suppressible with `runSchemaRefresh: false`.
3910
4074
  */
3911
- async IntegrationReactivateConnection(companyIntegrationID, ctx) {
4075
+ async IntegrationReactivateConnection(companyIntegrationID, runSchemaRefresh, ctx) {
3912
4076
  try {
3913
4077
  const user = this.getAuthenticatedUser(ctx);
3914
4078
  const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
@@ -3919,6 +4083,22 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3919
4083
  ci.IsActive = true;
3920
4084
  if (!await ci.Save())
3921
4085
  return { Success: false, Message: `Failed to reactivate: ${ci.LatestResult?.Message ?? 'Unknown error'}` };
4086
+ if (runSchemaRefresh) {
4087
+ try {
4088
+ const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md);
4089
+ return {
4090
+ Success: true,
4091
+ Message: `Reactivated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
4092
+ };
4093
+ }
4094
+ catch (refreshErr) {
4095
+ // Refresh failure does NOT undo the reactivation — the
4096
+ // connection is active, only discovery failed, and the
4097
+ // operator can re-run IntegrationRefreshConnectorSchema.
4098
+ LogError(`IntegrationReactivateConnection: pipeline error — ${refreshErr}`);
4099
+ return { Success: true, Message: `Reactivated (schema refresh failed: ${this.formatError(refreshErr)})` };
4100
+ }
4101
+ }
3922
4102
  return { Success: true, Message: 'Reactivated' };
3923
4103
  }
3924
4104
  catch (e) {
@@ -4266,11 +4446,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4266
4446
  // Step 3: Build schema and RSU pipeline input
4267
4447
  const { schemaOutput, rsuInput } = await this.buildSchemaForConnector(input.CompanyIntegrationID, objects, validatedPlatform, user, skipGitCommit, skipRestart, provider);
4268
4448
  // Step 4: Inject integration post-restart payload into RSU input.
4269
- const { join } = await import('node:path');
4270
- const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
4271
- const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
4272
- const pendingFilePath = join(pendingWorkDir, `${Date.now()}.json`);
4273
- // Build per-object field map for pending file (null = all fields).
4449
+ // Build per-object field map for the pending payload (null = all fields).
4274
4450
  // resolved.sourceObjects is order-aligned with resolvedNames after the
4275
4451
  // resolveSourceObjectsToNames refactor — pair them directly instead
4276
4452
  // of looking up by ID (which broke for name-only selections).
@@ -4300,13 +4476,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4300
4476
  ScheduleTimezone: input.ScheduleTimezone,
4301
4477
  StartSync: input.StartSync,
4302
4478
  FullSync: effectiveFullSync,
4303
- SyncScope: input.SyncScope ?? 'created',
4479
+ SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
4304
4480
  UnselectedAction: (input.UnselectedAction ?? 'disable'),
4305
4481
  CreatedAt: new Date().toISOString(),
4306
4482
  };
4307
- rsuInput.PostRestartFiles = [
4308
- { Path: pendingFilePath, Content: JSON.stringify(pendingPayload, null, 2) }
4309
- ];
4483
+ rsuInput.PendingWork = [pendingPayload];
4484
+ rsuInput.ContextUser = user;
4310
4485
  // Step 5: Run pipeline (restart kills process at the end).
4311
4486
  // Retrying variant — an install should not die because the migration hit a dropped
4312
4487
  // connection or a transient lock; it only replays while nothing has been applied.
@@ -4316,12 +4491,9 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4316
4491
  const pipelineSteps = batchResult.Results[0]?.Steps.map((s) => ({
4317
4492
  Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
4318
4493
  }));
4319
- // If pipeline failed, clean up pending file and return error
4494
+ // Pending work is registered only for migrations that succeeded, so a
4495
+ // failed pipeline has nothing to clean up.
4320
4496
  if (!migrationSucceeded) {
4321
- try {
4322
- (await import('node:fs')).unlinkSync(pendingFilePath);
4323
- }
4324
- catch { /* may not exist */ }
4325
4497
  return {
4326
4498
  Success: false,
4327
4499
  Message: `Pipeline failed: ${batchResult.Results[0]?.ErrorMessage ?? 'unknown error'}`,
@@ -4365,10 +4537,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4365
4537
  console.warn(`[Integration] Schedule creation failed: ${schedErr}`);
4366
4538
  }
4367
4539
  }
4368
- try {
4369
- (await import('node:fs')).unlinkSync(pendingFilePath);
4540
+ // The work just happened inline (no restart), so close out its durable rows.
4541
+ for (const pendingWorkID of batchResult.Results[0]?.PendingWorkIDs ?? []) {
4542
+ await rsm.CompletePendingWork(pendingWorkID, user);
4370
4543
  }
4371
- catch { /* already consumed */ }
4372
4544
  return {
4373
4545
  Success: true,
4374
4546
  Message: `Applied ${objects.length} object(s) — ${entityMapsCreated.length} entity maps created${syncRunID ? ', sync started' : ''}${scheduledJobID ? ', schedule created' : ''}`,
@@ -4849,14 +5021,56 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4849
5021
  return { Success: false, Message: this.formatError(e) };
4850
5022
  }
4851
5023
  }
5024
+ /**
5025
+ * Enqueue a sync for worker execution (PR 1 item 8) instead of running it in this
5026
+ * process. Creates the run row with `Status='Queued'` and returns its ID immediately —
5027
+ * so the caller gets a real, trackable RunID with no fire-and-forget polling window,
5028
+ * and the sync survives this process going away before it starts.
5029
+ *
5030
+ * Requires a process running the integration sync worker (`integrationSyncWorker.enabled`);
5031
+ * without one, rows accumulate in the queue unexecuted.
5032
+ */
5033
+ async IntegrationEnqueueSync(companyIntegrationID, fullSync, entityMapIDs, syncDirection, ctx) {
5034
+ try {
5035
+ const user = this.getAuthenticatedUser(ctx);
5036
+ const provider = GetReadWriteProvider(ctx.providers);
5037
+ await IntegrationEngine.Instance.Config(false, user, provider);
5038
+ // Same upfront gates as IntegrationStartSync — refusing here beats queueing work
5039
+ // that the engine will only refuse later, in another process, out of the caller's sight.
5040
+ const ciCheck = await provider.GetEntityObject('MJ: Company Integrations', user);
5041
+ if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
5042
+ return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not enqueued' };
5043
+ }
5044
+ const maintenance = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
5045
+ if (maintenance) {
5046
+ return { Success: false, Message: `Sync not enqueued: ${maintenance.Reason} is in progress for this connection (since ${maintenance.AcquiredAt.toISOString()}). Retry after it completes.` };
5047
+ }
5048
+ const syncOptions = {};
5049
+ if (fullSync)
5050
+ syncOptions.FullSync = true;
5051
+ if (entityMapIDs?.length)
5052
+ syncOptions.EntityMapIDs = entityMapIDs;
5053
+ if (syncDirection)
5054
+ syncOptions.SyncDirection = syncDirection;
5055
+ const runID = await IntegrationEngine.Instance.EnqueueSync(companyIntegrationID, user, 'Manual', Object.keys(syncOptions).length > 0 ? syncOptions : undefined, provider);
5056
+ return { Success: true, Message: 'Sync enqueued', RunID: runID };
5057
+ }
5058
+ catch (e) {
5059
+ LogError(`IntegrationEnqueueSync error: ${e}`);
5060
+ return { Success: false, Message: this.formatError(e) };
5061
+ }
5062
+ }
4852
5063
  /**
4853
5064
  * Cancels a running sync by marking its status as Cancelled.
4854
5065
  */
4855
5066
  async IntegrationCancelSync(companyIntegrationID, ctx) {
4856
5067
  try {
4857
- this.getAuthenticatedUser(ctx);
4858
- // Signal the engine to abort the running sync
4859
- const cancelled = IntegrationEngine.CancelSync(companyIntegrationID);
5068
+ const user = this.getAuthenticatedUser(ctx);
5069
+ const provider = GetReadWriteProvider(ctx.providers);
5070
+ // DB-backed cancel (durable runs): stamps CancelRequestedAt on the live run row, so
5071
+ // the request reaches the owning process wherever it is — this server, another API
5072
+ // node, or a worker — via its heartbeat/boundary checks. No in-memory signal.
5073
+ const cancelled = await IntegrationEngine.CancelSyncAsync(companyIntegrationID, user, provider);
4860
5074
  if (!cancelled) {
4861
5075
  return { Success: false, Message: 'No active sync found for this connector' };
4862
5076
  }
@@ -5395,8 +5609,11 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5395
5609
  }
5396
5610
  async IntegrationGetSyncProgress(companyIntegrationID, ctx) {
5397
5611
  try {
5398
- this.getAuthenticatedUser(ctx);
5399
- const syncProgress = IntegrationEngine.GetSyncProgress(companyIntegrationID);
5612
+ const user = this.getAuthenticatedUser(ctx);
5613
+ const provider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true });
5614
+ // DB-backed progress (durable runs): reads ProgressJSON from the live run row, so
5615
+ // progress is visible from ANY process — not just the one executing the sync.
5616
+ const syncProgress = await IntegrationEngine.GetSyncProgressAsync(companyIntegrationID, user, provider);
5400
5617
  if (syncProgress) {
5401
5618
  return {
5402
5619
  Success: true,
@@ -5840,16 +6057,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5840
6057
  return obj;
5841
6058
  });
5842
6059
  const { schemaOutput, rsuInput } = await this.buildSchemaForConnector(connInput.CompanyIntegrationID, objects, validatedPlatform, user, skipGitCommit, skipRestart, provider, sourceSchema);
5843
- // Build per-object field map for pending file
6060
+ // Build per-object field map for the pending payload
5844
6061
  const sourceObjectFields = {};
5845
6062
  for (const name of resolvedNames) {
5846
6063
  sourceObjectFields[name] = fieldsByName.get(name.toLowerCase()) ?? null;
5847
6064
  }
5848
6065
  // Inject post-restart pending work payload
5849
- const { join } = await import('node:path');
5850
- const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
5851
- const pendingWorkDir = join(rsuWorkDir, '.rsu_pending');
5852
- const pendingFilePath = join(pendingWorkDir, `${Date.now()}_${connInput.CompanyIntegrationID}.json`);
5853
6066
  const pendingPayload = {
5854
6067
  CompanyIntegrationID: connInput.CompanyIntegrationID,
5855
6068
  SourceObjectNames: resolvedNames,
@@ -5859,15 +6072,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5859
6072
  ScheduleTimezone: connInput.ScheduleTimezone,
5860
6073
  StartSync: input.StartSync,
5861
6074
  FullSync: input.FullSync ?? false,
5862
- SyncScope: input.SyncScope ?? 'created',
5863
- SyncDirection: input.SyncDirection,
5864
- ScheduleSyncDirection: input.ScheduleSyncDirection,
6075
+ SyncScope: input.SyncScope === 'all' ? 'all' : 'created',
6076
+ // Unrecognized directions fall back to undefined = "use the entity map's own SyncDirection"
6077
+ SyncDirection: input.SyncDirection && isValidSyncDirection(input.SyncDirection) ? input.SyncDirection : undefined,
6078
+ ScheduleSyncDirection: input.ScheduleSyncDirection && isValidSyncDirection(input.ScheduleSyncDirection) ? input.ScheduleSyncDirection : undefined,
5865
6079
  UnselectedAction: (input.UnselectedAction ?? 'disable'),
5866
6080
  CreatedAt: new Date().toISOString(),
5867
6081
  };
5868
- rsuInput.PostRestartFiles = [
5869
- { Path: pendingFilePath, Content: JSON.stringify(pendingPayload, null, 2) }
5870
- ];
6082
+ rsuInput.PendingWork = [pendingPayload];
6083
+ rsuInput.ContextUser = user;
5871
6084
  return {
5872
6085
  connInput,
5873
6086
  connector,
@@ -5876,7 +6089,6 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5876
6089
  objects,
5877
6090
  schemaOutput,
5878
6091
  rsuInput,
5879
- pendingFilePath,
5880
6092
  };
5881
6093
  }));
5882
6094
  // Separate successes and failures
@@ -5927,11 +6139,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5927
6139
  Message: pipelineResult?.ErrorMessage ?? 'Pipeline failed',
5928
6140
  Warnings: build.schemaOutput.Warnings.length > 0 ? build.schemaOutput.Warnings : undefined,
5929
6141
  });
5930
- // Clean up pending file on failure
5931
- try {
5932
- (await import('node:fs')).unlinkSync(build.pendingFilePath);
5933
- }
5934
- catch { /* may not exist */ }
6142
+ // No pending-work cleanup needed — rows are registered only for
6143
+ // migrations that succeeded.
5935
6144
  continue;
5936
6145
  }
5937
6146
  const connResult = {
@@ -5967,11 +6176,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
5967
6176
  if (scheduleResult)
5968
6177
  connResult.ScheduledJobID = scheduleResult;
5969
6178
  }
5970
- // Clean up pending file
5971
- try {
5972
- (await import('node:fs')).unlinkSync(build.pendingFilePath);
6179
+ // The work just happened inline (no restart), so close out its durable rows.
6180
+ for (const pendingWorkID of pipelineResult.PendingWorkIDs ?? []) {
6181
+ await rsm.CompletePendingWork(pendingWorkID, user);
5973
6182
  }
5974
- catch { /* already consumed */ }
5975
6183
  connResult.Message = `Applied ${build.objects.length} object(s) — ${entityMapsCreated.length} entity maps created${syncRunID ? ', sync started' : ''}`;
5976
6184
  }
5977
6185
  connectorResults.push(connResult);
@@ -6476,26 +6684,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
6476
6684
  // ApplyAll uses. default: created DISABLED (user enables after the refresh);
6477
6685
  // autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
6478
6686
  if (newObjects.length > 0 && !skipRestart) {
6479
- const { join } = await import('node:path');
6480
- const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
6481
- const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
6482
- rsuInput.PostRestartFiles = [
6483
- ...(rsuInput.PostRestartFiles ?? []),
6687
+ rsuInput.PendingWork = [
6688
+ ...(rsuInput.PendingWork ?? []),
6484
6689
  {
6485
- Path: pendingFilePath,
6486
- Content: JSON.stringify({
6487
- CompanyIntegrationID: companyIntegrationID,
6488
- SourceObjectNames: newObjects,
6489
- SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
6490
- SchemaName: schemaName,
6491
- StartSync: false,
6492
- SyncScope: 'created',
6493
- UnselectedAction: 'ignore',
6494
- CreateDisabled: !autoEnableNewObjects,
6495
- CreatedAt: new Date().toISOString(),
6496
- }, null, 2),
6690
+ CompanyIntegrationID: companyIntegrationID,
6691
+ SourceObjectNames: newObjects,
6692
+ SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
6693
+ SchemaName: schemaName,
6694
+ StartSync: false,
6695
+ SyncScope: 'created',
6696
+ UnselectedAction: 'ignore',
6697
+ CreateDisabled: !autoEnableNewObjects,
6698
+ CreatedAt: new Date().toISOString(),
6497
6699
  },
6498
6700
  ];
6701
+ rsuInput.ContextUser = user;
6499
6702
  }
6500
6703
  const rsm = RuntimeSchemaManager.Instance;
6501
6704
  const batchResult = await rsm.RunPipelineBatch([rsuInput]);
@@ -6753,9 +6956,10 @@ __decorate([
6753
6956
  __param(1, Arg("testConnection", () => Boolean, { defaultValue: false })),
6754
6957
  __param(2, 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." })),
6755
6958
  __param(3, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot). Improves SoftPKClassifier convergence." })),
6756
- __param(4, Ctx()),
6959
+ __param(4, 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)." })),
6960
+ __param(5, Ctx()),
6757
6961
  __metadata("design:type", Function),
6758
- __metadata("design:paramtypes", [CreateConnectionInput, Boolean, Boolean, String, Object]),
6962
+ __metadata("design:paramtypes", [CreateConnectionInput, Boolean, Boolean, String, Boolean, Object]),
6759
6963
  __metadata("design:returntype", Promise)
6760
6964
  ], IntegrationDiscoveryResolver.prototype, "IntegrationCreateConnection", null);
6761
6965
  __decorate([
@@ -6767,9 +6971,10 @@ __decorate([
6767
6971
  __param(4, Arg("testConnection", () => Boolean, { defaultValue: false })),
6768
6972
  __param(5, Arg("runSchemaRefresh", () => Boolean, { defaultValue: true, description: "When true (default) and TestConnection succeeds, automatically runs IntegrationConnectorCreationPipeline. Same intermittent server-side step as the create flow." })),
6769
6973
  __param(6, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot)" })),
6770
- __param(7, Ctx()),
6974
+ __param(7, 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." })),
6975
+ __param(8, Ctx()),
6771
6976
  __metadata("design:type", Function),
6772
- __metadata("design:paramtypes", [String, String, String, String, Boolean, Boolean, String, Object]),
6977
+ __metadata("design:paramtypes", [String, String, String, String, Boolean, Boolean, String, Boolean, Object]),
6773
6978
  __metadata("design:returntype", Promise)
6774
6979
  ], IntegrationDiscoveryResolver.prototype, "IntegrationUpdateConnection", null);
6775
6980
  __decorate([
@@ -6819,9 +7024,10 @@ __decorate([
6819
7024
  __decorate([
6820
7025
  Mutation(() => MutationResultOutput),
6821
7026
  __param(0, Arg("companyIntegrationID")),
6822
- __param(1, Ctx()),
7027
+ __param(1, 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." })),
7028
+ __param(2, Ctx()),
6823
7029
  __metadata("design:type", Function),
6824
- __metadata("design:paramtypes", [String, Object]),
7030
+ __metadata("design:paramtypes", [String, Boolean, Object]),
6825
7031
  __metadata("design:returntype", Promise)
6826
7032
  ], IntegrationDiscoveryResolver.prototype, "IntegrationReactivateConnection", null);
6827
7033
  __decorate([
@@ -6883,6 +7089,18 @@ __decorate([
6883
7089
  __metadata("design:paramtypes", [String, String, Boolean, Array, String, Object]),
6884
7090
  __metadata("design:returntype", Promise)
6885
7091
  ], IntegrationDiscoveryResolver.prototype, "IntegrationStartSync", null);
7092
+ __decorate([
7093
+ Mutation(() => StartSyncOutput),
7094
+ RequireSystemUser(),
7095
+ __param(0, Arg("companyIntegrationID")),
7096
+ __param(1, Arg("fullSync", () => Boolean, { defaultValue: false, description: 'If true, ignores watermarks and re-fetches all records from the source' })),
7097
+ __param(2, Arg("entityMapIDs", () => [String], { nullable: true, description: 'Optional: sync only these entity maps. If omitted, syncs all maps for the connector.' })),
7098
+ __param(3, Arg("syncDirection", () => String, { nullable: true, description: 'Override sync direction: Pull | Push | Bidirectional. If omitted, each entity map\'s own SyncDirection is used.' })),
7099
+ __param(4, Ctx()),
7100
+ __metadata("design:type", Function),
7101
+ __metadata("design:paramtypes", [String, Boolean, Array, String, Object]),
7102
+ __metadata("design:returntype", Promise)
7103
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationEnqueueSync", null);
6886
7104
  __decorate([
6887
7105
  Mutation(() => MutationResultOutput),
6888
7106
  __param(0, Arg("companyIntegrationID")),