@memberjunction/server 5.48.0 → 5.50.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/README.md +9 -0
- package/dist/auth/magicLink/MagicLinkKeys.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkKeys.js +11 -4
- package/dist/auth/magicLink/MagicLinkKeys.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +2 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +4 -0
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +6 -4
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +28 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +373 -6
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1975 -39
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +4 -2
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +1 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -11
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts +15 -2
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +113 -11
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/integration/EntityMapLifecycle.d.ts +66 -0
- package/dist/integration/EntityMapLifecycle.d.ts.map +1 -0
- package/dist/integration/EntityMapLifecycle.js +189 -0
- package/dist/integration/EntityMapLifecycle.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +10 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +67 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.js +6 -2
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts +12 -1
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js +23 -8
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +17 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +81 -25
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts +16 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +26 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +53 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +531 -64
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +0 -6
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RSUResolver.d.ts +10 -0
- package/dist/resolvers/RSUResolver.d.ts.map +1 -1
- package/dist/resolvers/RSUResolver.js +28 -0
- package/dist/resolvers/RSUResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +9 -6
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +135 -132
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +8 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +29 -1
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/adhoc-query-helpers.d.ts +27 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +1 -0
- package/dist/resolvers/adhoc-query-helpers.js +42 -0
- package/dist/resolvers/adhoc-query-helpers.js.map +1 -0
- package/dist/rest/OAuthCallbackHandler.d.ts +9 -0
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +21 -4
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/telephony/media-upgrade-router.d.ts +14 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -1
- package/dist/telephony/media-upgrade-router.js +17 -1
- package/dist/telephony/media-upgrade-router.js.map +1 -1
- package/package.json +91 -89
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +114 -0
- package/src/__tests__/EntityMapLifecycle.dagWarnings.test.ts +91 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +83 -0
- package/src/__tests__/RealtimeProxyServer.test.ts +53 -1
- package/src/__tests__/WidgetSessionService.test.ts +96 -0
- package/src/__tests__/adhoc-query-helpers.test.ts +50 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +106 -0
- package/src/__tests__/media-upgrade-router.test.ts +43 -0
- package/src/__tests__/unifiedAuth.test.ts +18 -3
- package/src/auth/magicLink/MagicLinkKeys.ts +12 -6
- package/src/auth/magicLink/MagicLinkRouter.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +4 -0
- package/src/auth/newUsers.ts +6 -5
- package/src/config.ts +14 -0
- package/src/context.ts +28 -2
- package/src/generated/generated.ts +1385 -28
- package/src/generic/ResolverBase.ts +5 -2
- package/src/generic/RunViewResolver.ts +11 -0
- package/src/index.ts +75 -11
- package/src/integration/CustomColumnPromoter.ts +123 -6
- package/src/integration/EntityMapLifecycle.ts +221 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +70 -1
- package/src/realtimeWidget/WidgetRouter.ts +6 -2
- package/src/realtimeWidget/WidgetSessionService.ts +24 -8
- package/src/resolvers/AdhocQueryResolver.ts +97 -30
- package/src/resolvers/GetDataResolver.ts +30 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +523 -64
- package/src/resolvers/QueryResolver.ts +1 -7
- package/src/resolvers/RSUResolver.ts +28 -0
- package/src/resolvers/RunAIAgentResolver.ts +74 -70
- package/src/resolvers/TransactionGroupResolver.ts +36 -2
- package/src/resolvers/__tests__/GetDataResolver.tokens.test.ts +138 -0
- package/src/resolvers/adhoc-query-helpers.ts +50 -0
- package/src/rest/OAuthCallbackHandler.ts +22 -4
- package/src/telephony/media-upgrade-router.ts +18 -1
|
@@ -14,6 +14,7 @@ var IntegrationDiscoveryResolver_1;
|
|
|
14
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
|
+
import { UUIDsEqual } from "@memberjunction/global";
|
|
17
18
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
18
19
|
import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync, decideSchemaLimitViolations, IntegrationConnectorCreationPipeline, IntegrationActionGenerator } from "@memberjunction/integration-engine";
|
|
19
20
|
import { IntegrationEngineBase } from "@memberjunction/integration-engine-base";
|
|
@@ -22,6 +23,7 @@ import { RuntimeSchemaManager } from "@memberjunction/schema-engine";
|
|
|
22
23
|
import { IntegrationProgressReader } from "@memberjunction/integration-progress-artifacts";
|
|
23
24
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
24
25
|
import { IntegrationCustomColumnPromoter } from "../integration/CustomColumnPromoter.js";
|
|
26
|
+
import { ComputeCascadeRemovalSet, ComputeRemovedDependencyWarnings, DisableUnselectedEntityMaps, ReenableFieldMapsForEntityMap, ResetPullWatermarks, SetEntityMapEnabled } from "../integration/EntityMapLifecycle.js";
|
|
25
27
|
import { RequireSystemUser } from "../directives/RequireSystemUser.js";
|
|
26
28
|
import { UserCache } from "@memberjunction/sqlserver-dataprovider";
|
|
27
29
|
// ─── RSU Pipeline Output Types ──────────────────────────────────────────────
|
|
@@ -43,6 +45,14 @@ __decorate([
|
|
|
43
45
|
Field(),
|
|
44
46
|
__metadata("design:type", String)
|
|
45
47
|
], RSUStepOutput.prototype, "Message", void 0);
|
|
48
|
+
__decorate([
|
|
49
|
+
Field(() => Int, { nullable: true }),
|
|
50
|
+
__metadata("design:type", Number)
|
|
51
|
+
], RSUStepOutput.prototype, "StepIndex", void 0);
|
|
52
|
+
__decorate([
|
|
53
|
+
Field(() => Int, { nullable: true }),
|
|
54
|
+
__metadata("design:type", Number)
|
|
55
|
+
], RSUStepOutput.prototype, "StepTotal", void 0);
|
|
46
56
|
RSUStepOutput = __decorate([
|
|
47
57
|
ObjectType()
|
|
48
58
|
], RSUStepOutput);
|
|
@@ -137,7 +147,7 @@ __decorate([
|
|
|
137
147
|
__metadata("design:type", Boolean)
|
|
138
148
|
], ApplyAllInput.prototype, "StartSync", void 0);
|
|
139
149
|
__decorate([
|
|
140
|
-
Field(() => Boolean, { nullable: true,
|
|
150
|
+
Field(() => Boolean, { nullable: true, description: 'If true, ignores watermarks and does a full re-fetch. When OMITTED: defaults to TRUE on the connector\'s FIRST apply (no prior entity maps — a full sync by default for the first sync), FALSE thereafter.' }),
|
|
141
151
|
__metadata("design:type", Boolean)
|
|
142
152
|
], ApplyAllInput.prototype, "FullSync", void 0);
|
|
143
153
|
__decorate([
|
|
@@ -148,6 +158,10 @@ __decorate([
|
|
|
148
158
|
Field({ nullable: true, defaultValue: 'Pull', description: 'SyncDirection applied to all created entity maps: Pull | Push | Bidirectional. Defaults to Pull.' }),
|
|
149
159
|
__metadata("design:type", String)
|
|
150
160
|
], ApplyAllInput.prototype, "DefaultSyncDirection", void 0);
|
|
161
|
+
__decorate([
|
|
162
|
+
Field({ nullable: true, defaultValue: 'disable', description: 'Remove-as-disable: what to do with existing entity maps whose object is NOT in this selection. "disable" (default) = Status=Disabled + SyncEnabled=false (+ field maps disabled; data kept, re-selection re-enables). "ignore" = leave them untouched (additive apply — use for subset/partial applies).' }),
|
|
163
|
+
__metadata("design:type", String)
|
|
164
|
+
], ApplyAllInput.prototype, "UnselectedAction", void 0);
|
|
151
165
|
ApplyAllInput = __decorate([
|
|
152
166
|
InputType()
|
|
153
167
|
], ApplyAllInput);
|
|
@@ -315,6 +329,10 @@ __decorate([
|
|
|
315
329
|
Field({ nullable: true, description: 'Override sync direction stored in the created schedule: Pull | Push | Bidirectional.' }),
|
|
316
330
|
__metadata("design:type", String)
|
|
317
331
|
], ApplyAllBatchInput.prototype, "ScheduleSyncDirection", void 0);
|
|
332
|
+
__decorate([
|
|
333
|
+
Field({ nullable: true, defaultValue: 'disable', description: 'Remove-as-disable (same as ApplyAllInput.UnselectedAction): "disable" (default) disables entity maps whose object is absent from each connector\'s selection; "ignore" leaves them untouched.' }),
|
|
334
|
+
__metadata("design:type", String)
|
|
335
|
+
], ApplyAllBatchInput.prototype, "UnselectedAction", void 0);
|
|
318
336
|
ApplyAllBatchInput = __decorate([
|
|
319
337
|
InputType()
|
|
320
338
|
], ApplyAllBatchInput);
|
|
@@ -461,6 +479,26 @@ __decorate([
|
|
|
461
479
|
Field(() => [String], { nullable: true }),
|
|
462
480
|
__metadata("design:type", Array)
|
|
463
481
|
], SchemaEvolutionOutput.prototype, "Warnings", void 0);
|
|
482
|
+
__decorate([
|
|
483
|
+
Field(() => [String], { nullable: true }),
|
|
484
|
+
__metadata("design:type", Array)
|
|
485
|
+
], SchemaEvolutionOutput.prototype, "NewObjects", void 0);
|
|
486
|
+
__decorate([
|
|
487
|
+
Field(() => [String], { nullable: true }),
|
|
488
|
+
__metadata("design:type", Array)
|
|
489
|
+
], SchemaEvolutionOutput.prototype, "RemovedObjects", void 0);
|
|
490
|
+
__decorate([
|
|
491
|
+
Field(() => [String], { nullable: true }),
|
|
492
|
+
__metadata("design:type", Array)
|
|
493
|
+
], SchemaEvolutionOutput.prototype, "CascadeRemovedObjects", void 0);
|
|
494
|
+
__decorate([
|
|
495
|
+
Field(() => [String], { nullable: true }),
|
|
496
|
+
__metadata("design:type", Array)
|
|
497
|
+
], SchemaEvolutionOutput.prototype, "ChangedObjects", void 0);
|
|
498
|
+
__decorate([
|
|
499
|
+
Field(() => [String], { nullable: true }),
|
|
500
|
+
__metadata("design:type", Array)
|
|
501
|
+
], SchemaEvolutionOutput.prototype, "WatermarksReset", void 0);
|
|
464
502
|
SchemaEvolutionOutput = __decorate([
|
|
465
503
|
ObjectType()
|
|
466
504
|
], SchemaEvolutionOutput);
|
|
@@ -1812,6 +1850,14 @@ __decorate([
|
|
|
1812
1850
|
Field({ nullable: true }),
|
|
1813
1851
|
__metadata("design:type", Boolean)
|
|
1814
1852
|
], OperationProgressOutput.prototype, "RSURunning", void 0);
|
|
1853
|
+
__decorate([
|
|
1854
|
+
Field(() => Int, { nullable: true }),
|
|
1855
|
+
__metadata("design:type", Number)
|
|
1856
|
+
], OperationProgressOutput.prototype, "RSUStepIndex", void 0);
|
|
1857
|
+
__decorate([
|
|
1858
|
+
Field(() => Int, { nullable: true }),
|
|
1859
|
+
__metadata("design:type", Number)
|
|
1860
|
+
], OperationProgressOutput.prototype, "RSUStepTotal", void 0);
|
|
1815
1861
|
__decorate([
|
|
1816
1862
|
Field({ nullable: true }),
|
|
1817
1863
|
__metadata("design:type", Number)
|
|
@@ -2148,6 +2194,10 @@ __decorate([
|
|
|
2148
2194
|
Field(),
|
|
2149
2195
|
__metadata("design:type", Boolean)
|
|
2150
2196
|
], ListSourceObjectsItem.prototype, "IsCustom", void 0);
|
|
2197
|
+
__decorate([
|
|
2198
|
+
Field(() => [String], { nullable: true }),
|
|
2199
|
+
__metadata("design:type", Array)
|
|
2200
|
+
], ListSourceObjectsItem.prototype, "DependsOn", void 0);
|
|
2151
2201
|
ListSourceObjectsItem = __decorate([
|
|
2152
2202
|
ObjectType()
|
|
2153
2203
|
], ListSourceObjectsItem);
|
|
@@ -2274,8 +2324,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2274
2324
|
SupportsIncrementalSync: true,
|
|
2275
2325
|
SupportsWrite: true,
|
|
2276
2326
|
}));
|
|
2327
|
+
// DAG exposure: per-object parent names from the same FK graph the sync
|
|
2328
|
+
// engine layers by, so a consumer can auto-check dependencies at selection time.
|
|
2329
|
+
const depsByName = this.computeObjectDependencyNames(companyIntegration.IntegrationID);
|
|
2277
2330
|
const merged = sourceObjects.map(o => {
|
|
2278
2331
|
const existing = existingByName.get(o.Name);
|
|
2332
|
+
const dependsOn = depsByName.get(o.Name.toLowerCase());
|
|
2279
2333
|
return {
|
|
2280
2334
|
Name: o.Name,
|
|
2281
2335
|
Label: o.Label,
|
|
@@ -2285,6 +2339,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2285
2339
|
AlreadyPersisted: existing != null,
|
|
2286
2340
|
IntegrationObjectID: existing?.ID,
|
|
2287
2341
|
IsCustom: this.isCustomObjectName(o.Name, existing?.IsCustom),
|
|
2342
|
+
DependsOn: dependsOn && dependsOn.length > 0 ? dependsOn : undefined,
|
|
2288
2343
|
};
|
|
2289
2344
|
});
|
|
2290
2345
|
merged.sort((a, b) => a.Name.localeCompare(b.Name));
|
|
@@ -2326,6 +2381,53 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2326
2381
|
return existingIsCustom;
|
|
2327
2382
|
return name.endsWith('__c');
|
|
2328
2383
|
}
|
|
2384
|
+
/**
|
|
2385
|
+
* DAG exposure: object name (lowercased) → sorted parent object names, derived from
|
|
2386
|
+
* the SAME dependency edges the sync engine layers by — hard FK pointers
|
|
2387
|
+
* (IntegrationObjectField.RelatedIntegrationObjectID) plus soft-FK declarations in the IO's
|
|
2388
|
+
* Configuration (parentObjectName / parentObjectNames map / ReferencedType). Reads the
|
|
2389
|
+
* engine cache only (no DB round-trip); objects with no persisted IO row have no entry.
|
|
2390
|
+
*/
|
|
2391
|
+
computeObjectDependencyNames(integrationID) {
|
|
2392
|
+
const engine = IntegrationEngineBase.Instance;
|
|
2393
|
+
const ios = engine.GetIntegrationObjectsByIntegrationID(integrationID);
|
|
2394
|
+
const nameById = new Map(ios.map(io => [io.ID.toUpperCase(), io.Name]));
|
|
2395
|
+
const byLowerName = new Map(ios.map(io => [io.Name.toLowerCase(), io]));
|
|
2396
|
+
const deps = new Map();
|
|
2397
|
+
for (const io of ios) {
|
|
2398
|
+
const parents = new Set();
|
|
2399
|
+
for (const f of engine.GetIntegrationObjectFields(io.ID)) {
|
|
2400
|
+
const parentID = f.RelatedIntegrationObjectID?.toUpperCase();
|
|
2401
|
+
if (!parentID || UUIDsEqual(parentID, io.ID))
|
|
2402
|
+
continue;
|
|
2403
|
+
const parentName = nameById.get(parentID);
|
|
2404
|
+
if (parentName)
|
|
2405
|
+
parents.add(parentName);
|
|
2406
|
+
}
|
|
2407
|
+
const cfgRaw = io.Configuration;
|
|
2408
|
+
if (cfgRaw) {
|
|
2409
|
+
try {
|
|
2410
|
+
const cfg = JSON.parse(cfgRaw);
|
|
2411
|
+
const candidates = [];
|
|
2412
|
+
if (cfg.parentObjectName)
|
|
2413
|
+
candidates.push(cfg.parentObjectName);
|
|
2414
|
+
if (cfg.ReferencedType)
|
|
2415
|
+
candidates.push(cfg.ReferencedType);
|
|
2416
|
+
if (cfg.parentObjectNames && typeof cfg.parentObjectNames === 'object' && !Array.isArray(cfg.parentObjectNames)) {
|
|
2417
|
+
candidates.push(...Object.values(cfg.parentObjectNames).filter((v) => typeof v === 'string'));
|
|
2418
|
+
}
|
|
2419
|
+
for (const c of candidates) {
|
|
2420
|
+
const match = byLowerName.get(c.toLowerCase());
|
|
2421
|
+
if (match && !UUIDsEqual(match.ID, io.ID))
|
|
2422
|
+
parents.add(match.Name);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
catch { /* non-JSON Configuration → no soft-FK parents */ }
|
|
2426
|
+
}
|
|
2427
|
+
deps.set(io.Name.toLowerCase(), [...parents].sort());
|
|
2428
|
+
}
|
|
2429
|
+
return deps;
|
|
2430
|
+
}
|
|
2329
2431
|
/**
|
|
2330
2432
|
* Discovers fields on a specific external object.
|
|
2331
2433
|
*/
|
|
@@ -2396,6 +2498,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2396
2498
|
* `<cwd>/logs/integration-runs/<runID>/progress.jsonl` artifact.
|
|
2397
2499
|
*/
|
|
2398
2500
|
async IntegrationRefreshConnectorSchema(companyIntegrationID, universalPKConvention, deactivateAbsent, ctx) {
|
|
2501
|
+
// sync lock: a metadata refresh rewrites the IO/IOF rows a sync reads.
|
|
2502
|
+
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'metadata refresh')) {
|
|
2503
|
+
return {
|
|
2504
|
+
Success: false, RunID: 'not-started',
|
|
2505
|
+
Message: 'Refresh not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.',
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2399
2508
|
try {
|
|
2400
2509
|
const user = this.getAuthenticatedUser(ctx);
|
|
2401
2510
|
const provider = GetReadWriteProvider(ctx.providers);
|
|
@@ -2456,6 +2565,9 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2456
2565
|
RunID: 'error',
|
|
2457
2566
|
};
|
|
2458
2567
|
}
|
|
2568
|
+
finally {
|
|
2569
|
+
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
2570
|
+
}
|
|
2459
2571
|
}
|
|
2460
2572
|
/**
|
|
2461
2573
|
* Generates + persists strongly-typed Action metadata on demand for an
|
|
@@ -4035,7 +4147,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4035
4147
|
Message: `Batch complete: ${batchResult.SuccessCount} succeeded, ${batchResult.FailureCount} failed`,
|
|
4036
4148
|
Items: itemResults,
|
|
4037
4149
|
Steps: batchResult.Results[0]?.Steps.map((s) => ({
|
|
4038
|
-
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message,
|
|
4150
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
4039
4151
|
})),
|
|
4040
4152
|
GitCommitSuccess: batchResult.Results[0]?.GitCommitSuccess,
|
|
4041
4153
|
APIRestarted: batchResult.Results[0]?.APIRestarted,
|
|
@@ -4164,6 +4276,19 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4164
4276
|
for (let i = 0; i < resolvedNames.length; i++) {
|
|
4165
4277
|
sourceObjectFields[resolvedNames[i]] = resolved.sourceObjects[i].Fields ?? null;
|
|
4166
4278
|
}
|
|
4279
|
+
// the sync after a connector's FIRST apply defaults to FULL ("always full
|
|
4280
|
+
// generally as the default"); later applies default incremental. "First" = no entity
|
|
4281
|
+
// map existed for this connection before this apply. Explicit input always wins.
|
|
4282
|
+
const priorMapsResult = await new RunView().RunView({
|
|
4283
|
+
EntityName: 'MJ: Company Integration Entity Maps',
|
|
4284
|
+
ExtraFilter: `CompanyIntegrationID='${input.CompanyIntegrationID.replace(/'/g, "''")}'`,
|
|
4285
|
+
Fields: ['ID'],
|
|
4286
|
+
MaxRows: 1,
|
|
4287
|
+
ResultType: 'simple',
|
|
4288
|
+
BypassCache: true,
|
|
4289
|
+
}, user);
|
|
4290
|
+
const hadPriorMaps = priorMapsResult.Success && priorMapsResult.Results.length > 0;
|
|
4291
|
+
const effectiveFullSync = input.FullSync ?? !hadPriorMaps;
|
|
4167
4292
|
const pendingPayload = {
|
|
4168
4293
|
CompanyIntegrationID: input.CompanyIntegrationID,
|
|
4169
4294
|
SourceObjectNames: resolvedNames,
|
|
@@ -4172,19 +4297,22 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4172
4297
|
CronExpression: input.CronExpression,
|
|
4173
4298
|
ScheduleTimezone: input.ScheduleTimezone,
|
|
4174
4299
|
StartSync: input.StartSync,
|
|
4175
|
-
FullSync:
|
|
4300
|
+
FullSync: effectiveFullSync,
|
|
4176
4301
|
SyncScope: input.SyncScope ?? 'created',
|
|
4302
|
+
UnselectedAction: (input.UnselectedAction ?? 'disable'),
|
|
4177
4303
|
CreatedAt: new Date().toISOString(),
|
|
4178
4304
|
};
|
|
4179
4305
|
rsuInput.PostRestartFiles = [
|
|
4180
4306
|
{ Path: pendingFilePath, Content: JSON.stringify(pendingPayload, null, 2) }
|
|
4181
4307
|
];
|
|
4182
|
-
// Step 5: Run pipeline (restart kills process at the end)
|
|
4308
|
+
// Step 5: Run pipeline (restart kills process at the end).
|
|
4309
|
+
// Retrying variant — an install should not die because the migration hit a dropped
|
|
4310
|
+
// connection or a transient lock; it only replays while nothing has been applied.
|
|
4183
4311
|
const rsm = RuntimeSchemaManager.Instance;
|
|
4184
|
-
const batchResult = await rsm.
|
|
4312
|
+
const batchResult = await rsm.RunPipelineBatchWithRetry([rsuInput]);
|
|
4185
4313
|
const migrationSucceeded = batchResult.SuccessCount > 0;
|
|
4186
4314
|
const pipelineSteps = batchResult.Results[0]?.Steps.map((s) => ({
|
|
4187
|
-
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message,
|
|
4315
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
4188
4316
|
}));
|
|
4189
4317
|
// If pipeline failed, clean up pending file and return error
|
|
4190
4318
|
if (!migrationSucceeded) {
|
|
@@ -4205,6 +4333,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4205
4333
|
if (skipRestart) {
|
|
4206
4334
|
await provider.Refresh();
|
|
4207
4335
|
const entityMapsCreated = await this.createEntityAndFieldMaps(input.CompanyIntegrationID, objects, connector, companyIntegration, schemaName, user, provider, input.DefaultSyncDirection ?? 'Pull');
|
|
4336
|
+
// Remove-as-disable: objects absent from THIS selection get their entity
|
|
4337
|
+
// map + field maps disabled (data kept; re-selection re-enables). 'ignore' opts out
|
|
4338
|
+
// for additive/subset applies (e.g. test-harness per-scope runs).
|
|
4339
|
+
if ((input.UnselectedAction ?? 'disable') !== 'ignore') {
|
|
4340
|
+
const disabledObjects = await DisableUnselectedEntityMaps(input.CompanyIntegrationID, resolvedNames, user, provider);
|
|
4341
|
+
if (disabledObjects.length > 0) {
|
|
4342
|
+
console.log(`[IntegrationApplyAll] Disabled ${disabledObjects.length} unselected entity map(s): ${disabledObjects.join(', ')}`);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4208
4345
|
const createdMapIDs = entityMapsCreated.map(em => em.EntityMapID).filter(Boolean);
|
|
4209
4346
|
const scopedMapIDs = input.SyncScope === 'all' ? undefined : createdMapIDs;
|
|
4210
4347
|
// Skip sync when SyncScope='created' but 0 new maps were
|
|
@@ -4214,7 +4351,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4214
4351
|
const shouldStartSync = input.StartSync !== false &&
|
|
4215
4352
|
(input.SyncScope === 'all' || createdMapIDs.length > 0);
|
|
4216
4353
|
const syncRunID = shouldStartSync
|
|
4217
|
-
? await this.startSyncAfterApply(input.CompanyIntegrationID, user, scopedMapIDs,
|
|
4354
|
+
? await this.startSyncAfterApply(input.CompanyIntegrationID, user, scopedMapIDs, effectiveFullSync)
|
|
4218
4355
|
: null;
|
|
4219
4356
|
// Create schedule if requested
|
|
4220
4357
|
let scheduledJobID;
|
|
@@ -4340,6 +4477,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4340
4477
|
existing = existingMapResult.Results[0];
|
|
4341
4478
|
}
|
|
4342
4479
|
let em;
|
|
4480
|
+
const wasDisabled = existing ? existing.Status !== 'Active' || existing.SyncEnabled === false : false;
|
|
4343
4481
|
if (existing) {
|
|
4344
4482
|
em = existing; // reuse — keeps the map stable across re-applies
|
|
4345
4483
|
}
|
|
@@ -4358,6 +4496,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4358
4496
|
LogError(`IntegrationApplyAll: failed to save entity map for ${obj.SourceObjectName}`);
|
|
4359
4497
|
return null;
|
|
4360
4498
|
}
|
|
4499
|
+
// Re-add re-enables: a previously-removed (disabled) object that is re-selected
|
|
4500
|
+
// comes back WITH its field maps — the create-field-maps idempotency below skips existing
|
|
4501
|
+
// rows, so without this they would stay Disabled forever.
|
|
4502
|
+
if (wasDisabled) {
|
|
4503
|
+
await ReenableFieldMapsForEntityMap(em.ID, user, md);
|
|
4504
|
+
}
|
|
4361
4505
|
// Discover fields from the source and create 1:1 field maps
|
|
4362
4506
|
const fieldMapCount = await this.createFieldMapsForEntityMap(em.ID, obj.SourceObjectName, connector, companyIntegration, user, md);
|
|
4363
4507
|
return {
|
|
@@ -4393,7 +4537,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4393
4537
|
fm.EntityMapID = entityMapID;
|
|
4394
4538
|
fm.SourceFieldName = field.Name;
|
|
4395
4539
|
fm.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
4396
|
-
fm.IsKeyField = field.
|
|
4540
|
+
fm.IsKeyField = field.IsPrimaryKey ?? false; // key on the record's PRIMARY key (identity), consistent with processRSUPendingWork + U1 (PK ≠ unique)
|
|
4397
4541
|
fm.IsRequired = field.IsRequired;
|
|
4398
4542
|
fm.Direction = 'SourceToDest';
|
|
4399
4543
|
fm.Status = 'Active';
|
|
@@ -4600,6 +4744,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4600
4744
|
if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
|
|
4601
4745
|
return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not started' };
|
|
4602
4746
|
}
|
|
4747
|
+
// sync lock: refuse while a metadata refresh / schema evolution / RSU pipeline
|
|
4748
|
+
// holds the maintenance lock for this connection (the engine also refuses — this gives
|
|
4749
|
+
// the client an immediate, unambiguous answer).
|
|
4750
|
+
const maintenance = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
4751
|
+
if (maintenance) {
|
|
4752
|
+
return { Success: false, Message: `Sync not started: ${maintenance.Reason} is in progress for this connection (since ${maintenance.AcquiredAt.toISOString()}). Retry after it completes.` };
|
|
4753
|
+
}
|
|
4603
4754
|
const syncOptions = {};
|
|
4604
4755
|
if (fullSync)
|
|
4605
4756
|
syncOptions.FullSync = true;
|
|
@@ -4648,10 +4799,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4648
4799
|
// null RunID: an optimistic, untrackable result that read as "started" when nothing
|
|
4649
4800
|
// could be followed. Poll briefly for ANY run for this connector stamped at/after the
|
|
4650
4801
|
// fire instant; that catches both the still-running and the already-finished cases.
|
|
4802
|
+
//
|
|
4803
|
+
// The row itself isn't created until executeSyncInternal reaches CreateRunRecord,
|
|
4804
|
+
// which runs AFTER LoadRunConfiguration (loads every entity map + field map + builds
|
|
4805
|
+
// the connector instance) — for a large connector (HubSpot, 168 entity maps) that
|
|
4806
|
+
// synchronous setup alone can take several seconds, so a short fixed poll window can
|
|
4807
|
+
// elapse before the row exists at all, not because the sync failed. Live-verified: a
|
|
4808
|
+
// HubSpot StartSync call reported "no run created" while the run (confirmed present in
|
|
4809
|
+
// the DB moments later) was genuinely syncing. Keep the fast path fast (first 2s still
|
|
4810
|
+
// polled every 200ms) but extend the tail so large connectors get a fair window before
|
|
4811
|
+
// we honestly report failure.
|
|
4651
4812
|
const firedAtFilter = firedAt.toISOString();
|
|
4652
4813
|
let run = null;
|
|
4653
|
-
|
|
4654
|
-
|
|
4814
|
+
const pollIntervalsMs = [...Array(10).fill(200), ...Array(20).fill(1000)];
|
|
4815
|
+
for (let attempt = 0; attempt < pollIntervalsMs.length && !run; attempt++) {
|
|
4816
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalsMs[attempt]));
|
|
4655
4817
|
const rv = new RunView();
|
|
4656
4818
|
const runResult = await rv.RunView({
|
|
4657
4819
|
EntityName: 'MJ: Company Integration Runs',
|
|
@@ -4797,8 +4959,20 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4797
4959
|
return { Success: false, Message: `Scheduled job type not found for driver '${driverClass}' (kind='${jobKind}')` };
|
|
4798
4960
|
}
|
|
4799
4961
|
const jobTypeID = jobTypeResult.Results[0].ID;
|
|
4962
|
+
// U7 — SERVER-SIDE uniqueness per (CompanyIntegration, job kind/driver): a client
|
|
4963
|
+
// name-regex is not a guarantee, and duplicate jobs mean duplicate sync/rediscovery
|
|
4964
|
+
// runs. When an active/paused job of this kind already targets this connection,
|
|
4965
|
+
// UPDATE it in place instead of creating a twin.
|
|
4800
4966
|
const job = await md.GetEntityObject('MJ: Scheduled Jobs', user);
|
|
4801
|
-
|
|
4967
|
+
const existingJob = await this.findExistingScheduledJob(jobTypeID, input.CompanyIntegrationID, user);
|
|
4968
|
+
let updatedInPlace = false;
|
|
4969
|
+
if (existingJob) {
|
|
4970
|
+
const loaded = await job.InnerLoad(CompositeKey.FromID(existingJob.ID));
|
|
4971
|
+
if (loaded)
|
|
4972
|
+
updatedInPlace = true;
|
|
4973
|
+
}
|
|
4974
|
+
if (!updatedInPlace)
|
|
4975
|
+
job.NewRecord();
|
|
4802
4976
|
job.JobTypeID = jobTypeID;
|
|
4803
4977
|
job.Name = input.Name;
|
|
4804
4978
|
if (input.Description)
|
|
@@ -4833,13 +5007,44 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4833
5007
|
ci.CronExpression = input.CronExpression;
|
|
4834
5008
|
await ci.Save();
|
|
4835
5009
|
}
|
|
4836
|
-
return {
|
|
5010
|
+
return {
|
|
5011
|
+
Success: true,
|
|
5012
|
+
Message: updatedInPlace
|
|
5013
|
+
? 'Schedule updated — an active schedule of this kind already existed for this connection (server-side uniqueness)'
|
|
5014
|
+
: 'Schedule created',
|
|
5015
|
+
ScheduledJobID: job.ID,
|
|
5016
|
+
};
|
|
4837
5017
|
}
|
|
4838
5018
|
catch (e) {
|
|
4839
5019
|
LogError(`IntegrationCreateSchedule error: ${e}`);
|
|
4840
5020
|
return { Success: false, Message: this.formatError(e) };
|
|
4841
5021
|
}
|
|
4842
5022
|
}
|
|
5023
|
+
/**
|
|
5024
|
+
* U7 — server-side schedule uniqueness: finds an existing (non-deleted) scheduled job of the
|
|
5025
|
+
* given job type that targets the given CompanyIntegration (its Configuration JSON carries
|
|
5026
|
+
* CompanyIntegrationID). Matched case-insensitively across UUID casings (SQL Server upper /
|
|
5027
|
+
* PostgreSQL lower). Returns the newest match or null.
|
|
5028
|
+
*/
|
|
5029
|
+
async findExistingScheduledJob(jobTypeID, companyIntegrationID, user) {
|
|
5030
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
5031
|
+
const lower = esc(companyIntegrationID.toLowerCase());
|
|
5032
|
+
const upper = esc(companyIntegrationID.toUpperCase());
|
|
5033
|
+
const result = await new RunView().RunView({
|
|
5034
|
+
EntityName: 'MJ: Scheduled Jobs',
|
|
5035
|
+
ExtraFilter: `JobTypeID='${esc(jobTypeID)}' AND Status IN ('Active','Paused') AND ` +
|
|
5036
|
+
// Structured "CompanyIntegrationID":"<uuid>" predicate (same form as IntegrationListSchedules),
|
|
5037
|
+
// NOT a bare-UUID substring — so this CI's id can't false-match the same UUID appearing in
|
|
5038
|
+
// another config key (e.g. RelatedCompanyIntegrationID) and repurpose an unrelated job.
|
|
5039
|
+
`(Configuration LIKE '%"CompanyIntegrationID":"${lower}"%' OR Configuration LIKE '%"CompanyIntegrationID":"${upper}"%')`,
|
|
5040
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
5041
|
+
Fields: ['ID'],
|
|
5042
|
+
MaxRows: 1,
|
|
5043
|
+
ResultType: 'simple',
|
|
5044
|
+
BypassCache: true, // uniqueness must see committed jobs, not a stale filtered cache
|
|
5045
|
+
}, user);
|
|
5046
|
+
return result.Success && result.Results.length > 0 ? result.Results[0] : null;
|
|
5047
|
+
}
|
|
4843
5048
|
async IntegrationUpdateSchedule(scheduledJobID, cronExpression, timezone, name, ctx) {
|
|
4844
5049
|
try {
|
|
4845
5050
|
const user = this.getAuthenticatedUser(ctx);
|
|
@@ -5172,7 +5377,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5172
5377
|
OperationType: 'rsu',
|
|
5173
5378
|
IsRunning: true,
|
|
5174
5379
|
RSURunning: true,
|
|
5175
|
-
|
|
5380
|
+
// U11 — determinate "step N of M: <name>" instead of an indeterminate spinner
|
|
5381
|
+
RSUStep: rsuStatus.CurrentStepName ?? rsuStatus.LastRunResult ?? 'running',
|
|
5382
|
+
RSUStepIndex: rsuStatus.CurrentStepIndex ?? undefined,
|
|
5383
|
+
RSUStepTotal: rsuStatus.StepTotal ?? undefined,
|
|
5176
5384
|
StartedAt: rsuStatus.LastRunAt?.toISOString(),
|
|
5177
5385
|
ElapsedMs: rsuStatus.LastRunAt ? Date.now() - rsuStatus.LastRunAt.getTime() : undefined,
|
|
5178
5386
|
};
|
|
@@ -5652,6 +5860,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5652
5860
|
SyncScope: input.SyncScope ?? 'created',
|
|
5653
5861
|
SyncDirection: input.SyncDirection,
|
|
5654
5862
|
ScheduleSyncDirection: input.ScheduleSyncDirection,
|
|
5863
|
+
UnselectedAction: (input.UnselectedAction ?? 'disable'),
|
|
5655
5864
|
CreatedAt: new Date().toISOString(),
|
|
5656
5865
|
};
|
|
5657
5866
|
rsuInput.PostRestartFiles = [
|
|
@@ -5696,10 +5905,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5696
5905
|
FailureCount: connectorResults.length,
|
|
5697
5906
|
};
|
|
5698
5907
|
}
|
|
5699
|
-
// Phase 2: Run all successful RSU inputs through one pipeline batch
|
|
5908
|
+
// Phase 2: Run all successful RSU inputs through one pipeline batch.
|
|
5909
|
+
// Retrying variant — a multi-connector install is the longest-running thing a user
|
|
5910
|
+
// does here, and it should not be lost to one transient step. Replays only while
|
|
5911
|
+
// nothing has been applied, so a partially-committed batch is never re-executed.
|
|
5700
5912
|
const pipelineInputs = successfulBuilds.map(b => b.rsuInput);
|
|
5701
5913
|
const rsm = RuntimeSchemaManager.Instance;
|
|
5702
|
-
const batchResult = await rsm.
|
|
5914
|
+
const batchResult = await rsm.RunPipelineBatchWithRetry(pipelineInputs);
|
|
5703
5915
|
// Phase 3: Post-pipeline — create entity maps, field maps, schedules for each success
|
|
5704
5916
|
for (let i = 0; i < successfulBuilds.length; i++) {
|
|
5705
5917
|
const build = successfulBuilds[i];
|
|
@@ -5763,7 +5975,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5763
5975
|
connectorResults.push(connResult);
|
|
5764
5976
|
}
|
|
5765
5977
|
const pipelineSteps = batchResult.Results[0]?.Steps.map((s) => ({
|
|
5766
|
-
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message,
|
|
5978
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
5767
5979
|
}));
|
|
5768
5980
|
const successCount = connectorResults.filter(r => r.Success).length;
|
|
5769
5981
|
const failureCount = connectorResults.filter(r => !r.Success).length;
|
|
@@ -5804,8 +6016,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5804
6016
|
return null;
|
|
5805
6017
|
}
|
|
5806
6018
|
const jobTypeID = jobTypeResult.Results[0].ID;
|
|
6019
|
+
// U7 — same server-side uniqueness as IntegrationCreateSchedule: update the existing
|
|
6020
|
+
// sync job for this connection in place rather than creating a duplicate.
|
|
5807
6021
|
const job = await md.GetEntityObject('MJ: Scheduled Jobs', user);
|
|
5808
|
-
|
|
6022
|
+
const existingJob = await this.findExistingScheduledJob(jobTypeID, companyIntegrationID, user);
|
|
6023
|
+
const loadedExisting = existingJob ? await job.InnerLoad(CompositeKey.FromID(existingJob.ID)) : false;
|
|
6024
|
+
if (!loadedExisting)
|
|
6025
|
+
job.NewRecord();
|
|
5809
6026
|
job.JobTypeID = jobTypeID;
|
|
5810
6027
|
job.Name = `${integrationName} Sync`;
|
|
5811
6028
|
job.CronExpression = cronExpression;
|
|
@@ -6024,7 +6241,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6024
6241
|
* applies ALTER TABLE migrations via the RSU pipeline.
|
|
6025
6242
|
* Compares the current connector introspection against existing MJ entities.
|
|
6026
6243
|
*/
|
|
6027
|
-
async IntegrationSchemaEvolution(companyIntegrationID, platform, skipGitCommit, skipRestart, ctx) {
|
|
6244
|
+
async IntegrationSchemaEvolution(companyIntegrationID, platform, skipGitCommit, skipRestart, autoEnableNewObjects, deactivateAbsent, cascadeRemoveDependents, ctx) {
|
|
6245
|
+
// sync lock: schema evolution rewrites the metadata, field maps and DDL a sync
|
|
6246
|
+
// reads — data syncs (manual + scheduled) must not run while this is in flight.
|
|
6247
|
+
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema evolution')) {
|
|
6248
|
+
return {
|
|
6249
|
+
Success: false, HasChanges: false,
|
|
6250
|
+
Message: 'Schema evolution not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.',
|
|
6251
|
+
};
|
|
6252
|
+
}
|
|
6028
6253
|
try {
|
|
6029
6254
|
const user = this.getAuthenticatedUser(ctx);
|
|
6030
6255
|
const validatedPlatform = this.validatePlatform(platform);
|
|
@@ -6032,22 +6257,89 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6032
6257
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, md);
|
|
6033
6258
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
6034
6259
|
const rv = new RunView();
|
|
6035
|
-
//
|
|
6036
|
-
|
|
6260
|
+
// ── Phase 1 — FULL RE-RESOLUTION (: "the exact same algorithm as before") ──
|
|
6261
|
+
// Re-run the creation pipeline: introspect (static + runtime discovery), persist with
|
|
6262
|
+
// the external-wins-when-present overlay, deactivate absent (gated on authoritative
|
|
6263
|
+
// discovery), PK-classify the residual. The diff below is computed against the
|
|
6264
|
+
// RESULT of this resolution, exactly as the first-time flow computed its state.
|
|
6265
|
+
const pipeline = new IntegrationConnectorCreationPipeline();
|
|
6266
|
+
const refresh = await pipeline.Run({
|
|
6267
|
+
Connector: connector,
|
|
6268
|
+
CompanyIntegration: companyIntegration,
|
|
6269
|
+
ContextUser: user,
|
|
6270
|
+
Provider: md,
|
|
6271
|
+
TriggerType: 'Manual',
|
|
6272
|
+
ConsoleMirror: true,
|
|
6273
|
+
DeactivateAbsent: deactivateAbsent ?? this.readConfigBool(companyIntegration.Configuration, 'deactivateAbsent') ?? true,
|
|
6274
|
+
});
|
|
6275
|
+
if (!refresh.Success) {
|
|
6276
|
+
return { Success: false, HasChanges: false, Message: `Schema evolution aborted — metadata re-resolution failed: ${refresh.FailureMessage ?? 'unknown error'}` };
|
|
6277
|
+
}
|
|
6278
|
+
await md.Refresh();
|
|
6279
|
+
await IntegrationEngine.Instance.Config(true, user, md);
|
|
6280
|
+
// ── Phase 2 — DIFF: resolved-Active objects vs prior entity maps ──
|
|
6281
|
+
const activeIOs = IntegrationEngineBase.Instance.GetActiveIntegrationObjects(companyIntegration.IntegrationID);
|
|
6282
|
+
const activeNamesLower = new Set(activeIOs.map(o => o.Name.toLowerCase()));
|
|
6283
|
+
const priorMapsResult = await rv.RunView({
|
|
6037
6284
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
6038
6285
|
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
6039
|
-
ResultType: '
|
|
6040
|
-
|
|
6286
|
+
ResultType: 'entity_object',
|
|
6287
|
+
BypassCache: true, // the diff must read committed state
|
|
6041
6288
|
}, user);
|
|
6042
|
-
|
|
6289
|
+
const priorMaps = priorMapsResult.Success ? priorMapsResult.Results : [];
|
|
6290
|
+
const priorByLowerName = new Map(priorMaps.map(m => [(m.ExternalObjectName ?? '').toLowerCase(), m]));
|
|
6291
|
+
const newObjects = activeIOs.filter(io => !priorByLowerName.has(io.Name.toLowerCase())).map(io => io.Name);
|
|
6292
|
+
const removedObjects = priorMaps
|
|
6293
|
+
.filter(m => m.ExternalObjectName && !activeNamesLower.has(m.ExternalObjectName.toLowerCase()))
|
|
6294
|
+
.map(m => m.ExternalObjectName);
|
|
6295
|
+
const continuingMaps = priorMaps.filter(m => m.ExternalObjectName && activeNamesLower.has(m.ExternalObjectName.toLowerCase()));
|
|
6296
|
+
// ── Phase 3 — REMOVED objects: disable EM + EFMs (data + tables KEPT) ──
|
|
6297
|
+
// Uses the same remove-as-disable helper as ApplyAll: everything not in the resolved
|
|
6298
|
+
// set is disabled; a later re-appearance re-enables (reactivate-on-rediscover).
|
|
6299
|
+
// DAG handling (rsuplan "adding and removing tables"): dependents of a removed object
|
|
6300
|
+
// are NOT auto-removed by default — each broken parent edge is surfaced as a warning so
|
|
6301
|
+
// a consumer can prompt the user. With cascadeRemoveDependents=true (the "force remove
|
|
6302
|
+
// those too" affordance), the TRANSITIVE closure of still-active dependents is disabled
|
|
6303
|
+
// alongside the removed objects (still remove-as-disable, reversible). Edges come from
|
|
6304
|
+
// the same dependency map the sync engine layers by.
|
|
6305
|
+
const depMap = removedObjects.length > 0
|
|
6306
|
+
? this.computeObjectDependencyNames(companyIntegration.IntegrationID)
|
|
6307
|
+
: new Map();
|
|
6308
|
+
const cascadeRemoved = cascadeRemoveDependents && removedObjects.length > 0
|
|
6309
|
+
? ComputeCascadeRemovalSet(activeIOs.map(io => io.Name), removedObjects, depMap)
|
|
6310
|
+
: [];
|
|
6311
|
+
const cascadeLower = new Set(cascadeRemoved.map(n => n.toLowerCase()));
|
|
6312
|
+
const keepNames = activeIOs.map(io => io.Name).filter(n => !cascadeLower.has(n.toLowerCase()));
|
|
6313
|
+
const dagRemovalWarnings = removedObjects.length > 0 && !cascadeRemoveDependents
|
|
6314
|
+
? ComputeRemovedDependencyWarnings(keepNames, removedObjects, depMap)
|
|
6315
|
+
: [];
|
|
6316
|
+
for (const w of dagRemovalWarnings)
|
|
6317
|
+
LogStatus(`[SchemaEvolution][DAG] ${w}`);
|
|
6318
|
+
if (cascadeRemoved.length > 0)
|
|
6319
|
+
LogStatus(`[SchemaEvolution][DAG] cascade-removing ${cascadeRemoved.length} dependent object(s): ${cascadeRemoved.join(', ')}`);
|
|
6320
|
+
if (removedObjects.length > 0 || cascadeRemoved.length > 0) {
|
|
6321
|
+
await DisableUnselectedEntityMaps(companyIntegrationID, keepNames, user, md);
|
|
6322
|
+
}
|
|
6323
|
+
// ── Phase 4 — schema build over continuing + new objects (minus cascade-removed) ──
|
|
6324
|
+
const evolveNames = [
|
|
6325
|
+
...continuingMaps.map(m => m.ExternalObjectName).filter(n => !cascadeLower.has(n.toLowerCase())),
|
|
6326
|
+
...newObjects,
|
|
6327
|
+
];
|
|
6328
|
+
if (evolveNames.length === 0) {
|
|
6329
|
+
const removedTotal = removedObjects.length + cascadeRemoved.length;
|
|
6043
6330
|
return {
|
|
6044
|
-
Success:
|
|
6045
|
-
|
|
6331
|
+
Success: true, HasChanges: removedTotal > 0,
|
|
6332
|
+
Message: removedTotal > 0
|
|
6333
|
+
? `No objects to evolve — ${removedTotal} removed object(s) disabled${cascadeRemoved.length > 0 ? ` (${cascadeRemoved.length} via cascade)` : ''}`
|
|
6334
|
+
: 'No entity maps and no resolved objects — nothing to evolve',
|
|
6335
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6336
|
+
CascadeRemovedObjects: cascadeRemoved.length > 0 ? cascadeRemoved : undefined,
|
|
6337
|
+
Warnings: dagRemovalWarnings.length > 0 ? dagRemovalWarnings : undefined,
|
|
6046
6338
|
};
|
|
6047
6339
|
}
|
|
6048
|
-
const
|
|
6049
|
-
|
|
6050
|
-
//
|
|
6340
|
+
const objects = this.buildObjectInputsFromNames(evolveNames, schemaName);
|
|
6341
|
+
// Build ExistingTables from Metadata.Entities matching the schema (continuing objects
|
|
6342
|
+
// have a table; new objects don't — SchemaBuilder CREATEs those, ALTERs the rest)
|
|
6051
6343
|
const existingTables = [];
|
|
6052
6344
|
for (const obj of objects) {
|
|
6053
6345
|
const entityInfo = md.Entities.find(e => e.SchemaName.toLowerCase() === schemaName.toLowerCase()
|
|
@@ -6067,12 +6359,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6067
6359
|
});
|
|
6068
6360
|
}
|
|
6069
6361
|
}
|
|
6070
|
-
//
|
|
6071
|
-
|
|
6072
|
-
const sourceSchema =
|
|
6362
|
+
// Source of truth for the evolution = the PERSISTED post-resolution rows (the refresh in
|
|
6363
|
+
// Phase 1 just wrote them via the overlay) — no duplicate vendor introspect.
|
|
6364
|
+
const sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID);
|
|
6073
6365
|
// Normalize names to match source schema casing
|
|
6074
6366
|
const nameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
|
|
6075
|
-
const normalizedNames =
|
|
6367
|
+
const normalizedNames = evolveNames.map(n => nameMap.get(n.toLowerCase()) ?? n);
|
|
6076
6368
|
// Also normalize the objects array SourceObjectName
|
|
6077
6369
|
for (const obj of objects) {
|
|
6078
6370
|
const exact = nameMap.get(obj.SourceObjectName.toLowerCase());
|
|
@@ -6083,7 +6375,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6083
6375
|
const filteredSchema = {
|
|
6084
6376
|
Objects: sourceSchema.Objects.filter(o => requestedNames.has(o.ExternalName))
|
|
6085
6377
|
};
|
|
6086
|
-
//
|
|
6378
|
+
// Build target configs and SchemaBuilder input with ExistingTables
|
|
6087
6379
|
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, validatedPlatform, connector);
|
|
6088
6380
|
const schemaInput = {
|
|
6089
6381
|
SourceSchema: filteredSchema,
|
|
@@ -6097,7 +6389,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6097
6389
|
ExistingTables: existingTables,
|
|
6098
6390
|
EntitySettingsForTargets: {}
|
|
6099
6391
|
};
|
|
6100
|
-
//
|
|
6392
|
+
// ── Phase 5 — build schema (SchemaBuilder CREATEs new tables, ALTERs changed ones) ──
|
|
6101
6393
|
const builder = new SchemaBuilder();
|
|
6102
6394
|
const schemaOutput = builder.BuildSchema(schemaInput);
|
|
6103
6395
|
if (schemaOutput.Errors.length > 0) {
|
|
@@ -6108,18 +6400,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6108
6400
|
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6109
6401
|
};
|
|
6110
6402
|
}
|
|
6111
|
-
//
|
|
6112
|
-
if (schemaOutput.MigrationFiles.length === 0) {
|
|
6113
|
-
return {
|
|
6114
|
-
Success: true,
|
|
6115
|
-
Message: 'No schema changes detected',
|
|
6116
|
-
HasChanges: false,
|
|
6117
|
-
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6118
|
-
};
|
|
6119
|
-
}
|
|
6120
|
-
// Step 7: Count added/modified columns by re-diffing
|
|
6403
|
+
// Per-object DDL diff for CHANGED detection (drives U10 watermark resets + reporting)
|
|
6121
6404
|
let addedColumns = 0;
|
|
6122
6405
|
let modifiedColumns = 0;
|
|
6406
|
+
const changedObjects = [];
|
|
6123
6407
|
const evolution = new SchemaEvolution();
|
|
6124
6408
|
for (const existing of existingTables) {
|
|
6125
6409
|
const config = targetConfigs.find(c => c.SchemaName.toLowerCase() === existing.SchemaName.toLowerCase()
|
|
@@ -6132,36 +6416,216 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6132
6416
|
const diff = evolution.DiffSchema(sourceObj, config, existing, validatedPlatform);
|
|
6133
6417
|
addedColumns += diff.AddedColumns.length;
|
|
6134
6418
|
modifiedColumns += diff.ModifiedColumns.length;
|
|
6419
|
+
if (diff.AddedColumns.length > 0 || diff.ModifiedColumns.length > 0) {
|
|
6420
|
+
changedObjects.push(config.SourceObjectName);
|
|
6421
|
+
}
|
|
6135
6422
|
}
|
|
6136
|
-
//
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
const
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6423
|
+
// ── Phase 6 — reconcile CONTINUING maps' field maps to the resolution ──
|
|
6424
|
+
// Added source fields get field maps (Active on enabled maps, Disabled on disabled
|
|
6425
|
+
// maps); field maps whose source field vanished from the resolution are disabled.
|
|
6426
|
+
// Field maps carry no entity FK, so this is restart-safe done here, pre-RSU.
|
|
6427
|
+
const iosByLowerName = new Map(activeIOs.map(io => [io.Name.toLowerCase(), io]));
|
|
6428
|
+
for (const em of continuingMaps) {
|
|
6429
|
+
const io = iosByLowerName.get((em.ExternalObjectName ?? '').toLowerCase());
|
|
6430
|
+
if (!io)
|
|
6431
|
+
continue;
|
|
6432
|
+
const fmChanges = await this.reconcileFieldMapsForEntityMap(em, io.ID, user, md);
|
|
6433
|
+
if ((fmChanges.Added > 0 || fmChanges.Disabled > 0) && em.ExternalObjectName && !changedObjects.includes(em.ExternalObjectName)) {
|
|
6434
|
+
changedObjects.push(em.ExternalObjectName);
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
// ── Phase 7 — U10: reset the Pull watermark of every CHANGED object so the next sync
|
|
6438
|
+
// full-fetches + backfills the schema change (content-hash bounds the rewrite cost).
|
|
6439
|
+
const changedLower = new Set(changedObjects.map(n => n.toLowerCase()));
|
|
6440
|
+
const changedMapIDs = continuingMaps
|
|
6441
|
+
.filter(m => m.ExternalObjectName && changedLower.has(m.ExternalObjectName.toLowerCase()))
|
|
6442
|
+
.map(m => m.ID);
|
|
6443
|
+
const resetMapIDs = await ResetPullWatermarks(changedMapIDs, user, md);
|
|
6444
|
+
const watermarksReset = continuingMaps
|
|
6445
|
+
.filter(m => resetMapIDs.some(id => UUIDsEqual(id, m.ID)))
|
|
6446
|
+
.map(m => m.ExternalObjectName);
|
|
6447
|
+
const hasDDLChanges = schemaOutput.MigrationFiles.length > 0;
|
|
6448
|
+
const hasChanges = hasDDLChanges || newObjects.length > 0 || removedObjects.length > 0 || changedObjects.length > 0;
|
|
6449
|
+
if (!hasChanges) {
|
|
6450
|
+
return {
|
|
6451
|
+
Success: true,
|
|
6452
|
+
Message: 'No schema changes detected',
|
|
6453
|
+
HasChanges: false,
|
|
6454
|
+
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6455
|
+
};
|
|
6456
|
+
}
|
|
6457
|
+
// ── Phase 8 — run RSU when DDL changed (migrate → codegen → commit → compile → restart) ──
|
|
6458
|
+
let pipelineSteps;
|
|
6459
|
+
let gitCommitSuccess;
|
|
6460
|
+
let apiRestarted;
|
|
6461
|
+
if (hasDDLChanges) {
|
|
6462
|
+
const rsuInput = builder.BuildRSUInput(schemaOutput, schemaInput, {
|
|
6463
|
+
SkipGitCommit: skipGitCommit,
|
|
6464
|
+
SkipRestart: skipRestart,
|
|
6465
|
+
// Schema evolution runs a FULL re-resolution of this connector's schema, so its
|
|
6466
|
+
// soft-constraint payload is the complete current truth — authorize the writer to
|
|
6467
|
+
// prune additionalSchemaInfo entries for tables that vanished from the resolution
|
|
6468
|
+
// (rsuplan: "adds new ones, removes old ones that no longer exist"). Subset
|
|
6469
|
+
// ApplyAll builds deliberately do NOT set this.
|
|
6470
|
+
AdditionalSchemaInfoAuthoritative: true,
|
|
6471
|
+
});
|
|
6472
|
+
// NEW objects need entities (post-codegen/restart) before their entity maps can be
|
|
6473
|
+
// created — hand them to the post-restart consumer via the SAME pending-work channel
|
|
6474
|
+
// ApplyAll uses. default: created DISABLED (user enables after the refresh);
|
|
6475
|
+
// autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
|
|
6476
|
+
if (newObjects.length > 0 && !skipRestart) {
|
|
6477
|
+
const { join } = await import('node:path');
|
|
6478
|
+
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
6479
|
+
const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
|
|
6480
|
+
rsuInput.PostRestartFiles = [
|
|
6481
|
+
...(rsuInput.PostRestartFiles ?? []),
|
|
6482
|
+
{
|
|
6483
|
+
Path: pendingFilePath,
|
|
6484
|
+
Content: JSON.stringify({
|
|
6485
|
+
CompanyIntegrationID: companyIntegrationID,
|
|
6486
|
+
SourceObjectNames: newObjects,
|
|
6487
|
+
SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
|
|
6488
|
+
SchemaName: schemaName,
|
|
6489
|
+
StartSync: false,
|
|
6490
|
+
SyncScope: 'created',
|
|
6491
|
+
UnselectedAction: 'ignore',
|
|
6492
|
+
CreateDisabled: !autoEnableNewObjects,
|
|
6493
|
+
CreatedAt: new Date().toISOString(),
|
|
6494
|
+
}, null, 2),
|
|
6495
|
+
},
|
|
6496
|
+
];
|
|
6497
|
+
}
|
|
6498
|
+
const rsm = RuntimeSchemaManager.Instance;
|
|
6499
|
+
const batchResult = await rsm.RunPipelineBatch([rsuInput]);
|
|
6500
|
+
const pipelineResult = batchResult.Results[0];
|
|
6501
|
+
pipelineSteps = pipelineResult?.Steps.map((s) => ({
|
|
6502
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
6503
|
+
}));
|
|
6504
|
+
gitCommitSuccess = pipelineResult?.GitCommitSuccess;
|
|
6505
|
+
apiRestarted = pipelineResult?.APIRestarted;
|
|
6506
|
+
if (!pipelineResult?.Success) {
|
|
6507
|
+
return {
|
|
6508
|
+
Success: false,
|
|
6509
|
+
Message: `Pipeline failed: ${pipelineResult?.ErrorMessage ?? 'unknown error'}`,
|
|
6510
|
+
HasChanges: true,
|
|
6511
|
+
AddedColumns: addedColumns,
|
|
6512
|
+
ModifiedColumns: modifiedColumns,
|
|
6513
|
+
Steps: pipelineSteps,
|
|
6514
|
+
NewObjects: newObjects.length > 0 ? newObjects : undefined,
|
|
6515
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6516
|
+
ChangedObjects: changedObjects.length > 0 ? changedObjects : undefined,
|
|
6517
|
+
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6518
|
+
};
|
|
6519
|
+
}
|
|
6520
|
+
// skipRestart=true: entities exist after provider.Refresh() — create the new
|
|
6521
|
+
// objects' entity maps inline, then apply the disabled-by-default rule.
|
|
6522
|
+
if (newObjects.length > 0 && skipRestart) {
|
|
6523
|
+
await md.Refresh();
|
|
6524
|
+
const newObjectInputs = objects.filter(o => newObjects.some(n => n.toLowerCase() === o.SourceObjectName.toLowerCase()));
|
|
6525
|
+
const created = await this.createEntityAndFieldMaps(companyIntegrationID, newObjectInputs, connector, companyIntegration, schemaName, user, md, 'Pull');
|
|
6526
|
+
if (!autoEnableNewObjects) {
|
|
6527
|
+
for (const c of created) {
|
|
6528
|
+
if (c.EntityMapID)
|
|
6529
|
+
await SetEntityMapEnabled(c.EntityMapID, false, user, md);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
const summary = [
|
|
6535
|
+
newObjects.length > 0 ? `${newObjects.length} new object(s) (${autoEnableNewObjects ? 'enabled' : 'created disabled'})` : null,
|
|
6536
|
+
removedObjects.length > 0 ? `${removedObjects.length} removed object(s) disabled` : null,
|
|
6537
|
+
changedObjects.length > 0 ? `${changedObjects.length} changed object(s), ${addedColumns} column(s) added, ${modifiedColumns} modified` : null,
|
|
6538
|
+
watermarksReset.length > 0 ? `${watermarksReset.length} watermark(s) reset for backfill` : null,
|
|
6539
|
+
].filter(Boolean).join('; ');
|
|
6147
6540
|
return {
|
|
6148
|
-
Success:
|
|
6149
|
-
Message:
|
|
6150
|
-
? `Schema evolution applied — ${addedColumns} column(s) added, ${modifiedColumns} column(s) modified`
|
|
6151
|
-
: `Pipeline failed: ${pipelineResult?.ErrorMessage ?? 'unknown error'}`,
|
|
6541
|
+
Success: true,
|
|
6542
|
+
Message: `Schema evolution applied — ${summary || 'no per-object deltas'}`,
|
|
6152
6543
|
HasChanges: true,
|
|
6153
6544
|
AddedColumns: addedColumns,
|
|
6154
6545
|
ModifiedColumns: modifiedColumns,
|
|
6155
6546
|
Steps: pipelineSteps,
|
|
6156
|
-
GitCommitSuccess:
|
|
6157
|
-
APIRestarted:
|
|
6158
|
-
|
|
6547
|
+
GitCommitSuccess: gitCommitSuccess,
|
|
6548
|
+
APIRestarted: apiRestarted,
|
|
6549
|
+
NewObjects: newObjects.length > 0 ? newObjects : undefined,
|
|
6550
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6551
|
+
CascadeRemovedObjects: cascadeRemoved.length > 0 ? cascadeRemoved : undefined,
|
|
6552
|
+
ChangedObjects: changedObjects.length > 0 ? changedObjects : undefined,
|
|
6553
|
+
WatermarksReset: watermarksReset.length > 0 ? watermarksReset : undefined,
|
|
6554
|
+
Warnings: (dagRemovalWarnings.length + schemaOutput.Warnings.length) > 0
|
|
6555
|
+
? [...dagRemovalWarnings, ...schemaOutput.Warnings]
|
|
6556
|
+
: undefined,
|
|
6159
6557
|
};
|
|
6160
6558
|
}
|
|
6161
6559
|
catch (e) {
|
|
6162
6560
|
LogError(`IntegrationSchemaEvolution error: ${e}`);
|
|
6163
6561
|
return { Success: false, Message: this.formatError(e), HasChanges: false };
|
|
6164
6562
|
}
|
|
6563
|
+
finally {
|
|
6564
|
+
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
/**
|
|
6568
|
+
* Refresh diff (continuing objects): reconciles one entity map's field maps to the
|
|
6569
|
+
* post-resolution IOF set — source fields WITHOUT a field map get one created (Active when
|
|
6570
|
+
* the map is enabled, Disabled when it isn't), and field maps whose source field is no
|
|
6571
|
+
* longer Active in the resolution are DISABLED (never deleted — a later re-appearance
|
|
6572
|
+
* re-enables via the same reconciliation, since a disabled FM whose field returns flips
|
|
6573
|
+
* back with SetFieldMapsStatus on re-enable or is simply left disabled until then).
|
|
6574
|
+
*/
|
|
6575
|
+
async reconcileFieldMapsForEntityMap(em, integrationObjectID, user, md) {
|
|
6576
|
+
const result = { Added: 0, Disabled: 0 };
|
|
6577
|
+
const activeFields = IntegrationEngineBase.Instance
|
|
6578
|
+
.GetIntegrationObjectFields(integrationObjectID)
|
|
6579
|
+
.filter(f => f.Status === 'Active');
|
|
6580
|
+
const activeFieldNames = new Set(activeFields.map(f => f.Name.toLowerCase()));
|
|
6581
|
+
const mapEnabled = em.Status === 'Active' && em.SyncEnabled === true;
|
|
6582
|
+
const fms = await new RunView().RunView({
|
|
6583
|
+
EntityName: 'MJ: Company Integration Field Maps',
|
|
6584
|
+
ExtraFilter: `EntityMapID='${em.ID}'`,
|
|
6585
|
+
ResultType: 'entity_object',
|
|
6586
|
+
BypassCache: true,
|
|
6587
|
+
}, user);
|
|
6588
|
+
const existingByLower = new Map((fms.Success ? fms.Results : []).map(fm => [(fm.SourceFieldName ?? '').toLowerCase(), fm]));
|
|
6589
|
+
// Added source fields → new field maps (state follows the map's enabled state)
|
|
6590
|
+
for (const field of activeFields) {
|
|
6591
|
+
const existing = existingByLower.get(field.Name.toLowerCase());
|
|
6592
|
+
if (existing) {
|
|
6593
|
+
// Field is back in the resolution — re-enable a previously-disabled map row.
|
|
6594
|
+
if (existing.Status !== 'Active' && mapEnabled) {
|
|
6595
|
+
existing.Status = 'Active';
|
|
6596
|
+
if (await existing.Save())
|
|
6597
|
+
result.Added++; // counted as a change for reporting
|
|
6598
|
+
}
|
|
6599
|
+
continue;
|
|
6600
|
+
}
|
|
6601
|
+
const fm = await md.GetEntityObject('MJ: Company Integration Field Maps', user);
|
|
6602
|
+
fm.NewRecord();
|
|
6603
|
+
fm.EntityMapID = em.ID;
|
|
6604
|
+
fm.SourceFieldName = field.Name;
|
|
6605
|
+
fm.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
6606
|
+
fm.IsKeyField = field.IsPrimaryKey ?? false; // key on the PRIMARY key (identity), consistent with the initial-apply path + U1 (PK ≠ unique)
|
|
6607
|
+
fm.IsRequired = field.IsRequired === true;
|
|
6608
|
+
fm.Direction = 'SourceToDest';
|
|
6609
|
+
fm.Status = mapEnabled ? 'Active' : 'Inactive';
|
|
6610
|
+
fm.Priority = 0;
|
|
6611
|
+
if (await fm.Save())
|
|
6612
|
+
result.Added++;
|
|
6613
|
+
else
|
|
6614
|
+
LogError(`[IntegrationSchemaEvolution] Failed to create field map '${field.Name}' on ${em.ExternalObjectName}: ${fm.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
6615
|
+
}
|
|
6616
|
+
// Vanished source fields → disable their field maps (data/columns kept)
|
|
6617
|
+
for (const [lowerName, fm] of existingByLower) {
|
|
6618
|
+
if (activeFieldNames.has(lowerName))
|
|
6619
|
+
continue;
|
|
6620
|
+
if (fm.Status === 'Inactive')
|
|
6621
|
+
continue;
|
|
6622
|
+
fm.Status = 'Inactive';
|
|
6623
|
+
if (await fm.Save())
|
|
6624
|
+
result.Disabled++;
|
|
6625
|
+
else
|
|
6626
|
+
LogError(`[IntegrationSchemaEvolution] Failed to disable field map '${fm.SourceFieldName}' on ${em.ExternalObjectName}: ${fm.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
6627
|
+
}
|
|
6628
|
+
return result;
|
|
6165
6629
|
}
|
|
6166
6630
|
// ── WEBHOOK HELPER ──────────────────────────────────────────────────
|
|
6167
6631
|
async sendWebhook(url, payload) {
|
|
@@ -6611,9 +7075,12 @@ __decorate([
|
|
|
6611
7075
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
6612
7076
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|
|
6613
7077
|
__param(3, Arg("skipRestart", { defaultValue: false })),
|
|
6614
|
-
__param(4,
|
|
7078
|
+
__param(4, Arg("autoEnableNewObjects", { defaultValue: false, description: 'newly-appeared objects get their entity maps created DISABLED by default (the user enables them after the refresh). Pass true to auto-enable them instead.' })),
|
|
7079
|
+
__param(5, Arg("deactivateAbsent", { nullable: true, description: 'Deactivate IO/IOF absent from this re-discovery (default true — comprehensive refresh; gated on the connector\'s authoritative-discovery getter).' })),
|
|
7080
|
+
__param(6, Arg("cascadeRemoveDependents", { defaultValue: false, description: 'When a removed object has still-active dependents (DAG parent edges), also disable the transitive dependent closure ("force remove those too"). Default false: dependents stay active and each broken edge is surfaced as a warning.' })),
|
|
7081
|
+
__param(7, Ctx()),
|
|
6615
7082
|
__metadata("design:type", Function),
|
|
6616
|
-
__metadata("design:paramtypes", [String, String, Boolean, Boolean, Object]),
|
|
7083
|
+
__metadata("design:paramtypes", [String, String, Boolean, Boolean, Boolean, Boolean, Boolean, Object]),
|
|
6617
7084
|
__metadata("design:returntype", Promise)
|
|
6618
7085
|
], IntegrationDiscoveryResolver.prototype, "IntegrationSchemaEvolution", null);
|
|
6619
7086
|
IntegrationDiscoveryResolver = IntegrationDiscoveryResolver_1 = __decorate([
|