@memberjunction/server 5.48.0 → 5.49.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/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 +122 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +629 -0
- 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/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 +522 -60
- 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 +88 -88
- 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__/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/config.ts +14 -0
- package/src/context.ts +28 -2
- package/src/generated/generated.ts +446 -1
- 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/resolvers/AdhocQueryResolver.ts +97 -30
- package/src/resolvers/GetDataResolver.ts +30 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +514 -60
- 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,8 +4297,9 @@ 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 = [
|
|
@@ -4184,7 +4310,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4184
4310
|
const batchResult = await rsm.RunPipelineBatch([rsuInput]);
|
|
4185
4311
|
const migrationSucceeded = batchResult.SuccessCount > 0;
|
|
4186
4312
|
const pipelineSteps = batchResult.Results[0]?.Steps.map((s) => ({
|
|
4187
|
-
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message,
|
|
4313
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
4188
4314
|
}));
|
|
4189
4315
|
// If pipeline failed, clean up pending file and return error
|
|
4190
4316
|
if (!migrationSucceeded) {
|
|
@@ -4205,6 +4331,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4205
4331
|
if (skipRestart) {
|
|
4206
4332
|
await provider.Refresh();
|
|
4207
4333
|
const entityMapsCreated = await this.createEntityAndFieldMaps(input.CompanyIntegrationID, objects, connector, companyIntegration, schemaName, user, provider, input.DefaultSyncDirection ?? 'Pull');
|
|
4334
|
+
// Remove-as-disable: objects absent from THIS selection get their entity
|
|
4335
|
+
// map + field maps disabled (data kept; re-selection re-enables). 'ignore' opts out
|
|
4336
|
+
// for additive/subset applies (e.g. test-harness per-scope runs).
|
|
4337
|
+
if ((input.UnselectedAction ?? 'disable') !== 'ignore') {
|
|
4338
|
+
const disabledObjects = await DisableUnselectedEntityMaps(input.CompanyIntegrationID, resolvedNames, user, provider);
|
|
4339
|
+
if (disabledObjects.length > 0) {
|
|
4340
|
+
console.log(`[IntegrationApplyAll] Disabled ${disabledObjects.length} unselected entity map(s): ${disabledObjects.join(', ')}`);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4208
4343
|
const createdMapIDs = entityMapsCreated.map(em => em.EntityMapID).filter(Boolean);
|
|
4209
4344
|
const scopedMapIDs = input.SyncScope === 'all' ? undefined : createdMapIDs;
|
|
4210
4345
|
// Skip sync when SyncScope='created' but 0 new maps were
|
|
@@ -4214,7 +4349,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4214
4349
|
const shouldStartSync = input.StartSync !== false &&
|
|
4215
4350
|
(input.SyncScope === 'all' || createdMapIDs.length > 0);
|
|
4216
4351
|
const syncRunID = shouldStartSync
|
|
4217
|
-
? await this.startSyncAfterApply(input.CompanyIntegrationID, user, scopedMapIDs,
|
|
4352
|
+
? await this.startSyncAfterApply(input.CompanyIntegrationID, user, scopedMapIDs, effectiveFullSync)
|
|
4218
4353
|
: null;
|
|
4219
4354
|
// Create schedule if requested
|
|
4220
4355
|
let scheduledJobID;
|
|
@@ -4340,6 +4475,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4340
4475
|
existing = existingMapResult.Results[0];
|
|
4341
4476
|
}
|
|
4342
4477
|
let em;
|
|
4478
|
+
const wasDisabled = existing ? existing.Status !== 'Active' || existing.SyncEnabled === false : false;
|
|
4343
4479
|
if (existing) {
|
|
4344
4480
|
em = existing; // reuse — keeps the map stable across re-applies
|
|
4345
4481
|
}
|
|
@@ -4358,6 +4494,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4358
4494
|
LogError(`IntegrationApplyAll: failed to save entity map for ${obj.SourceObjectName}`);
|
|
4359
4495
|
return null;
|
|
4360
4496
|
}
|
|
4497
|
+
// Re-add re-enables: a previously-removed (disabled) object that is re-selected
|
|
4498
|
+
// comes back WITH its field maps — the create-field-maps idempotency below skips existing
|
|
4499
|
+
// rows, so without this they would stay Disabled forever.
|
|
4500
|
+
if (wasDisabled) {
|
|
4501
|
+
await ReenableFieldMapsForEntityMap(em.ID, user, md);
|
|
4502
|
+
}
|
|
4361
4503
|
// Discover fields from the source and create 1:1 field maps
|
|
4362
4504
|
const fieldMapCount = await this.createFieldMapsForEntityMap(em.ID, obj.SourceObjectName, connector, companyIntegration, user, md);
|
|
4363
4505
|
return {
|
|
@@ -4393,7 +4535,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4393
4535
|
fm.EntityMapID = entityMapID;
|
|
4394
4536
|
fm.SourceFieldName = field.Name;
|
|
4395
4537
|
fm.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
4396
|
-
fm.IsKeyField = field.
|
|
4538
|
+
fm.IsKeyField = field.IsPrimaryKey ?? false; // key on the record's PRIMARY key (identity), consistent with processRSUPendingWork + U1 (PK ≠ unique)
|
|
4397
4539
|
fm.IsRequired = field.IsRequired;
|
|
4398
4540
|
fm.Direction = 'SourceToDest';
|
|
4399
4541
|
fm.Status = 'Active';
|
|
@@ -4600,6 +4742,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4600
4742
|
if (await ciCheck.InnerLoad(CompositeKey.FromID(companyIntegrationID)) && ciCheck.IsActive === false) {
|
|
4601
4743
|
return { Success: false, Message: 'Connector is deactivated (IsActive=false); sync not started' };
|
|
4602
4744
|
}
|
|
4745
|
+
// sync lock: refuse while a metadata refresh / schema evolution / RSU pipeline
|
|
4746
|
+
// holds the maintenance lock for this connection (the engine also refuses — this gives
|
|
4747
|
+
// the client an immediate, unambiguous answer).
|
|
4748
|
+
const maintenance = IntegrationEngine.GetMaintenanceLock(companyIntegrationID);
|
|
4749
|
+
if (maintenance) {
|
|
4750
|
+
return { Success: false, Message: `Sync not started: ${maintenance.Reason} is in progress for this connection (since ${maintenance.AcquiredAt.toISOString()}). Retry after it completes.` };
|
|
4751
|
+
}
|
|
4603
4752
|
const syncOptions = {};
|
|
4604
4753
|
if (fullSync)
|
|
4605
4754
|
syncOptions.FullSync = true;
|
|
@@ -4648,10 +4797,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4648
4797
|
// null RunID: an optimistic, untrackable result that read as "started" when nothing
|
|
4649
4798
|
// could be followed. Poll briefly for ANY run for this connector stamped at/after the
|
|
4650
4799
|
// fire instant; that catches both the still-running and the already-finished cases.
|
|
4800
|
+
//
|
|
4801
|
+
// The row itself isn't created until executeSyncInternal reaches CreateRunRecord,
|
|
4802
|
+
// which runs AFTER LoadRunConfiguration (loads every entity map + field map + builds
|
|
4803
|
+
// the connector instance) — for a large connector (HubSpot, 168 entity maps) that
|
|
4804
|
+
// synchronous setup alone can take several seconds, so a short fixed poll window can
|
|
4805
|
+
// elapse before the row exists at all, not because the sync failed. Live-verified: a
|
|
4806
|
+
// HubSpot StartSync call reported "no run created" while the run (confirmed present in
|
|
4807
|
+
// the DB moments later) was genuinely syncing. Keep the fast path fast (first 2s still
|
|
4808
|
+
// polled every 200ms) but extend the tail so large connectors get a fair window before
|
|
4809
|
+
// we honestly report failure.
|
|
4651
4810
|
const firedAtFilter = firedAt.toISOString();
|
|
4652
4811
|
let run = null;
|
|
4653
|
-
|
|
4654
|
-
|
|
4812
|
+
const pollIntervalsMs = [...Array(10).fill(200), ...Array(20).fill(1000)];
|
|
4813
|
+
for (let attempt = 0; attempt < pollIntervalsMs.length && !run; attempt++) {
|
|
4814
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalsMs[attempt]));
|
|
4655
4815
|
const rv = new RunView();
|
|
4656
4816
|
const runResult = await rv.RunView({
|
|
4657
4817
|
EntityName: 'MJ: Company Integration Runs',
|
|
@@ -4797,8 +4957,20 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4797
4957
|
return { Success: false, Message: `Scheduled job type not found for driver '${driverClass}' (kind='${jobKind}')` };
|
|
4798
4958
|
}
|
|
4799
4959
|
const jobTypeID = jobTypeResult.Results[0].ID;
|
|
4960
|
+
// U7 — SERVER-SIDE uniqueness per (CompanyIntegration, job kind/driver): a client
|
|
4961
|
+
// name-regex is not a guarantee, and duplicate jobs mean duplicate sync/rediscovery
|
|
4962
|
+
// runs. When an active/paused job of this kind already targets this connection,
|
|
4963
|
+
// UPDATE it in place instead of creating a twin.
|
|
4800
4964
|
const job = await md.GetEntityObject('MJ: Scheduled Jobs', user);
|
|
4801
|
-
|
|
4965
|
+
const existingJob = await this.findExistingScheduledJob(jobTypeID, input.CompanyIntegrationID, user);
|
|
4966
|
+
let updatedInPlace = false;
|
|
4967
|
+
if (existingJob) {
|
|
4968
|
+
const loaded = await job.InnerLoad(CompositeKey.FromID(existingJob.ID));
|
|
4969
|
+
if (loaded)
|
|
4970
|
+
updatedInPlace = true;
|
|
4971
|
+
}
|
|
4972
|
+
if (!updatedInPlace)
|
|
4973
|
+
job.NewRecord();
|
|
4802
4974
|
job.JobTypeID = jobTypeID;
|
|
4803
4975
|
job.Name = input.Name;
|
|
4804
4976
|
if (input.Description)
|
|
@@ -4833,13 +5005,44 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
4833
5005
|
ci.CronExpression = input.CronExpression;
|
|
4834
5006
|
await ci.Save();
|
|
4835
5007
|
}
|
|
4836
|
-
return {
|
|
5008
|
+
return {
|
|
5009
|
+
Success: true,
|
|
5010
|
+
Message: updatedInPlace
|
|
5011
|
+
? 'Schedule updated — an active schedule of this kind already existed for this connection (server-side uniqueness)'
|
|
5012
|
+
: 'Schedule created',
|
|
5013
|
+
ScheduledJobID: job.ID,
|
|
5014
|
+
};
|
|
4837
5015
|
}
|
|
4838
5016
|
catch (e) {
|
|
4839
5017
|
LogError(`IntegrationCreateSchedule error: ${e}`);
|
|
4840
5018
|
return { Success: false, Message: this.formatError(e) };
|
|
4841
5019
|
}
|
|
4842
5020
|
}
|
|
5021
|
+
/**
|
|
5022
|
+
* U7 — server-side schedule uniqueness: finds an existing (non-deleted) scheduled job of the
|
|
5023
|
+
* given job type that targets the given CompanyIntegration (its Configuration JSON carries
|
|
5024
|
+
* CompanyIntegrationID). Matched case-insensitively across UUID casings (SQL Server upper /
|
|
5025
|
+
* PostgreSQL lower). Returns the newest match or null.
|
|
5026
|
+
*/
|
|
5027
|
+
async findExistingScheduledJob(jobTypeID, companyIntegrationID, user) {
|
|
5028
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
5029
|
+
const lower = esc(companyIntegrationID.toLowerCase());
|
|
5030
|
+
const upper = esc(companyIntegrationID.toUpperCase());
|
|
5031
|
+
const result = await new RunView().RunView({
|
|
5032
|
+
EntityName: 'MJ: Scheduled Jobs',
|
|
5033
|
+
ExtraFilter: `JobTypeID='${esc(jobTypeID)}' AND Status IN ('Active','Paused') AND ` +
|
|
5034
|
+
// Structured "CompanyIntegrationID":"<uuid>" predicate (same form as IntegrationListSchedules),
|
|
5035
|
+
// NOT a bare-UUID substring — so this CI's id can't false-match the same UUID appearing in
|
|
5036
|
+
// another config key (e.g. RelatedCompanyIntegrationID) and repurpose an unrelated job.
|
|
5037
|
+
`(Configuration LIKE '%"CompanyIntegrationID":"${lower}"%' OR Configuration LIKE '%"CompanyIntegrationID":"${upper}"%')`,
|
|
5038
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
5039
|
+
Fields: ['ID'],
|
|
5040
|
+
MaxRows: 1,
|
|
5041
|
+
ResultType: 'simple',
|
|
5042
|
+
BypassCache: true, // uniqueness must see committed jobs, not a stale filtered cache
|
|
5043
|
+
}, user);
|
|
5044
|
+
return result.Success && result.Results.length > 0 ? result.Results[0] : null;
|
|
5045
|
+
}
|
|
4843
5046
|
async IntegrationUpdateSchedule(scheduledJobID, cronExpression, timezone, name, ctx) {
|
|
4844
5047
|
try {
|
|
4845
5048
|
const user = this.getAuthenticatedUser(ctx);
|
|
@@ -5172,7 +5375,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5172
5375
|
OperationType: 'rsu',
|
|
5173
5376
|
IsRunning: true,
|
|
5174
5377
|
RSURunning: true,
|
|
5175
|
-
|
|
5378
|
+
// U11 — determinate "step N of M: <name>" instead of an indeterminate spinner
|
|
5379
|
+
RSUStep: rsuStatus.CurrentStepName ?? rsuStatus.LastRunResult ?? 'running',
|
|
5380
|
+
RSUStepIndex: rsuStatus.CurrentStepIndex ?? undefined,
|
|
5381
|
+
RSUStepTotal: rsuStatus.StepTotal ?? undefined,
|
|
5176
5382
|
StartedAt: rsuStatus.LastRunAt?.toISOString(),
|
|
5177
5383
|
ElapsedMs: rsuStatus.LastRunAt ? Date.now() - rsuStatus.LastRunAt.getTime() : undefined,
|
|
5178
5384
|
};
|
|
@@ -5652,6 +5858,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5652
5858
|
SyncScope: input.SyncScope ?? 'created',
|
|
5653
5859
|
SyncDirection: input.SyncDirection,
|
|
5654
5860
|
ScheduleSyncDirection: input.ScheduleSyncDirection,
|
|
5861
|
+
UnselectedAction: (input.UnselectedAction ?? 'disable'),
|
|
5655
5862
|
CreatedAt: new Date().toISOString(),
|
|
5656
5863
|
};
|
|
5657
5864
|
rsuInput.PostRestartFiles = [
|
|
@@ -5763,7 +5970,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5763
5970
|
connectorResults.push(connResult);
|
|
5764
5971
|
}
|
|
5765
5972
|
const pipelineSteps = batchResult.Results[0]?.Steps.map((s) => ({
|
|
5766
|
-
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message,
|
|
5973
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
5767
5974
|
}));
|
|
5768
5975
|
const successCount = connectorResults.filter(r => r.Success).length;
|
|
5769
5976
|
const failureCount = connectorResults.filter(r => !r.Success).length;
|
|
@@ -5804,8 +6011,13 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
5804
6011
|
return null;
|
|
5805
6012
|
}
|
|
5806
6013
|
const jobTypeID = jobTypeResult.Results[0].ID;
|
|
6014
|
+
// U7 — same server-side uniqueness as IntegrationCreateSchedule: update the existing
|
|
6015
|
+
// sync job for this connection in place rather than creating a duplicate.
|
|
5807
6016
|
const job = await md.GetEntityObject('MJ: Scheduled Jobs', user);
|
|
5808
|
-
|
|
6017
|
+
const existingJob = await this.findExistingScheduledJob(jobTypeID, companyIntegrationID, user);
|
|
6018
|
+
const loadedExisting = existingJob ? await job.InnerLoad(CompositeKey.FromID(existingJob.ID)) : false;
|
|
6019
|
+
if (!loadedExisting)
|
|
6020
|
+
job.NewRecord();
|
|
5809
6021
|
job.JobTypeID = jobTypeID;
|
|
5810
6022
|
job.Name = `${integrationName} Sync`;
|
|
5811
6023
|
job.CronExpression = cronExpression;
|
|
@@ -6024,7 +6236,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6024
6236
|
* applies ALTER TABLE migrations via the RSU pipeline.
|
|
6025
6237
|
* Compares the current connector introspection against existing MJ entities.
|
|
6026
6238
|
*/
|
|
6027
|
-
async IntegrationSchemaEvolution(companyIntegrationID, platform, skipGitCommit, skipRestart, ctx) {
|
|
6239
|
+
async IntegrationSchemaEvolution(companyIntegrationID, platform, skipGitCommit, skipRestart, autoEnableNewObjects, deactivateAbsent, cascadeRemoveDependents, ctx) {
|
|
6240
|
+
// sync lock: schema evolution rewrites the metadata, field maps and DDL a sync
|
|
6241
|
+
// reads — data syncs (manual + scheduled) must not run while this is in flight.
|
|
6242
|
+
if (!IntegrationEngine.AcquireMaintenanceLock(companyIntegrationID, 'schema evolution')) {
|
|
6243
|
+
return {
|
|
6244
|
+
Success: false, HasChanges: false,
|
|
6245
|
+
Message: 'Schema evolution not started: a sync or another maintenance operation is currently running for this connection. Retry after it completes.',
|
|
6246
|
+
};
|
|
6247
|
+
}
|
|
6028
6248
|
try {
|
|
6029
6249
|
const user = this.getAuthenticatedUser(ctx);
|
|
6030
6250
|
const validatedPlatform = this.validatePlatform(platform);
|
|
@@ -6032,22 +6252,89 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6032
6252
|
const { connector, companyIntegration } = await this.resolveConnector(companyIntegrationID, user, md);
|
|
6033
6253
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
6034
6254
|
const rv = new RunView();
|
|
6035
|
-
//
|
|
6036
|
-
|
|
6255
|
+
// ── Phase 1 — FULL RE-RESOLUTION (: "the exact same algorithm as before") ──
|
|
6256
|
+
// Re-run the creation pipeline: introspect (static + runtime discovery), persist with
|
|
6257
|
+
// the external-wins-when-present overlay, deactivate absent (gated on authoritative
|
|
6258
|
+
// discovery), PK-classify the residual. The diff below is computed against the
|
|
6259
|
+
// RESULT of this resolution, exactly as the first-time flow computed its state.
|
|
6260
|
+
const pipeline = new IntegrationConnectorCreationPipeline();
|
|
6261
|
+
const refresh = await pipeline.Run({
|
|
6262
|
+
Connector: connector,
|
|
6263
|
+
CompanyIntegration: companyIntegration,
|
|
6264
|
+
ContextUser: user,
|
|
6265
|
+
Provider: md,
|
|
6266
|
+
TriggerType: 'Manual',
|
|
6267
|
+
ConsoleMirror: true,
|
|
6268
|
+
DeactivateAbsent: deactivateAbsent ?? this.readConfigBool(companyIntegration.Configuration, 'deactivateAbsent') ?? true,
|
|
6269
|
+
});
|
|
6270
|
+
if (!refresh.Success) {
|
|
6271
|
+
return { Success: false, HasChanges: false, Message: `Schema evolution aborted — metadata re-resolution failed: ${refresh.FailureMessage ?? 'unknown error'}` };
|
|
6272
|
+
}
|
|
6273
|
+
await md.Refresh();
|
|
6274
|
+
await IntegrationEngine.Instance.Config(true, user, md);
|
|
6275
|
+
// ── Phase 2 — DIFF: resolved-Active objects vs prior entity maps ──
|
|
6276
|
+
const activeIOs = IntegrationEngineBase.Instance.GetActiveIntegrationObjects(companyIntegration.IntegrationID);
|
|
6277
|
+
const activeNamesLower = new Set(activeIOs.map(o => o.Name.toLowerCase()));
|
|
6278
|
+
const priorMapsResult = await rv.RunView({
|
|
6037
6279
|
EntityName: 'MJ: Company Integration Entity Maps',
|
|
6038
6280
|
ExtraFilter: `CompanyIntegrationID='${companyIntegrationID}'`,
|
|
6039
|
-
ResultType: '
|
|
6040
|
-
|
|
6281
|
+
ResultType: 'entity_object',
|
|
6282
|
+
BypassCache: true, // the diff must read committed state
|
|
6041
6283
|
}, user);
|
|
6042
|
-
|
|
6284
|
+
const priorMaps = priorMapsResult.Success ? priorMapsResult.Results : [];
|
|
6285
|
+
const priorByLowerName = new Map(priorMaps.map(m => [(m.ExternalObjectName ?? '').toLowerCase(), m]));
|
|
6286
|
+
const newObjects = activeIOs.filter(io => !priorByLowerName.has(io.Name.toLowerCase())).map(io => io.Name);
|
|
6287
|
+
const removedObjects = priorMaps
|
|
6288
|
+
.filter(m => m.ExternalObjectName && !activeNamesLower.has(m.ExternalObjectName.toLowerCase()))
|
|
6289
|
+
.map(m => m.ExternalObjectName);
|
|
6290
|
+
const continuingMaps = priorMaps.filter(m => m.ExternalObjectName && activeNamesLower.has(m.ExternalObjectName.toLowerCase()));
|
|
6291
|
+
// ── Phase 3 — REMOVED objects: disable EM + EFMs (data + tables KEPT) ──
|
|
6292
|
+
// Uses the same remove-as-disable helper as ApplyAll: everything not in the resolved
|
|
6293
|
+
// set is disabled; a later re-appearance re-enables (reactivate-on-rediscover).
|
|
6294
|
+
// DAG handling (rsuplan "adding and removing tables"): dependents of a removed object
|
|
6295
|
+
// are NOT auto-removed by default — each broken parent edge is surfaced as a warning so
|
|
6296
|
+
// a consumer can prompt the user. With cascadeRemoveDependents=true (the "force remove
|
|
6297
|
+
// those too" affordance), the TRANSITIVE closure of still-active dependents is disabled
|
|
6298
|
+
// alongside the removed objects (still remove-as-disable, reversible). Edges come from
|
|
6299
|
+
// the same dependency map the sync engine layers by.
|
|
6300
|
+
const depMap = removedObjects.length > 0
|
|
6301
|
+
? this.computeObjectDependencyNames(companyIntegration.IntegrationID)
|
|
6302
|
+
: new Map();
|
|
6303
|
+
const cascadeRemoved = cascadeRemoveDependents && removedObjects.length > 0
|
|
6304
|
+
? ComputeCascadeRemovalSet(activeIOs.map(io => io.Name), removedObjects, depMap)
|
|
6305
|
+
: [];
|
|
6306
|
+
const cascadeLower = new Set(cascadeRemoved.map(n => n.toLowerCase()));
|
|
6307
|
+
const keepNames = activeIOs.map(io => io.Name).filter(n => !cascadeLower.has(n.toLowerCase()));
|
|
6308
|
+
const dagRemovalWarnings = removedObjects.length > 0 && !cascadeRemoveDependents
|
|
6309
|
+
? ComputeRemovedDependencyWarnings(keepNames, removedObjects, depMap)
|
|
6310
|
+
: [];
|
|
6311
|
+
for (const w of dagRemovalWarnings)
|
|
6312
|
+
LogStatus(`[SchemaEvolution][DAG] ${w}`);
|
|
6313
|
+
if (cascadeRemoved.length > 0)
|
|
6314
|
+
LogStatus(`[SchemaEvolution][DAG] cascade-removing ${cascadeRemoved.length} dependent object(s): ${cascadeRemoved.join(', ')}`);
|
|
6315
|
+
if (removedObjects.length > 0 || cascadeRemoved.length > 0) {
|
|
6316
|
+
await DisableUnselectedEntityMaps(companyIntegrationID, keepNames, user, md);
|
|
6317
|
+
}
|
|
6318
|
+
// ── Phase 4 — schema build over continuing + new objects (minus cascade-removed) ──
|
|
6319
|
+
const evolveNames = [
|
|
6320
|
+
...continuingMaps.map(m => m.ExternalObjectName).filter(n => !cascadeLower.has(n.toLowerCase())),
|
|
6321
|
+
...newObjects,
|
|
6322
|
+
];
|
|
6323
|
+
if (evolveNames.length === 0) {
|
|
6324
|
+
const removedTotal = removedObjects.length + cascadeRemoved.length;
|
|
6043
6325
|
return {
|
|
6044
|
-
Success:
|
|
6045
|
-
|
|
6326
|
+
Success: true, HasChanges: removedTotal > 0,
|
|
6327
|
+
Message: removedTotal > 0
|
|
6328
|
+
? `No objects to evolve — ${removedTotal} removed object(s) disabled${cascadeRemoved.length > 0 ? ` (${cascadeRemoved.length} via cascade)` : ''}`
|
|
6329
|
+
: 'No entity maps and no resolved objects — nothing to evolve',
|
|
6330
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6331
|
+
CascadeRemovedObjects: cascadeRemoved.length > 0 ? cascadeRemoved : undefined,
|
|
6332
|
+
Warnings: dagRemovalWarnings.length > 0 ? dagRemovalWarnings : undefined,
|
|
6046
6333
|
};
|
|
6047
6334
|
}
|
|
6048
|
-
const
|
|
6049
|
-
|
|
6050
|
-
//
|
|
6335
|
+
const objects = this.buildObjectInputsFromNames(evolveNames, schemaName);
|
|
6336
|
+
// Build ExistingTables from Metadata.Entities matching the schema (continuing objects
|
|
6337
|
+
// have a table; new objects don't — SchemaBuilder CREATEs those, ALTERs the rest)
|
|
6051
6338
|
const existingTables = [];
|
|
6052
6339
|
for (const obj of objects) {
|
|
6053
6340
|
const entityInfo = md.Entities.find(e => e.SchemaName.toLowerCase() === schemaName.toLowerCase()
|
|
@@ -6067,12 +6354,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6067
6354
|
});
|
|
6068
6355
|
}
|
|
6069
6356
|
}
|
|
6070
|
-
//
|
|
6071
|
-
|
|
6072
|
-
const sourceSchema =
|
|
6357
|
+
// Source of truth for the evolution = the PERSISTED post-resolution rows (the refresh in
|
|
6358
|
+
// Phase 1 just wrote them via the overlay) — no duplicate vendor introspect.
|
|
6359
|
+
const sourceSchema = this.buildSourceSchemaFromPersistedRows(companyIntegration.IntegrationID);
|
|
6073
6360
|
// Normalize names to match source schema casing
|
|
6074
6361
|
const nameMap = new Map(sourceSchema.Objects.map(o => [o.ExternalName.toLowerCase(), o.ExternalName]));
|
|
6075
|
-
const normalizedNames =
|
|
6362
|
+
const normalizedNames = evolveNames.map(n => nameMap.get(n.toLowerCase()) ?? n);
|
|
6076
6363
|
// Also normalize the objects array SourceObjectName
|
|
6077
6364
|
for (const obj of objects) {
|
|
6078
6365
|
const exact = nameMap.get(obj.SourceObjectName.toLowerCase());
|
|
@@ -6083,7 +6370,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6083
6370
|
const filteredSchema = {
|
|
6084
6371
|
Objects: sourceSchema.Objects.filter(o => requestedNames.has(o.ExternalName))
|
|
6085
6372
|
};
|
|
6086
|
-
//
|
|
6373
|
+
// Build target configs and SchemaBuilder input with ExistingTables
|
|
6087
6374
|
const targetConfigs = this.buildTargetConfigs(objects, filteredSchema, validatedPlatform, connector);
|
|
6088
6375
|
const schemaInput = {
|
|
6089
6376
|
SourceSchema: filteredSchema,
|
|
@@ -6097,7 +6384,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6097
6384
|
ExistingTables: existingTables,
|
|
6098
6385
|
EntitySettingsForTargets: {}
|
|
6099
6386
|
};
|
|
6100
|
-
//
|
|
6387
|
+
// ── Phase 5 — build schema (SchemaBuilder CREATEs new tables, ALTERs changed ones) ──
|
|
6101
6388
|
const builder = new SchemaBuilder();
|
|
6102
6389
|
const schemaOutput = builder.BuildSchema(schemaInput);
|
|
6103
6390
|
if (schemaOutput.Errors.length > 0) {
|
|
@@ -6108,18 +6395,10 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6108
6395
|
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6109
6396
|
};
|
|
6110
6397
|
}
|
|
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
|
|
6398
|
+
// Per-object DDL diff for CHANGED detection (drives U10 watermark resets + reporting)
|
|
6121
6399
|
let addedColumns = 0;
|
|
6122
6400
|
let modifiedColumns = 0;
|
|
6401
|
+
const changedObjects = [];
|
|
6123
6402
|
const evolution = new SchemaEvolution();
|
|
6124
6403
|
for (const existing of existingTables) {
|
|
6125
6404
|
const config = targetConfigs.find(c => c.SchemaName.toLowerCase() === existing.SchemaName.toLowerCase()
|
|
@@ -6132,36 +6411,216 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
6132
6411
|
const diff = evolution.DiffSchema(sourceObj, config, existing, validatedPlatform);
|
|
6133
6412
|
addedColumns += diff.AddedColumns.length;
|
|
6134
6413
|
modifiedColumns += diff.ModifiedColumns.length;
|
|
6414
|
+
if (diff.AddedColumns.length > 0 || diff.ModifiedColumns.length > 0) {
|
|
6415
|
+
changedObjects.push(config.SourceObjectName);
|
|
6416
|
+
}
|
|
6135
6417
|
}
|
|
6136
|
-
//
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
const
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6418
|
+
// ── Phase 6 — reconcile CONTINUING maps' field maps to the resolution ──
|
|
6419
|
+
// Added source fields get field maps (Active on enabled maps, Disabled on disabled
|
|
6420
|
+
// maps); field maps whose source field vanished from the resolution are disabled.
|
|
6421
|
+
// Field maps carry no entity FK, so this is restart-safe done here, pre-RSU.
|
|
6422
|
+
const iosByLowerName = new Map(activeIOs.map(io => [io.Name.toLowerCase(), io]));
|
|
6423
|
+
for (const em of continuingMaps) {
|
|
6424
|
+
const io = iosByLowerName.get((em.ExternalObjectName ?? '').toLowerCase());
|
|
6425
|
+
if (!io)
|
|
6426
|
+
continue;
|
|
6427
|
+
const fmChanges = await this.reconcileFieldMapsForEntityMap(em, io.ID, user, md);
|
|
6428
|
+
if ((fmChanges.Added > 0 || fmChanges.Disabled > 0) && em.ExternalObjectName && !changedObjects.includes(em.ExternalObjectName)) {
|
|
6429
|
+
changedObjects.push(em.ExternalObjectName);
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
// ── Phase 7 — U10: reset the Pull watermark of every CHANGED object so the next sync
|
|
6433
|
+
// full-fetches + backfills the schema change (content-hash bounds the rewrite cost).
|
|
6434
|
+
const changedLower = new Set(changedObjects.map(n => n.toLowerCase()));
|
|
6435
|
+
const changedMapIDs = continuingMaps
|
|
6436
|
+
.filter(m => m.ExternalObjectName && changedLower.has(m.ExternalObjectName.toLowerCase()))
|
|
6437
|
+
.map(m => m.ID);
|
|
6438
|
+
const resetMapIDs = await ResetPullWatermarks(changedMapIDs, user, md);
|
|
6439
|
+
const watermarksReset = continuingMaps
|
|
6440
|
+
.filter(m => resetMapIDs.some(id => UUIDsEqual(id, m.ID)))
|
|
6441
|
+
.map(m => m.ExternalObjectName);
|
|
6442
|
+
const hasDDLChanges = schemaOutput.MigrationFiles.length > 0;
|
|
6443
|
+
const hasChanges = hasDDLChanges || newObjects.length > 0 || removedObjects.length > 0 || changedObjects.length > 0;
|
|
6444
|
+
if (!hasChanges) {
|
|
6445
|
+
return {
|
|
6446
|
+
Success: true,
|
|
6447
|
+
Message: 'No schema changes detected',
|
|
6448
|
+
HasChanges: false,
|
|
6449
|
+
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6450
|
+
};
|
|
6451
|
+
}
|
|
6452
|
+
// ── Phase 8 — run RSU when DDL changed (migrate → codegen → commit → compile → restart) ──
|
|
6453
|
+
let pipelineSteps;
|
|
6454
|
+
let gitCommitSuccess;
|
|
6455
|
+
let apiRestarted;
|
|
6456
|
+
if (hasDDLChanges) {
|
|
6457
|
+
const rsuInput = builder.BuildRSUInput(schemaOutput, schemaInput, {
|
|
6458
|
+
SkipGitCommit: skipGitCommit,
|
|
6459
|
+
SkipRestart: skipRestart,
|
|
6460
|
+
// Schema evolution runs a FULL re-resolution of this connector's schema, so its
|
|
6461
|
+
// soft-constraint payload is the complete current truth — authorize the writer to
|
|
6462
|
+
// prune additionalSchemaInfo entries for tables that vanished from the resolution
|
|
6463
|
+
// (rsuplan: "adds new ones, removes old ones that no longer exist"). Subset
|
|
6464
|
+
// ApplyAll builds deliberately do NOT set this.
|
|
6465
|
+
AdditionalSchemaInfoAuthoritative: true,
|
|
6466
|
+
});
|
|
6467
|
+
// NEW objects need entities (post-codegen/restart) before their entity maps can be
|
|
6468
|
+
// created — hand them to the post-restart consumer via the SAME pending-work channel
|
|
6469
|
+
// ApplyAll uses. default: created DISABLED (user enables after the refresh);
|
|
6470
|
+
// autoEnableNewObjects opts in. Removal was already handled in Phase 3 → 'ignore'.
|
|
6471
|
+
if (newObjects.length > 0 && !skipRestart) {
|
|
6472
|
+
const { join } = await import('node:path');
|
|
6473
|
+
const rsuWorkDir = process.env.RSU_WORK_DIR || process.cwd();
|
|
6474
|
+
const pendingFilePath = join(rsuWorkDir, '.rsu_pending', `${Date.now()}-evolution.json`);
|
|
6475
|
+
rsuInput.PostRestartFiles = [
|
|
6476
|
+
...(rsuInput.PostRestartFiles ?? []),
|
|
6477
|
+
{
|
|
6478
|
+
Path: pendingFilePath,
|
|
6479
|
+
Content: JSON.stringify({
|
|
6480
|
+
CompanyIntegrationID: companyIntegrationID,
|
|
6481
|
+
SourceObjectNames: newObjects,
|
|
6482
|
+
SourceObjectFields: Object.fromEntries(newObjects.map(n => [n, null])),
|
|
6483
|
+
SchemaName: schemaName,
|
|
6484
|
+
StartSync: false,
|
|
6485
|
+
SyncScope: 'created',
|
|
6486
|
+
UnselectedAction: 'ignore',
|
|
6487
|
+
CreateDisabled: !autoEnableNewObjects,
|
|
6488
|
+
CreatedAt: new Date().toISOString(),
|
|
6489
|
+
}, null, 2),
|
|
6490
|
+
},
|
|
6491
|
+
];
|
|
6492
|
+
}
|
|
6493
|
+
const rsm = RuntimeSchemaManager.Instance;
|
|
6494
|
+
const batchResult = await rsm.RunPipelineBatch([rsuInput]);
|
|
6495
|
+
const pipelineResult = batchResult.Results[0];
|
|
6496
|
+
pipelineSteps = pipelineResult?.Steps.map((s) => ({
|
|
6497
|
+
Name: s.Name, Status: s.Status, DurationMs: s.DurationMs, Message: s.Message, StepIndex: s.StepIndex, StepTotal: s.StepTotal,
|
|
6498
|
+
}));
|
|
6499
|
+
gitCommitSuccess = pipelineResult?.GitCommitSuccess;
|
|
6500
|
+
apiRestarted = pipelineResult?.APIRestarted;
|
|
6501
|
+
if (!pipelineResult?.Success) {
|
|
6502
|
+
return {
|
|
6503
|
+
Success: false,
|
|
6504
|
+
Message: `Pipeline failed: ${pipelineResult?.ErrorMessage ?? 'unknown error'}`,
|
|
6505
|
+
HasChanges: true,
|
|
6506
|
+
AddedColumns: addedColumns,
|
|
6507
|
+
ModifiedColumns: modifiedColumns,
|
|
6508
|
+
Steps: pipelineSteps,
|
|
6509
|
+
NewObjects: newObjects.length > 0 ? newObjects : undefined,
|
|
6510
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6511
|
+
ChangedObjects: changedObjects.length > 0 ? changedObjects : undefined,
|
|
6512
|
+
Warnings: schemaOutput.Warnings.length > 0 ? schemaOutput.Warnings : undefined,
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
// skipRestart=true: entities exist after provider.Refresh() — create the new
|
|
6516
|
+
// objects' entity maps inline, then apply the disabled-by-default rule.
|
|
6517
|
+
if (newObjects.length > 0 && skipRestart) {
|
|
6518
|
+
await md.Refresh();
|
|
6519
|
+
const newObjectInputs = objects.filter(o => newObjects.some(n => n.toLowerCase() === o.SourceObjectName.toLowerCase()));
|
|
6520
|
+
const created = await this.createEntityAndFieldMaps(companyIntegrationID, newObjectInputs, connector, companyIntegration, schemaName, user, md, 'Pull');
|
|
6521
|
+
if (!autoEnableNewObjects) {
|
|
6522
|
+
for (const c of created) {
|
|
6523
|
+
if (c.EntityMapID)
|
|
6524
|
+
await SetEntityMapEnabled(c.EntityMapID, false, user, md);
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
const summary = [
|
|
6530
|
+
newObjects.length > 0 ? `${newObjects.length} new object(s) (${autoEnableNewObjects ? 'enabled' : 'created disabled'})` : null,
|
|
6531
|
+
removedObjects.length > 0 ? `${removedObjects.length} removed object(s) disabled` : null,
|
|
6532
|
+
changedObjects.length > 0 ? `${changedObjects.length} changed object(s), ${addedColumns} column(s) added, ${modifiedColumns} modified` : null,
|
|
6533
|
+
watermarksReset.length > 0 ? `${watermarksReset.length} watermark(s) reset for backfill` : null,
|
|
6534
|
+
].filter(Boolean).join('; ');
|
|
6147
6535
|
return {
|
|
6148
|
-
Success:
|
|
6149
|
-
Message:
|
|
6150
|
-
? `Schema evolution applied — ${addedColumns} column(s) added, ${modifiedColumns} column(s) modified`
|
|
6151
|
-
: `Pipeline failed: ${pipelineResult?.ErrorMessage ?? 'unknown error'}`,
|
|
6536
|
+
Success: true,
|
|
6537
|
+
Message: `Schema evolution applied — ${summary || 'no per-object deltas'}`,
|
|
6152
6538
|
HasChanges: true,
|
|
6153
6539
|
AddedColumns: addedColumns,
|
|
6154
6540
|
ModifiedColumns: modifiedColumns,
|
|
6155
6541
|
Steps: pipelineSteps,
|
|
6156
|
-
GitCommitSuccess:
|
|
6157
|
-
APIRestarted:
|
|
6158
|
-
|
|
6542
|
+
GitCommitSuccess: gitCommitSuccess,
|
|
6543
|
+
APIRestarted: apiRestarted,
|
|
6544
|
+
NewObjects: newObjects.length > 0 ? newObjects : undefined,
|
|
6545
|
+
RemovedObjects: removedObjects.length > 0 ? removedObjects : undefined,
|
|
6546
|
+
CascadeRemovedObjects: cascadeRemoved.length > 0 ? cascadeRemoved : undefined,
|
|
6547
|
+
ChangedObjects: changedObjects.length > 0 ? changedObjects : undefined,
|
|
6548
|
+
WatermarksReset: watermarksReset.length > 0 ? watermarksReset : undefined,
|
|
6549
|
+
Warnings: (dagRemovalWarnings.length + schemaOutput.Warnings.length) > 0
|
|
6550
|
+
? [...dagRemovalWarnings, ...schemaOutput.Warnings]
|
|
6551
|
+
: undefined,
|
|
6159
6552
|
};
|
|
6160
6553
|
}
|
|
6161
6554
|
catch (e) {
|
|
6162
6555
|
LogError(`IntegrationSchemaEvolution error: ${e}`);
|
|
6163
6556
|
return { Success: false, Message: this.formatError(e), HasChanges: false };
|
|
6164
6557
|
}
|
|
6558
|
+
finally {
|
|
6559
|
+
IntegrationEngine.ReleaseMaintenanceLock(companyIntegrationID);
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
/**
|
|
6563
|
+
* Refresh diff (continuing objects): reconciles one entity map's field maps to the
|
|
6564
|
+
* post-resolution IOF set — source fields WITHOUT a field map get one created (Active when
|
|
6565
|
+
* the map is enabled, Disabled when it isn't), and field maps whose source field is no
|
|
6566
|
+
* longer Active in the resolution are DISABLED (never deleted — a later re-appearance
|
|
6567
|
+
* re-enables via the same reconciliation, since a disabled FM whose field returns flips
|
|
6568
|
+
* back with SetFieldMapsStatus on re-enable or is simply left disabled until then).
|
|
6569
|
+
*/
|
|
6570
|
+
async reconcileFieldMapsForEntityMap(em, integrationObjectID, user, md) {
|
|
6571
|
+
const result = { Added: 0, Disabled: 0 };
|
|
6572
|
+
const activeFields = IntegrationEngineBase.Instance
|
|
6573
|
+
.GetIntegrationObjectFields(integrationObjectID)
|
|
6574
|
+
.filter(f => f.Status === 'Active');
|
|
6575
|
+
const activeFieldNames = new Set(activeFields.map(f => f.Name.toLowerCase()));
|
|
6576
|
+
const mapEnabled = em.Status === 'Active' && em.SyncEnabled === true;
|
|
6577
|
+
const fms = await new RunView().RunView({
|
|
6578
|
+
EntityName: 'MJ: Company Integration Field Maps',
|
|
6579
|
+
ExtraFilter: `EntityMapID='${em.ID}'`,
|
|
6580
|
+
ResultType: 'entity_object',
|
|
6581
|
+
BypassCache: true,
|
|
6582
|
+
}, user);
|
|
6583
|
+
const existingByLower = new Map((fms.Success ? fms.Results : []).map(fm => [(fm.SourceFieldName ?? '').toLowerCase(), fm]));
|
|
6584
|
+
// Added source fields → new field maps (state follows the map's enabled state)
|
|
6585
|
+
for (const field of activeFields) {
|
|
6586
|
+
const existing = existingByLower.get(field.Name.toLowerCase());
|
|
6587
|
+
if (existing) {
|
|
6588
|
+
// Field is back in the resolution — re-enable a previously-disabled map row.
|
|
6589
|
+
if (existing.Status !== 'Active' && mapEnabled) {
|
|
6590
|
+
existing.Status = 'Active';
|
|
6591
|
+
if (await existing.Save())
|
|
6592
|
+
result.Added++; // counted as a change for reporting
|
|
6593
|
+
}
|
|
6594
|
+
continue;
|
|
6595
|
+
}
|
|
6596
|
+
const fm = await md.GetEntityObject('MJ: Company Integration Field Maps', user);
|
|
6597
|
+
fm.NewRecord();
|
|
6598
|
+
fm.EntityMapID = em.ID;
|
|
6599
|
+
fm.SourceFieldName = field.Name;
|
|
6600
|
+
fm.DestinationFieldName = field.Name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
6601
|
+
fm.IsKeyField = field.IsPrimaryKey ?? false; // key on the PRIMARY key (identity), consistent with the initial-apply path + U1 (PK ≠ unique)
|
|
6602
|
+
fm.IsRequired = field.IsRequired === true;
|
|
6603
|
+
fm.Direction = 'SourceToDest';
|
|
6604
|
+
fm.Status = mapEnabled ? 'Active' : 'Inactive';
|
|
6605
|
+
fm.Priority = 0;
|
|
6606
|
+
if (await fm.Save())
|
|
6607
|
+
result.Added++;
|
|
6608
|
+
else
|
|
6609
|
+
LogError(`[IntegrationSchemaEvolution] Failed to create field map '${field.Name}' on ${em.ExternalObjectName}: ${fm.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
6610
|
+
}
|
|
6611
|
+
// Vanished source fields → disable their field maps (data/columns kept)
|
|
6612
|
+
for (const [lowerName, fm] of existingByLower) {
|
|
6613
|
+
if (activeFieldNames.has(lowerName))
|
|
6614
|
+
continue;
|
|
6615
|
+
if (fm.Status === 'Inactive')
|
|
6616
|
+
continue;
|
|
6617
|
+
fm.Status = 'Inactive';
|
|
6618
|
+
if (await fm.Save())
|
|
6619
|
+
result.Disabled++;
|
|
6620
|
+
else
|
|
6621
|
+
LogError(`[IntegrationSchemaEvolution] Failed to disable field map '${fm.SourceFieldName}' on ${em.ExternalObjectName}: ${fm.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
6622
|
+
}
|
|
6623
|
+
return result;
|
|
6165
6624
|
}
|
|
6166
6625
|
// ── WEBHOOK HELPER ──────────────────────────────────────────────────
|
|
6167
6626
|
async sendWebhook(url, payload) {
|
|
@@ -6611,9 +7070,12 @@ __decorate([
|
|
|
6611
7070
|
__param(1, Arg("platform", { defaultValue: "sqlserver" })),
|
|
6612
7071
|
__param(2, Arg("skipGitCommit", { defaultValue: false })),
|
|
6613
7072
|
__param(3, Arg("skipRestart", { defaultValue: false })),
|
|
6614
|
-
__param(4,
|
|
7073
|
+
__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.' })),
|
|
7074
|
+
__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).' })),
|
|
7075
|
+
__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.' })),
|
|
7076
|
+
__param(7, Ctx()),
|
|
6615
7077
|
__metadata("design:type", Function),
|
|
6616
|
-
__metadata("design:paramtypes", [String, String, Boolean, Boolean, Object]),
|
|
7078
|
+
__metadata("design:paramtypes", [String, String, Boolean, Boolean, Boolean, Boolean, Boolean, Object]),
|
|
6617
7079
|
__metadata("design:returntype", Promise)
|
|
6618
7080
|
], IntegrationDiscoveryResolver.prototype, "IntegrationSchemaEvolution", null);
|
|
6619
7081
|
IntegrationDiscoveryResolver = IntegrationDiscoveryResolver_1 = __decorate([
|