@memberjunction/server 5.27.0 → 5.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/generated.d.ts +627 -1
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2522 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +14 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +37 -3
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RestoreContextInput.d.ts +27 -0
- package/dist/generic/RestoreContextInput.d.ts.map +1 -0
- package/dist/generic/RestoreContextInput.js +39 -0
- package/dist/generic/RestoreContextInput.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -6
- package/dist/index.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +18 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +247 -22
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/package.json +66 -66
- package/src/generated/generated.ts +1890 -1
- package/src/generic/ResolverBase.ts +41 -4
- package/src/generic/RestoreContextInput.ts +32 -0
- package/src/index.ts +24 -7
- package/src/resolvers/IntegrationDiscoveryResolver.ts +224 -20
|
@@ -12,9 +12,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
var IntegrationDiscoveryResolver_1;
|
|
14
14
|
import { Resolver, Query, Mutation, Arg, Ctx, ObjectType, Field, InputType } from "type-graphql";
|
|
15
|
-
import { CompositeKey, Metadata, RunView, LogError } from "@memberjunction/core";
|
|
15
|
+
import { CompositeKey, LocalCacheManager, Metadata, RunView, LogError } from "@memberjunction/core";
|
|
16
16
|
import { CronExpressionHelper } from "@memberjunction/scheduling-engine";
|
|
17
|
-
import { ConnectorFactory, IntegrationEngine } from "@memberjunction/integration-engine";
|
|
17
|
+
import { ConnectorFactory, IntegrationEngine, IntegrationSchemaSync } from "@memberjunction/integration-engine";
|
|
18
18
|
import { SchemaBuilder, TypeMapper, SchemaEvolution } from "@memberjunction/integration-schema-builder";
|
|
19
19
|
import { RuntimeSchemaManager } from "@memberjunction/schema-engine";
|
|
20
20
|
import { ResolverBase } from "../generic/ResolverBase.js";
|
|
@@ -135,6 +135,10 @@ __decorate([
|
|
|
135
135
|
Field({ nullable: true, defaultValue: 'created', description: 'Sync scope: "created" = only newly created entity maps, "all" = all maps for the connector' }),
|
|
136
136
|
__metadata("design:type", String)
|
|
137
137
|
], ApplyAllInput.prototype, "SyncScope", void 0);
|
|
138
|
+
__decorate([
|
|
139
|
+
Field({ nullable: true, defaultValue: 'Pull', description: 'SyncDirection applied to all created entity maps: Pull | Push | Bidirectional. Defaults to Pull.' }),
|
|
140
|
+
__metadata("design:type", String)
|
|
141
|
+
], ApplyAllInput.prototype, "DefaultSyncDirection", void 0);
|
|
138
142
|
ApplyAllInput = __decorate([
|
|
139
143
|
InputType()
|
|
140
144
|
], ApplyAllInput);
|
|
@@ -269,6 +273,10 @@ __decorate([
|
|
|
269
273
|
Field({ nullable: true }),
|
|
270
274
|
__metadata("design:type", String)
|
|
271
275
|
], ApplyAllBatchConnectorInput.prototype, "ScheduleTimezone", void 0);
|
|
276
|
+
__decorate([
|
|
277
|
+
Field({ nullable: true, defaultValue: 'Pull', description: 'SyncDirection applied to all created entity maps for this connector: Pull | Push | Bidirectional. Defaults to Pull.' }),
|
|
278
|
+
__metadata("design:type", String)
|
|
279
|
+
], ApplyAllBatchConnectorInput.prototype, "DefaultSyncDirection", void 0);
|
|
272
280
|
ApplyAllBatchConnectorInput = __decorate([
|
|
273
281
|
InputType()
|
|
274
282
|
], ApplyAllBatchConnectorInput);
|
|
@@ -290,6 +298,14 @@ __decorate([
|
|
|
290
298
|
Field({ nullable: true, defaultValue: 'created', description: 'Sync scope: "created" = only newly created entity maps, "all" = all maps for the connector' }),
|
|
291
299
|
__metadata("design:type", String)
|
|
292
300
|
], ApplyAllBatchInput.prototype, "SyncScope", void 0);
|
|
301
|
+
__decorate([
|
|
302
|
+
Field({ nullable: true, description: 'Override sync direction for the initial sync: Pull | Push | Bidirectional. Defaults to entity map SyncDirection.' }),
|
|
303
|
+
__metadata("design:type", String)
|
|
304
|
+
], ApplyAllBatchInput.prototype, "SyncDirection", void 0);
|
|
305
|
+
__decorate([
|
|
306
|
+
Field({ nullable: true, description: 'Override sync direction stored in the created schedule: Pull | Push | Bidirectional.' }),
|
|
307
|
+
__metadata("design:type", String)
|
|
308
|
+
], ApplyAllBatchInput.prototype, "ScheduleSyncDirection", void 0);
|
|
293
309
|
ApplyAllBatchInput = __decorate([
|
|
294
310
|
InputType()
|
|
295
311
|
], ApplyAllBatchInput);
|
|
@@ -997,6 +1013,27 @@ __decorate([
|
|
|
997
1013
|
StartSyncOutput = __decorate([
|
|
998
1014
|
ObjectType()
|
|
999
1015
|
], StartSyncOutput);
|
|
1016
|
+
let WriteRecordOutput = class WriteRecordOutput {
|
|
1017
|
+
};
|
|
1018
|
+
__decorate([
|
|
1019
|
+
Field(),
|
|
1020
|
+
__metadata("design:type", Boolean)
|
|
1021
|
+
], WriteRecordOutput.prototype, "Success", void 0);
|
|
1022
|
+
__decorate([
|
|
1023
|
+
Field(),
|
|
1024
|
+
__metadata("design:type", String)
|
|
1025
|
+
], WriteRecordOutput.prototype, "Message", void 0);
|
|
1026
|
+
__decorate([
|
|
1027
|
+
Field({ nullable: true }),
|
|
1028
|
+
__metadata("design:type", String)
|
|
1029
|
+
], WriteRecordOutput.prototype, "ExternalID", void 0);
|
|
1030
|
+
__decorate([
|
|
1031
|
+
Field({ nullable: true }),
|
|
1032
|
+
__metadata("design:type", Number)
|
|
1033
|
+
], WriteRecordOutput.prototype, "StatusCode", void 0);
|
|
1034
|
+
WriteRecordOutput = __decorate([
|
|
1035
|
+
ObjectType()
|
|
1036
|
+
], WriteRecordOutput);
|
|
1000
1037
|
let CreateScheduleInput = class CreateScheduleInput {
|
|
1001
1038
|
};
|
|
1002
1039
|
__decorate([
|
|
@@ -1019,6 +1056,14 @@ __decorate([
|
|
|
1019
1056
|
Field({ nullable: true }),
|
|
1020
1057
|
__metadata("design:type", String)
|
|
1021
1058
|
], CreateScheduleInput.prototype, "Description", void 0);
|
|
1059
|
+
__decorate([
|
|
1060
|
+
Field({ nullable: true }),
|
|
1061
|
+
__metadata("design:type", String)
|
|
1062
|
+
], CreateScheduleInput.prototype, "SyncDirection", void 0);
|
|
1063
|
+
__decorate([
|
|
1064
|
+
Field({ nullable: true }),
|
|
1065
|
+
__metadata("design:type", Boolean)
|
|
1066
|
+
], CreateScheduleInput.prototype, "FullSync", void 0);
|
|
1022
1067
|
CreateScheduleInput = __decorate([
|
|
1023
1068
|
InputType()
|
|
1024
1069
|
], CreateScheduleInput);
|
|
@@ -1713,14 +1758,21 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
1713
1758
|
// DB entities may not have Description populated yet on first run,
|
|
1714
1759
|
// but the connector's GetIntegrationObjects() always has them.
|
|
1715
1760
|
const connectorDescriptions = this.buildDescriptionLookup(connector);
|
|
1716
|
-
|
|
1761
|
+
const results = [];
|
|
1762
|
+
for (const obj of objects) {
|
|
1717
1763
|
const sourceObj = sourceSchema.Objects.find(o => o.ExternalName.toLowerCase() === obj.SourceObjectName.toLowerCase());
|
|
1718
1764
|
const objDescriptions = connectorDescriptions.get(obj.SourceObjectName.toLowerCase());
|
|
1765
|
+
// If the object wasn't discovered in IntrospectSchema (e.g. API error), skip it
|
|
1766
|
+
// rather than generating a broken table with no columns and a fallback PK.
|
|
1767
|
+
if (!sourceObj) {
|
|
1768
|
+
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — not found in source schema (IntrospectSchema may have failed for this object)`);
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1719
1771
|
// Filter fields if caller specified a subset
|
|
1720
1772
|
const selectedFieldSet = obj.Fields?.length
|
|
1721
1773
|
? new Set(obj.Fields.map(f => f.toLowerCase()))
|
|
1722
1774
|
: null;
|
|
1723
|
-
const sourceFields =
|
|
1775
|
+
const sourceFields = sourceObj.Fields.filter(f => !selectedFieldSet || selectedFieldSet.has(f.Name.toLowerCase()) || f.IsPrimaryKey);
|
|
1724
1776
|
const columns = sourceFields.map(f => ({
|
|
1725
1777
|
SourceFieldName: f.Name,
|
|
1726
1778
|
TargetColumnName: f.Name.replace(/[^A-Za-z0-9_]/g, '_'),
|
|
@@ -1732,20 +1784,34 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
1732
1784
|
DefaultValue: f.DefaultValue,
|
|
1733
1785
|
Description: f.Description ?? objDescriptions?.fields.get(f.Name.toLowerCase()),
|
|
1734
1786
|
}));
|
|
1735
|
-
const primaryKeyFields =
|
|
1787
|
+
const primaryKeyFields = sourceObj.Fields
|
|
1736
1788
|
.filter(f => f.IsPrimaryKey)
|
|
1737
1789
|
.map(f => f.Name.replace(/[^A-Za-z0-9_]/g, '_'));
|
|
1738
|
-
|
|
1790
|
+
// If no columns were discovered, skip rather than generating a broken table
|
|
1791
|
+
// (DDL with UNIQUE ([ID]) on a non-existent column will always fail).
|
|
1792
|
+
if (columns.length === 0 && primaryKeyFields.length === 0) {
|
|
1793
|
+
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — 0 fields discovered (live API likely failed and no DB-cached fields available)`);
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
// If columns exist but no PK was found, log diagnostic info and skip rather than
|
|
1797
|
+
// generating broken DDL with UNIQUE ([ID]) on a non-existent column.
|
|
1798
|
+
if (primaryKeyFields.length === 0 && columns.length > 0) {
|
|
1799
|
+
const fieldNames = sourceObj.Fields.map(f => `${f.Name}(pk=${f.IsPrimaryKey})`).join(', ');
|
|
1800
|
+
LogError(`[buildTargetConfigs] Skipping "${obj.SourceObjectName}" — ${columns.length} columns but NO primary key field found. Fields: [${fieldNames}]`);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
results.push({
|
|
1739
1804
|
SourceObjectName: obj.SourceObjectName,
|
|
1740
1805
|
SchemaName: obj.SchemaName,
|
|
1741
1806
|
TableName: obj.TableName,
|
|
1742
1807
|
EntityName: obj.EntityName,
|
|
1743
|
-
Description: sourceObj
|
|
1808
|
+
Description: sourceObj.Description ?? objDescriptions?.objectDescription,
|
|
1744
1809
|
Columns: columns,
|
|
1745
|
-
PrimaryKeyFields: primaryKeyFields
|
|
1810
|
+
PrimaryKeyFields: primaryKeyFields,
|
|
1746
1811
|
SoftForeignKeys: []
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
return results;
|
|
1749
1815
|
}
|
|
1750
1816
|
/** Builds a lookup of object name → { objectDescription, fields: fieldName → description } from the connector's static metadata. */
|
|
1751
1817
|
/** Build ExistingTableInfo[] from MJ Metadata for tables that already exist in the target schemas. */
|
|
@@ -2403,8 +2469,68 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2403
2469
|
// Step 1: Resolve connector and derive schema name
|
|
2404
2470
|
const { connector, companyIntegration } = await this.resolveConnector(input.CompanyIntegrationID, user);
|
|
2405
2471
|
const schemaName = this.deriveSchemaName(companyIntegration.Integration);
|
|
2406
|
-
// Step
|
|
2472
|
+
// Step 1b: Ensure IntegrationEngine cache is populated so IntrospectSchema's
|
|
2473
|
+
// DB fallback (GetCachedObject/GetCachedFields) can find IntegrationObject records
|
|
2474
|
+
await IntegrationEngine.Instance.Config(false, user);
|
|
2475
|
+
// Step 2: Introspect source schema and persist discovered objects/fields
|
|
2407
2476
|
const sourceSchema = await connector.IntrospectSchema.bind(connector)(companyIntegration, user);
|
|
2477
|
+
// Step 2b: Persist discovered objects/fields to IntegrationObject/IntegrationObjectField.
|
|
2478
|
+
// Static records (IsCustom=false) are preserved; new/custom records get IsCustom=true.
|
|
2479
|
+
// This ensures custom objects are available for future sync runs, action generation, etc.
|
|
2480
|
+
try {
|
|
2481
|
+
const persistResult = await IntegrationSchemaSync.PersistDiscoveredSchema({
|
|
2482
|
+
IntegrationID: companyIntegration.IntegrationID,
|
|
2483
|
+
SourceSchema: sourceSchema,
|
|
2484
|
+
ContextUser: user,
|
|
2485
|
+
});
|
|
2486
|
+
if (persistResult.ObjectsCreated > 0 || persistResult.FieldsCreated > 0) {
|
|
2487
|
+
console.log(`[IntegrationApplyAll] Persisted discovered schema: ` +
|
|
2488
|
+
`${persistResult.ObjectsCreated} new objects, ${persistResult.FieldsCreated} new fields, ` +
|
|
2489
|
+
`${persistResult.ObjectsUpdated} updated objects, ${persistResult.FieldsUpdated} updated fields`);
|
|
2490
|
+
}
|
|
2491
|
+
// Step 2c: Generate CRUD actions for newly discovered custom objects.
|
|
2492
|
+
// Uses the same ActionMetadataGenerator as the offline CLI, persisted via BaseEntity.Save().
|
|
2493
|
+
if (persistResult.ObjectsCreated > 0) {
|
|
2494
|
+
try {
|
|
2495
|
+
const engineObjects = IntegrationEngine.Instance
|
|
2496
|
+
.GetIntegrationObjectsByIntegrationID(companyIntegration.IntegrationID);
|
|
2497
|
+
const customObjects = sourceSchema.Objects
|
|
2498
|
+
.filter(o => !engineObjects
|
|
2499
|
+
.some(ex => ex.Name.toLowerCase() === o.ExternalName.toLowerCase() && !ex.IsCustom))
|
|
2500
|
+
.map(o => ({
|
|
2501
|
+
Name: o.ExternalName,
|
|
2502
|
+
DisplayName: o.ExternalLabel || o.ExternalName,
|
|
2503
|
+
Description: o.Description,
|
|
2504
|
+
SupportsWrite: false,
|
|
2505
|
+
Fields: o.Fields.map(f => ({
|
|
2506
|
+
Name: f.Name,
|
|
2507
|
+
DisplayName: f.Label || f.Name,
|
|
2508
|
+
Description: f.Description || '',
|
|
2509
|
+
Type: f.SourceType || 'string',
|
|
2510
|
+
IsRequired: f.IsRequired,
|
|
2511
|
+
IsReadOnly: false,
|
|
2512
|
+
IsPrimaryKey: f.IsPrimaryKey,
|
|
2513
|
+
})),
|
|
2514
|
+
}));
|
|
2515
|
+
await IntegrationSchemaSync.GenerateActionsForCustomObjects({
|
|
2516
|
+
IntegrationName: companyIntegration.Integration,
|
|
2517
|
+
CustomObjects: customObjects,
|
|
2518
|
+
SupportsSearch: connector.SupportsSearch,
|
|
2519
|
+
SupportsListing: connector.SupportsListing,
|
|
2520
|
+
ContextUser: user,
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
catch (actionErr) {
|
|
2524
|
+
const msg = actionErr instanceof Error ? actionErr.message : String(actionErr);
|
|
2525
|
+
console.warn(`[IntegrationApplyAll] Action generation warning (non-fatal): ${msg}`);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
catch (persistErr) {
|
|
2530
|
+
// Non-fatal: schema persistence failure should not block table creation
|
|
2531
|
+
const msg = persistErr instanceof Error ? persistErr.message : String(persistErr);
|
|
2532
|
+
console.warn(`[IntegrationApplyAll] Schema persistence warning (non-fatal): ${msg}`);
|
|
2533
|
+
}
|
|
2408
2534
|
const objectIDs = input.SourceObjects.map(so => so.SourceObjectID);
|
|
2409
2535
|
const resolvedNames = await this.resolveSourceObjectNames(objectIDs, undefined, sourceSchema, companyIntegration.IntegrationID, user);
|
|
2410
2536
|
// Build SchemaPreviewObjectInput with Fields carried from SourceObjectInput
|
|
@@ -2472,7 +2598,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2472
2598
|
// If skipRestart=true, we can do entity maps now.
|
|
2473
2599
|
if (skipRestart) {
|
|
2474
2600
|
await Metadata.Provider.Refresh();
|
|
2475
|
-
const entityMapsCreated = await this.createEntityAndFieldMaps(input.CompanyIntegrationID, objects, connector, companyIntegration, schemaName, user);
|
|
2601
|
+
const entityMapsCreated = await this.createEntityAndFieldMaps(input.CompanyIntegrationID, objects, connector, companyIntegration, schemaName, user, input.DefaultSyncDirection ?? 'Pull');
|
|
2476
2602
|
const createdMapIDs = entityMapsCreated.map(em => em.EntityMapID).filter(Boolean);
|
|
2477
2603
|
const scopedMapIDs = input.SyncScope === 'all' ? undefined : createdMapIDs;
|
|
2478
2604
|
const syncRunID = input.StartSync !== false
|
|
@@ -2539,11 +2665,11 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2539
2665
|
* After pipeline success, creates CompanyIntegrationEntityMap + CompanyIntegrationFieldMap
|
|
2540
2666
|
* records for each source object by matching schema + table name to newly created entities.
|
|
2541
2667
|
*/
|
|
2542
|
-
async createEntityAndFieldMaps(companyIntegrationID, objects, connector, companyIntegration, schemaName, user) {
|
|
2668
|
+
async createEntityAndFieldMaps(companyIntegrationID, objects, connector, companyIntegration, schemaName, user, defaultSyncDirection = 'Pull') {
|
|
2543
2669
|
const md = new Metadata();
|
|
2544
2670
|
const results = [];
|
|
2545
2671
|
for (const obj of objects) {
|
|
2546
|
-
const entityMapResult = await this.createSingleEntityMap(companyIntegrationID, obj, connector, companyIntegration, schemaName, user, md);
|
|
2672
|
+
const entityMapResult = await this.createSingleEntityMap(companyIntegrationID, obj, connector, companyIntegration, schemaName, user, md, defaultSyncDirection);
|
|
2547
2673
|
if (entityMapResult) {
|
|
2548
2674
|
results.push(entityMapResult);
|
|
2549
2675
|
}
|
|
@@ -2551,7 +2677,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2551
2677
|
return results;
|
|
2552
2678
|
}
|
|
2553
2679
|
/** Creates a single entity map + field maps for one source object. */
|
|
2554
|
-
async createSingleEntityMap(companyIntegrationID, obj, connector, companyIntegration, schemaName, user, md) {
|
|
2680
|
+
async createSingleEntityMap(companyIntegrationID, obj, connector, companyIntegration, schemaName, user, md, defaultSyncDirection = 'Pull') {
|
|
2555
2681
|
// Find the entity by schema + table name
|
|
2556
2682
|
const entityInfo = md.Entities.find(e => e.SchemaName.toLowerCase() === schemaName.toLowerCase()
|
|
2557
2683
|
&& e.BaseTable.toLowerCase() === obj.TableName.toLowerCase());
|
|
@@ -2565,8 +2691,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2565
2691
|
em.CompanyIntegrationID = companyIntegrationID;
|
|
2566
2692
|
em.ExternalObjectName = obj.SourceObjectName;
|
|
2567
2693
|
em.EntityID = entityInfo.ID;
|
|
2568
|
-
em.SyncDirection = 'Pull';
|
|
2569
|
-
em.Priority = 0;
|
|
2694
|
+
em.SyncDirection = isValidSyncDirection(defaultSyncDirection) ? defaultSyncDirection : 'Pull';
|
|
2695
|
+
em.Priority = obj.SourceObjectName.startsWith('assoc_') ? 10 : 0;
|
|
2570
2696
|
em.Status = 'Active';
|
|
2571
2697
|
em.SyncEnabled = true;
|
|
2572
2698
|
if (!await em.Save()) {
|
|
@@ -2687,7 +2813,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2687
2813
|
* Starts an async integration sync. Returns immediately with the run ID.
|
|
2688
2814
|
* Sends a webhook to the registered callback when complete.
|
|
2689
2815
|
*/
|
|
2690
|
-
async IntegrationStartSync(companyIntegrationID, webhookURL, fullSync, entityMapIDs, ctx) {
|
|
2816
|
+
async IntegrationStartSync(companyIntegrationID, webhookURL, fullSync, entityMapIDs, syncDirection, ctx) {
|
|
2691
2817
|
try {
|
|
2692
2818
|
const user = this.getAuthenticatedUser(ctx);
|
|
2693
2819
|
await IntegrationEngine.Instance.Config(false, user);
|
|
@@ -2696,6 +2822,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2696
2822
|
syncOptions.FullSync = true;
|
|
2697
2823
|
if (entityMapIDs?.length)
|
|
2698
2824
|
syncOptions.EntityMapIDs = entityMapIDs;
|
|
2825
|
+
if (syncDirection)
|
|
2826
|
+
syncOptions.SyncDirection = syncDirection;
|
|
2699
2827
|
// Fire and forget — progress is tracked inside IntegrationEngine
|
|
2700
2828
|
const syncPromise = IntegrationEngine.Instance.RunSync(companyIntegrationID, user, 'Manual', undefined, undefined, Object.keys(syncOptions).length > 0 ? syncOptions : undefined);
|
|
2701
2829
|
syncPromise
|
|
@@ -2765,6 +2893,74 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2765
2893
|
return { Success: false, Message: this.formatError(e) };
|
|
2766
2894
|
}
|
|
2767
2895
|
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Writes a single record to an external system via the integration connector.
|
|
2898
|
+
* Supports create, update, and delete operations.
|
|
2899
|
+
*/
|
|
2900
|
+
async IntegrationWriteRecord(companyIntegrationID, objectName, operation, externalID, attributesJson, ctx) {
|
|
2901
|
+
try {
|
|
2902
|
+
const user = this.getAuthenticatedUser(ctx);
|
|
2903
|
+
await IntegrationEngine.Instance.Config(false, user);
|
|
2904
|
+
const rv = new RunView();
|
|
2905
|
+
const ciResult = await rv.RunView({
|
|
2906
|
+
EntityName: 'MJ: Company Integrations',
|
|
2907
|
+
ExtraFilter: `ID='${companyIntegrationID}'`,
|
|
2908
|
+
MaxRows: 1,
|
|
2909
|
+
ResultType: 'entity_object',
|
|
2910
|
+
}, user);
|
|
2911
|
+
if (!ciResult.Success || ciResult.Results.length === 0) {
|
|
2912
|
+
return { Success: false, Message: `Company Integration not found: ${companyIntegrationID}` };
|
|
2913
|
+
}
|
|
2914
|
+
const companyIntegration = ciResult.Results[0];
|
|
2915
|
+
// Load the Integration entity to get the ClassName for connector resolution
|
|
2916
|
+
const integResult = await rv.RunView({
|
|
2917
|
+
EntityName: 'Integrations',
|
|
2918
|
+
ExtraFilter: `ID='${companyIntegration.IntegrationID}'`,
|
|
2919
|
+
MaxRows: 1,
|
|
2920
|
+
ResultType: 'entity_object',
|
|
2921
|
+
}, user);
|
|
2922
|
+
if (!integResult.Success || integResult.Results.length === 0) {
|
|
2923
|
+
return { Success: false, Message: `Integration not found: ${companyIntegration.IntegrationID}` };
|
|
2924
|
+
}
|
|
2925
|
+
const connector = ConnectorFactory.Resolve(integResult.Results[0]);
|
|
2926
|
+
const attributes = attributesJson ? JSON.parse(attributesJson) : {};
|
|
2927
|
+
const crudBase = { CompanyIntegration: companyIntegration, ObjectName: objectName, ContextUser: user };
|
|
2928
|
+
let result;
|
|
2929
|
+
switch (operation.toLowerCase()) {
|
|
2930
|
+
case 'create':
|
|
2931
|
+
if (!connector.SupportsCreate)
|
|
2932
|
+
return { Success: false, Message: 'Connector does not support create' };
|
|
2933
|
+
result = await connector.CreateRecord({ ...crudBase, Attributes: attributes });
|
|
2934
|
+
break;
|
|
2935
|
+
case 'update':
|
|
2936
|
+
if (!connector.SupportsUpdate)
|
|
2937
|
+
return { Success: false, Message: 'Connector does not support update' };
|
|
2938
|
+
if (!externalID)
|
|
2939
|
+
return { Success: false, Message: 'externalID is required for update' };
|
|
2940
|
+
result = await connector.UpdateRecord({ ...crudBase, ExternalID: externalID, Attributes: attributes });
|
|
2941
|
+
break;
|
|
2942
|
+
case 'delete':
|
|
2943
|
+
if (!connector.SupportsDelete)
|
|
2944
|
+
return { Success: false, Message: 'Connector does not support delete' };
|
|
2945
|
+
if (!externalID)
|
|
2946
|
+
return { Success: false, Message: 'externalID is required for delete' };
|
|
2947
|
+
result = await connector.DeleteRecord({ ...crudBase, ExternalID: externalID });
|
|
2948
|
+
break;
|
|
2949
|
+
default:
|
|
2950
|
+
return { Success: false, Message: `Invalid operation: ${operation}. Must be create, update, or delete` };
|
|
2951
|
+
}
|
|
2952
|
+
return {
|
|
2953
|
+
Success: result.Success,
|
|
2954
|
+
Message: result.Success ? `${operation} succeeded` : (result.ErrorMessage ?? `${operation} failed`),
|
|
2955
|
+
ExternalID: result.ExternalID,
|
|
2956
|
+
StatusCode: result.StatusCode,
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
catch (e) {
|
|
2960
|
+
LogError(`IntegrationWriteRecord error: ${e}`);
|
|
2961
|
+
return { Success: false, Message: this.formatError(e) };
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2768
2964
|
// ── SCHEDULE ────────────────────────────────────────────────────────
|
|
2769
2965
|
async IntegrationCreateSchedule(input, ctx) {
|
|
2770
2966
|
try {
|
|
@@ -2793,7 +2989,12 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
2793
2989
|
job.Timezone = input.Timezone || 'UTC';
|
|
2794
2990
|
job.Status = 'Active';
|
|
2795
2991
|
job.OwnerUserID = user.ID;
|
|
2796
|
-
|
|
2992
|
+
const jobConfig = { CompanyIntegrationID: input.CompanyIntegrationID };
|
|
2993
|
+
if (input.SyncDirection)
|
|
2994
|
+
jobConfig.SyncDirection = input.SyncDirection;
|
|
2995
|
+
if (input.FullSync)
|
|
2996
|
+
jobConfig.FullSync = input.FullSync;
|
|
2997
|
+
job.Configuration = JSON.stringify(jobConfig);
|
|
2797
2998
|
job.NextRunAt = CronExpressionHelper.GetNextRunTime(input.CronExpression, input.Timezone || 'UTC');
|
|
2798
2999
|
if (!await job.Save())
|
|
2799
3000
|
return { Success: false, Message: 'Failed to create schedule' };
|
|
@@ -3296,6 +3497,15 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
3296
3497
|
try {
|
|
3297
3498
|
const user = this.getAuthenticatedUser(ctx);
|
|
3298
3499
|
const validatedPlatform = this.validatePlatform(platform);
|
|
3500
|
+
// Bust RunView caches for integration metadata BEFORE Config(true).
|
|
3501
|
+
// mj sync push writes records via stored procedures which do NOT fire
|
|
3502
|
+
// BaseEntity change events, so the RunView cache is never auto-invalidated.
|
|
3503
|
+
// Explicitly clearing these entries ensures Config(true) re-queries the DB.
|
|
3504
|
+
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Objects');
|
|
3505
|
+
await LocalCacheManager.Instance.InvalidateEntityCaches('MJ: Integration Object Fields');
|
|
3506
|
+
// Force-refresh integration metadata cache so IntrospectSchema
|
|
3507
|
+
// picks up any IntegrationObject/Field changes made via mj sync push
|
|
3508
|
+
await IntegrationEngine.Instance.Config(true, user);
|
|
3299
3509
|
// Phase 1: Build schema for each connector in parallel
|
|
3300
3510
|
const buildResults = await Promise.allSettled(input.Connectors.map(async (connInput) => {
|
|
3301
3511
|
const { connector, companyIntegration } = await this.resolveConnector(connInput.CompanyIntegrationID, user);
|
|
@@ -3337,6 +3547,8 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
3337
3547
|
StartSync: input.StartSync,
|
|
3338
3548
|
FullSync: input.FullSync ?? false,
|
|
3339
3549
|
SyncScope: input.SyncScope ?? 'created',
|
|
3550
|
+
SyncDirection: input.SyncDirection,
|
|
3551
|
+
ScheduleSyncDirection: input.ScheduleSyncDirection,
|
|
3340
3552
|
CreatedAt: new Date().toISOString(),
|
|
3341
3553
|
};
|
|
3342
3554
|
rsuInput.PostRestartFiles = [
|
|
@@ -3415,7 +3627,7 @@ let IntegrationDiscoveryResolver = class IntegrationDiscoveryResolver extends Re
|
|
|
3415
3627
|
if (skipRestart) {
|
|
3416
3628
|
// Entity maps, field maps, sync
|
|
3417
3629
|
await Metadata.Provider.Refresh();
|
|
3418
|
-
const entityMapsCreated = await this.createEntityAndFieldMaps(build.connInput.CompanyIntegrationID, build.objects, build.connector, build.companyIntegration, build.schemaName, user);
|
|
3630
|
+
const entityMapsCreated = await this.createEntityAndFieldMaps(build.connInput.CompanyIntegrationID, build.objects, build.connector, build.companyIntegration, build.schemaName, user, build.connInput.DefaultSyncDirection ?? 'Pull');
|
|
3419
3631
|
connResult.EntityMapsCreated = entityMapsCreated;
|
|
3420
3632
|
const createdMapIDs = entityMapsCreated.map(em => em.EntityMapID).filter(Boolean);
|
|
3421
3633
|
const scopedMapIDs = input.SyncScope === 'all' ? undefined : createdMapIDs;
|
|
@@ -3995,9 +4207,10 @@ __decorate([
|
|
|
3995
4207
|
__param(1, Arg("webhookURL", { nullable: true })),
|
|
3996
4208
|
__param(2, Arg("fullSync", () => Boolean, { defaultValue: false, description: 'If true, ignores watermarks and re-fetches all records from the source' })),
|
|
3997
4209
|
__param(3, Arg("entityMapIDs", () => [String], { nullable: true, description: 'Optional: sync only these entity maps. If omitted, syncs all maps for the connector.' })),
|
|
3998
|
-
__param(4,
|
|
4210
|
+
__param(4, Arg("syncDirection", () => String, { nullable: true, description: 'Override sync direction: Pull | Push | Bidirectional. If omitted, each entity map\'s own SyncDirection is used.' })),
|
|
4211
|
+
__param(5, Ctx()),
|
|
3999
4212
|
__metadata("design:type", Function),
|
|
4000
|
-
__metadata("design:paramtypes", [String, String, Boolean, Array, Object]),
|
|
4213
|
+
__metadata("design:paramtypes", [String, String, Boolean, Array, String, Object]),
|
|
4001
4214
|
__metadata("design:returntype", Promise)
|
|
4002
4215
|
], IntegrationDiscoveryResolver.prototype, "IntegrationStartSync", null);
|
|
4003
4216
|
__decorate([
|
|
@@ -4008,6 +4221,18 @@ __decorate([
|
|
|
4008
4221
|
__metadata("design:paramtypes", [String, Object]),
|
|
4009
4222
|
__metadata("design:returntype", Promise)
|
|
4010
4223
|
], IntegrationDiscoveryResolver.prototype, "IntegrationCancelSync", null);
|
|
4224
|
+
__decorate([
|
|
4225
|
+
Mutation(() => WriteRecordOutput),
|
|
4226
|
+
__param(0, Arg("companyIntegrationID")),
|
|
4227
|
+
__param(1, Arg("objectName")),
|
|
4228
|
+
__param(2, Arg("operation", () => String, { description: 'create, update, or delete' })),
|
|
4229
|
+
__param(3, Arg("externalID", { nullable: true, description: 'Required for update/delete' })),
|
|
4230
|
+
__param(4, Arg("attributes", () => String, { nullable: true, description: 'JSON object of field values for create/update' })),
|
|
4231
|
+
__param(5, Ctx()),
|
|
4232
|
+
__metadata("design:type", Function),
|
|
4233
|
+
__metadata("design:paramtypes", [String, String, String, String, String, Object]),
|
|
4234
|
+
__metadata("design:returntype", Promise)
|
|
4235
|
+
], IntegrationDiscoveryResolver.prototype, "IntegrationWriteRecord", null);
|
|
4011
4236
|
__decorate([
|
|
4012
4237
|
Mutation(() => CreateScheduleOutput),
|
|
4013
4238
|
__param(0, Arg("input")),
|