@memberjunction/server 5.38.0 → 5.40.0

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 (180) hide show
  1. package/README.md +67 -0
  2. package/dist/apolloServer/index.d.ts +0 -8
  3. package/dist/apolloServer/index.d.ts.map +1 -1
  4. package/dist/apolloServer/index.js +61 -0
  5. package/dist/apolloServer/index.js.map +1 -1
  6. package/dist/auth/index.js +1 -1
  7. package/dist/auth/index.js.map +1 -1
  8. package/dist/auth/magicLink/MagicLinkKeys.d.ts +58 -0
  9. package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -0
  10. package/dist/auth/magicLink/MagicLinkKeys.js +99 -0
  11. package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -0
  12. package/dist/auth/magicLink/MagicLinkRouter.d.ts +33 -0
  13. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -0
  14. package/dist/auth/magicLink/MagicLinkRouter.js +184 -0
  15. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -0
  16. package/dist/auth/magicLink/MagicLinkService.d.ts +123 -0
  17. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -0
  18. package/dist/auth/magicLink/MagicLinkService.js +605 -0
  19. package/dist/auth/magicLink/MagicLinkService.js.map +1 -0
  20. package/dist/auth/magicLink/index.d.ts +6 -0
  21. package/dist/auth/magicLink/index.d.ts.map +1 -0
  22. package/dist/auth/magicLink/index.js +6 -0
  23. package/dist/auth/magicLink/index.js.map +1 -0
  24. package/dist/auth/magicLink/magicLinkCore.d.ts +82 -0
  25. package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -0
  26. package/dist/auth/magicLink/magicLinkCore.js +164 -0
  27. package/dist/auth/magicLink/magicLinkCore.js.map +1 -0
  28. package/dist/auth/magicLink/redeemLanding.d.ts +22 -0
  29. package/dist/auth/magicLink/redeemLanding.d.ts.map +1 -0
  30. package/dist/auth/magicLink/redeemLanding.js +61 -0
  31. package/dist/auth/magicLink/redeemLanding.js.map +1 -0
  32. package/dist/auth/magicLink/types.d.ts +131 -0
  33. package/dist/auth/magicLink/types.d.ts.map +1 -0
  34. package/dist/auth/magicLink/types.js +6 -0
  35. package/dist/auth/magicLink/types.js.map +1 -0
  36. package/dist/config.d.ts +479 -0
  37. package/dist/config.d.ts.map +1 -1
  38. package/dist/config.js +84 -0
  39. package/dist/config.js.map +1 -1
  40. package/dist/context.d.ts.map +1 -1
  41. package/dist/context.js +223 -19
  42. package/dist/context.js.map +1 -1
  43. package/dist/generated/generated.d.ts +952 -4
  44. package/dist/generated/generated.d.ts.map +1 -1
  45. package/dist/generated/generated.js +25663 -20321
  46. package/dist/generated/generated.js.map +1 -1
  47. package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
  48. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
  49. package/dist/generic/FireAndForgetHeartbeat.js +44 -0
  50. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
  51. package/dist/generic/ResolverBase.d.ts.map +1 -1
  52. package/dist/generic/ResolverBase.js +35 -7
  53. package/dist/generic/ResolverBase.js.map +1 -1
  54. package/dist/index.d.ts +5 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +145 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/logging/NoLog.d.ts +50 -0
  59. package/dist/logging/NoLog.d.ts.map +1 -0
  60. package/dist/logging/NoLog.js +80 -0
  61. package/dist/logging/NoLog.js.map +1 -0
  62. package/dist/logging/bootAudit.d.ts +43 -0
  63. package/dist/logging/bootAudit.d.ts.map +1 -0
  64. package/dist/logging/bootAudit.js +83 -0
  65. package/dist/logging/bootAudit.js.map +1 -0
  66. package/dist/logging/boundaryLogPayload.d.ts +18 -0
  67. package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
  68. package/dist/logging/boundaryLogPayload.js +18 -0
  69. package/dist/logging/boundaryLogPayload.js.map +1 -0
  70. package/dist/logging/secretRedactor.d.ts +23 -0
  71. package/dist/logging/secretRedactor.d.ts.map +1 -0
  72. package/dist/logging/secretRedactor.js +53 -0
  73. package/dist/logging/secretRedactor.js.map +1 -0
  74. package/dist/logging/shortenForLog.d.ts +8 -0
  75. package/dist/logging/shortenForLog.d.ts.map +1 -0
  76. package/dist/logging/shortenForLog.js +21 -0
  77. package/dist/logging/shortenForLog.js.map +1 -0
  78. package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
  79. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
  80. package/dist/logging/variablesLoggingMiddleware.js +127 -0
  81. package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
  82. package/dist/resolvers/CurrentUserContextResolver.d.ts +9 -3
  83. package/dist/resolvers/CurrentUserContextResolver.d.ts.map +1 -1
  84. package/dist/resolvers/CurrentUserContextResolver.js +19 -5
  85. package/dist/resolvers/CurrentUserContextResolver.js.map +1 -1
  86. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  87. package/dist/resolvers/EntityResolver.js +13 -2
  88. package/dist/resolvers/EntityResolver.js.map +1 -1
  89. package/dist/resolvers/GenerateSeedTaxonomyResolver.d.ts +28 -0
  90. package/dist/resolvers/GenerateSeedTaxonomyResolver.d.ts.map +1 -0
  91. package/dist/resolvers/GenerateSeedTaxonomyResolver.js +100 -0
  92. package/dist/resolvers/GenerateSeedTaxonomyResolver.js.map +1 -0
  93. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  94. package/dist/resolvers/GetDataResolver.js +8 -4
  95. package/dist/resolvers/GetDataResolver.js.map +1 -1
  96. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
  97. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  98. package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
  99. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  100. package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
  101. package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
  102. package/dist/resolvers/IntegrationProgressResolver.js +196 -0
  103. package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
  104. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  105. package/dist/resolvers/MCPResolver.js +19 -14
  106. package/dist/resolvers/MCPResolver.js.map +1 -1
  107. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  108. package/dist/resolvers/RunAIAgentResolver.js +26 -5
  109. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  110. package/dist/resolvers/RunClusterAnalysisResolver.d.ts +74 -0
  111. package/dist/resolvers/RunClusterAnalysisResolver.d.ts.map +1 -0
  112. package/dist/resolvers/RunClusterAnalysisResolver.js +243 -0
  113. package/dist/resolvers/RunClusterAnalysisResolver.js.map +1 -0
  114. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  115. package/dist/resolvers/RunTestResolver.js +12 -2
  116. package/dist/resolvers/RunTestResolver.js.map +1 -1
  117. package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
  118. package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
  119. package/dist/resolvers/SearchEntitiesResolver.js +216 -0
  120. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
  121. package/dist/resolvers/UserResolver.d.ts +16 -2
  122. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  123. package/dist/resolvers/UserResolver.js +45 -2
  124. package/dist/resolvers/UserResolver.js.map +1 -1
  125. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  126. package/dist/resolvers/VectorizeEntityResolver.js +14 -1
  127. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  128. package/dist/rest/SignatureWebhookHandler.d.ts +19 -0
  129. package/dist/rest/SignatureWebhookHandler.d.ts.map +1 -0
  130. package/dist/rest/SignatureWebhookHandler.js +86 -0
  131. package/dist/rest/SignatureWebhookHandler.js.map +1 -0
  132. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  133. package/dist/services/ScheduledJobsService.js +14 -2
  134. package/dist/services/ScheduledJobsService.js.map +1 -1
  135. package/package.json +79 -74
  136. package/src/__tests__/NoLog.test.ts +76 -0
  137. package/src/__tests__/bootAudit.test.ts +188 -0
  138. package/src/__tests__/boundaryLogPayload.test.ts +31 -0
  139. package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
  140. package/src/__tests__/magicLink.test.ts +387 -0
  141. package/src/__tests__/secretRedactor.test.ts +163 -0
  142. package/src/__tests__/subscriptionRedaction.test.ts +217 -0
  143. package/src/apolloServer/index.ts +58 -0
  144. package/src/auth/index.ts +2 -2
  145. package/src/auth/magicLink/MagicLinkKeys.ts +122 -0
  146. package/src/auth/magicLink/MagicLinkRouter.ts +209 -0
  147. package/src/auth/magicLink/MagicLinkService.ts +724 -0
  148. package/src/auth/magicLink/index.ts +17 -0
  149. package/src/auth/magicLink/magicLinkCore.ts +216 -0
  150. package/src/auth/magicLink/redeemLanding.ts +62 -0
  151. package/src/auth/magicLink/types.ts +137 -0
  152. package/src/config.ts +89 -0
  153. package/src/context.ts +249 -17
  154. package/src/generated/generated.ts +12528 -8866
  155. package/src/generic/FireAndForgetHeartbeat.ts +85 -0
  156. package/src/generic/ResolverBase.ts +35 -7
  157. package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
  158. package/src/index.ts +165 -2
  159. package/src/logging/NoLog.ts +88 -0
  160. package/src/logging/bootAudit.ts +117 -0
  161. package/src/logging/boundaryLogPayload.ts +19 -0
  162. package/src/logging/secretRedactor.ts +82 -0
  163. package/src/logging/shortenForLog.ts +17 -0
  164. package/src/logging/variablesLoggingMiddleware.ts +191 -0
  165. package/src/resolvers/CurrentUserContextResolver.ts +21 -5
  166. package/src/resolvers/EntityResolver.ts +17 -5
  167. package/src/resolvers/GenerateSeedTaxonomyResolver.ts +90 -0
  168. package/src/resolvers/GetDataResolver.ts +9 -5
  169. package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
  170. package/src/resolvers/IntegrationProgressResolver.ts +220 -0
  171. package/src/resolvers/MCPResolver.ts +18 -14
  172. package/src/resolvers/RunAIAgentResolver.ts +28 -5
  173. package/src/resolvers/RunClusterAnalysisResolver.ts +249 -0
  174. package/src/resolvers/RunTestResolver.ts +14 -2
  175. package/src/resolvers/SearchEntitiesResolver.ts +173 -0
  176. package/src/resolvers/UserResolver.ts +38 -2
  177. package/src/resolvers/VectorizeEntityResolver.ts +14 -1
  178. package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
  179. package/src/rest/SignatureWebhookHandler.ts +103 -0
  180. package/src/services/ScheduledJobsService.ts +15 -2
@@ -12,13 +12,14 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  var IntegrationDiscoveryResolver_1;
14
14
  import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType } from "type-graphql";
15
- import { CompositeKey, LocalCacheManager, RunView, LogError } from "@memberjunction/core";
15
+ import { CompositeKey, LocalCacheManager, Metadata, RunView, LogError, LogStatus } from "@memberjunction/core";
16
16
  import { GetReadOnlyProvider, GetReadWriteProvider } from "../util.js";
17
17
  import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
18
- import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync } from "@memberjunction/integration-engine";
18
+ import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync, IntegrationConnectorCreationPipeline, IntegrationActionGenerator } from "@memberjunction/integration-engine";
19
19
  import { IntegrationEngineBase } from "@memberjunction/integration-engine-base";
20
20
  import { SchemaBuilder, TypeMapper, SchemaEvolution } from "@memberjunction/integration-schema-builder";
21
21
  import { RuntimeSchemaManager } from "@memberjunction/schema-engine";
22
+ import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
22
23
  import { ResolverBase } from "../generic/ResolverBase.js";
23
24
  import { UserCache } from "@memberjunction/sqlserver-dataprovider";
24
25
  // ─── RSU Pipeline Output Types ──────────────────────────────────────────────
@@ -412,6 +413,10 @@ __decorate([
412
413
  Field({ nullable: true }),
413
414
  __metadata("design:type", Number)
414
415
  ], DeleteConnectionOutput.prototype, "SchedulesDeleted", void 0);
416
+ __decorate([
417
+ Field({ nullable: true }),
418
+ __metadata("design:type", Boolean)
419
+ ], DeleteConnectionOutput.prototype, "CredentialDeleted", void 0);
415
420
  DeleteConnectionOutput = __decorate([
416
421
  ObjectType()
417
422
  ], DeleteConnectionOutput);
