Skip to content

@memberjunction/integration-engine

MemberJunction Integration Engine — orchestration, field mapping, and connector framework for synchronizing external systems with MJ entities.

The integration engine follows a pipeline architecture:

External System → Connector → FieldMappingEngine → MatchEngine → MJ Entity Persistence
ComponentResponsibility
BaseIntegrationConnectorAbstract base for external system connectors
ConnectorFactoryResolves connector instances via MJGlobal.ClassFactory
FieldMappingEngineApplies field-level transformations from external→MJ fields
MatchEngineDetermines Create/Update/Delete/Skip for each record
WatermarkServiceManages incremental sync watermarks
IntegrationOrchestratorTop-level coordinator that runs end-to-end sync

Field mapping — the shared transform engine

Section titled “Field mapping — the shared transform engine”

FieldMappingEngine owns only the integration-specific concerns: source flattening, field-map iteration, and unmapped-field overflow capture. The actual per-value transform pipeline (direct / regex / split / combine / lookup / format / coerce / substring / custom) is not implemented here — it’s the shared FieldTransformEngine in @memberjunction/global, so integration sync and the rules-based bulk-update tool run the exact same transform implementation (fix or extend a transform once, everywhere benefits).

Integration is the right home when the other side is a live external system — it owns the protocol, auth, discovery, match resolution, sync direction, watermarks, and dead-lettering, and borrows the shared engine only for the field transforms. When both sides are MJ data (updating an entity from its own fields / related entities / provided context), use EntityFieldRules in @memberjunction/core instead — same engine underneath, metadata-aware on top.

import { IntegrationOrchestrator } from '@memberjunction/integration-engine';
const orchestrator = new IntegrationOrchestrator();
const result = await orchestrator.RunSync(companyIntegrationID, contextUser, 'Manual');
console.log(`Processed: ${result.RecordsProcessed}`);
console.log(`Created: ${result.RecordsCreated}, Updated: ${result.RecordsUpdated}`);
console.log(`Errors: ${result.RecordsErrored}`);
import { RegisterClass } from '@memberjunction/global';
import {
BaseIntegrationConnector,
ConnectionTestResult,
ExternalObjectSchema,
ExternalFieldSchema,
FetchContext,
FetchBatchResult,
} from '@memberjunction/integration-engine';
@RegisterClass(BaseIntegrationConnector, 'HubSpotConnector')
export class HubSpotConnector extends BaseIntegrationConnector {
async TestConnection(companyIntegration, contextUser): Promise<ConnectionTestResult> {
// Test HubSpot API connectivity
}
async DiscoverObjects(companyIntegration, contextUser): Promise<ExternalObjectSchema[]> {
// Return available HubSpot objects (Contacts, Companies, Deals, etc.)
}
async DiscoverFields(companyIntegration, objectName, contextUser): Promise<ExternalFieldSchema[]> {
// Return fields for a specific HubSpot object
}
async FetchChanges(ctx: FetchContext): Promise<FetchBatchResult> {
// Fetch changed records using HubSpot's API with watermark-based pagination
}
}

The FieldMappingEngine supports 9 transform types configured via JSON in the TransformPipeline field:

TypeDescriptionConfig
directPass-through with optional default{ DefaultValue?: unknown }
regexRegex find/replace{ Pattern, Replacement, Flags? }
splitSplit string, extract by index{ Delimiter, Index }
combineMerge multiple fields{ SourceFields, Separator }
lookupCase-insensitive value mapping{ Map, Default? }
formatDate/number formatting{ FormatString, FormatType }
coerceType conversion`{ TargetType: ‘string''number''boolean''date’ }`
substringExtract portion of string{ Start, Length? }
customJavaScript expression{ Expression }
[
{ "Type": "regex", "Config": { "Pattern": "[^0-9]", "Replacement": "", "Flags": "g" } },
{ "Type": "substring", "Config": { "Start": 0, "Length": 3 } }
]

This pipeline strips non-numeric characters then extracts the first 3 digits (e.g., area code from phone number).

Each transform step can specify OnError:

  • "Fail" (default) — throws, halting the record
  • "Skip" — skips the field entirely
  • "Null" — sets the field to null

