@memberjunction/server 5.27.1 → 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.
@@ -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
- return objects.map(obj => {
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 = (sourceObj?.Fields ?? []).filter(f => !selectedFieldSet || selectedFieldSet.has(f.Name.toLowerCase()) || f.IsPrimaryKey);
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 = (sourceObj?.Fields ?? [])
1787
+ const primaryKeyFields = sourceObj.Fields
1736
1788
  .filter(f => f.IsPrimaryKey)
1737
1789
  .map(f => f.Name.replace(/[^A-Za-z0-9_]/g, '_'));
1738
- return {
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?.Description ?? objDescriptions?.objectDescription,
1808
+ Description: sourceObj.Description ?? objDescriptions?.objectDescription,
1744
1809
  Columns: columns,
1745
- PrimaryKeyFields: primaryKeyFields.length > 0 ? primaryKeyFields : ['ID'],
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 2: Resolve object IDs to names, build inputs with per-object Fields
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
- job.Configuration = JSON.stringify({ CompanyIntegrationID: input.CompanyIntegrationID });
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, Ctx()),
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")),