@@ -683,6 +688,151 @@ __decorate([
683
688
  ConnectionTestOutput = __decorate([
684
689
  ObjectType()
685
690
  ], ConnectionTestOutput);
691
+ // ─── Refresh Connector Schema (Phase 0 v5.39.x) ─────────────────────────────
692
+ // Invokes IntegrationConnectorCreationPipeline.Run() which drives
693
+ // TestConnection → IntrospectSchema (parallel describe) →
694
+ // PersistDiscoveredSchema (overlay precedence: declared wins for semantic,
695
+ // discovered wins for technical) → SoftPKClassifier (4-tier cascade) and emits
696
+ // structured progress events the operator can grep in the MJAPI log file:
697
+ // "event":"discovery.object.added" with source: Declared | Discovered | Custom
698
+ // "event":"discovery.field.added" ditto
699
+ // "event":"pk.classifier.invoked" per object missing an explicit PK
700
+ // "event":"pk.classifier.result" the classifier's verdict + strategy + reason
701
+ // "event":"entity.generated" IO has a PK → eligible for MJ entity generation
702
+ // "event":"entity.skipped-no-pk" IO has NO PK → not eligible, deferred
703
+ //
704
+ // Per-run JSONL artifacts also land at:
705
+ // <cwd>/logs/integration-runs/<runID>/{manifest,progress,result}.json
706
+ let RefreshConnectorSchemaPKVerdictOutput = class RefreshConnectorSchemaPKVerdictOutput {
707
+ };
708
+ __decorate([
709
+ Field(),
710
+ __metadata("design:type", String)
711
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "ObjectName", void 0);
712
+ __decorate([
713
+ Field(),
714
+ __metadata("design:type", Boolean)
715
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "Confident", void 0);
716
+ __decorate([
717
+ Field({ nullable: true }),
718
+ __metadata("design:type", String)
719
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "Nominee", void 0);
720
+ __decorate([
721
+ Field(),
722
+ __metadata("design:type", Number)
723
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "Confidence", void 0);
724
+ __decorate([
725
+ Field(),
726
+ __metadata("design:type", String)
727
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "Strategy", void 0);
728
+ __decorate([
729
+ Field(),
730
+ __metadata("design:type", String)
731
+ ], RefreshConnectorSchemaPKVerdictOutput.prototype, "Reason", void 0);
732
+ RefreshConnectorSchemaPKVerdictOutput = __decorate([
733
+ ObjectType()
734
+ ], RefreshConnectorSchemaPKVerdictOutput);
735
+ let RefreshConnectorSchemaOutput = class RefreshConnectorSchemaOutput {
736
+ };
737
+ __decorate([
738
+ Field(),
739
+ __metadata("design:type", Boolean)
740
+ ], RefreshConnectorSchemaOutput.prototype, "Success", void 0);
741
+ __decorate([
742
+ Field(),
743
+ __metadata("design:type", String)
744
+ ], RefreshConnectorSchemaOutput.prototype, "Message", void 0);
745
+ __decorate([
746
+ Field(),
747
+ __metadata("design:type", String)
748
+ ], RefreshConnectorSchemaOutput.prototype, "RunID", void 0);
749
+ __decorate([
750
+ Field({ nullable: true }),
751
+ __metadata("design:type", Number)
752
+ ], RefreshConnectorSchemaOutput.prototype, "ObjectsCreated", void 0);
753
+ __decorate([
754
+ Field({ nullable: true }),
755
+ __metadata("design:type", Number)
756
+ ], RefreshConnectorSchemaOutput.prototype, "ObjectsUpdated", void 0);
757
+ __decorate([
758
+ Field({ nullable: true }),
759
+ __metadata("design:type", Number)
760
+ ], RefreshConnectorSchemaOutput.prototype, "FieldsCreated", void 0);
761
+ __decorate([
762
+ Field({ nullable: true }),
763
+ __metadata("design:type", Number)
764
+ ], RefreshConnectorSchemaOutput.prototype, "FieldsUpdated", void 0);
765
+ __decorate([
766
+ Field(() => [RefreshConnectorSchemaPKVerdictOutput], { nullable: true }),
767
+ __metadata("design:type", Array)
768
+ ], RefreshConnectorSchemaOutput.prototype, "PKVerdicts", void 0);
769
+ __decorate([
770
+ Field(() => [String], { nullable: true }),
771
+ __metadata("design:type", Array)
772
+ ], RefreshConnectorSchemaOutput.prototype, "UnresolvedObjects", void 0);
773
+ __decorate([
774
+ Field({ nullable: true }),
775
+ __metadata("design:type", String)
776
+ ], RefreshConnectorSchemaOutput.prototype, "FailureMessage", void 0);
777
+ RefreshConnectorSchemaOutput = __decorate([
778
+ ObjectType()
779
+ ], RefreshConnectorSchemaOutput);
780
+ // ─── Generate Integration Action (on-demand Integration-as-Actions) ─────────
781
+ // Generates + persists a strongly-typed Action (DriverClass='IntegrationActionExecutor')
782
+ // for one integration/object/verb (or all applicable verbs when verb is omitted) via
783
+ // the engine's IntegrationActionGenerator. Idempotent on the deterministic Action Name
784
+ // "<Integration> - <Verb> <DisplayName>": a matching Action is reused (AlreadyExisted=true)
785
+ // and its params/result codes reconciled rather than duplicated.
786
+ let IntegrationGenerateActionResult = class IntegrationGenerateActionResult {
787
+ };
788
+ __decorate([
789
+ Field(),
790
+ __metadata("design:type", Boolean)
791
+ ], IntegrationGenerateActionResult.prototype, "Success", void 0);
792
+ __decorate([
793
+ Field({ nullable: true }),
794
+ __metadata("design:type", String)
795
+ ], IntegrationGenerateActionResult.prototype, "ActionID", void 0);
796
+ __decorate([
797
+ Field({ nullable: true }),
798
+ __metadata("design:type", String)
799
+ ], IntegrationGenerateActionResult.prototype, "ActionName", void 0);
800
+ __decorate([
801
+ Field(),
802
+ __metadata("design:type", Boolean)
803
+ ], IntegrationGenerateActionResult.prototype, "AlreadyExisted", void 0);
804
+ __decorate([
805
+ Field({ nullable: true }),
806
+ __metadata("design:type", String)
807
+ ], IntegrationGenerateActionResult.prototype, "Verb", void 0);
808
+ __decorate([
809
+ Field({ nullable: true }),
810
+ __metadata("design:type", String)
811
+ ], IntegrationGenerateActionResult.prototype, "ObjectName", void 0);
812
+ __decorate([
813
+ Field(),
814
+ __metadata("design:type", String)
815
+ ], IntegrationGenerateActionResult.prototype, "Message", void 0);
816
+ IntegrationGenerateActionResult = __decorate([
817
+ ObjectType()
818
+ ], IntegrationGenerateActionResult);
819
+ let IntegrationGenerateActionOutput = class IntegrationGenerateActionOutput {
820
+ };
821
+ __decorate([
822
+ Field(),
823
+ __metadata("design:type", Boolean)
824
+ ], IntegrationGenerateActionOutput.prototype, "Success", void 0);
825
+ __decorate([
826
+ Field(),
827
+ __metadata("design:type", String)
828
+ ], IntegrationGenerateActionOutput.prototype, "Message", void 0);
829
+ __decorate([
830
+ Field(() => [IntegrationGenerateActionResult], { nullable: true }),
831
+ __metadata("design:type", Array)
832
+ ], IntegrationGenerateActionOutput.prototype, "Results", void 0);
833
+ IntegrationGenerateActionOutput = __decorate([
834
+ ObjectType()
835
+ ], IntegrationGenerateActionOutput);
686
836
  // --- Preview Data Types ---
687
837
  let PreviewRecordOutput = class PreviewRecordOutput {
688
838
  };
@@ -876,6 +1026,39 @@ __decorate([
876
1026
  CreateConnectionInput = __decorate([
877
1027
  InputType()
878
1028
  ], CreateConnectionInput);
1029
+ let CreateConnectionPipelineSummary = class CreateConnectionPipelineSummary {
1030
+ };
1031
+ __decorate([
1032
+ Field(),
1033
+ __metadata("design:type", String)
1034
+ ], CreateConnectionPipelineSummary.prototype, "RunID", void 0);
1035
+ __decorate([
1036
+ Field(),
1037
+ __metadata("design:type", Number)
1038
+ ], CreateConnectionPipelineSummary.prototype, "ObjectsCreated", void 0);
1039
+ __decorate([
1040
+ Field(),
1041
+ __metadata("design:type", Number)
1042
+ ], CreateConnectionPipelineSummary.prototype, "ObjectsUpdated", void 0);
1043
+ __decorate([
1044
+ Field(),
1045
+ __metadata("design:type", Number)
1046
+ ], CreateConnectionPipelineSummary.prototype, "FieldsCreated", void 0);
1047
+ __decorate([
1048
+ Field(),
1049
+ __metadata("design:type", Number)
1050
+ ], CreateConnectionPipelineSummary.prototype, "FieldsUpdated", void 0);
1051
+ __decorate([
1052
+ Field(() => [String]),
1053
+ __metadata("design:type", Array)
1054
+ ], CreateConnectionPipelineSummary.prototype, "UnresolvedObjects", void 0);
1055
+ __decorate([
1056
+ Field(() => [RefreshConnectorSchemaPKVerdictOutput], { nullable: true }),
1057
+ __metadata("design:type", Array)
1058
+ ], CreateConnectionPipelineSummary.prototype, "PKVerdicts", void 0);
1059
+ CreateConnectionPipelineSummary = __decorate([
1060
+ ObjectType()
1061
+ ], CreateConnectionPipelineSummary);
879
1062
  let CreateConnectionOutput = class CreateConnectionOutput {
880
1063
  };
881
1064
  __decorate([
@@ -902,6 +1085,10 @@ __decorate([
902
1085
  Field({ nullable: true }),
903
1086
  __metadata("design:type", String)
904
1087
  ], CreateConnectionOutput.prototype, "ConnectionTestMessage", void 0);
1088
+ __decorate([
1089
+ Field(() => CreateConnectionPipelineSummary, { nullable: true }),
1090
+ __metadata("design:type", CreateConnectionPipelineSummary)
1091
+ ], CreateConnectionOutput.prototype, "SchemaRefresh", void 0);
905
1092
  CreateConnectionOutput = __decorate([
906
1093
  ObjectType()
907
1094
  ], CreateConnectionOutput);
@@ -961,6 +1148,10 @@ __decorate([
961
1148
  Field({ nullable: true, defaultValue: 0 }),
962
1149
  __metadata("design:type", Number)
963
1150
  ], EntityMapInput.prototype, "Priority", void 0);
1151
+ __decorate([
1152
+ Field({ nullable: true }),
1153
+ __metadata("design:type", String)
1154
+ ], EntityMapInput.prototype, "Configuration", void 0);
964
1155
  __decorate([
965
1156
  Field(() => [FieldMapInput], { nullable: true }),
966
1157
  __metadata("design:type", Array)
@@ -1170,6 +1361,10 @@ __decorate([
1170
1361
  Field({ nullable: true }),
1171
1362
  __metadata("design:type", String)
1172
1363
  ], EntityMapUpdateInput.prototype, "Status", void 0);
1364
+ __decorate([
1365
+ Field({ nullable: true }),
1366
+ __metadata("design:type", String)
1367
+ ], EntityMapUpdateInput.prototype, "Configuration", void 0);
1173
1368
  EntityMapUpdateInput = __decorate([
1174
1369
  InputType()
1175
1370
  ], EntityMapUpdateInput);
@@ -1203,6 +1398,10 @@ __decorate([
1203
1398
  Field({ nullable: true }),
1204
1399
  __metadata("design:type", String)
1205
1400
  ], EntityMapSummaryOutput.prototype, "Status", void 0);
1401
+ __decorate([
1402
+ Field({ nullable: true }),
1403
+ __metadata("design:type", String)
1404
+ ], EntityMapSummaryOutput.prototype, "Configuration", void 0);
1206
1405
  EntityMapSummaryOutput = __decorate([
1207
1406
  ObjectType()
1208
1407
  ], EntityMapSummaryOutput);
@@ -1432,6 +1631,218 @@ __decorate([
1432
1631
  OperationProgressOutput = __decorate([
1433
1632
  ObjectType()
1434
1633
  ], OperationProgressOutput);
1634
+ // ── STRUCTURED RUN ARTIFACTS (durable JSONL progress streams) ─────────
1635
+ // These expose the IntegrationProgressReader over GraphQL so a tenant can ask,
1636
+ // at any time, "what exactly happened (or is happening) on this run?" — backed
1637
+ // by the append-only <cwd>/logs/integration-runs/<runID>/progress.jsonl files
1638
+ // that survive an MJAPI restart and grow as the run progresses. Poll
1639
+ // IntegrationTailRunEvents(runID, sinceSeq) to follow a live run incrementally.
1640
+ let IntegrationRunCountsOutput = class IntegrationRunCountsOutput {
1641
+ };
1642
+ __decorate([
1643
+ Field({ nullable: true }),
1644
+ __metadata("design:type", Number)
1645
+ ], IntegrationRunCountsOutput.prototype, "Processed", void 0);
1646
+ __decorate([
1647
+ Field({ nullable: true }),
1648
+ __metadata("design:type", Number)
1649
+ ], IntegrationRunCountsOutput.prototype, "Succeeded", void 0);
1650
+ __decorate([
1651
+ Field({ nullable: true }),
1652
+ __metadata("design:type", Number)
1653
+ ], IntegrationRunCountsOutput.prototype, "Failed", void 0);
1654
+ __decorate([
1655
+ Field({ nullable: true }),
1656
+ __metadata("design:type", Number)
1657
+ ], IntegrationRunCountsOutput.prototype, "Skipped", void 0);
1658
+ __decorate([
1659
+ Field({ nullable: true }),
1660
+ __metadata("design:type", Number)
1661
+ ], IntegrationRunCountsOutput.prototype, "TotalKnown", void 0);
1662
+ IntegrationRunCountsOutput = __decorate([
1663
+ ObjectType()
1664
+ ], IntegrationRunCountsOutput);
1665
+ let IntegrationRunSummaryArtifactOutput = class IntegrationRunSummaryArtifactOutput {
1666
+ };
1667
+ __decorate([
1668
+ Field(),
1669
+ __metadata("design:type", String)
1670
+ ], IntegrationRunSummaryArtifactOutput.prototype, "RunID", void 0);
1671
+ __decorate([
1672
+ Field(),
1673
+ __metadata("design:type", String)
1674
+ ], IntegrationRunSummaryArtifactOutput.prototype, "RunKind", void 0);
1675
+ __decorate([
1676
+ Field({ nullable: true }),
1677
+ __metadata("design:type", String)
1678
+ ], IntegrationRunSummaryArtifactOutput.prototype, "IntegrationID", void 0);
1679
+ __decorate([
1680
+ Field({ nullable: true }),
1681
+ __metadata("design:type", String)
1682
+ ], IntegrationRunSummaryArtifactOutput.prototype, "CompanyIntegrationID", void 0);
1683
+ __decorate([
1684
+ Field({ nullable: true }),
1685
+ __metadata("design:type", String)
1686
+ ], IntegrationRunSummaryArtifactOutput.prototype, "ObjectName", void 0);
1687
+ __decorate([
1688
+ Field({ nullable: true }),
1689
+ __metadata("design:type", String)
1690
+ ], IntegrationRunSummaryArtifactOutput.prototype, "TriggerType", void 0);
1691
+ __decorate([
1692
+ Field(),
1693
+ __metadata("design:type", String)
1694
+ ], IntegrationRunSummaryArtifactOutput.prototype, "StartedAt", void 0);
1695
+ __decorate([
1696
+ Field(),
1697
+ __metadata("design:type", Boolean)
1698
+ ], IntegrationRunSummaryArtifactOutput.prototype, "IsInFlight", void 0);
1699
+ __decorate([
1700
+ Field(),
1701
+ __metadata("design:type", Number)
1702
+ ], IntegrationRunSummaryArtifactOutput.prototype, "EventCount", void 0);
1703
+ __decorate([
1704
+ Field({ nullable: true }),
1705
+ __metadata("design:type", Boolean)
1706
+ ], IntegrationRunSummaryArtifactOutput.prototype, "Success", void 0);
1707
+ __decorate([
1708
+ Field({ nullable: true }),
1709
+ __metadata("design:type", String)
1710
+ ], IntegrationRunSummaryArtifactOutput.prototype, "ExitReason", void 0);
1711
+ __decorate([
1712
+ Field({ nullable: true }),
1713
+ __metadata("design:type", String)
1714
+ ], IntegrationRunSummaryArtifactOutput.prototype, "CompletedAt", void 0);
1715
+ __decorate([
1716
+ Field({ nullable: true }),
1717
+ __metadata("design:type", Number)
1718
+ ], IntegrationRunSummaryArtifactOutput.prototype, "DurationMs", void 0);
1719
+ __decorate([
1720
+ Field({ nullable: true }),
1721
+ __metadata("design:type", String)
1722
+ ], IntegrationRunSummaryArtifactOutput.prototype, "LatestEventType", void 0);
1723
+ __decorate([
1724
+ Field({ nullable: true }),
1725
+ __metadata("design:type", String)
1726
+ ], IntegrationRunSummaryArtifactOutput.prototype, "LatestMessage", void 0);
1727
+ __decorate([
1728
+ Field(() => IntegrationRunCountsOutput, { nullable: true }),
1729
+ __metadata("design:type", IntegrationRunCountsOutput)
1730
+ ], IntegrationRunSummaryArtifactOutput.prototype, "Counts", void 0);
1731
+ __decorate([
1732
+ Field({ nullable: true }),
1733
+ __metadata("design:type", Number)
1734
+ ], IntegrationRunSummaryArtifactOutput.prototype, "WarningCount", void 0);
1735
+ __decorate([
1736
+ Field(() => [String], { nullable: true }),
1737
+ __metadata("design:type", Array)
1738
+ ], IntegrationRunSummaryArtifactOutput.prototype, "Warnings", void 0);
1739
+ IntegrationRunSummaryArtifactOutput = __decorate([
1740
+ ObjectType()
1741
+ ], IntegrationRunSummaryArtifactOutput);
1742
+ let IntegrationRunEventOutput = class IntegrationRunEventOutput {
1743
+ };
1744
+ __decorate([
1745
+ Field(),
1746
+ __metadata("design:type", String)
1747
+ ], IntegrationRunEventOutput.prototype, "Ts", void 0);
1748
+ __decorate([
1749
+ Field(),
1750
+ __metadata("design:type", Number)
1751
+ ], IntegrationRunEventOutput.prototype, "Seq", void 0);
1752
+ __decorate([
1753
+ Field(),
1754
+ __metadata("design:type", String)
1755
+ ], IntegrationRunEventOutput.prototype, "EventType", void 0);
1756
+ __decorate([
1757
+ Field({ nullable: true }),
1758
+ __metadata("design:type", String)
1759
+ ], IntegrationRunEventOutput.prototype, "Level", void 0);
1760
+ __decorate([
1761
+ Field({ nullable: true }),
1762
+ __metadata("design:type", String)
1763
+ ], IntegrationRunEventOutput.prototype, "Stage", void 0);
1764
+ __decorate([
1765
+ Field({ nullable: true }),
1766
+ __metadata("design:type", String)
1767
+ ], IntegrationRunEventOutput.prototype, "Message", void 0);
1768
+ __decorate([
1769
+ Field(() => IntegrationRunCountsOutput, { nullable: true }),
1770
+ __metadata("design:type", IntegrationRunCountsOutput)
1771
+ ], IntegrationRunEventOutput.prototype, "Counts", void 0);
1772
+ __decorate([
1773
+ Field({ nullable: true }),
1774
+ __metadata("design:type", String)
1775
+ ], IntegrationRunEventOutput.prototype, "DataJSON", void 0);
1776
+ __decorate([
1777
+ Field({ nullable: true }),
1778
+ __metadata("design:type", String)
1779
+ ], IntegrationRunEventOutput.prototype, "ResumableStateJSON", void 0);
1780
+ IntegrationRunEventOutput = __decorate([
1781
+ ObjectType()
1782
+ ], IntegrationRunEventOutput);
1783
+ let IntegrationListRunsOutput = class IntegrationListRunsOutput {
1784
+ };
1785
+ __decorate([
1786
+ Field(),
1787
+ __metadata("design:type", Boolean)
1788
+ ], IntegrationListRunsOutput.prototype, "Success", void 0);
1789
+ __decorate([
1790
+ Field(),
1791
+ __metadata("design:type", String)
1792
+ ], IntegrationListRunsOutput.prototype, "Message", void 0);
1793
+ __decorate([
1794
+ Field(() => [IntegrationRunSummaryArtifactOutput], { nullable: true }),
1795
+ __metadata("design:type", Array)
1796
+ ], IntegrationListRunsOutput.prototype, "Runs", void 0);
1797
+ IntegrationListRunsOutput = __decorate([
1798
+ ObjectType()
1799
+ ], IntegrationListRunsOutput);
1800
+ let IntegrationRunDetailOutput = class IntegrationRunDetailOutput {
1801
+ };
1802
+ __decorate([
1803
+ Field(),
1804
+ __metadata("design:type", Boolean)
1805
+ ], IntegrationRunDetailOutput.prototype, "Success", void 0);
1806
+ __decorate([
1807
+ Field(),
1808
+ __metadata("design:type", String)
1809
+ ], IntegrationRunDetailOutput.prototype, "Message", void 0);
1810
+ __decorate([
1811
+ Field(() => IntegrationRunSummaryArtifactOutput, { nullable: true }),
1812
+ __metadata("design:type", IntegrationRunSummaryArtifactOutput)
1813
+ ], IntegrationRunDetailOutput.prototype, "Run", void 0);
1814
+ __decorate([
1815
+ Field(() => [String], { nullable: true }),
1816
+ __metadata("design:type", Array)
1817
+ ], IntegrationRunDetailOutput.prototype, "Errors", void 0);
1818
+ IntegrationRunDetailOutput = __decorate([
1819
+ ObjectType()
1820
+ ], IntegrationRunDetailOutput);
1821
+ let IntegrationRunEventsOutput = class IntegrationRunEventsOutput {
1822
+ };
1823
+ __decorate([
1824
+ Field(),
1825
+ __metadata("design:type", Boolean)
1826
+ ], IntegrationRunEventsOutput.prototype, "Success", void 0);
1827
+ __decorate([
1828
+ Field(),
1829
+ __metadata("design:type", String)
1830
+ ], IntegrationRunEventsOutput.prototype, "Message", void 0);
1831
+ __decorate([
1832
+ Field(() => [IntegrationRunEventOutput], { nullable: true }),
1833
+ __metadata("design:type", Array)
1834
+ ], IntegrationRunEventsOutput.prototype, "Events", void 0);
1835
+ __decorate([
1836
+ Field(),
1837
+ __metadata("design:type", Number)
1838
+ ], IntegrationRunEventsOutput.prototype, "LatestSeq", void 0);
1839
+ __decorate([
1840
+ Field(),
1841
+ __metadata("design:type", Boolean)
1842
+ ], IntegrationRunEventsOutput.prototype, "IsInFlight", void 0);
1843
+ IntegrationRunEventsOutput = __decorate([
1844
+ ObjectType()
1845
+ ], IntegrationRunEventsOutput);
1435
1846
  // Sync progress is now tracked inside IntegrationEngine itself via IntegrationEngine.GetSyncProgress()
1436
1847
  let ConnectionSummaryOutput = class ConnectionSummaryOutput {
1437
1848
  };
@@ -1501,6 +1912,11 @@ const VALID_ENTITY_MAP_STATUSES = ['Active', 'Inactive'];
1501
1912
  function isValidEntityMapStatus(value) {
1502
1913
  return VALID_ENTITY_MAP_STATUSES.includes(value);
1503
1914
  }
1915
+ // IntegrationActionVerb is the engine-defined union ('Get'|'Create'|'Update'|'Delete'|'Search'|'List').
1916
+ const VALID_INTEGRATION_ACTION_VERBS = ['Get', 'Create', 'Update', 'Delete', 'Search', 'List'];
1917
+ function isValidIntegrationActionVerb(value) {
1918
+ return VALID_INTEGRATION_ACTION_VERBS.includes(value);
1919
+ }
1504
1920
  // ─── List Source Objects (Full-Catalog Picker) ──────────────────────────────
1505
1921
  // Returns every object the source system exposes (e.g. all ~1,800 Salesforce
1506
1922
  // sobjects), merged with any existing IntegrationObject metadata so the UI
@@ -1771,6 +2187,144 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
1771
2187
  };
1772
2188
  }
1773
2189
  }
2190
+ /**
2191
+ * Refreshes the connector's IntegrationObject + IntegrationObjectField
2192
+ * catalog by running the Phase 0 v5.39.x IntegrationConnectorCreationPipeline.
2193
+ * The pipeline:
2194
+ * 1. TestConnection — validates credentials before any heavy work.
2195
+ * 2. IntrospectSchema — parallel describe across all objects.
2196
+ * 3. PersistDiscoveredSchema — overlay-aware upsert (declared wins for
2197
+ * semantic, discovered wins for technical attributes), populates
2198
+ * MetadataSource and per-attribute AttributeWinners.
2199
+ * 4. PKClassify — SoftPKClassifier 4-tier cascade for any IO
2200
+ * that still lacks an explicit PK marker.
2201
+ *
2202
+ * Structured progress events ride the IntegrationProgressEmitter and land
2203
+ * both on stdout (visible in the MJAPI log file) and in a per-run
2204
+ * `<cwd>/logs/integration-runs/<runID>/progress.jsonl` artifact.
2205
+ */
2206
+ async IntegrationRefreshConnectorSchema(companyIntegrationID, universalPKConvention, ctx) {
2207
+ try {
2208
+ const user = this.getAuthenticatedUser(ctx);
2209
+ const provider = GetReadWriteProvider(ctx.providers);
2210
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
2211
+ const pipeline = new IntegrationConnectorCreationPipeline();
2212
+ // Cast through unknown — duplicate package type declarations
2213
+ // between integration-engine's resolved core-entities and this
2214
+ // resolver's resolved core-entities (same shape, different
2215
+ // identities at type level).
2216
+ const runOpts = {
2217
+ Connector: connector,
2218
+ CompanyIntegration: companyIntegration,
2219
+ ContextUser: user,
2220
+ Provider: provider,
2221
+ UniversalPKConvention: universalPKConvention || undefined,
2222
+ ConsoleMirror: true,
2223
+ TriggerType: 'Manual',
2224
+ };
2225
+ const result = await pipeline.Run(runOpts);
2226
+ // Refresh the metadata cache so subsequent reads see the new IO/IOF
2227
+ // rows the pipeline just wrote. Without this the engine returns the
2228
+ // pre-pipeline snapshot until the next process bootstrap.
2229
+ const md = provider ?? new Metadata();
2230
+ await md.Refresh();
2231
+ await IntegrationEngine.Instance.Config(true, user, provider);
2232
+ return {
2233
+ Success: result.Success,
2234
+ Message: result.Success
2235
+ ? `Refresh complete: ${result.PersistResult?.ObjectsCreated ?? 0} created, ${result.PersistResult?.ObjectsUpdated ?? 0} updated, ${result.UnresolvedObjects.length} IOs still PK-less (deferred to additionalSchemaInfo authoring)`
2236
+ : `Refresh failed: ${result.FailureMessage ?? 'unknown error'}`,
2237
+ RunID: result.RunID,
2238
+ ObjectsCreated: result.PersistResult?.ObjectsCreated,
2239
+ ObjectsUpdated: result.PersistResult?.ObjectsUpdated,
2240
+ FieldsCreated: result.PersistResult?.FieldsCreated,
2241
+ FieldsUpdated: result.PersistResult?.FieldsUpdated,
2242
+ PKVerdicts: result.PKVerdicts.map(v => ({
2243
+ ObjectName: v.ObjectName,
2244
+ Confident: v.Confident,
2245
+ Nominee: v.Nominee,
2246
+ Confidence: v.Confidence,
2247
+ Strategy: v.Strategy,
2248
+ Reason: v.Reason,
2249
+ })),
2250
+ UnresolvedObjects: result.UnresolvedObjects,
2251
+ FailureMessage: result.FailureMessage,
2252
+ };
2253
+ }
2254
+ catch (e) {
2255
+ LogError(`IntegrationRefreshConnectorSchema error: ${this.formatError(e)}`);
2256
+ return {
2257
+ Success: false,
2258
+ Message: `Error: ${this.formatError(e)}`,
2259
+ RunID: 'error',
2260
+ };
2261
+ }
2262
+ }
2263
+ /**
2264
+ * Generates + persists strongly-typed Action metadata on demand for an
2265
+ * integration object. When `verb` is supplied, a single Action is generated
2266
+ * for that (integration, object, verb); when omitted, all applicable verbs
2267
+ * for the object are generated (Get/Search/List always, Create/Update/Delete
2268
+ * only when the object supports writes).
2269
+ *
2270
+ * Idempotent: each generated Action is keyed on the deterministic Name
2271
+ * "<Integration> - <Verb> <DisplayName>". An existing Action with that Name is
2272
+ * reused (AlreadyExisted=true) and its params/result codes reconciled rather
2273
+ * than duplicated. Generated Actions use DriverClass='IntegrationActionExecutor'
2274
+ * and carry routing info ({IntegrationName, ObjectName, Verb}) in Action.Config_;
2275
+ * the IntegrationActionExecutor (CoreActions) is the single runtime dispatcher.
2276
+ */
2277
+ async IntegrationGenerateAction(integrationName, objectName, verb, ctx) {
2278
+ try {
2279
+ const user = this.getAuthenticatedUser(ctx);
2280
+ const provider = GetReadWriteProvider(ctx.providers);
2281
+ const generator = new IntegrationActionGenerator();
2282
+ const results = await this.runActionGenerator(generator, integrationName, objectName, verb, user, provider);
2283
+ return this.mapGenerateActionResults(results);
2284
+ }
2285
+ catch (e) {
2286
+ LogError(`IntegrationGenerateAction error: ${this.formatError(e)}`);
2287
+ return { Success: false, Message: `Error: ${this.formatError(e)}` };
2288
+ }
2289
+ }
2290
+ /**
2291
+ * Drives the engine's IntegrationActionGenerator: one verb when `verb` is a
2292
+ * valid value, all applicable verbs when it is omitted. Casts user/provider
2293
+ * through `unknown` at the boundary — integration-engine resolves its own copy
2294
+ * of core/core-entities, so the types are structurally identical but nominally
2295
+ * distinct (same bridge pattern used by IntegrationRefreshConnectorSchema).
2296
+ */
2297
+ async runActionGenerator(generator, integrationName, objectName, verb, user, provider) {
2298
+ const u = user;
2299
+ const p = provider;
2300
+ if (verb != null && verb.length > 0) {
2301
+ if (!isValidIntegrationActionVerb(verb)) {
2302
+ throw new Error(`Invalid verb "${verb}". Must be one of: ${VALID_INTEGRATION_ACTION_VERBS.join(', ')}`);
2303
+ }
2304
+ const single = await generator.GenerateAction(integrationName, objectName, verb, u, p);
2305
+ return [single];
2306
+ }
2307
+ return generator.GenerateActionsForObject(integrationName, objectName, u, p);
2308
+ }
2309
+ /** Maps engine GenerateIntegrationActionResult[] into the GraphQL output shape. */
2310
+ mapGenerateActionResults(results) {
2311
+ const mapped = results.map(r => ({
2312
+ Success: r.Success,
2313
+ ActionID: r.ActionID,
2314
+ ActionName: r.ActionName,
2315
+ AlreadyExisted: r.AlreadyExisted,
2316
+ Verb: r.Verb,
2317
+ ObjectName: r.ObjectName,
2318
+ Message: r.Message,
2319
+ }));
2320
+ const successCount = mapped.filter(r => r.Success).length;
2321
+ const overallSuccess = mapped.length > 0 && mapped.every(r => r.Success);
2322
+ return {
2323
+ Success: overallSuccess,
2324
+ Message: `Generated ${successCount}/${mapped.length} action(s)`,
2325
+ Results: mapped,
2326
+ };
2327
+ }
1774
2328
  /**
1775
2329
  * Returns the connector's default configuration for quick setup.
1776
2330
  * Not all connectors provide defaults — returns Success: false if unavailable.
@@ -2301,6 +2855,56 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2301
2855
  throw new Error('System user not available');
2302
2856
  return sysUser;
2303
2857
  }
2858
+ /**
2859
+ * Per-company read-authorization check for run artifacts.
2860
+ *
2861
+ * Reuses MJ's established row-level-security pattern: a `RunView` on
2862
+ * `MJ: Company Integrations` executed under the *calling user's* context.
2863
+ * The data provider applies the same read-permission / RLS filtering it
2864
+ * applies to every other user-context read, so the record only comes back
2865
+ * when the caller is genuinely authorized to see that CompanyIntegration.
2866
+ * No row returned ⇒ either the ID doesn't exist or the caller has no rights
2867
+ * to it — in both cases we treat the caller as unauthorized.
2868
+ *
2869
+ * Results are memoized per call via the supplied `cache` map so a list of
2870
+ * many runs sharing a CompanyIntegrationID incurs at most one lookup each.
2871
+ */
2872
+ async userCanReadCompanyIntegration(companyIntegrationID, user, cache) {
2873
+ const cached = cache.get(companyIntegrationID);
2874
+ if (cached !== undefined) {
2875
+ return cached;
2876
+ }
2877
+ const rv = new RunView();
2878
+ const result = await rv.RunView({
2879
+ EntityName: 'MJ: Company Integrations',
2880
+ ExtraFilter: `ID='${companyIntegrationID}'`,
2881
+ MaxRows: 1,
2882
+ ResultType: 'simple',
2883
+ Fields: ['ID']
2884
+ }, user);
2885
+ const authorized = result.Success && result.Results.length > 0;
2886
+ cache.set(companyIntegrationID, authorized);
2887
+ return authorized;
2888
+ }
2889
+ /**
2890
+ * Authorizes the caller for a single run artifact based on the run's
2891
+ * manifest CompanyIntegrationID. Returns true when the run is tenant-scoped
2892
+ * and the caller is authorized for that CompanyIntegration. Returns false
2893
+ * for tenant-scoped runs the caller may not read, OR for runs with no
2894
+ * CompanyIntegrationID (non-tenant-scoped artifacts are not exposed through
2895
+ * these per-company endpoints).
2896
+ */
2897
+ async userCanReadRunArtifact(snap, user, cache) {
2898
+ const ciID = snap.manifest.companyIntegrationID;
2899
+ if (!ciID) {
2900
+ return false;
2901
+ }
2902
+ return this.userCanReadCompanyIntegration(ciID, user, cache);
2903
+ }
2904
+ /** Standard authorization-failure message for run-artifact endpoints. */
2905
+ notAuthorizedForCompanyIntegrationMessage(companyIntegrationID) {
2906
+ return `Not authorized to access runs for CompanyIntegration '${companyIntegrationID}'`;
2907
+ }
2304
2908
  /**
2305
2909
  * Loads the CompanyIntegration + its parent Integration, then resolves the
2306
2910
  * appropriate connector via ConnectorFactory.
@@ -2356,6 +2960,126 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2356
2960
  const testFn = connector.TestConnection.bind(connector);
2357
2961
  return testFn(companyIntegration, user);
2358
2962
  }
2963
+ /**
2964
+ * Runs IntegrationConnectorCreationPipeline + refreshes the metadata cache.
2965
+ * Shared by IntegrationCreateConnection's auto-refresh path and the
2966
+ * standalone IntegrationRefreshConnectorSchema mutation.
2967
+ */
2968
+ /**
2969
+ * Reconstructs a `SourceSchemaInfo` from already-persisted IntegrationObject
2970
+ * and IntegrationObjectField rows held by `IntegrationEngineBase`'s
2971
+ * in-memory cache.
2972
+ *
2973
+ * Why this exists: after the Phase 0 v5.39.x `MJCompanyIntegrationEntityServer`
2974
+ * hook fires the pipeline on `IsActive false→true`, the IO/IOF rows are
2975
+ * already fresh. The legacy `IntegrationApplyAllBatch` / `ApplyAll` /
2976
+ * `ApplySchema` resolvers then called `connector.IntrospectSchema()` AGAIN
2977
+ * to feed `SchemaBuilder`, which produced a second full vendor-API
2978
+ * roundtrip (HubSpot 130 objects × 60+ DiscoverFields probes, ~100s
2979
+ * wasted on the user's HubSpot run). Using the persisted rows skips
2980
+ * that round-trip entirely.
2981
+ *
2982
+ * Caveats:
2983
+ * - The persisted `Type` is the MJ canonical type (string / int / datetime / …),
2984
+ * NOT the original vendor source type. `TypeMapper` in SchemaBuilder
2985
+ * accepts canonical types so DDL generation still works; if a connector
2986
+ * has unusual nuances around its source-type strings, fall back to live
2987
+ * introspect (the caller can pass `forceLive: true`).
2988
+ * - PrimaryKeyFields are recomputed from the IOF rows where
2989
+ * `IsPrimaryKey=true`.
2990
+ * - Foreign-key relationships are reconstructed from
2991
+ * `RelatedIntegrationObjectID` lookups against the same cache.
2992
+ */
2993
+ buildSourceSchemaFromPersistedRows(integrationID, requestedNames) {
2994
+ const engine = IntegrationEngineBase.Instance;
2995
+ const ios = engine.GetIntegrationObjectsByIntegrationID(integrationID);
2996
+ const filter = requestedNames && requestedNames.length > 0
2997
+ ? new Set(requestedNames.map(n => n.toLowerCase()))
2998
+ : null;
2999
+ // Cache (id → name) for FK relationship reconstruction. Same-integration
3000
+ // only — cross-integration relationships are not modeled in the slot table.
3001
+ const ioByID = new Map();
3002
+ for (const io of ios)
3003
+ ioByID.set(io.ID, io.Name);
3004
+ const result = { Objects: [] };
3005
+ for (const io of ios) {
3006
+ if (filter && !filter.has(io.Name.toLowerCase()))
3007
+ continue;
3008
+ const iofs = engine.GetIntegrationObjectFields(io.ID);
3009
+ const fields = iofs.map(iof => {
3010
+ const targetIOName = iof.RelatedIntegrationObjectID
3011
+ ? ioByID.get(iof.RelatedIntegrationObjectID) ?? null
3012
+ : null;
3013
+ return {
3014
+ Name: iof.Name,
3015
+ Label: iof.DisplayName ?? iof.Name,
3016
+ Description: iof.Description ?? undefined,
3017
+ SourceType: iof.Type ?? 'string',
3018
+ IsRequired: iof.IsRequired ?? false,
3019
+ AllowsNull: iof.AllowsNull ?? undefined,
3020
+ MaxLength: iof.Length ?? null,
3021
+ Precision: iof.Precision ?? null,
3022
+ Scale: iof.Scale ?? null,
3023
+ DefaultValue: iof.DefaultValue ?? null,
3024
+ IsPrimaryKey: iof.IsPrimaryKey ?? false,
3025
+ IsUniqueKey: iof.IsUniqueKey ?? false,
3026
+ IsReadOnly: iof.IsReadOnly ?? false,
3027
+ IsForeignKey: !!iof.RelatedIntegrationObjectID,
3028
+ ForeignKeyTarget: targetIOName,
3029
+ };
3030
+ });
3031
+ result.Objects.push({
3032
+ ExternalName: io.Name,
3033
+ ExternalLabel: io.DisplayName ?? io.Name,
3034
+ Description: io.Description ?? undefined,
3035
+ Fields: fields,
3036
+ PrimaryKeyFields: fields.filter(f => f.IsPrimaryKey).map(f => f.Name),
3037
+ Relationships: fields
3038
+ .filter(f => f.IsForeignKey && f.ForeignKeyTarget)
3039
+ .map(f => ({
3040
+ FieldName: f.Name,
3041
+ TargetObject: f.ForeignKeyTarget,
3042
+ TargetField: 'ID',
3043
+ })),
3044
+ IncrementalWatermarkField: io.IncrementalWatermarkField ?? undefined,
3045
+ });
3046
+ }
3047
+ return result;
3048
+ }
3049
+ async runSchemaRefreshPipeline(companyIntegrationID, user, provider, universalPKConvention) {
3050
+ const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
3051
+ const pipeline = new IntegrationConnectorCreationPipeline();
3052
+ const runOpts = {
3053
+ Connector: connector,
3054
+ CompanyIntegration: companyIntegration,
3055
+ ContextUser: user,
3056
+ Provider: provider,
3057
+ UniversalPKConvention: universalPKConvention || undefined,
3058
+ ConsoleMirror: true,
3059
+ TriggerType: 'Manual',
3060
+ };
3061
+ const result = await pipeline.Run(runOpts);
3062
+ // Refresh in-memory caches so downstream queries (object picker,
3063
+ // ApplyAll, etc.) see the just-written IO/IOF rows.
3064
+ await (provider ?? new Metadata()).Refresh();
3065
+ await IntegrationEngine.Instance.Config(true, user, provider);
3066
+ return {
3067
+ RunID: result.RunID,
3068
+ ObjectsCreated: result.PersistResult?.ObjectsCreated ?? 0,
3069
+ ObjectsUpdated: result.PersistResult?.ObjectsUpdated ?? 0,
3070
+ FieldsCreated: result.PersistResult?.FieldsCreated ?? 0,
3071
+ FieldsUpdated: result.PersistResult?.FieldsUpdated ?? 0,
3072
+ UnresolvedObjects: result.UnresolvedObjects,
3073
+ PKVerdicts: result.PKVerdicts.map(v => ({
3074
+ ObjectName: v.ObjectName,
3075
+ Confident: v.Confident,
3076
+ Nominee: v.Nominee,
3077
+ Confidence: v.Confidence,
3078
+ Strategy: v.Strategy,
3079
+ Reason: v.Reason,
3080
+ })),
3081
+ };
3082
+ }
2359
3083
  /**
2360
3084
  * Rolls back a freshly created connection by deleting both the CompanyIntegration and Credential records.
2361
3085
  */
@@ -2373,6 +3097,40 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2373
3097
  LogError(`Rollback: failed to delete Credential: ${this.formatError(e)}`);
2374
3098
  }
2375
3099
  }
3100
+ /**
3101
+ * Cascades the deletion of a CompanyIntegration's linked Credential as part of the
3102
+ * supplied TransactionGroup, so the encrypted-credential row is removed atomically
3103
+ * with the rest of the connection cascade (rolls back together on tg failure).
3104
+ *
3105
+ * Safety:
3106
+ * - No-op when {@link credentialID} is null/empty.
3107
+ * - Skips the delete when ANOTHER CompanyIntegration still references the same
3108
+ * CredentialID (shared credential) — only the sole referencer may delete it.
3109
+ *
3110
+ * @returns true if the credential was attached to the tg for deletion; false otherwise.
3111
+ */
3112
+ async cascadeDeleteCredential(credentialID, companyIntegrationID, tg, rv, provider, user) {
3113
+ if (!credentialID)
3114
+ return false; // no linked credential — nothing to do
3115
+ // Shared-credential safety: do not delete if any OTHER CompanyIntegration uses it.
3116
+ const sharedResult = await rv.RunView({
3117
+ EntityName: 'MJ: Company Integrations',
3118
+ ExtraFilter: `CredentialID='${credentialID}' AND ID<>'${companyIntegrationID}'`,
3119
+ ResultType: 'simple',
3120
+ Fields: ['ID']
3121
+ }, user);
3122
+ if (sharedResult.Success && sharedResult.Results.length > 0) {
3123
+ LogStatus(`IntegrationDeleteConnection: credential ${credentialID} is shared by ${sharedResult.Results.length} other connection(s); leaving it in place`);
3124
+ return false;
3125
+ }
3126
+ const credential = await provider.GetEntityObject('MJ: Credentials', user);
3127
+ const loaded = await credential.InnerLoad(CompositeKey.FromID(credentialID));
3128
+ if (!loaded)
3129
+ return false; // already gone — treat as nothing to cascade
3130
+ credential.TransactionGroup = tg;
3131
+ await credential.Delete();
3132
+ return true;
3133
+ }
2376
3134
  /**
2377
3135
  * Snapshots the current credential Values for a given credential ID so they can be restored on rollback.
2378
3136
  */
@@ -2472,7 +3230,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2472
3230
  /**
2473
3231
  * Creates a CompanyIntegration with a linked Credential entity for encrypted credential storage.
2474
3232
  */
2475
- async IntegrationCreateConnection(input, testConnection, ctx) {
3233
+ async IntegrationCreateConnection(input, testConnection, runSchemaRefresh, universalPKConvention, ctx) {
2476
3234
  try {
2477
3235
  const user = this.getAuthenticatedUser(ctx);
2478
3236
  const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
@@ -2507,6 +3265,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2507
3265
  return { Success: false, Message: `Failed to save CompanyIntegration: ${validationErrors}` };
2508
3266
  }
2509
3267
  // 3. Optionally test the connection; rollback on failure
3268
+ let testPassed = !testConnection; // if no test asked, treat as "passed" so the refresh below still runs
3269
+ let testMessage = '';
2510
3270
  if (testConnection) {
2511
3271
  const testResult = await this.testConnectionForCI(ci.ID, user, md);
2512
3272
  if (!testResult.Success) {
@@ -2518,13 +3278,36 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2518
3278
  ConnectionTestMessage: testResult.Message
2519
3279
  };
2520
3280
  }
3281
+ testPassed = true;
3282
+ testMessage = testResult.Message;
3283
+ }
3284
+ // 4. Auto-run schema refresh pipeline (intermittent server-side period).
3285
+ // Fires whenever runSchemaRefresh=true, regardless of whether the
3286
+ // caller also asked for a test. The wizard may have tested separately
3287
+ // and just be hitting Create to save.
3288
+ let schemaRefreshSummary;
3289
+ if (runSchemaRefresh) {
3290
+ try {
3291
+ const refreshResult = await this.runSchemaRefreshPipeline(ci.ID, user, md, universalPKConvention);
3292
+ schemaRefreshSummary = refreshResult;
3293
+ }
3294
+ catch (refreshErr) {
3295
+ // Refresh failure does NOT roll back the connection —
3296
+ // user can re-run via IntegrationRefreshConnectorSchema.
3297
+ LogError(`IntegrationCreateConnection: pipeline error — ${refreshErr}`);
3298
+ }
3299
+ }
3300
+ if (testConnection || schemaRefreshSummary) {
2521
3301
  return {
2522
3302
  Success: true,
2523
- Message: 'Connection created and test passed',
3303
+ Message: schemaRefreshSummary
3304
+ ? `Connection created${testConnection ? ', test passed' : ''}, schema refresh: ${schemaRefreshSummary.ObjectsCreated} created, ${schemaRefreshSummary.ObjectsUpdated} updated, ${schemaRefreshSummary.UnresolvedObjects.length} PK-unresolved`
3305
+ : 'Connection created and test passed',
2524
3306
  CompanyIntegrationID: ci.ID,
2525
3307
  CredentialID: credentialID,
2526
- ConnectionTestSuccess: true,
2527
- ConnectionTestMessage: testResult.Message
3308
+ ConnectionTestSuccess: testPassed,
3309
+ ConnectionTestMessage: testMessage,
3310
+ SchemaRefresh: schemaRefreshSummary,
2528
3311
  };
2529
3312
  }
2530
3313
  return {
@@ -2542,7 +3325,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2542
3325
  /**
2543
3326
  * Updates credential values and/or configuration on an existing CompanyIntegration.
2544
3327
  */
2545
- async IntegrationUpdateConnection(companyIntegrationID, credentialValues, configuration, externalSystemID, testConnection, ctx) {
3328
+ async IntegrationUpdateConnection(companyIntegrationID, credentialValues, configuration, externalSystemID, testConnection, runSchemaRefresh, universalPKConvention, ctx) {
2546
3329
  try {
2547
3330
  const user = this.getAuthenticatedUser(ctx);
2548
3331
  const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
@@ -2586,7 +3369,23 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2586
3369
  await this.revertUpdateConnection(ci, oldConfiguration, oldExternalSystemID, oldCredentialValues, user, md);
2587
3370
  return { Success: false, Message: `Connection test failed: ${testResult.Message}. Changes have been reverted.` };
2588
3371
  }
2589
- return { Success: true, Message: 'Updated and connection test passed' };
3372
+ }
3373
+ // Auto-run schema refresh pipeline (intermittent server-side period).
3374
+ // Fires whenever runSchemaRefresh=true, regardless of whether the
3375
+ // caller also asked for a test — the wizard may have tested separately
3376
+ // already and is just hitting Update to save edits.
3377
+ if (runSchemaRefresh) {
3378
+ try {
3379
+ const refreshResult = await this.runSchemaRefreshPipeline(companyIntegrationID, user, md, universalPKConvention);
3380
+ return {
3381
+ Success: true,
3382
+ Message: `Updated, schema refresh: ${refreshResult.ObjectsCreated} created, ${refreshResult.ObjectsUpdated} updated, ${refreshResult.UnresolvedObjects.length} PK-unresolved`,
3383
+ };
3384
+ }
3385
+ catch (refreshErr) {
3386
+ LogError(`IntegrationUpdateConnection: pipeline error — ${refreshErr}`);
3387
+ return { Success: true, Message: `Updated (schema refresh failed: ${this.formatError(refreshErr)})` };
3388
+ }
2590
3389
  }
2591
3390
  return { Success: true, Message: 'Updated' };
2592
3391
  }
@@ -2671,25 +3470,54 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2671
3470
  if (!entityID) {
2672
3471
  return { Success: false, Message: `No EntityID or EntityName for "${mapDef.ExternalObjectName}"`, Created: created };
2673
3472
  }
2674
- const em = await md.GetEntityObject('MJ: Company Integration Entity Maps', user);
2675
- em.NewRecord();
2676
- em.CompanyIntegrationID = companyIntegrationID;
2677
- em.ExternalObjectName = mapDef.ExternalObjectName;
2678
- em.EntityID = entityID;
2679
3473
  const syncDir = mapDef.SyncDirection || 'Pull';
2680
3474
  if (!isValidSyncDirection(syncDir)) {
2681
3475
  return { Success: false, Message: `Invalid SyncDirection "${syncDir}" for "${mapDef.ExternalObjectName}". Must be one of: ${VALID_SYNC_DIRECTIONS.join(', ')}`, Created: created };
2682
3476
  }
3477
+ // Create-or-reuse by (connection, external object) — same idempotency rule as ApplyAll's
3478
+ // createSingleEntityMap. A blind NewRecord() here duplicates maps on every re-apply.
3479
+ const escapedObjectName = mapDef.ExternalObjectName.replace(/'/g, "''");
3480
+ const existingMapResult = await new RunView().RunView({
3481
+ EntityName: 'MJ: Company Integration Entity Maps',
3482
+ ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND ExternalObjectName='${escapedObjectName}'`,
3483
+ OrderBy: '__mj_CreatedAt ASC',
3484
+ MaxRows: 1,
3485
+ ResultType: 'entity_object',
3486
+ BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
3487
+ }, user);
3488
+ let em;
3489
+ if (existingMapResult.Success && existingMapResult.Results.length > 0) {
3490
+ em = existingMapResult.Results[0];
3491
+ }
3492
+ else {
3493
+ em = await md.GetEntityObject('MJ: Company Integration Entity Maps', user);
3494
+ em.NewRecord();
3495
+ em.CompanyIntegrationID = companyIntegrationID;
3496
+ em.ExternalObjectName = mapDef.ExternalObjectName;
3497
+ }
3498
+ em.EntityID = entityID;
2683
3499
  em.SyncDirection = syncDir;
2684
3500
  em.Priority = mapDef.Priority || 0;
2685
3501
  em.Status = 'Active';
3502
+ if (mapDef.Configuration != null)
3503
+ em.Configuration = mapDef.Configuration;
2686
3504
  if (!await em.Save()) {
2687
3505
  return { Success: false, Message: `Failed to create map for ${mapDef.ExternalObjectName}`, Created: created };
2688
3506
  }
2689
3507
  const entityMapID = em.ID;
2690
- // Create field maps if provided
3508
+ // Create field maps if provided (skip ones already mapped for this entity map)
2691
3509
  if (mapDef.FieldMaps) {
3510
+ const existingFieldMaps = await new RunView().RunView({
3511
+ EntityName: 'MJ: Company Integration Field Maps',
3512
+ ExtraFilter: `EntityMapID='${entityMapID}'`,
3513
+ Fields: ['SourceFieldName'],
3514
+ ResultType: 'simple',
3515
+ BypassCache: true, // read committed field-map state for the idempotency skip
3516
+ }, user);
3517
+ const alreadyMapped = new Set((existingFieldMaps.Results ?? []).map(r => (r.SourceFieldName ?? '').toLowerCase()));
2692
3518
  for (const fmDef of mapDef.FieldMaps) {
3519
+ if (alreadyMapped.has((fmDef.SourceFieldName ?? '').toLowerCase()))
3520
+ continue;
2693
3521
  const fm = await md.GetEntityObject('MJ: Company Integration Field Maps', user);
2694
3522
  fm.NewRecord();
2695
3523
  fm.EntityMapID = entityMapID;
@@ -2728,10 +3556,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2728
3556
  const user = this.getAuthenticatedUser(ctx);
2729
3557
  const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
2730
3558
  const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
2731
- const introspect = connector.IntrospectSchema.bind(connector);
2732
- const sourceSchema = await introspect(companyIntegration, user);
2733
- await this.resolveObjectInputs(objects, sourceSchema, user);
3559
+ // Reconstruct source schema from the IO/IOF rows already persisted by
3560
+ // the Phase 0 v5.39.x Save hook. Fall back to live IntrospectSchema only
3561
+ // for direct-API callers bypassing the wizard (empty IO cache).
2734
3562
  const requestedNames = new Set(objects.map(o => o.SourceObjectName));
3563
+ let sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID, Array.from(requestedNames));
3564
+ if (sourceSchema.Objects.length === 0) {
3565
+ LogError(`[IntegrationApplySchema] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
3566
+ const introspect = connector.IntrospectSchema.bind(connector);
3567
+ sourceSchema = await introspect(companyIntegration, user);
3568
+ }
3569
+ else {
3570
+ console.log(`[IntegrationApplySchema] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
3571
+ `— skipped duplicate IntrospectSchema (Save-hook already discovered).`);
3572
+ }
3573
+ await this.resolveObjectInputs(objects, sourceSchema, user);
2735
3574
  const filteredSchema = {
2736
3575
  Objects: sourceSchema.Objects.filter(o => requestedNames.has(o.ExternalName))
2737
3576
  };
@@ -2849,67 +3688,75 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
2849
3688
  // Step 1: Resolve connector and derive schema name
2850
3689
  const { connector, companyIntegration } = await this.resolveConnector(input.CompanyIntegrationID, user, provider);
2851
3690
  const schemaName = this.deriveSchemaName(companyIntegration.Integration);
2852
- // Step 1b: Ensure IntegrationEngine cache is populated so IntrospectSchema's
2853
- // DB fallback (GetCachedObject/GetCachedFields) can find IntegrationObject records
3691
+ // Step 1b: Ensure IntegrationEngine cache is populated so the persisted
3692
+ // IO/IOF rows are available for reconstruction below.
2854
3693
  await IntegrationEngine.Instance.Config(false, user);
2855
- // Step 2: Introspect source schema and persist discovered objects/fields
2856
- const sourceSchema = await connector.IntrospectSchema.bind(connector)(companyIntegration, user);
2857
- // Step 2b: Persist discovered objects/fields to IntegrationObject/IntegrationObjectField.
2858
- // Static records (IsCustom=false) are preserved; new/custom records get IsCustom=true.
2859
- // This ensures custom objects are available for future sync runs, action generation, etc.
2860
- try {
2861
- const persistResult = await IntegrationSchemaSync.PersistDiscoveredSchema({
2862
- IntegrationID: companyIntegration.IntegrationID,
2863
- SourceSchema: sourceSchema,
2864
- ContextUser: user,
2865
- });
2866
- if (persistResult.ObjectsCreated > 0 || persistResult.FieldsCreated > 0) {
2867
- console.log(`[IntegrationApplyAll] Persisted discovered schema: ` +
2868
- `${persistResult.ObjectsCreated} new objects, ${persistResult.FieldsCreated} new fields, ` +
2869
- `${persistResult.ObjectsUpdated} updated objects, ${persistResult.FieldsUpdated} updated fields`);
2870
- }
2871
- // Step 2c: Generate CRUD actions for newly discovered custom objects.
2872
- // Uses the same ActionMetadataGenerator as the offline CLI, persisted via BaseEntity.Save().
2873
- if (persistResult.ObjectsCreated > 0) {
2874
- try {
2875
- const engineObjects = IntegrationEngine.Instance
2876
- .GetIntegrationObjectsByIntegrationID(companyIntegration.IntegrationID);
2877
- const customObjects = sourceSchema.Objects
2878
- .filter(o => !engineObjects
2879
- .some(ex => ex.Name.toLowerCase() === o.ExternalName.toLowerCase() && !ex.IsCustom))
2880
- .map(o => ({
2881
- Name: o.ExternalName,
2882
- DisplayName: o.ExternalLabel || o.ExternalName,
2883
- Description: o.Description,
2884
- SupportsWrite: false,
2885
- Fields: o.Fields.map(f => ({
2886
- Name: f.Name,
2887
- DisplayName: f.Label || f.Name,
2888
- Description: f.Description || '',
2889
- Type: f.SourceType || 'string',
2890
- IsRequired: f.IsRequired,
2891
- IsReadOnly: false,
2892
- IsPrimaryKey: f.IsPrimaryKey,
2893
- })),
2894
- }));
2895
- await IntegrationSchemaSync.GenerateActionsForCustomObjects({
2896
- IntegrationName: companyIntegration.Integration,
2897
- CustomObjects: customObjects,
2898
- SupportsSearch: connector.SupportsSearch,
2899
- SupportsListing: connector.SupportsListing,
2900
- ContextUser: user,
2901
- });
3694
+ // Step 2: Reconstruct SourceSchemaInfo from the persisted IO/IOF rows
3695
+ // (already freshened by the Phase 0 v5.39.x MJCompanyIntegrationEntityServer
3696
+ // Save hook on IsActive false→true). Avoids the duplicate vendor-API
3697
+ // introspect that used to fire here.
3698
+ let sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID);
3699
+ if (sourceSchema.Objects.length === 0) {
3700
+ // Fallback: the engine cache is empty (Save hook didn't run, or this
3701
+ // is a direct-API caller bypassing the wizard). Do a one-time live
3702
+ // introspect + persist + action-generation so the apply still proceeds.
3703
+ LogError(`[IntegrationApplyAll] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
3704
+ sourceSchema = await connector.IntrospectSchema.bind(connector)(companyIntegration, user);
3705
+ try {
3706
+ const persistResult = await IntegrationSchemaSync.PersistDiscoveredSchema({
3707
+ IntegrationID: companyIntegration.IntegrationID,
3708
+ SourceSchema: sourceSchema,
3709
+ ContextUser: user,
3710
+ });
3711
+ if (persistResult.ObjectsCreated > 0 || persistResult.FieldsCreated > 0) {
3712
+ console.log(`[IntegrationApplyAll] Fallback persist: ` +
3713
+ `${persistResult.ObjectsCreated} new objects, ${persistResult.FieldsCreated} new fields, ` +
3714
+ `${persistResult.ObjectsUpdated} updated objects, ${persistResult.FieldsUpdated} updated fields`);
2902
3715
  }
2903
- catch (actionErr) {
2904
- const msg = actionErr instanceof Error ? actionErr.message : String(actionErr);
2905
- console.warn(`[IntegrationApplyAll] Action generation warning (non-fatal): ${msg}`);
3716
+ if (persistResult.ObjectsCreated > 0) {
3717
+ try {
3718
+ const engineObjects = IntegrationEngine.Instance
3719
+ .GetIntegrationObjectsByIntegrationID(companyIntegration.IntegrationID);
3720
+ const customObjects = sourceSchema.Objects
3721
+ .filter(o => !engineObjects
3722
+ .some(ex => ex.Name.toLowerCase() === o.ExternalName.toLowerCase() && !ex.IsCustom))
3723
+ .map(o => ({
3724
+ Name: o.ExternalName,
3725
+ DisplayName: o.ExternalLabel || o.ExternalName,
3726
+ Description: o.Description,
3727
+ SupportsWrite: false,
3728
+ Fields: o.Fields.map(f => ({
3729
+ Name: f.Name,
3730
+ DisplayName: f.Label || f.Name,
3731
+ Description: f.Description || '',
3732
+ Type: f.SourceType || 'string',
3733
+ IsRequired: f.IsRequired,
3734
+ IsReadOnly: false,
3735
+ IsPrimaryKey: f.IsPrimaryKey,
3736
+ })),
3737
+ }));
3738
+ await IntegrationSchemaSync.GenerateActionsForCustomObjects({
3739
+ IntegrationName: companyIntegration.Integration,
3740
+ CustomObjects: customObjects,
3741
+ SupportsSearch: connector.SupportsSearch,
3742
+ SupportsListing: connector.SupportsListing,
3743
+ ContextUser: user,
3744
+ });
3745
+ }
3746
+ catch (actionErr) {
3747
+ const msg = actionErr instanceof Error ? actionErr.message : String(actionErr);
3748
+ console.warn(`[IntegrationApplyAll] Action generation warning (non-fatal): ${msg}`);
3749
+ }
2906
3750
  }
2907
3751
  }
3752
+ catch (persistErr) {
3753
+ const msg = persistErr instanceof Error ? persistErr.message : String(persistErr);
3754
+ console.warn(`[IntegrationApplyAll] Schema persistence warning (non-fatal): ${msg}`);
3755
+ }
2908
3756
  }
2909
- catch (persistErr) {
2910
- // Non-fatal: schema persistence failure should not block table creation
2911
- const msg = persistErr instanceof Error ? persistErr.message : String(persistErr);
2912
- console.warn(`[IntegrationApplyAll] Schema persistence warning (non-fatal): ${msg}`);
3757
+ else {
3758
+ console.log(`[IntegrationApplyAll] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
3759
+ `— skipped duplicate IntrospectSchema (Save-hook already discovered).`);
2913
3760
  }
2914
3761
  const resolved = await this.resolveSourceObjectsToNames(input.SourceObjects, sourceSchema, user);
2915
3762
  const resolvedNames = resolved.names;
@@ -3073,11 +3920,30 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3073
3920
  LogError(`IntegrationApplyAll: entity not found for ${schemaName}.${obj.TableName}`);
3074
3921
  return null;
3075
3922
  }
3076
- // Create entity map
3077
- const em = await md.GetEntityObject('MJ: Company Integration Entity Maps', user);
3078
- em.NewRecord();
3079
- em.CompanyIntegrationID = companyIntegrationID;
3080
- em.ExternalObjectName = obj.SourceObjectName;
3923
+ // Create-or-reuse the entity map. Idempotency is REQUIRED here: ApplyAll is re-run on
3924
+ // every wizard re-apply (and by the test harness on every pull). Blindly NewRecord()-ing
3925
+ // multiplies the maps in lockstep (N applies → N duplicate maps per object), which
3926
+ // silently corrupts the record-map 1:1 completeness gate and makes the forward sync
3927
+ // process each object N times. Reuse the existing (connection, external object) map instead.
3928
+ const escapedObjectName = obj.SourceObjectName.replace(/'/g, "''");
3929
+ const existingMapResult = await new RunView().RunView({
3930
+ EntityName: 'MJ: Company Integration Entity Maps',
3931
+ ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND ExternalObjectName='${escapedObjectName}'`,
3932
+ OrderBy: '__mj_CreatedAt ASC',
3933
+ MaxRows: 1,
3934
+ ResultType: 'entity_object',
3935
+ BypassCache: true, // idempotency must read COMMITTED state, not a possibly-stale filtered cache
3936
+ }, user);
3937
+ let em;
3938
+ if (existingMapResult.Success && existingMapResult.Results.length > 0) {
3939
+ em = existingMapResult.Results[0]; // reuse — keeps the map stable across re-applies
3940
+ }
3941
+ else {
3942
+ em = await md.GetEntityObject('MJ: Company Integration Entity Maps', user);
3943
+ em.NewRecord();
3944
+ em.CompanyIntegrationID = companyIntegrationID;
3945
+ em.ExternalObjectName = obj.SourceObjectName;
3946
+ }
3081
3947
  em.EntityID = entityInfo.ID;
3082
3948
  em.SyncDirection = isValidSyncDirection(defaultSyncDirection) ? defaultSyncDirection : 'Pull';
3083
3949
  em.Priority = obj.SourceObjectName.startsWith('assoc_') ? 10 : 0;
@@ -3102,7 +3968,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3102
3968
  try {
3103
3969
  const discoverFields = connector.DiscoverFields.bind(connector);
3104
3970
  const fields = await discoverFields(companyIntegration, sourceObjectName, user);
3971
+ // Idempotency (mirrors the entity-map reuse above): don't re-create field maps that
3972
+ // already exist for this entity map, or re-applies multiply them in lockstep.
3973
+ const existingFieldMaps = await new RunView().RunView({
3974
+ EntityName: 'MJ: Company Integration Field Maps',
3975
+ ExtraFilter: `EntityMapID='${entityMapID}'`,
3976
+ Fields: ['SourceFieldName'],
3977
+ ResultType: 'simple',
3978
+ BypassCache: true, // read committed field-map state for the idempotency skip
3979
+ }, user);
3980
+ const alreadyMapped = new Set((existingFieldMaps.Results ?? []).map(r => (r.SourceFieldName ?? '').toLowerCase()));
3105
3981
  for (const field of fields) {
3982
+ if (alreadyMapped.has(field.Name.toLowerCase())) {
3983
+ fieldCount++; // already present — count it so the reported total stays stable
3984
+ continue;
3985
+ }
3106
3986
  const fm = await md.GetEntityObject('MJ: Company Integration Field Maps', user);
3107
3987
  fm.NewRecord();
3108
3988
  fm.EntityMapID = entityMapID;
@@ -3162,20 +4042,34 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3162
4042
  */
3163
4043
  async buildSchemaForConnector(companyIntegrationID, objects, platform, user, skipGitCommit, skipRestart, provider, prefetchedSourceSchema) {
3164
4044
  const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
3165
- // If the caller already ran IntrospectSchema (e.g. IntegrationApplyAllBatch),
3166
- // reuse it. The legacy path was running introspect TWICE per apply once
3167
- // in the resolver and once here which doubled probe time on connectors
3168
- // like Sage Intacct AND silently dropped selections when the second pass
3169
- // returned fewer objects than the first (rate limits, transient errors).
3170
- // The picked items would then fail to match `filteredSchema` below and
3171
- // get silently stripped before reaching buildTargetConfigs.
4045
+ // Source-schema resolution order:
4046
+ // 1. Use prefetched schema if the caller passed one (legacy ApplyAllBatch path).
4047
+ // 2. Reconstruct from persisted IO/IOF rowsthe Phase 0 v5.39.x Save hook
4048
+ // already discovered + persisted everything when the wizard flipped
4049
+ // `IsActive false→true`. No need to re-hit the vendor API.
4050
+ // 3. Only when both above are empty do we fall back to live IntrospectSchema
4051
+ // (direct-API callers bypassing the wizard).
4052
+ //
4053
+ // Pre-Phase-0 the legacy path ran introspect TWICE per apply — once in the
4054
+ // resolver and once here — which doubled probe time on connectors like Sage
4055
+ // Intacct AND silently dropped selections when the second pass returned
4056
+ // fewer objects than the first (rate limits, transient errors).
3172
4057
  let sourceSchema;
3173
4058
  if (prefetchedSourceSchema) {
3174
4059
  sourceSchema = prefetchedSourceSchema;
3175
4060
  }
3176
4061
  else {
3177
- const introspect = connector.IntrospectSchema.bind(connector);
3178
- sourceSchema = await introspect(companyIntegration, user);
4062
+ const requestedNamesForReuse = objects.map(o => o.SourceObjectName);
4063
+ sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID, requestedNamesForReuse);
4064
+ if (sourceSchema.Objects.length === 0) {
4065
+ LogError(`[buildSchemaForConnector] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
4066
+ const introspect = connector.IntrospectSchema.bind(connector);
4067
+ sourceSchema = await introspect(companyIntegration, user);
4068
+ }
4069
+ else {
4070
+ console.log(`[buildSchemaForConnector] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
4071
+ `— skipped duplicate IntrospectSchema (Save-hook already discovered).`);
4072
+ }
3179
4073
  }
3180
4074
  // Normalize names to match source schema casing
3181
4075
  const nameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
@@ -3218,6 +4112,16 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3218
4112
  try {
3219
4113
  const user = this.getAuthenticatedUser(ctx);
3220
4114
  await IntegrationEngine.Instance.Config(false, user);
4115
+ // Reject upfront if the connector is deactivated. The engine also gates this
4116
+ // authoritatively (no run record is created), but returning Success:false here gives the
4117
+ // client immediate, unambiguous feedback instead of an optimistic fire-and-forget that
4118
+ // silently no-ops. IsActive is boolean|null — only an explicit false rejects (null/unset
4119
+ // connections predating the flag are unaffected).
4120
+ const ciProvider = GetReadOnlyProvider(ctx.providers, { allowFallbackToReadWrite: true });
4121
+ const ciCheck = await ciProvider.GetEntityObject('MJ: Company Integrations', user);
4122
+ if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
4123
+ return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not started' };
4124
+ }
3221
4125
  const syncOptions = {};
3222
4126
  if (fullSync)
3223
4127
  syncOptions.FullSync = true;
@@ -3313,9 +4217,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3313
4217
  return { Success: false, Message: `Company Integration not found: ${companyIntegrationID}` };
3314
4218
  }
3315
4219
  const companyIntegration = ciResult.Results[0];
3316
- // Load the Integration entity to get the ClassName for connector resolution
4220
+ // Load the Integration entity to get the ClassName for connector resolution.
4221
+ // Entity is registered as 'MJ: Integrations' (the "MJ: " prefix is required — the sibling
4222
+ // CompanyIntegration lookup above uses 'MJ: Company Integrations'); the bare 'Integrations'
4223
+ // name is not in metadata, which dead-lettered the entire IntegrationWriteRecord (push) path.
3317
4224
  const integResult = await rv.RunView({
3318
- EntityName: 'Integrations',
4225
+ EntityName: 'MJ: Integrations',
3319
4226
  ExtraFilter: `ID='${companyIntegration.IntegrationID}'`,
3320
4227
  MaxRows: 1,
3321
4228
  ResultType: 'entity_object',
@@ -3585,7 +4492,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3585
4492
  ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
3586
4493
  OrderBy: 'Priority ASC',
3587
4494
  ResultType: 'simple',
3588
- Fields: ['ID', 'EntityID', 'Entity', 'ExternalObjectName', 'SyncDirection', 'Priority', 'Status']
4495
+ Fields: ['ID', 'EntityID', 'Entity', 'ExternalObjectName', 'SyncDirection', 'Priority', 'Status', 'Configuration'],
4496
+ BypassCache: true, // operational list must reflect COMMITTED state (wizard/lifecycle act on it)
3589
4497
  }, user);
3590
4498
  if (!result.Success)
3591
4499
  return { Success: false, Message: result.ErrorMessage || 'Query failed' };
@@ -3609,6 +4517,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3609
4517
  ExtraFilter: `EntityMapID='${entityMapID}'`,
3610
4518
  OrderBy: 'SourceFieldName',
3611
4519
  ResultType: 'simple',
4520
+ BypassCache: true, // operational list must reflect COMMITTED state
3612
4521
  Fields: ['ID', 'EntityMapID', 'SourceFieldName', 'DestinationFieldName', 'Status']
3613
4522
  }, user);
3614
4523
  if (!result.Success)
@@ -3645,6 +4554,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3645
4554
  }
3646
4555
  if (update.Priority != null)
3647
4556
  em.Priority = update.Priority;
4557
+ if (update.Configuration != null)
4558
+ em.Configuration = update.Configuration;
3648
4559
  if (update.Status != null) {
3649
4560
  if (!isValidEntityMapStatus(update.Status)) {
3650
4561
  errors.push(`${update.EntityMapID}: invalid Status "${update.Status}"`);
@@ -3653,7 +4564,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3653
4564
  em.Status = update.Status;
3654
4565
  }
3655
4566
  if (!await em.Save())
3656
- errors.push(`${update.EntityMapID}: failed to save`);
4567
+ errors.push(`${update.EntityMapID}: failed to save — ${em.LatestResult?.CompleteMessage ?? 'unknown error'}`);
3657
4568
  }
3658
4569
  if (errors.length > 0)
3659
4570
  return { Success: false, Message: `Errors: ${errors.join('; ')}` };
@@ -3796,7 +4707,11 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3796
4707
  EntityName: 'MJ: Company Integration Entity Maps',
3797
4708
  ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
3798
4709
  ResultType: 'simple',
3799
- Fields: ['ID', 'Status']
4710
+ Fields: ['ID', 'Status'],
4711
+ // Status must reflect the live state immediately after a deselect/reselect
4712
+ // (UpdateEntityMaps). Bypass the server RunView cache so ActiveEntityMaps never
4713
+ // reports a stale count right after a map's Status is toggled.
4714
+ BypassCache: true
3800
4715
  },
3801
4716
  {
3802
4717
  EntityName: 'MJ: Company Integration Runs',
@@ -3804,7 +4719,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3804
4719
  OrderBy: 'StartedAt DESC',
3805
4720
  MaxRows: 1,
3806
4721
  ResultType: 'simple',
3807
- Fields: ['ID', 'Status', 'StartedAt', 'EndedAt', 'TotalRecords']
4722
+ Fields: ['ID', 'Status', 'StartedAt', 'EndedAt', 'TotalRecords'],
4723
+ BypassCache: true
3808
4724
  }
3809
4725
  ], user);
3810
4726
  const maps = mapsResult.Success ? mapsResult.Results : [];
@@ -3862,6 +4778,171 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3862
4778
  return { Success: false, Message: this.formatError(e) };
3863
4779
  }
3864
4780
  }
4781
+ // ── STRUCTURED RUN ARTIFACTS (durable, growing JSONL streams) ────────
4782
+ /**
4783
+ * Lists integration runs (sync, connector-creation, RSU, discovery, …) from the
4784
+ * durable progress-artifact store, newest-first. Use this to render run history
4785
+ * with live status for a multi-tenant control surface. Optionally scope to a
4786
+ * single connector and/or run kind, or to only in-flight runs.
4787
+ */
4788
+ async IntegrationListRuns(ctx, companyIntegrationID, runKind, inFlightOnly, limit) {
4789
+ try {
4790
+ const user = this.getAuthenticatedUser(ctx);
4791
+ // When a specific connector is requested, authorize it up front so an
4792
+ // unauthorized caller gets a clear denial rather than an empty list.
4793
+ const authCache = new Map();
4794
+ if (companyIntegrationID) {
4795
+ const authorized = await this.userCanReadCompanyIntegration(companyIntegrationID, user, authCache);
4796
+ if (!authorized) {
4797
+ return { Success: false, Message: this.notAuthorizedForCompanyIntegrationMessage(companyIntegrationID) };
4798
+ }
4799
+ }
4800
+ const reader = new IntegrationProgressReader();
4801
+ const snaps = await reader.ListRuns({
4802
+ companyIntegrationID,
4803
+ runKind: runKind,
4804
+ inFlightOnly: inFlightOnly ?? false,
4805
+ }, limit ?? 50);
4806
+ // Filter to only the runs the caller is authorized to read. When
4807
+ // scoped to a single (already-authorized) connector this is a no-op;
4808
+ // for the cross-connector listing it prevents one tenant from seeing
4809
+ // another tenant's runs.
4810
+ const authorizedSnaps = await this.filterAuthorizedRuns(snaps, user, authCache);
4811
+ return { Success: true, Message: `${authorizedSnaps.length} run(s)`, Runs: authorizedSnaps.map(s => this.toRunSummaryArtifact(s)) };
4812
+ }
4813
+ catch (e) {
4814
+ LogError(`IntegrationListRuns error: ${e}`);
4815
+ return { Success: false, Message: this.formatError(e) };
4816
+ }
4817
+ }
4818
+ /** Returns only the run snapshots the caller is authorized to read. */
4819
+ async filterAuthorizedRuns(snaps, user, authCache) {
4820
+ const authorized = [];
4821
+ for (const snap of snaps) {
4822
+ if (await this.userCanReadRunArtifact(snap, user, authCache)) {
4823
+ authorized.push(snap);
4824
+ }
4825
+ }
4826
+ return authorized;
4827
+ }
4828
+ /**
4829
+ * Returns the summary of a single run (manifest + terminal result + latest counts).
4830
+ * Pair with IntegrationTailRunEvents to read the full event stream.
4831
+ */
4832
+ async IntegrationGetRun(runID, ctx) {
4833
+ try {
4834
+ const user = this.getAuthenticatedUser(ctx);
4835
+ const reader = new IntegrationProgressReader();
4836
+ const snap = await reader.GetRun(runID);
4837
+ if (!snap)
4838
+ return { Success: false, Message: `Run '${runID}' not found` };
4839
+ const authorized = await this.userCanReadRunArtifact(snap, user, new Map());
4840
+ if (!authorized) {
4841
+ const ciID = snap.manifest.companyIntegrationID;
4842
+ return {
4843
+ Success: false,
4844
+ Message: ciID
4845
+ ? this.notAuthorizedForCompanyIntegrationMessage(ciID)
4846
+ : `Not authorized to access run '${runID}'`,
4847
+ };
4848
+ }
4849
+ return {
4850
+ Success: true,
4851
+ Message: 'OK',
4852
+ Run: this.toRunSummaryArtifact(snap),
4853
+ Errors: snap.result?.errors?.map(er => er.stage ? `[${er.stage}] ${er.message}` : er.message),
4854
+ };
4855
+ }
4856
+ catch (e) {
4857
+ LogError(`IntegrationGetRun error: ${e}`);
4858
+ return { Success: false, Message: this.formatError(e) };
4859
+ }
4860
+ }
4861
+ /**
4862
+ * Tails a run's structured event stream from a given sequence number. The stream
4863
+ * grows over the life of the run, so a client polls with the last LatestSeq it
4864
+ * saw to fetch only the new events. IsInFlight=false signals the run is terminal
4865
+ * and polling can stop.
4866
+ */
4867
+ async IntegrationTailRunEvents(runID, ctx, sinceSeq) {
4868
+ try {
4869
+ const user = this.getAuthenticatedUser(ctx);
4870
+ const reader = new IntegrationProgressReader();
4871
+ const snap = await reader.GetRun(runID);
4872
+ if (!snap)
4873
+ return { Success: false, Message: `Run '${runID}' not found`, LatestSeq: sinceSeq ?? 0, IsInFlight: false };
4874
+ const authorized = await this.userCanReadRunArtifact(snap, user, new Map());
4875
+ if (!authorized) {
4876
+ const ciID = snap.manifest.companyIntegrationID;
4877
+ return {
4878
+ Success: false,
4879
+ Message: ciID
4880
+ ? this.notAuthorizedForCompanyIntegrationMessage(ciID)
4881
+ : `Not authorized to access run '${runID}'`,
4882
+ LatestSeq: sinceSeq ?? 0,
4883
+ IsInFlight: false,
4884
+ };
4885
+ }
4886
+ const events = await reader.Tail(runID, sinceSeq ?? 0);
4887
+ const latestSeq = events.length > 0 ? events[events.length - 1].seq : (sinceSeq ?? 0);
4888
+ return {
4889
+ Success: true,
4890
+ Message: `${events.length} event(s)`,
4891
+ Events: events.map(ev => ({
4892
+ Ts: ev.ts,
4893
+ Seq: ev.seq,
4894
+ EventType: ev.eventType,
4895
+ Level: ev.level,
4896
+ Stage: ev.stage,
4897
+ Message: ev.message,
4898
+ Counts: this.toCountsOutput(ev.counts),
4899
+ DataJSON: ev.data ? JSON.stringify(ev.data) : undefined,
4900
+ ResumableStateJSON: ev.resumableState ? JSON.stringify(ev.resumableState) : undefined,
4901
+ })),
4902
+ LatestSeq: latestSeq,
4903
+ IsInFlight: snap.isInFlight,
4904
+ };
4905
+ }
4906
+ catch (e) {
4907
+ LogError(`IntegrationTailRunEvents error: ${e}`);
4908
+ return { Success: false, Message: this.formatError(e), LatestSeq: sinceSeq ?? 0, IsInFlight: false };
4909
+ }
4910
+ }
4911
+ /** Maps a reader snapshot to the GraphQL summary shape. */
4912
+ toRunSummaryArtifact(s) {
4913
+ return {
4914
+ RunID: s.manifest.runID,
4915
+ RunKind: s.manifest.runKind,
4916
+ IntegrationID: s.manifest.integrationID,
4917
+ CompanyIntegrationID: s.manifest.companyIntegrationID,
4918
+ ObjectName: s.manifest.objectName,
4919
+ TriggerType: s.manifest.triggerType,
4920
+ StartedAt: s.manifest.startedAt,
4921
+ IsInFlight: s.isInFlight,
4922
+ EventCount: s.eventCount,
4923
+ Success: s.result?.success,
4924
+ ExitReason: s.result?.exitReason,
4925
+ CompletedAt: s.result?.completedAt,
4926
+ DurationMs: s.result?.durationMs,
4927
+ LatestEventType: s.latestEvent?.eventType,
4928
+ LatestMessage: s.latestEvent?.message,
4929
+ Counts: this.toCountsOutput(s.counts),
4930
+ WarningCount: s.warningCount,
4931
+ Warnings: s.warnings?.map(w => `[${w.code}] ${w.stage}: ${w.message}`),
4932
+ };
4933
+ }
4934
+ /** Maps the reader's lowercase counts shape to the PascalCase GraphQL output. */
4935
+ toCountsOutput(c) {
4936
+ if (!c)
4937
+ return undefined;
4938
+ return {
4939
+ Processed: c.processed,
4940
+ Succeeded: c.succeeded,
4941
+ Failed: c.failed,
4942
+ Skipped: c.skipped,
4943
+ TotalKnown: c.totalKnown,
4944
+ };
4945
+ }
3865
4946
  // ── CONNECTOR CAPABILITIES ──────────────────────────────────────────
3866
4947
  /**
3867
4948
  * Returns the CRUD capability flags for the connector bound to a CompanyIntegration.
@@ -3953,26 +5034,41 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
3953
5034
  }
3954
5035
  }
3955
5036
  else {
3956
- // Legacy path (HubSpot, YourMembership, Sage Intacct, etc.)
3957
- // — describe all, persist all, then resolve by either
3958
- // SourceObjectID (legacy clients) OR SourceObjectName
3959
- // (newly-discovered objects from connectors that probe
3960
- // their full catalog at picker time, e.g. SI's 666
3961
- // candidates). Without this fallback, freshly-probed
3962
- // selections silently drop.
3963
- sourceSchema = await connector.IntrospectSchema.bind(connector)(companyIntegration, user);
3964
- try {
3965
- const persistResult = await IntegrationSchemaSync.PersistDiscoveredSchema({
3966
- IntegrationID: companyIntegration.IntegrationID,
3967
- SourceSchema: sourceSchema,
3968
- ContextUser: user,
3969
- });
3970
- console.log(`[IntegrationApplyAllBatch] Persisted discovered schema for ${companyIntegration.Integration}: ` +
3971
- `${persistResult.ObjectsCreated} new objects, ${persistResult.FieldsCreated} new fields, ` +
3972
- `${persistResult.ObjectsUpdated} updated objects, ${persistResult.FieldsUpdated} updated fields`);
5037
+ // Legacy path (HubSpot, YourMembership, Sage Intacct, etc.).
5038
+ //
5039
+ // Phase 0 v5.39.x change: the `MJCompanyIntegrationEntityServer.Save()`
5040
+ // hook already ran the full discovery + persist pipeline when
5041
+ // the wizard's Finish flipped `IsActive false→true`. The IO/IOF
5042
+ // rows in `IntegrationEngineBase`'s cache are already fresh.
5043
+ // Calling `connector.IntrospectSchema()` again here just to feed
5044
+ // `SchemaBuilder` doubles the vendor-API roundtrip — on HubSpot
5045
+ // (130 objects, 60+ DiscoverFields probes) that's an extra ~100s
5046
+ // wasted per Apply. Reconstruct `SourceSchemaInfo` from the
5047
+ // persisted rows instead.
5048
+ //
5049
+ // Persist is also skipped here — the Save hook already did it.
5050
+ sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID);
5051
+ if (sourceSchema.Objects.length === 0) {
5052
+ // Defensive fallback: if the engine cache is empty (hook
5053
+ // didn't run, or this is a direct-API caller that bypasses
5054
+ // the wizard), do a one-time live introspect + persist so
5055
+ // the apply still proceeds. Rare.
5056
+ LogError(`[IntegrationApplyAllBatch] Persisted IO cache empty for ${companyIntegration.Integration}; falling back to live introspect.`);
5057
+ sourceSchema = await connector.IntrospectSchema.bind(connector)(companyIntegration, user);
5058
+ try {
5059
+ await IntegrationSchemaSync.PersistDiscoveredSchema({
5060
+ IntegrationID: companyIntegration.IntegrationID,
5061
+ SourceSchema: sourceSchema,
5062
+ ContextUser: user,
5063
+ });
5064
+ }
5065
+ catch (persistErr) {
5066
+ LogError(`IntegrationApplyAllBatch: PersistDiscoveredSchema fallback failed for ${companyIntegration.Integration}: ${persistErr}`);
5067
+ }
3973
5068
  }
3974
- catch (persistErr) {
3975
- LogError(`IntegrationApplyAllBatch: PersistDiscoveredSchema failed for ${companyIntegration.Integration}: ${persistErr}`);
5069
+ else {
5070
+ console.log(`[IntegrationApplyAllBatch] Reusing ${sourceSchema.Objects.length} persisted IOs for ${companyIntegration.Integration} ` +
5071
+ `— skipped duplicate IntrospectSchema (Save-hook already discovered).`);
3976
5072
  }
3977
5073
  // Resolve names from BOTH ID lookups and direct names.
3978
5074
  // Direct names skip the IntegrationObject DB roundtrip
@@ -4362,9 +5458,20 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4362
5458
  await emp.Delete();
4363
5459
  }
4364
5460
  }
4365
- // Step 7: Delete the CompanyIntegration itself
5461
+ // Step 7: Delete the CompanyIntegration itself FIRST, so its CredentialID FK reference
5462
+ // is gone before we delete the Credential. (Deleting the Credential first violates the
5463
+ // CompanyIntegration.CredentialID foreign key — the constraint is checked per-statement
5464
+ // inside the transaction — and rolls back the entire cascade.) Capture CredentialID
5465
+ // before Delete() since we still need it for Step 8.
5466
+ const linkedCredentialID = ci.CredentialID;
4366
5467
  ci.TransactionGroup = tg;
4367
5468
  await ci.Delete();
5469
+ // Step 8: Delete the linked Credential (encrypted-credential row) in the SAME
5470
+ // transaction group so it commits/rolls back atomically with the cascade — now safe
5471
+ // because the referencing CompanyIntegration delete is queued ahead of it. Skips
5472
+ // silently when there is no CredentialID, and refuses to delete a credential still
5473
+ // referenced by ANOTHER CompanyIntegration (shared credential).
5474
+ const credentialDeleted = await this.cascadeDeleteCredential(linkedCredentialID, companyIntegrationID, tg, rv, md, sysUser);
4368
5475
  // Submit the transaction
4369
5476
  const submitted = await tg.Submit();
4370
5477
  if (!submitted) {
@@ -4379,6 +5486,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
4379
5486
  EntityMapsDeleted: entityMapsDeleted,
4380
5487
  FieldMapsDeleted: fieldMapsDeleted,
4381
5488
  SchedulesDeleted: schedulesDeleted,
5489
+ CredentialDeleted: credentialDeleted,
4382
5490
  };
4383
5491
  }
4384
5492
  catch (e) {
@@ -4589,6 +5697,25 @@ __decorate([
4589
5697
  __metadata("design:paramtypes", [String, Object]),
4590
5698
  __metadata("design:returntype", Promise)
4591
5699
  ], IntegrationDiscoveryResolver.prototype, "IntegrationTestConnection", null);
5700
+ __decorate([
5701
+ Mutation(() => RefreshConnectorSchemaOutput),
5702
+ __param(0, Arg("companyIntegrationID")),
5703
+ __param(1, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK convention hint (e.g. 'id' for HubSpot)" })),
5704
+ __param(2, Ctx()),
5705
+ __metadata("design:type", Function),
5706
+ __metadata("design:paramtypes", [String, String, Object]),
5707
+ __metadata("design:returntype", Promise)
5708
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationRefreshConnectorSchema", null);
5709
+ __decorate([
5710
+ Mutation(() => IntegrationGenerateActionOutput),
5711
+ __param(0, Arg("integrationName")),
5712
+ __param(1, Arg("objectName")),
5713
+ __param(2, Arg("verb", { nullable: true, description: "Optional CRUD verb (Get|Create|Update|Delete|Search|List). Omit to generate all applicable verbs for the object." })),
5714
+ __param(3, Ctx()),
5715
+ __metadata("design:type", Function),
5716
+ __metadata("design:paramtypes", [String, String, String, Object]),
5717
+ __metadata("design:returntype", Promise)
5718
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationGenerateAction", null);
4592
5719
  __decorate([
4593
5720
  Query(() => DefaultConfigOutput),
4594
5721
  __param(0, Arg("companyIntegrationID")),
@@ -4630,9 +5757,11 @@ __decorate([
4630
5757
  Mutation(() => CreateConnectionOutput),
4631
5758
  __param(0, Arg("input")),
4632
5759
  __param(1, Arg("testConnection", () => Boolean, { defaultValue: false })),
4633
- __param(2, Ctx()),
5760
+ __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." })),
5761
+ __param(3, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot). Improves SoftPKClassifier convergence." })),
5762
+ __param(4, Ctx()),
4634
5763
  __metadata("design:type", Function),
4635
- __metadata("design:paramtypes", [CreateConnectionInput, Boolean, Object]),
5764
+ __metadata("design:paramtypes", [CreateConnectionInput, Boolean, Boolean, String, Object]),
4636
5765
  __metadata("design:returntype", Promise)
4637
5766
  ], IntegrationDiscoveryResolver.prototype, "IntegrationCreateConnection", null);
4638
5767
  __decorate([
@@ -4642,9 +5771,11 @@ __decorate([
4642
5771
  __param(2, Arg("configuration", { nullable: true })),
4643
5772
  __param(3, Arg("externalSystemID", { nullable: true })),
4644
5773
  __param(4, Arg("testConnection", () => Boolean, { defaultValue: false })),
4645
- __param(5, Ctx()),
5774
+ __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." })),
5775
+ __param(6, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK hint (e.g. 'id' for HubSpot)" })),
5776
+ __param(7, Ctx()),
4646
5777
  __metadata("design:type", Function),
4647
- __metadata("design:paramtypes", [String, String, String, String, Boolean, Object]),
5778
+ __metadata("design:paramtypes", [String, String, String, String, Boolean, Boolean, String, Object]),
4648
5779
  __metadata("design:returntype", Promise)
4649
5780
  ], IntegrationDiscoveryResolver.prototype, "IntegrationUpdateConnection", null);
4650
5781
  __decorate([
@@ -4847,6 +5978,34 @@ __decorate([
4847
5978
  __metadata("design:paramtypes", [String, Number, Object]),
4848
5979
  __metadata("design:returntype", Promise)
4849
5980
  ], IntegrationDiscoveryResolver.prototype, "IntegrationGetSyncHistory", null);
5981
+ __decorate([
5982
+ Query(() => IntegrationListRunsOutput),
5983
+ __param(0, Ctx()),
5984
+ __param(1, Arg("companyIntegrationID", { nullable: true })),
5985
+ __param(2, Arg("runKind", { nullable: true })),
5986
+ __param(3, Arg("inFlightOnly", { nullable: true })),
5987
+ __param(4, Arg("limit", { defaultValue: 50 })),
5988
+ __metadata("design:type", Function),
5989
+ __metadata("design:paramtypes", [Object, String, String, Boolean, Number]),
5990
+ __metadata("design:returntype", Promise)
5991
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationListRuns", null);
5992
+ __decorate([
5993
+ Query(() => IntegrationRunDetailOutput),
5994
+ __param(0, Arg("runID")),
5995
+ __param(1, Ctx()),
5996
+ __metadata("design:type", Function),
5997
+ __metadata("design:paramtypes", [String, Object]),
5998
+ __metadata("design:returntype", Promise)
5999
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationGetRun", null);
6000
+ __decorate([
6001
+ Query(() => IntegrationRunEventsOutput),
6002
+ __param(0, Arg("runID")),
6003
+ __param(1, Ctx()),
6004
+ __param(2, Arg("sinceSeq", { defaultValue: 0 })),
6005
+ __metadata("design:type", Function),
6006
+ __metadata("design:paramtypes", [String, Object, Number]),
6007
+ __metadata("design:returntype", Promise)
6008
+ ], IntegrationDiscoveryResolver.prototype, "IntegrationTailRunEvents", null);
4850
6009
  __decorate([
4851
6010
  Query(() => ConnectorCapabilitiesOutput),
4852
6011
  __param(0, Arg("companyIntegrationID")),