After a sync completes or fails, an optional onNotification callback receives a SyncNotification with pre-formatted subject and body text suitable for email delivery:

const result = await orchestrator.RunSync(
companyIntegrationID, contextUser, 'Scheduled',
undefined, // onProgress
(notification) => {
if (notification.Severity !== 'Info') {
emailService.send({
to: 'ops@example.com',
subject: notification.Subject,
text: notification.Body,
});
}
}
);

SyncNotification properties:

  • Event: 'SyncCompleted' | 'SyncCompletedWithErrors' | 'SyncFailed'
  • Severity: 'Info' | 'Warning' | 'Error'
  • Subject / Body: human-readable text ready for email
  • Result: the full SyncResult for programmatic access
  • RunSync(companyIntegrationID, contextUser, triggerType?, onProgress?, onNotification?) — Executes a full sync run
  • Apply(records, fieldMaps, entityName) — Maps external records to MJ entity fields
  • Resolve(records, entityMap, fieldMaps, contextUser) — Resolves Create/Update/Delete/Skip
  • Load(entityMapID, contextUser) — Loads current watermark
  • Update(entityMapID, newValue, contextUser) — Updates or creates watermark
  • Resolve(integration, sourceTypes) — Creates connector instance via ClassFactory
  • TestConnection(companyIntegration, contextUser) — Tests external system connectivity
  • DiscoverObjects(companyIntegration, contextUser) — Lists available external objects
  • DiscoverFields(companyIntegration, objectName, contextUser) — Lists fields on an object
  • FetchChanges(ctx) — Fetches a batch of changed records
  • GetDefaultFieldMappings(objectName, entityName) — Suggests default mappings

Operator-tunable knobs read from the process environment. All are optional with safe defaults.

Schema materialization caps (operator guardrails)

Section titled “Schema materialization caps (operator guardrails)”

These bound what a create-tables / RSU apply (IntegrationApplyAll / ApplyAllBatch / ApplySchemaBatch) may materialize. They are deliberately env-only — NOT per-connection or GraphQL-settable — because a cap a user could raise via the same API they apply with would be no guardrail. An over-limit selection is rejected with a clear error (never truncated); discovery still surfaces every object/field, only materialization is capped.

VarDefaultEffect
MJ_INTEGRATION_MAX_TABLESunset = unboundedMax tables a single apply may select; over-limit → apply rejected.
MJ_INTEGRATION_MAX_COLUMNS_PER_TABLEunset = unboundedMax columns any one selected table may have; over-limit → apply rejected.

Discovery (stage-2 streaming field discovery — no-describe / file-feed sources only)

Section titled “Discovery (stage-2 streaming field discovery — no-describe / file-feed sources only)”

Bound the data sample used to build a column corpus + lightweight PK guess (NOT a full scan). Also overridable per-connection via IntegrationSetSyncConfig (discoveryBatchSize / discoveryMaxRecords / discoveryTimeBudgetMs); precedence is explicit-opts > per-connection Configuration > env > default.

VarDefaultEffect
MJ_INTEGRATION_DISCOVERY_MAX_RECORDS500Max records sampled before discovery stops and uses what it gathered.
MJ_INTEGRATION_DISCOVERY_BATCH_SIZE500Records per FetchChanges page during discovery.
MJ_INTEGRATION_DISCOVERY_TIME_BUDGET_MS300000 (5 min)Wall-clock budget for the read-path discovery sweep.
MJ_INTEGRATION_DISCOVERY_COMPOSITE_ROW_CAP2000Row cap for composite-PK uniqueness checking.
MJ_INTEGRATION_DISCOVERY_MAX_DISTINCT100000Cap on distinct values tracked per field for uniqueness inference.
MJ_INTEGRATION_DISCOVERY_SAMPLE_VALUE_CAP10Sample values retained per field (for type/shape inference).
VarDefaultEffect
MJ_INTEGRATION_FULL_PUSH_MAX_RECORDSunset = unboundedCap on records pushed in a single full push.
MJ_INTEGRATION_MAX_RUNS_PER_CI100Run-history rows retained per CompanyIntegration (≤0 disables pruning).
MJ_INTEGRATION_VERBOSE_RECORD_LOGSoffWhen true, emits per-record console logs (structured artifacts are unaffected either way).