@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3

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