@memberjunction/server 5.41.0 → 5.42.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.
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +5 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
- package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
- package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +4 -7
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +591 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +6832 -3610
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -16
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +108 -0
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
- package/dist/integration/CustomColumnPromoter.js +508 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -0
- package/dist/logging/StartupLogger.d.ts +57 -1
- package/dist/logging/StartupLogger.d.ts.map +1 -1
- package/dist/logging/StartupLogger.js +115 -6
- package/dist/logging/StartupLogger.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +78 -5
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +227 -15
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +0 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +5 -3
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/package.json +83 -78
- package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +8 -1
- package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
- package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
- package/src/agentSessions/index.ts +5 -0
- package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
- package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
- package/src/auth/initializeProviders.ts +4 -7
- package/src/config.ts +13 -2
- package/src/generated/generated.ts +2444 -205
- package/src/index.ts +33 -16
- package/src/integration/CustomColumnPromoter.ts +597 -0
- package/src/logging/StartupLogger.ts +130 -6
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
- package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +232 -17
- package/src/resolvers/RunAIAgentResolver.ts +1 -9
- package/src/rest/setupRESTEndpoints.ts +5 -3
|
@@ -11,16 +11,18 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
var IntegrationDiscoveryResolver_1;
|
|
14
|
-
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType } from "type-graphql";
|
|
14
|
+
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType, Int, Float } from "type-graphql";
|
|
15
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, IntegrationConnectorCreationPipeline, IntegrationActionGenerator } from "@memberjunction/integration-engine";
|
|
18
|
+
import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync, decideSchemaLimitViolations, 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
22
|
import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
23
23
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
24
|
+
import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
|
|
25
|
+
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
24
26
|
import { UserCache } from "@memberjunction/sqlserver-dataprovider";
|
|
25
27
|
// ─── RSU Pipeline Output Types ──────────────────────────────────────────────
|
|
26
28
|
let RSUStepOutput = class RSUStepOutput {
|
|
@@ -1105,6 +1107,188 @@ __decorate([
|
|
|
1105
1107
|
MutationResultOutput = __decorate([
|
|
1106
1108
|
ObjectType()
|
|
1107
1109
|
], MutationResultOutput);
|
|
1110
|
+
// ─── Typed sync-config (rate-limit / concurrency / time-budget as STRUCTURED fields, not a raw
|
|
1111
|
+
// Configuration JSON blob). These map to the CompanyIntegration.Configuration keys the engine
|
|
1112
|
+
// reads at runtime, so they are customizable per-connection via the API instead of hidden code
|
|
1113
|
+
// constants. Set merges (preserves other Configuration keys); Get reads them back typed. ──────
|
|
1114
|
+
let IntegrationSyncConfigInput = class IntegrationSyncConfigInput {
|
|
1115
|
+
};
|
|
1116
|
+
__decorate([
|
|
1117
|
+
Field(() => Int, { nullable: true, description: 'Entity maps processed concurrently within a dependency layer (clamped 1-16). Default 1 (sequential).' }),
|
|
1118
|
+
__metadata("design:type", Number)
|
|
1119
|
+
], IntegrationSyncConfigInput.prototype, "SyncConcurrency", void 0);
|
|
1120
|
+
__decorate([
|
|
1121
|
+
Field(() => Int, { nullable: true, description: 'Upper bound the per-layer AIMD controller ramps toward. Default = connector MaxConcurrencyHint.' }),
|
|
1122
|
+
__metadata("design:type", Number)
|
|
1123
|
+
], IntegrationSyncConfigInput.prototype, "MaxConcurrency", void 0);
|
|
1124
|
+
__decorate([
|
|
1125
|
+
Field(() => Float, { nullable: true, description: 'Override the source rate limit (requests/sec). Default = connector RateLimitPolicy / derived.' }),
|
|
1126
|
+
__metadata("design:type", Number)
|
|
1127
|
+
], IntegrationSyncConfigInput.prototype, "RateLimitTokensPerSec", void 0);
|
|
1128
|
+
__decorate([
|
|
1129
|
+
Field(() => Int, { nullable: true, description: 'Override the rate-limiter burst capacity (tokens).' }),
|
|
1130
|
+
__metadata("design:type", Number)
|
|
1131
|
+
], IntegrationSyncConfigInput.prototype, "RateLimitBurst", void 0);
|
|
1132
|
+
__decorate([
|
|
1133
|
+
Field(() => Boolean, { nullable: true, description: '§4 cross-layer pipelining: a child map starts when ITS parents finish, not the whole layer.' }),
|
|
1134
|
+
__metadata("design:type", Boolean)
|
|
1135
|
+
], IntegrationSyncConfigInput.prototype, "CrossLayerPipeline", void 0);
|
|
1136
|
+
__decorate([
|
|
1137
|
+
Field(() => Boolean, { nullable: true, description: 'Merkle/partition hash-diff reconcile for watermark-less change detection (buffers the set in RAM).' }),
|
|
1138
|
+
__metadata("design:type", Boolean)
|
|
1139
|
+
], IntegrationSyncConfigInput.prototype, "PartitionReconcile", void 0);
|
|
1140
|
+
__decorate([
|
|
1141
|
+
Field(() => Int, { nullable: true, description: 'Time budget (ms) for stage-2 streaming field discovery before it stops and uses what it gathered.' }),
|
|
1142
|
+
__metadata("design:type", Number)
|
|
1143
|
+
], IntegrationSyncConfigInput.prototype, "DiscoveryTimeBudgetMs", void 0);
|
|
1144
|
+
__decorate([
|
|
1145
|
+
Field(() => Int, { nullable: true, description: 'Batch size for stage-2 streaming field discovery (records per FetchChanges page). Default 500.' }),
|
|
1146
|
+
__metadata("design:type", Number)
|
|
1147
|
+
], IntegrationSyncConfigInput.prototype, "DiscoveryBatchSize", void 0);
|
|
1148
|
+
__decorate([
|
|
1149
|
+
Field(() => Int, { nullable: true, description: 'Max records sampled in stage-2 streaming field discovery (a column corpus + PK guess; NOT a full scan). Default 500.' }),
|
|
1150
|
+
__metadata("design:type", Number)
|
|
1151
|
+
], IntegrationSyncConfigInput.prototype, "DiscoveryMaxRecords", void 0);
|
|
1152
|
+
__decorate([
|
|
1153
|
+
Field(() => Boolean, { nullable: true, description: '§7 Comprehensive refresh deactivates Declared objects/fields ABSENT from an AUTHORITATIVE discovery (reversible). Default false.' }),
|
|
1154
|
+
__metadata("design:type", Boolean)
|
|
1155
|
+
], IntegrationSyncConfigInput.prototype, "DeactivateAbsent", void 0);
|
|
1156
|
+
IntegrationSyncConfigInput = __decorate([
|
|
1157
|
+
InputType()
|
|
1158
|
+
], IntegrationSyncConfigInput);
|
|
1159
|
+
let IntegrationSyncConfigOutput = class IntegrationSyncConfigOutput {
|
|
1160
|
+
};
|
|
1161
|
+
__decorate([
|
|
1162
|
+
Field(),
|
|
1163
|
+
__metadata("design:type", Boolean)
|
|
1164
|
+
], IntegrationSyncConfigOutput.prototype, "Success", void 0);
|
|
1165
|
+
__decorate([
|
|
1166
|
+
Field(),
|
|
1167
|
+
__metadata("design:type", String)
|
|
1168
|
+
], IntegrationSyncConfigOutput.prototype, "Message", void 0);
|
|
1169
|
+
__decorate([
|
|
1170
|
+
Field(() => Int, { nullable: true }),
|
|
1171
|
+
__metadata("design:type", Number)
|
|
1172
|
+
], IntegrationSyncConfigOutput.prototype, "SyncConcurrency", void 0);
|
|
1173
|
+
__decorate([
|
|
1174
|
+
Field(() => Int, { nullable: true }),
|
|
1175
|
+
__metadata("design:type", Number)
|
|
1176
|
+
], IntegrationSyncConfigOutput.prototype, "MaxConcurrency", void 0);
|
|
1177
|
+
__decorate([
|
|
1178
|
+
Field(() => Float, { nullable: true }),
|
|
1179
|
+
__metadata("design:type", Number)
|
|
1180
|
+
], IntegrationSyncConfigOutput.prototype, "RateLimitTokensPerSec", void 0);
|
|
1181
|
+
__decorate([
|
|
1182
|
+
Field(() => Int, { nullable: true }),
|
|
1183
|
+
__metadata("design:type", Number)
|
|
1184
|
+
], IntegrationSyncConfigOutput.prototype, "RateLimitBurst", void 0);
|
|
1185
|
+
__decorate([
|
|
1186
|
+
Field(() => Boolean, { nullable: true }),
|
|
1187
|
+
__metadata("design:type", Boolean)
|
|
1188
|
+
], IntegrationSyncConfigOutput.prototype, "CrossLayerPipeline", void 0);
|
|
1189
|
+
__decorate([
|
|
1190
|
+
Field(() => Boolean, { nullable: true }),
|
|
1191
|
+
__metadata("design:type", Boolean)
|
|
1192
|
+
], IntegrationSyncConfigOutput.prototype, "PartitionReconcile", void 0);
|
|
1193
|
+
__decorate([
|
|
1194
|
+
Field(() => Int, { nullable: true }),
|
|
1195
|
+
__metadata("design:type", Number)
|
|
1196
|
+
], IntegrationSyncConfigOutput.prototype, "DiscoveryTimeBudgetMs", void 0);
|
|
1197
|
+
__decorate([
|
|
1198
|
+
Field(() => Int, { nullable: true }),
|
|
1199
|
+
__metadata("design:type", Number)
|
|
1200
|
+
], IntegrationSyncConfigOutput.prototype, "DiscoveryBatchSize", void 0);
|
|
1201
|
+
__decorate([
|
|
1202
|
+
Field(() => Int, { nullable: true }),
|
|
1203
|
+
__metadata("design:type", Number)
|
|
1204
|
+
], IntegrationSyncConfigOutput.prototype, "DiscoveryMaxRecords", void 0);
|
|
1205
|
+
__decorate([
|
|
1206
|
+
Field(() => Boolean, { nullable: true }),
|
|
1207
|
+
__metadata("design:type", Boolean)
|
|
1208
|
+
], IntegrationSyncConfigOutput.prototype, "DeactivateAbsent", void 0);
|
|
1209
|
+
IntegrationSyncConfigOutput = __decorate([
|
|
1210
|
+
ObjectType()
|
|
1211
|
+
], IntegrationSyncConfigOutput);
|
|
1212
|
+
let CustomColumnCandidate = class CustomColumnCandidate {
|
|
1213
|
+
};
|
|
1214
|
+
__decorate([
|
|
1215
|
+
Field(),
|
|
1216
|
+
__metadata("design:type", String)
|
|
1217
|
+
], CustomColumnCandidate.prototype, "EntityName", void 0);
|
|
1218
|
+
__decorate([
|
|
1219
|
+
Field(),
|
|
1220
|
+
__metadata("design:type", String)
|
|
1221
|
+
], CustomColumnCandidate.prototype, "SourceKey", void 0);
|
|
1222
|
+
__decorate([
|
|
1223
|
+
Field(),
|
|
1224
|
+
__metadata("design:type", String)
|
|
1225
|
+
], CustomColumnCandidate.prototype, "ColumnName", void 0);
|
|
1226
|
+
__decorate([
|
|
1227
|
+
Field(),
|
|
1228
|
+
__metadata("design:type", String)
|
|
1229
|
+
], CustomColumnCandidate.prototype, "InferredType", void 0);
|
|
1230
|
+
__decorate([
|
|
1231
|
+
Field(),
|
|
1232
|
+
__metadata("design:type", Boolean)
|
|
1233
|
+
], CustomColumnCandidate.prototype, "NeedsColumn", void 0);
|
|
1234
|
+
CustomColumnCandidate = __decorate([
|
|
1235
|
+
ObjectType()
|
|
1236
|
+
], CustomColumnCandidate);
|
|
1237
|
+
let CustomColumnCandidatesOutput = class CustomColumnCandidatesOutput {
|
|
1238
|
+
};
|
|
1239
|
+
__decorate([
|
|
1240
|
+
Field(),
|
|
1241
|
+
__metadata("design:type", Boolean)
|
|
1242
|
+
], CustomColumnCandidatesOutput.prototype, "Success", void 0);
|
|
1243
|
+
__decorate([
|
|
1244
|
+
Field(),
|
|
1245
|
+
__metadata("design:type", String)
|
|
1246
|
+
], CustomColumnCandidatesOutput.prototype, "Message", void 0);
|
|
1247
|
+
__decorate([
|
|
1248
|
+
Field(() => [CustomColumnCandidate]),
|
|
1249
|
+
__metadata("design:type", Array)
|
|
1250
|
+
], CustomColumnCandidatesOutput.prototype, "Candidates", void 0);
|
|
1251
|
+
CustomColumnCandidatesOutput = __decorate([
|
|
1252
|
+
ObjectType()
|
|
1253
|
+
], CustomColumnCandidatesOutput);
|
|
1254
|
+
let PromotedColumn = class PromotedColumn {
|
|
1255
|
+
};
|
|
1256
|
+
__decorate([
|
|
1257
|
+
Field(),
|
|
1258
|
+
__metadata("design:type", String)
|
|
1259
|
+
], PromotedColumn.prototype, "EntityName", void 0);
|
|
1260
|
+
__decorate([
|
|
1261
|
+
Field(),
|
|
1262
|
+
__metadata("design:type", String)
|
|
1263
|
+
], PromotedColumn.prototype, "ColumnName", void 0);
|
|
1264
|
+
PromotedColumn = __decorate([
|
|
1265
|
+
ObjectType()
|
|
1266
|
+
], PromotedColumn);
|
|
1267
|
+
let PromoteCustomColumnsOutput = class PromoteCustomColumnsOutput {
|
|
1268
|
+
};
|
|
1269
|
+
__decorate([
|
|
1270
|
+
Field(),
|
|
1271
|
+
__metadata("design:type", Boolean)
|
|
1272
|
+
], PromoteCustomColumnsOutput.prototype, "Success", void 0);
|
|
1273
|
+
__decorate([
|
|
1274
|
+
Field(),
|
|
1275
|
+
__metadata("design:type", String)
|
|
1276
|
+
], PromoteCustomColumnsOutput.prototype, "Message", void 0);
|
|
1277
|
+
__decorate([
|
|
1278
|
+
Field(),
|
|
1279
|
+
__metadata("design:type", Boolean)
|
|
1280
|
+
], PromoteCustomColumnsOutput.prototype, "Promoted", void 0);
|
|
1281
|
+
__decorate([
|
|
1282
|
+
Field(() => [PromotedColumn]),
|
|
1283
|
+
__metadata("design:type", Array)
|
|
1284
|
+
], PromoteCustomColumnsOutput.prototype, "ColumnsAdded", void 0);
|
|
1285
|
+
__decorate([
|
|
1286
|
+
Field(),
|
|
1287
|
+
__metadata("design:type", Boolean)
|
|
1288
|
+
], PromoteCustomColumnsOutput.prototype, "SchemaUpdatePending", void 0);
|
|
1289
|
+
PromoteCustomColumnsOutput = __decorate([
|
|
1290
|
+
ObjectType()
|
|
1291
|
+
], PromoteCustomColumnsOutput);
|
|
1108
1292
|
let FieldMapInput = class FieldMapInput {
|
|
1109
1293
|
};
|
|
1110
1294
|
__decorate([
|
|
@@ -1261,6 +1445,14 @@ __decorate([
|
|
|
1261
1445
|
Field({ nullable: true }),
|
|
1262
1446
|
__metadata("design:type", Boolean)
|
|
1263
1447
|
], CreateScheduleInput.prototype, "FullSync", void 0);
|
|
1448
|
+
__decorate([
|
|
1449
|
+
Field({ nullable: true, defaultValue: 'sync' }),
|
|
1450
|
+
__metadata("design:type", String)
|
|
1451
|
+
], CreateScheduleInput.prototype, "JobKind", void 0);
|
|
1452
|
+
__decorate([
|
|
1453
|
+
Field({ nullable: true }),
|
|
1454
|
+
__metadata("design:type", Boolean)
|
|
1455
|
+
], CreateScheduleInput.prototype, "DeactivateAbsent", void 0);
|
|
1264
1456
|
CreateScheduleInput = __decorate([
|
|
1265
1457
|
InputType()
|
|
1266
1458
|
], CreateScheduleInput);
|
|
@@ -2203,7 +2395,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2203
2395
|
* both on stdout (visible in the MJAPI log file) and in a per-run
|
|
2204
2396
|
* `<cwd>/logs/integration-runs/<runID>/progress.jsonl` artifact.
|
|
2205
2397
|
*/
|
|
2206
|
-
async IntegrationRefreshConnectorSchema(companyIntegrationID, universalPKConvention, ctx) {
|
|
2398
|
+
async IntegrationRefreshConnectorSchema(companyIntegrationID, universalPKConvention, deactivateAbsent, ctx) {
|
|
2207
2399
|
try {
|
|
2208
2400
|
const user = this.getAuthenticatedUser(ctx);
|
|
2209
2401
|
const provider = GetReadWriteProvider(ctx.providers);
|
|
@@ -2221,6 +2413,11 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2221
2413
|
UniversalPKConvention: universalPKConvention || undefined,
|
|
2222
2414
|
ConsoleMirror: true,
|
|
2223
2415
|
TriggerType: 'Manual',
|
|
2416
|
+
// §7 — explicit RefreshConnectorSchema is a comprehensive re-discovery: default to
|
|
2417
|
+
// deactivating objects/fields the source no longer exposes (reversible). Precedence:
|
|
2418
|
+
// explicit arg > persisted Configuration.deactivateAbsent (set via IntegrationSetSyncConfig) >
|
|
2419
|
+
// comprehensive default (true). Caller can opt out per-call or per-connection.
|
|
2420
|
+
DeactivateAbsent: deactivateAbsent ?? this.readConfigBool(companyIntegration.Configuration, 'deactivateAbsent') ?? true,
|
|
2224
2421
|
};
|
|
2225
2422
|
const result = await pipeline.Run(runOpts);
|
|
2226
2423
|
// Refresh the metadata cache so subsequent reads see the new IO/IOF
|
|
@@ -2502,14 +2699,20 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2502
2699
|
SourceFieldName: f.Name,
|
|
2503
2700
|
TargetColumnName: f.Name.replace(/[^A-Za-z0-9_]/g, '_'),
|
|
2504
2701
|
TargetSqlType: targetSqlType,
|
|
2505
|
-
// Synced shadow tables must NOT enforce NOT NULL on
|
|
2506
|
-
//
|
|
2507
|
-
// source of truth for business data,
|
|
2508
|
-
// constraints — and its describe output
|
|
2509
|
-
// fields required when real records actually
|
|
2510
|
-
// (deprecated, calculated, or edge-case fields).
|
|
2511
|
-
//
|
|
2512
|
-
|
|
2702
|
+
// Synced shadow tables must NOT enforce NOT NULL on ANY
|
|
2703
|
+
// column — including the PK. The external system (SF, HubSpot,
|
|
2704
|
+
// GrowthZone, etc.) is the source of truth for business data,
|
|
2705
|
+
// not for MJ's schema constraints — and its describe output
|
|
2706
|
+
// often declares fields required when real records actually
|
|
2707
|
+
// have nulls (deprecated, calculated, or edge-case fields).
|
|
2708
|
+
// Integration PKs are SOFT (tracked via SchemaBuilder.SoftPrimaryKeys
|
|
2709
|
+
// for upsert/dedup; identity falls back to a content-hash when the
|
|
2710
|
+
// PK is null/partial — §4). Emitting the PK column NOT NULL breaks
|
|
2711
|
+
// that fallback: a source row with a null PK (e.g. nested/derived
|
|
2712
|
+
// records like event sponsors, contact phones) aborts the insert
|
|
2713
|
+
// before content-hash can save it. So the soft-PK column is nullable
|
|
2714
|
+
// too; uniqueness/identity is enforced logically, not by the DDL.
|
|
2715
|
+
IsNullable: true,
|
|
2513
2716
|
MaxLength: f.MaxLength,
|
|
2514
2717
|
Precision: f.Precision,
|
|
2515
2718
|
Scale: f.Scale,
|
|
@@ -2527,12 +2730,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2527
2730
|
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — 0 fields discovered (live API likely failed and no DB-cached fields available)`);
|
|
2528
2731
|
continue;
|
|
2529
2732
|
}
|
|
2530
|
-
//
|
|
2531
|
-
//
|
|
2733
|
+
// Provable-only: no PK we could prove from the streamed data (single OR composite) means
|
|
2734
|
+
// the object is NOT added — and we say so clearly. We never fabricate a key (e.g. "all
|
|
2735
|
+
// columns as the PK"); a wrong identity is worse than an honest omission. The fix for a
|
|
2736
|
+
// missing PK is to STREAM MORE DATA at discovery time so the stats can prove one, not to
|
|
2737
|
+
// invent a key here.
|
|
2532
2738
|
if (primaryKeyFields.length === 0 && columns.length > 0) {
|
|
2533
2739
|
droppedNoPrimaryKey.push(obj.SourceObjectName);
|
|
2534
2740
|
const fieldNames = sourceObj.Fields.map(f => `${f.Name}(pk=${f.IsPrimaryKey})`).join(', ');
|
|
2535
|
-
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — ${columns.length} columns but NO primary key
|
|
2741
|
+
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — ${columns.length} columns but NO provable primary key. Fields: [${fieldNames}]`);
|
|
2536
2742
|
continue;
|
|
2537
2743
|
}
|
|
2538
2744
|
results.push({
|
|
@@ -2992,7 +3198,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2992
3198
|
*/
|
|
2993
3199
|
buildSourceSchemaFromPersistedRows(integrationID, requestedNames) {
|
|
2994
3200
|
const engine = IntegrationEngineBase.Instance;
|
|
2995
|
-
|
|
3201
|
+
// ACTIVE-only materialization: an object/field a given tenant doesn't expose is marked
|
|
3202
|
+
// Status='Inactive' by discovery (the phantom-skip), and MUST NOT be materialized — creating
|
|
3203
|
+
// empty tables/columns for absent objects wastes storage AND, more importantly, blows up the
|
|
3204
|
+
// per-entity CodeGen + advancedGen (AI form-layout) time on every ApplyAll. The sync path
|
|
3205
|
+
// already filters active (GetActiveIntegrationObjects); this build site now matches it.
|
|
3206
|
+
const ios = engine.GetActiveIntegrationObjects(integrationID);
|
|
2996
3207
|
const filter = requestedNames && requestedNames.length > 0
|
|
2997
3208
|
? new Set(requestedNames.map(n => n.toLowerCase()))
|
|
2998
3209
|
: null;
|
|
@@ -3005,7 +3216,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
3005
3216
|
for (const io of ios) {
|
|
3006
3217
|
if (filter && !filter.has(io.Name.toLowerCase()))
|
|
3007
3218
|
continue;
|
|
3008
|
-
|
|
3219
|
+
// Active fields only — an inactive (source-absent / deactivated) field is not materialized.
|
|
3220
|
+
const iofs = engine.GetIntegrationObjectFields(io.ID).filter(iof => iof.Status === 'Active');
|
|
3009
3221
|
const fields = iofs.map(iof => {
|
|
3010
3222
|
const targetIOName = iof.RelatedIntegrationObjectID
|
|
3011
3223
|
? ioByID.get(iof.RelatedIntegrationObjectID) ?? null
|
|
@@ -3394,6 +3606,170 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
3394
3606
|
return { Success: false, Message: this.formatError(e) };
|
|
3395
3607
|
}
|
|
3396
3608
|
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Sets the per-connection sync tuning (rate limit, concurrency, time budget, pipeline flags) as
|
|
3611
|
+
* STRUCTURED typed fields, merged into CompanyIntegration.Configuration (other keys preserved).
|
|
3612
|
+
* These are the exact keys the IntegrationEngine reads at runtime, so they become customizable
|
|
3613
|
+
* via the API instead of hidden code constants. Returns the merged config typed.
|
|
3614
|
+
*/
|
|
3615
|
+
async IntegrationSetSyncConfig(companyIntegrationID, config, ctx) {
|
|
3616
|
+
try {
|
|
3617
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
3618
|
+
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
|
|
3619
|
+
const ci = await md.GetEntityObject('MJ: Company Integrations', user);
|
|
3620
|
+
if (!await ci.InnerLoad(CompositeKey.FromID(companyIntegrationID))) {
|
|
3621
|
+
return { Success: false, Message: 'CompanyIntegration not found' };
|
|
3622
|
+
}
|
|
3623
|
+
let cfg = {};
|
|
3624
|
+
try {
|
|
3625
|
+
if (ci.Configuration)
|
|
3626
|
+
cfg = JSON.parse(ci.Configuration);
|
|
3627
|
+
}
|
|
3628
|
+
catch {
|
|
3629
|
+
cfg = {};
|
|
3630
|
+
}
|
|
3631
|
+
const set = (key, val) => { if (val !== undefined && val !== null)
|
|
3632
|
+
cfg[key] = val; };
|
|
3633
|
+
set('syncConcurrency', config.SyncConcurrency);
|
|
3634
|
+
set('maxConcurrency', config.MaxConcurrency);
|
|
3635
|
+
set('rateLimitTokensPerSec', config.RateLimitTokensPerSec);
|
|
3636
|
+
set('rateLimitBurst', config.RateLimitBurst);
|
|
3637
|
+
set('crossLayerPipeline', config.CrossLayerPipeline);
|
|
3638
|
+
set('partitionReconcile', config.PartitionReconcile);
|
|
3639
|
+
set('discoveryTimeBudgetMs', config.DiscoveryTimeBudgetMs);
|
|
3640
|
+
set('discoveryBatchSize', config.DiscoveryBatchSize);
|
|
3641
|
+
set('discoveryMaxRecords', config.DiscoveryMaxRecords);
|
|
3642
|
+
set('deactivateAbsent', config.DeactivateAbsent);
|
|
3643
|
+
ci.Configuration = JSON.stringify(cfg);
|
|
3644
|
+
if (!await ci.Save())
|
|
3645
|
+
return { Success: false, Message: `Failed to save: ${ci.LatestResult?.CompleteMessage ?? 'unknown'}` };
|
|
3646
|
+
return { Success: true, Message: 'Sync config updated', ...this.readSyncConfig(cfg) };
|
|
3647
|
+
}
|
|
3648
|
+
catch (e) {
|
|
3649
|
+
LogError(`IntegrationSetSyncConfig error: ${e}`);
|
|
3650
|
+
return { Success: false, Message: this.formatError(e) };
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
/** Reads the per-connection sync tuning back as STRUCTURED typed fields. */
|
|
3654
|
+
async IntegrationGetSyncConfig(companyIntegrationID, ctx) {
|
|
3655
|
+
try {
|
|
3656
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
3657
|
+
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
|
|
3658
|
+
const ci = await md.GetEntityObject('MJ: Company Integrations', user);
|
|
3659
|
+
if (!await ci.InnerLoad(CompositeKey.FromID(companyIntegrationID))) {
|
|
3660
|
+
return { Success: false, Message: 'CompanyIntegration not found' };
|
|
3661
|
+
}
|
|
3662
|
+
let cfg = {};
|
|
3663
|
+
try {
|
|
3664
|
+
if (ci.Configuration)
|
|
3665
|
+
cfg = JSON.parse(ci.Configuration);
|
|
3666
|
+
}
|
|
3667
|
+
catch {
|
|
3668
|
+
cfg = {};
|
|
3669
|
+
}
|
|
3670
|
+
return { Success: true, Message: 'OK', ...this.readSyncConfig(cfg) };
|
|
3671
|
+
}
|
|
3672
|
+
catch (e) {
|
|
3673
|
+
LogError(`IntegrationGetSyncConfig error: ${e}`);
|
|
3674
|
+
return { Success: false, Message: this.formatError(e) };
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
/** Reads a single boolean key from a CompanyIntegration.Configuration JSON string (undefined if absent/malformed). */
|
|
3678
|
+
readConfigBool(configuration, key) {
|
|
3679
|
+
try {
|
|
3680
|
+
if (!configuration)
|
|
3681
|
+
return undefined;
|
|
3682
|
+
const v = JSON.parse(configuration)[key];
|
|
3683
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
3684
|
+
}
|
|
3685
|
+
catch {
|
|
3686
|
+
return undefined;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
/** Extracts the typed sync-config fields from a parsed Configuration object (type-guarded). */
|
|
3690
|
+
readSyncConfig(cfg) {
|
|
3691
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
|
|
3692
|
+
const bool = (v) => (typeof v === 'boolean' ? v : undefined);
|
|
3693
|
+
return {
|
|
3694
|
+
SyncConcurrency: num(cfg.syncConcurrency),
|
|
3695
|
+
MaxConcurrency: num(cfg.maxConcurrency),
|
|
3696
|
+
RateLimitTokensPerSec: num(cfg.rateLimitTokensPerSec),
|
|
3697
|
+
RateLimitBurst: num(cfg.rateLimitBurst),
|
|
3698
|
+
CrossLayerPipeline: bool(cfg.crossLayerPipeline),
|
|
3699
|
+
PartitionReconcile: bool(cfg.partitionReconcile),
|
|
3700
|
+
DiscoveryTimeBudgetMs: num(cfg.discoveryTimeBudgetMs),
|
|
3701
|
+
DiscoveryBatchSize: num(cfg.discoveryBatchSize),
|
|
3702
|
+
DiscoveryMaxRecords: num(cfg.discoveryMaxRecords),
|
|
3703
|
+
DeactivateAbsent: bool(cfg.deactivateAbsent),
|
|
3704
|
+
};
|
|
3705
|
+
}
|
|
3706
|
+
/** The MJ entity names this connection has entity maps for (for whole-connection scope). */
|
|
3707
|
+
async getMappedEntityNames(companyIntegrationID, user) {
|
|
3708
|
+
const rv = new RunView();
|
|
3709
|
+
const res = await rv.RunView({
|
|
3710
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
3711
|
+
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
3712
|
+
Fields: ['Entity'],
|
|
3713
|
+
ResultType: 'simple',
|
|
3714
|
+
}, user);
|
|
3715
|
+
if (!res.Success)
|
|
3716
|
+
return [];
|
|
3717
|
+
return Array.from(new Set((res.Results ?? []).map(r => r.Entity).filter((e) => !!e)));
|
|
3718
|
+
}
|
|
3719
|
+
/**
|
|
3720
|
+
* Lists the custom-column CANDIDATES captured in the overflow column awaiting promotion — the "new
|
|
3721
|
+
* columns found" for a connection. READ-ONLY (no schema change, no RSU). Scope to one entity via
|
|
3722
|
+
* entityName, or omit to list across all the connection's mapped entities. Computed live (overflow keys
|
|
3723
|
+
* minus already-mapped/already-a-column), so it is inherently deduped against anything a concurrent
|
|
3724
|
+
* discovery already promoted.
|
|
3725
|
+
*/
|
|
3726
|
+
async IntegrationListCustomColumnCandidates(companyIntegrationID, entityName, ctx) {
|
|
3727
|
+
try {
|
|
3728
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
3729
|
+
const provider = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
|
|
3730
|
+
const promoter = new IntegrationCustomColumnPromoter(user, provider);
|
|
3731
|
+
const entityNames = entityName ? [entityName] : await this.getMappedEntityNames(companyIntegrationID, user);
|
|
3732
|
+
const candidates = [];
|
|
3733
|
+
for (const en of entityNames) {
|
|
3734
|
+
candidates.push(...await promoter.ListCandidates(companyIntegrationID, en));
|
|
3735
|
+
}
|
|
3736
|
+
return { Success: true, Message: `${candidates.length} candidate column(s) found`, Candidates: candidates };
|
|
3737
|
+
}
|
|
3738
|
+
catch (e) {
|
|
3739
|
+
LogError(`IntegrationListCustomColumnCandidates error: ${e}`);
|
|
3740
|
+
return { Success: false, Message: this.formatError(e), Candidates: [] };
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
/**
|
|
3744
|
+
* On-demand promotion of captured custom columns: runs RSU (ADD COLUMN + register EntityField + field map),
|
|
3745
|
+
* which may require a server restart to expose them over GraphQL. This is the USER-ACCEPTED trigger — by
|
|
3746
|
+
* default a sync only CAPTURES to the overflow column (auto-promote is opt-in per connection via
|
|
3747
|
+
* Configuration.autoPromoteCustomColumns). Scope via entityNames, or omit to promote across all mapped
|
|
3748
|
+
* entities. Idempotent: already-promoted/mapped keys are skipped (safe to re-run / run alongside discovery).
|
|
3749
|
+
*/
|
|
3750
|
+
async IntegrationPromoteCustomColumns(companyIntegrationID, entityNames, ctx) {
|
|
3751
|
+
try {
|
|
3752
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
3753
|
+
const provider = GetReadWriteProvider(ctx.providers);
|
|
3754
|
+
const targets = (entityNames && entityNames.length > 0) ? entityNames : await this.getMappedEntityNames(companyIntegrationID, user);
|
|
3755
|
+
if (targets.length === 0) {
|
|
3756
|
+
return { Success: true, Message: 'No mapped entities to promote', Promoted: false, ColumnsAdded: [], SchemaUpdatePending: false };
|
|
3757
|
+
}
|
|
3758
|
+
const promoter = new IntegrationCustomColumnPromoter(user, provider);
|
|
3759
|
+
const result = await promoter.PromoteForSync(companyIntegrationID, targets);
|
|
3760
|
+
return {
|
|
3761
|
+
Success: true,
|
|
3762
|
+
Message: result.Promoted ? `Promoted ${result.ColumnsAdded.length} column(s)` : 'No columns required promotion',
|
|
3763
|
+
Promoted: result.Promoted,
|
|
3764
|
+
ColumnsAdded: result.ColumnsAdded.map(c => ({ EntityName: c.EntityName, ColumnName: c.ColumnName })),
|
|
3765
|
+
SchemaUpdatePending: result.SchemaUpdatePending,
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
catch (e) {
|
|
3769
|
+
LogError(`IntegrationPromoteCustomColumns error: ${e}`);
|
|
3770
|
+
return { Success: false, Message: this.formatError(e), Promoted: false, ColumnsAdded: [], SchemaUpdatePending: false };
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3397
3773
|
/**
|
|
3398
3774
|
* Soft-deletes a CompanyIntegration by setting IsActive=false.
|
|
3399
3775
|
*/
|
|
@@ -4040,6 +4416,35 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4040
4416
|
* Build schema artifacts for a single connector's objects.
|
|
4041
4417
|
* Shared by IntegrationApplySchema (single) and IntegrationApplySchemaBatch (batch).
|
|
4042
4418
|
*/
|
|
4419
|
+
/**
|
|
4420
|
+
* §B — enforce OPTIONAL table/column caps at the create-tables (RSU) gate. These are OPERATOR
|
|
4421
|
+
* (deployment) guardrails read from ENV — `MJ_INTEGRATION_MAX_TABLES` / `MJ_INTEGRATION_MAX_COLUMNS_PER_TABLE`
|
|
4422
|
+
* (absent or ≤0 = unbounded, the common case). They are DELIBERATELY env-only, NOT per-connection
|
|
4423
|
+
* `Configuration`/GraphQL: a guardrail a user can raise via the same API they apply with is toothless.
|
|
4424
|
+
* THROWS a clear error when the selection exceeds a cap so NOTHING partial is created — the caller surfaces
|
|
4425
|
+
* it and the user narrows the selection (the cap itself is an operator concern). NEVER truncates. Discovery
|
|
4426
|
+
* still surfaces every object/field; only materialization is capped. Per-table column count = the selected
|
|
4427
|
+
* field subset, or the object's full discovered field count when all fields are selected.
|
|
4428
|
+
*/
|
|
4429
|
+
enforceSchemaLimits(objects, filteredSchema) {
|
|
4430
|
+
const envInt = (name) => {
|
|
4431
|
+
const v = parseInt(process.env[name] ?? '', 10);
|
|
4432
|
+
return Number.isFinite(v) && v > 0 ? v : null;
|
|
4433
|
+
};
|
|
4434
|
+
const fullCountByName = new Map(filteredSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.Fields.length]));
|
|
4435
|
+
// Delegate the cap decision to the engine's pure, unit-tested decideSchemaLimitViolations.
|
|
4436
|
+
const violations = decideSchemaLimitViolations({
|
|
4437
|
+
TableCount: objects.length,
|
|
4438
|
+
ColumnCountByTable: objects.map(o => ({
|
|
4439
|
+
Name: o.SourceObjectName,
|
|
4440
|
+
ColumnCount: o.Fields?.length ?? fullCountByName.get(o.SourceObjectName.toLowerCase()) ?? 0,
|
|
4441
|
+
})),
|
|
4442
|
+
MaxTables: envInt('MJ_INTEGRATION_MAX_TABLES'),
|
|
4443
|
+
MaxColumnsPerTable: envInt('MJ_INTEGRATION_MAX_COLUMNS_PER_TABLE'),
|
|
4444
|
+
});
|
|
4445
|
+
if (violations.length > 0)
|
|
4446
|
+
throw new Error(violations.join(' '));
|
|
4447
|
+
}
|
|
4043
4448
|
async buildSchemaForConnector(companyIntegrationID, objects, platform, user, skipGitCommit, skipRestart, provider, prefetchedSourceSchema) {
|
|
4044
4449
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, provider);
|
|
4045
4450
|
// Source-schema resolution order:
|
|
@@ -4082,6 +4487,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4082
4487
|
const filteredSchema = {
|
|
4083
4488
|
Objects: sourceSchema.Objects.filter(o => requestedNames.has(o.ExternalName))
|
|
4084
4489
|
};
|
|
4490
|
+
// §B — reject an over-limit table/column SELECTION before ANY table is materialized. This is the
|
|
4491
|
+
// single shared gate for ApplyAll / ApplyAllBatch / ApplySchemaBatch (all route through here).
|
|
4492
|
+
// Caps are operator/env guardrails (MJ_INTEGRATION_MAX_TABLES / _MAX_COLUMNS_PER_TABLE).
|
|
4493
|
+
this.enforceSchemaLimits(objects, filteredSchema);
|
|
4085
4494
|
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, platform, connector);
|
|
4086
4495
|
const input = {
|
|
4087
4496
|
SourceSchema: filteredSchema,
|
|
@@ -4129,6 +4538,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4129
4538
|
syncOptions.EntityMapIDs = entityMapIDs;
|
|
4130
4539
|
if (syncDirection)
|
|
4131
4540
|
syncOptions.SyncDirection = syncDirection;
|
|
4541
|
+
// Capture the fire instant BEFORE launching the sync. The run-record lookup below
|
|
4542
|
+
// finds the run by recency (StartedAt >= firedAt) rather than by transient status, so
|
|
4543
|
+
// a fast run (0-record / empty connector / quick failure) that finishes before we poll
|
|
4544
|
+
// is still reported with its real RunID instead of an untrackable null. RunSync stamps
|
|
4545
|
+
// StartedAt with an app-side `new Date()`, so this clock is consistent with the row.
|
|
4546
|
+
const firedAt = new Date();
|
|
4132
4547
|
// Fire and forget — progress is tracked inside IntegrationEngine
|
|
4133
4548
|
const syncPromise = IntegrationEngine.Instance.RunSync(companyIntegrationID, user, 'Manual', undefined, undefined, Object.keys(syncOptions).length > 0 ? syncOptions : undefined);
|
|
4134
4549
|
syncPromise
|
|
@@ -4157,22 +4572,43 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4157
4572
|
});
|
|
4158
4573
|
}
|
|
4159
4574
|
});
|
|
4160
|
-
//
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4575
|
+
// Resolve the run record by RECENCY, not by transient 'In Progress' status. The prior
|
|
4576
|
+
// implementation slept a fixed 200ms then filtered Status='In Progress' — when the run
|
|
4577
|
+
// finished in under 200ms (an empty connector, a 0-record sync, or a fast synchronous
|
|
4578
|
+
// failure) there was no 'In Progress' row to find, so it returned Success:true with a
|
|
4579
|
+
// null RunID: an optimistic, untrackable result that read as "started" when nothing
|
|
4580
|
+
// could be followed. Poll briefly for ANY run for this connector stamped at/after the
|
|
4581
|
+
// fire instant; that catches both the still-running and the already-finished cases.
|
|
4582
|
+
const firedAtFilter = firedAt.toISOString();
|
|
4583
|
+
let run = null;
|
|
4584
|
+
for (let attempt = 0; attempt < 15 && !run; attempt++) {
|
|
4585
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
4586
|
+
const rv = new RunView();
|
|
4587
|
+
const runResult = await rv.RunView({
|
|
4588
|
+
EntityName: 'MJ: Company Integration Runs',
|
|
4589
|
+
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}' AND StartedAt >= '${firedAtFilter}'`,
|
|
4590
|
+
OrderBy: 'StartedAt DESC',
|
|
4591
|
+
MaxRows: 1,
|
|
4592
|
+
ResultType: 'simple',
|
|
4593
|
+
Fields: ['ID', 'Status', 'StartedAt']
|
|
4594
|
+
}, user);
|
|
4595
|
+
if (runResult.Success && runResult.Results.length > 0) {
|
|
4596
|
+
run = runResult.Results[0];
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
// No run record after the poll window ⇒ the sync genuinely did not start (it no-op'd
|
|
4600
|
+
// before creating a run). Report that honestly rather than claiming Success with a null
|
|
4601
|
+
// RunID — a caller/scheduler can act on a false, but a true+null silently strands it.
|
|
4602
|
+
if (!run) {
|
|
4603
|
+
return {
|
|
4604
|
+
Success: false,
|
|
4605
|
+
Message: 'Sync did not start — no run record was created (the connector may be gated or misconfigured)'
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4172
4608
|
return {
|
|
4173
4609
|
Success: true,
|
|
4174
4610
|
Message: 'Sync started',
|
|
4175
|
-
RunID: run
|
|
4611
|
+
RunID: run.ID
|
|
4176
4612
|
};
|
|
4177
4613
|
}
|
|
4178
4614
|
catch (e) {
|
|
@@ -4275,16 +4711,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4275
4711
|
const user = this.getAuthenticatedUser(ctx);
|
|
4276
4712
|
const md = GetReadWriteProvider(ctx.providers, { allowFallbackToReadOnly: true });
|
|
4277
4713
|
const rv = new RunView();
|
|
4278
|
-
//
|
|
4714
|
+
// §13 — select the driver by job-kind: 'discovery' = schema-only refresh on a cron;
|
|
4715
|
+
// 'sync' (default) = the data-moving RunSync job.
|
|
4716
|
+
const jobKind = (input.JobKind ?? 'sync').toLowerCase();
|
|
4717
|
+
const driverClass = jobKind === 'discovery'
|
|
4718
|
+
? 'IntegrationDiscoveryScheduledJobDriver'
|
|
4719
|
+
: 'IntegrationSyncScheduledJobDriver';
|
|
4279
4720
|
const jobTypeResult = await rv.RunView({
|
|
4280
4721
|
EntityName: 'MJ: Scheduled Job Types',
|
|
4281
|
-
ExtraFilter: `DriverClass='
|
|
4722
|
+
ExtraFilter: `DriverClass='${driverClass}'`,
|
|
4282
4723
|
MaxRows: 1,
|
|
4283
4724
|
ResultType: 'simple',
|
|
4284
4725
|
Fields: ['ID']
|
|
4285
4726
|
}, user);
|
|
4286
4727
|
if (!jobTypeResult.Success || jobTypeResult.Results.length === 0) {
|
|
4287
|
-
return { Success: false, Message:
|
|
4728
|
+
return { Success: false, Message: `Scheduled job type not found for driver '${driverClass}' (kind='${jobKind}')` };
|
|
4288
4729
|
}
|
|
4289
4730
|
const jobTypeID = jobTypeResult.Results[0].ID;
|
|
4290
4731
|
const job = await md.GetEntityObject('MJ: Scheduled Jobs', user);
|
|
@@ -4298,10 +4739,18 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4298
4739
|
job.Status = 'Active';
|
|
4299
4740
|
job.OwnerUserID = user.ID;
|
|
4300
4741
|
const jobConfig = { CompanyIntegrationID: input.CompanyIntegrationID };
|
|
4301
|
-
if (
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4742
|
+
if (jobKind === 'discovery') {
|
|
4743
|
+
// Discovery job: schema-only. SyncDirection/FullSync are meaningless here; carry the
|
|
4744
|
+
// deactivate toggle instead (driver defaults it to true when omitted).
|
|
4745
|
+
if (input.DeactivateAbsent !== undefined && input.DeactivateAbsent !== null)
|
|
4746
|
+
jobConfig.DeactivateAbsent = input.DeactivateAbsent;
|
|
4747
|
+
}
|
|
4748
|
+
else {
|
|
4749
|
+
if (input.SyncDirection)
|
|
4750
|
+
jobConfig.SyncDirection = input.SyncDirection;
|
|
4751
|
+
if (input.FullSync)
|
|
4752
|
+
jobConfig.FullSync = input.FullSync;
|
|
4753
|
+
}
|
|
4305
4754
|
job.Configuration = JSON.stringify(jobConfig);
|
|
4306
4755
|
job.NextRunAt = CronExpressionHelper.GetNextRunTime(input.CronExpression, input.Timezone || 'UTC');
|
|
4307
4756
|
if (!await job.Save())
|
|
@@ -5477,12 +5926,18 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5477
5926
|
if (!submitted) {
|
|
5478
5927
|
return { Success: false, Message: 'Transaction failed — all deletes rolled back' };
|
|
5479
5928
|
}
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5929
|
+
// deleteData=true asks us to also DROP the physical mirror tables. We deliberately do NOT:
|
|
5930
|
+
// RuntimeSchemaManager rejects DROP TABLE on the __mj schema by design (a destructive-op
|
|
5931
|
+
// safety guard), and there is no DROP generator. Dropping integration tables is real data
|
|
5932
|
+
// loss and must be an explicit, separately-designed operation — not a silent side effect of
|
|
5933
|
+
// removing a connection. So we delete the connection + all METADATA (maps/fields/schedules/
|
|
5934
|
+
// credential) and RETAIN the data tables, and we say so honestly rather than claim otherwise.
|
|
5935
|
+
const dataNote = deleteData
|
|
5936
|
+
? ' The physical data tables were RETAINED (dropping integration tables is not performed automatically — it is a separate, explicit operation).'
|
|
5937
|
+
: '';
|
|
5483
5938
|
return {
|
|
5484
5939
|
Success: true,
|
|
5485
|
-
Message: `Deleted connection and all associated records`,
|
|
5940
|
+
Message: `Deleted connection and all associated metadata records.${dataNote}`,
|
|
5486
5941
|
EntityMapsDeleted: entityMapsDeleted,
|
|
5487
5942
|
FieldMapsDeleted: fieldMapsDeleted,
|
|
5488
5943
|
SchedulesDeleted: schedulesDeleted,
|
|
@@ -5699,15 +6154,18 @@ __decorate([
|
|
|
5699
6154
|
], IntegrationDiscoveryResolver.prototype, "IntegrationTestConnection", null);
|
|
5700
6155
|
__decorate([
|
|
5701
6156
|
Mutation(() => RefreshConnectorSchemaOutput),
|
|
6157
|
+
RequireSystemUser(),
|
|
5702
6158
|
__param(0, Arg("companyIntegrationID")),
|
|
5703
6159
|
__param(1, Arg("universalPKConvention", { nullable: true, description: "Optional vendor-wide PK convention hint (e.g. 'id' for HubSpot)" })),
|
|
5704
|
-
__param(2,
|
|
6160
|
+
__param(2, Arg("deactivateAbsent", { nullable: true, description: "Comprehensive refresh (default true): objects/fields ABSENT from this discovery are deactivated (Status='Disabled', never deleted, reversible on a later rediscovery). Pass false for a scoped/partial discovery so it never disables what it didn't probe." })),
|
|
6161
|
+
__param(3, Ctx()),
|
|
5705
6162
|
__metadata("design:type", Function),
|
|
5706
|
-
__metadata("design:paramtypes", [String, String, Object]),
|
|
6163
|
+
__metadata("design:paramtypes", [String, String, Boolean, Object]),
|
|
5707
6164
|
__metadata("design:returntype", Promise)
|
|
5708
6165
|
], IntegrationDiscoveryResolver.prototype, "IntegrationRefreshConnectorSchema", null);
|
|
5709
6166
|
__decorate([
|
|
5710
6167
|
Mutation(() => IntegrationGenerateActionOutput),
|
|
6168
|
+
RequireSystemUser(),
|
|
5711
6169
|
__param(0, Arg("integrationName")),
|
|
5712
6170
|
__param(1, Arg("objectName")),
|
|
5713
6171
|
__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." })),
|
|
@@ -5755,6 +6213,7 @@ __decorate([
|
|
|
5755
6213
|
], IntegrationDiscoveryResolver.prototype, "IntegrationListConnections", null);
|
|
5756
6214
|
__decorate([
|
|
5757
6215
|
Mutation(() => CreateConnectionOutput),
|
|
6216
|
+
RequireSystemUser(),
|
|
5758
6217
|
__param(0, Arg("input")),
|
|
5759
6218
|
__param(1, Arg("testConnection", () => Boolean, { defaultValue: false })),
|
|
5760
6219
|
__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." })),
|
|
@@ -5778,6 +6237,42 @@ __decorate([
|
|
|
5778
6237
|
__metadata("design:paramtypes", [String, String, String, String, Boolean, Boolean, String, Object]),
|
|
5779
6238
|
__metadata("design:returntype", Promise)
|
|
5780
6239
|
], IntegrationDiscoveryResolver.prototype, "IntegrationUpdateConnection", null);
|
|
6240
|
+
__decorate([
|
|
6241
|
+
Mutation(() => IntegrationSyncConfigOutput),
|
|
6242
|
+
__param(0, Arg("companyIntegrationID")),
|
|
6243
|
+
__param(1, Arg("config", () => IntegrationSyncConfigInput)),
|
|
6244
|
+
__param(2, Ctx()),
|
|
6245
|
+
__metadata("design:type", Function),
|
|
6246
|
+
__metadata("design:paramtypes", [String, IntegrationSyncConfigInput, Object]),
|
|
6247
|
+
__metadata("design:returntype", Promise)
|
|
6248
|
+
], IntegrationDiscoveryResolver.prototype, "IntegrationSetSyncConfig", null);
|
|
6249
|
+
__decorate([
|
|
6250
|
+
Query(() => IntegrationSyncConfigOutput),
|
|
6251
|
+
__param(0, Arg("companyIntegrationID")),
|
|
6252
|
+
__param(1, Ctx()),
|
|
6253
|
+
__metadata("design:type", Function),
|
|
6254
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
6255
|
+
__metadata("design:returntype", Promise)
|
|
6256
|
+
], IntegrationDiscoveryResolver.prototype, "IntegrationGetSyncConfig", null);
|
|
6257
|
+
__decorate([
|
|
6258
|
+
Query(() => CustomColumnCandidatesOutput),
|
|
6259
|
+
__param(0, Arg("companyIntegrationID")),
|
|
6260
|
+
__param(1, Arg("entityName", { nullable: true })),
|
|
6261
|
+
__param(2, Ctx()),
|
|
6262
|
+
__metadata("design:type", Function),
|
|
6263
|
+
__metadata("design:paramtypes", [String, String, Object]),
|
|
6264
|
+
__metadata("design:returntype", Promise)
|
|
6265
|
+
], IntegrationDiscoveryResolver.prototype, "IntegrationListCustomColumnCandidates", null);
|
|
6266
|
+
__decorate([
|
|
6267
|
+
Mutation(() => PromoteCustomColumnsOutput),
|
|
6268
|
+
RequireSystemUser(),
|
|
6269
|
+
__param(0, Arg("companyIntegrationID")),
|
|
6270
|
+
__param(1, Arg("entityNames", () => [String], { nullable: true })),
|
|
6271
|
+
__param(2, Ctx()),
|
|
6272
|
+
__metadata("design:type", Function),
|
|
6273
|
+
__metadata("design:paramtypes", [String, Array, Object]),
|
|
6274
|
+
__metadata("design:returntype", Promise)
|
|
6275
|
+
], IntegrationDiscoveryResolver.prototype, "IntegrationPromoteCustomColumns", null);
|
|
5781
6276
|
__decorate([
|
|
5782
6277
|
Mutation(() => MutationResultOutput),
|
|
5783
6278
|
__param(0, Arg("companyIntegrationID")),
|
|
@@ -5805,6 +6300,7 @@ __decorate([
|
|
|
5805
6300
|
], IntegrationDiscoveryResolver.prototype, "IntegrationCreateEntityMaps", null);
|
|
5806
6301
|
__decorate([
|
|
5807
6302
|
Mutation(() => ApplySchemaOutput),
|
|
6303
|
+
RequireSystemUser(),
|
|
5808
6304
|
__param(0, Arg("companyIntegrationID")),
|
|
5809
6305
|
__param(1, Arg("objects", () => [SchemaPreviewObjectInput])),
|
|
5810
6306
|
__param(2, Arg("platform", { defaultValue: "sqlserver" })),
|
|
@@ -5817,6 +6313,7 @@ __decorate([
|
|
|
5817
6313
|
], IntegrationDiscoveryResolver.prototype, "IntegrationApplySchema", null);
|
|
5818
6314
|
__decorate([
|
|
5819
6315
|
Mutation(() => ApplySchemaBatchOutput),
|
|
6316
|
+
RequireSystemUser(),
|
|
5820
6317
|
__param(0, Arg("items", () => [ApplySchemaBatchItemInput])),
|
|
5821
6318
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
5822
6319
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|
|
@@ -5828,6 +6325,7 @@ __decorate([
|
|
|
5828
6325
|
], IntegrationDiscoveryResolver.prototype, "IntegrationApplySchemaBatch", null);
|
|
5829
6326
|
__decorate([
|
|
5830
6327
|
Mutation(() => ApplyAllOutput),
|
|
6328
|
+
RequireSystemUser(),
|
|
5831
6329
|
__param(0, Arg("input")),
|
|
5832
6330
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
5833
6331
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|
|
@@ -5839,6 +6337,7 @@ __decorate([
|
|
|
5839
6337
|
], IntegrationDiscoveryResolver.prototype, "IntegrationApplyAll", null);
|
|
5840
6338
|
__decorate([
|
|
5841
6339
|
Mutation(() => StartSyncOutput),
|
|
6340
|
+
RequireSystemUser(),
|
|
5842
6341
|
__param(0, Arg("companyIntegrationID")),
|
|
5843
6342
|
__param(1, Arg("webhookURL", { nullable: true })),
|
|
5844
6343
|
__param(2, Arg("fullSync", () => Boolean, { defaultValue: false, description: 'If true, ignores watermarks and re-fetches all records from the source' })),
|
|
@@ -5859,6 +6358,7 @@ __decorate([
|
|
|
5859
6358
|
], IntegrationDiscoveryResolver.prototype, "IntegrationCancelSync", null);
|
|
5860
6359
|
__decorate([
|
|
5861
6360
|
Mutation(() => WriteRecordOutput),
|
|
6361
|
+
RequireSystemUser(),
|
|
5862
6362
|
__param(0, Arg("companyIntegrationID")),
|
|
5863
6363
|
__param(1, Arg("objectName")),
|
|
5864
6364
|
__param(2, Arg("operation", () => String, { description: 'create, update, or delete' })),
|
|
@@ -6016,6 +6516,7 @@ __decorate([
|
|
|
6016
6516
|
], IntegrationDiscoveryResolver.prototype, "IntegrationGetConnectorCapabilities", null);
|
|
6017
6517
|
__decorate([
|
|
6018
6518
|
Mutation(() => ApplyAllBatchOutput),
|
|
6519
|
+
RequireSystemUser(),
|
|
6019
6520
|
__param(0, Arg("input")),
|
|
6020
6521
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
6021
6522
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|
|
@@ -6036,6 +6537,7 @@ __decorate([
|
|
|
6036
6537
|
], IntegrationDiscoveryResolver.prototype, "IntegrationDeleteConnection", null);
|
|
6037
6538
|
__decorate([
|
|
6038
6539
|
Mutation(() => SchemaEvolutionOutput),
|
|
6540
|
+
RequireSystemUser(),
|
|
6039
6541
|
__param(0, Arg("companyIntegrationID")),
|
|
6040
6542
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
6041
6543
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|