AUTHORITATIVE discovery (inherited rationale): the scoped global describe enumerates the COMPLETE set of Fonteva managed-package objects the credentials expose, so absence in a refresh genuinely means the object was removed and the engine may safely deactivate it. Kept explicit so the Fonteva-scoped filter doesn't quietly inherit a non-authoritative default.
The canonical integration name (e.g., "HubSpot", "Rasa.io"). Used by GetActionGeneratorConfig() and IntegrationActionExecutor to match connectors to action Config.IntegrationName.
Override in subclasses. Defaults to the class name.
Highest SAFE per-layer concurrency the source tolerates (plan.md §7 peak parallelization) — the
ceiling the engine's adaptive controller ramps toward. null → use configured syncConcurrency.
Whether the watermark this connector returns (FetchBatchResult.NewWatermarkValue) is a
RELIABLE, monotonically-increasing global maximum — i.e. the connector fetches in watermark
order so the last batch's value IS the true high-water mark, and an updated record always
re-surfaces at a NEW (higher) watermark. When true, the engine uses that watermark to NARROW
the next incremental (instead of advancing a full sync to wall-clock "now", and instead of the
keyset clear-and-re-scan), so incrementals fetch only what's new.
Default false — the safe, backwards-compatible choice: a connector whose source returns
records out of watermark order (e.g. HubSpot's creation-ordered list API, where the last batch
can carry old modstamps) MUST stay false so the engine keeps advancing to "now" and never saves
a stale watermark. Override to true ONLY when the source guarantees monotonic ordering.
Token-bucket rate-limit policy for this connector's source API (plan.md §7 peak-aware rate
limiting). null → the engine derives a conservative rate from Integration.BatchRequestWaitTime.
Override to push to the source's real limits.
Whether this connector supports batched target writes (plan.md §7 aggressive batching).
Whether this connector supports creating new records in the external system.
Whether this connector supports deleting records from the external system.
Whether this connector supports reading/fetching records. Always true.
Whether this connector supports paginated listing of records.
Whether this connector supports searching/querying records with filters.
Whether this connector supports updating existing records in the external system.
Whether this connector supports idempotent upserts (create-or-update keyed
by a unique business property). Connectors override this AND Upsert to enable it.
ProtectedAppendAppends default query parameters from the IntegrationObject metadata. DefaultQueryParams is stored as a JSON object like {"key": "value"}. Automatically skips params whose key (case-insensitive) already appears in the URL to avoid duplicates with pagination params.
ProtectedapplyRuns TransformRecord, then RE-ADDS any source key the transform dropped (present in
raw, absent from the output) unless declared in ExcludedSourceKeys. This makes the
base-fetch path structurally full-record (§0 forward-compat contract): a TransformRecord
override may reshape/coerce but cannot SILENTLY drop a source key from ExternalRecord.Fields,
so the framework's custom-column capture sees everything the source returned. The default
identity transform returns raw unchanged → fast-path no-op (zero overhead, zero behavior
change for connectors that don't override TransformRecord).
ProtectedAuthenticateAuthenticate with the external system and return an auth context. Called once per FetchChanges invocation; the returned context is passed to BuildHeaders and MakeHTTPRequest for every request.
Batch-create. Default loops single-record CreateRecord, so the engine may always call the batch form.
Batch-delete. Default loops single-record DeleteRecord.
Batch-update. Default loops single-record UpdateRecord.
ProtectedBuildBuilds a CRUDResult for a record CREATE, failing LOUDLY when the external system returned no usable record ID. A 2xx response with an empty/undefined ID means the create did not durably produce a record we can track — returning Success:true there silently loses the record and causes duplicate creates on the next sync (the HubSpot-association class of bug, fixed in next commit 9f718a7e). This makes that failure explicit at the connector boundary.
ProtectedBuildBuild HTTP headers for an API request, including auth headers. Called before every HTTP request.
ProtectedBuildBuild the operation request body per BodyShape:
ProtectedBuildAppends pagination parameters to a URL based on the object's PaginationType. Override in subclasses to use vendor-specific parameter names.
Optionalcursor: stringOptionaleffectivePageSize: numberWhen provided by FetchPaginatedLoop, caps the requested page size to the remaining batch capacity. Subclasses should honor this to prevent overshoot.
Creates a new record in Salesforce. Dispatches to the appropriate API family endpoint (standard SObjects, Tooling, Bulk Ingest/Query jobs, or Composite) based on the IntegrationObject metadata.
Deletes a record from Salesforce. For Bulk Job families, delete aborts the job. For standard and Tooling SObjects, performs a soft delete (Recycle Bin, 15-day retention). Analytics/Composite/Knowledge do not support deletion through this path.
Discovers fields for a Fonteva object. Translates the friendly IO name back
to its backing sObject API name, then reuses the inherited Salesforce
/describe field discovery verbatim.
Discovery via the connector's READ PATH (FetchChanges), TIME-BOUNDED — the way to gather a statistically-significant sample when a single DiscoverFields sample is too small to PROVE a key. "Discovery is the sync read path with the save removed."
Loops FetchChanges as a read-only FULL fetch (WatermarkValue=null, nothing persisted), threading pagination/keyset cursors across batches, and streams every record through the data-informed field + provable-PK inference. It stops at the discovery TIME BUDGET (default 5 min), or a record cap, or source exhaustion — whichever comes first — so the provable-PK decision is made on as much real data as the budget allows. It NEVER fabricates a key: if even this larger sample yields no provable single/composite PK, the field set comes back PK-less and the object is honestly not added.
Falls back to the single-sample DiscoverFields if the read path can't run for this object (e.g. a connector whose FetchChanges needs an already-persisted IO row that doesn't exist yet).
Optionalopts: { BatchSize?: number; MaxRecords?: number; TimeBudgetMs?: number }ProtectedDiscoverStage-2 field discovery for sources WITHOUT a describe/introspection endpoint (file feeds,
undocumented JSON list endpoints): stream the source's actual records — READ-ONLY, no save,
no ack — and derive the full field set + data-informed PK/uniqueness/nullability from the
gathered statistics. The connector supplies whatever read-only fetch yields the records; this
helper turns that stream into ExternalFieldSchema[].
Why data-informed: streaming the real values lets pickPrimaryKeyFromStats pick the PK from
evidence (uniqueness/non-null statistics) COMBINED with the naming convention, rather than a
name guess alone. The PK is a SOFT key, so the pick is best-available, not strict-significance:
a confident unique+non-null column wins outright; otherwise a near-unique / convention-named
column is taken as a soft key (a PK-less object would stall CodeGen). The scan is time-bounded —
it stops on exhaustion OR opts.Discovery.TimeBudgetMs; more rows simply mean stronger claims.
Provable-only encoding into the standard flags:
IsPrimaryKey — set ONLY on the single statistics-first pick. Multiple equally-ranked unique
columns leave PK unset here (ambiguous → the pipeline's SoftPKClassifier LLM tiebreaker
decides, fed these same stats). Zero unique columns → no PK is fabricated.IsUniqueKey — set when the column was all-distinct over the scan AND uniqueness was provable
(the distinct-cap wasn't hit).AllowsNull — asserted true ONLY when a null/absent value was actually observed; otherwise
left undefined (permissive default). Never fabricates NOT NULL — critical under a time-capped
partial scan where unseen rows could still be null.The PK emitted here is SOFT (it rides additionalSchemaInfo via the persist + DDL path; it is
NEVER a hard DB key), so a wrong inference can never reject a valid row — the engine dedupes via
the record-map. IsReadOnly defaults to true (stream discovery targets read feeds); a writable
source overrides via opts.ReadOnly.
A read-only sync/async iterable of source records (the caller's fetch yields them).
Optionalopts: { Discovery?: StreamDiscoveryOptions; Pk?: PkPickOptions; ReadOnly?: boolean }OptionalDiscovery?: StreamDiscoveryOptionsTime budget / sample caps for the scan (see StreamDiscoveryOptions).
OptionalPk?: PkPickOptionsSignificance threshold + naming-rank tiebreaker (see PkPickOptions).
OptionalReadOnly?: booleanWhether discovered fields are read-only. Default true.
Discovers Fonteva objects by reusing the inherited Salesforce global
describe and filtering it to the managed-package namespaces. NO baked
catalog — the list comes entirely from the live /sobjects/ describe; this
method only scopes it and presents the friendly (namespace-stripped) IO name.
ProtectedDiscoveryDISCOVERY-ONLY: last-resort SAFETY bound on the recursion depth for multi-level template chains (a
parent that is itself a template-var child), so a malformed metadata cycle cannot loop unboundedly.
This is only the fallback when the consumer sets no discoverySampleMaxDepth in Configuration and
no env override is present — the consumer decides the real value. A recursion cap MUST have some
bound, so a conservative default remains here; override the getter to change the fallback.
ProtectedDiscoveryREST override (§sample-discover): a SINGLE-template-var CHILD is sampled with the recursive, record-constrained StreamRecordsForDiscovery. Flat objects fall back to the generic FetchChanges loop; MULTI-var (composition) children are deferred — StreamRecordsForDiscovery adjourns them (declared-only until first sync) rather than fire malformed URLs.
ProtectedExcludedThe Salesforce attributes blob is the ONLY key any object's TransformRecord
removes. Declaring it here makes the removal auditable and excludes it from the
base's re-add-dropped-keys safety net (and from change-detection).
ProtectedExtractBest-effort error message extraction from a vendor response. Override for vendor-specific shapes.
ProtectedExtractExtract the new record's external ID from a create response per IDLocation.
ProtectedExtractExtract pagination state from the vendor-specific response.
The parsed response body
The pagination strategy for this object
Current page number (1-based)
Current record offset
Page size used in the request
Pagination state indicating whether more data is available
Parse a Retry-After / rate-limit signal out of a failed response or thrown error into
milliseconds so the engine can back off precisely. Return undefined when the error is not a
throttle (or carries no hint).
Fetches changed records. Default path reuses the inherited Salesforce SOQL
incremental fetch (SystemModstamp watermark + nextRecordsUrl cursor)
verbatim, with the IO name translated to its backing sObject. When the
connection opts into FDService reads (UseFDServiceForRead), routes through
the FDService Apex REST domain service instead.
Returns the ActionGeneratorConfig for this connector, combining the integration name, category, icon, and objects into a ready-to-use configuration for ActionMetadataGenerator.Generate().
Override in subclasses to customize the config (e.g., icon, category). Returns null by default if GetIntegrationObjects() returns empty.
ProtectedGetGet the base URL for API requests (e.g., "https://api.example.com/v1"). Combined with the object's APIPath to form the full request URL.
ProtectedGetGets IntegrationObjectField records from the engine's cache for a given object ID. Returns only active fields sorted by Sequence.
ProtectedGetGets an IntegrationObject from the engine's cache by integration ID and object name. Throws if not found.
Returns a proposed default configuration for quick setup. Override in subclasses to provide connector-specific defaults including schema name, objects to sync, and field mappings. Returns null by default (no quick setup available).
Returns suggested default field mappings for an external object to MJ entity. Override in subclasses to provide intelligent defaults.
Name of the external object
Name of the target MJ entity
Array of default field mappings (empty by default)
Action-generation hint set. CORRECTION (IMPROVE build): this used to return
a baked famous-subset catalog (~9 objects hardcoded in this file). That is a
catalog-in-code defect — it froze the object universe to a famous subset.
It now derives the hint set ENTIRELY from the runtime-cached IntegrationObject /
IntegrationObjectField metadata for the Salesforce integration — the FULL
Declared (credential-free catalog) + Discovered (live DiscoverObjects describe,
custom __c included) gamut. There is NO baked catalog. If the metadata cache is
not yet populated (e.g. action generation runs before the integration is seeded),
it returns an empty array — the connector NEVER falls back to a hardcoded list.
The per-tenant object UNIVERSE for sync comes exclusively from DiscoverObjects
(live global describe / sObjects endpoint); this method only shapes the cached
catalog into the ActionMetadataGenerator's hint structure.
Retrieves a single record by its Salesforce ID. Routes to the API family endpoint indicated by IntegrationObject metadata.
Full schema introspection scoped to the managed-package namespaces. Defers
entirely to the inherited Salesforce introspection (which calls
DiscoverObjects — overridden here to scope — then /describes each), but
re-resolves any caller-supplied ObjectNames (friendly IO names) to their
backing sObjects so the inherited describe loop targets real sObjects.
Optionaloptions: IntrospectSchemaOptionsLists records with cursor-based pagination.
Override in subclasses that support paginated listing.
Check SupportsListing before calling.
ProtectedMakeExecute an HTTP request. The concrete connector owns the transport layer (fetch, axios, got, etc.).
Optionalbody: unknownProtectedNormalizeExtract the data array from the vendor-specific response envelope.
The parsed response body
The key to extract data from, or null for root-level arrays
Array of raw record objects
Type-driven post-processing hook (plan.md §10): a connector may normalize/enforce a record's
values to the resolved column formats AFTER transform/normalize and BEFORE write. Default
returns the record unchanged. (Named for this system — NOT MCP, not take.) The engine ALSO
applies target-type constraint enforcement; this is the connector-side complement.
Searches records using SOQL WHERE clauses built from the provided filters. Search is supported for Standard SObjects and Tooling SObjects; other API families return an empty result.
Name of a stable, monotonic ordering key (PK/identity) usable for KEYSET/seek resume on
watermark-less objects (plan.md §7 — resume from last-seen key, robust to mid-stream
insert/delete). null → keyset resume unavailable for this object.
ProtectedSubstituteSubstitute the ExternalID into a URL path template at runtime. Path templates may use {ID}, {id}, or {ExternalID} as the placeholder; for symmetry with FetchChanges template-var handling, any unsubstituted {var} in the template is left in place (caller's responsibility to ensure consistency).
For IDLocation='body' or 'header' the ID is not substituted into the path (caller must handle separately); we just return the raw path.
ProtectedTemplatePer-call cap on how many PARENTS a parent-iterated object processes before yielding a resumable batch (the engine loops on HasMore). Kept conservative so a single FetchChanges call fits the engine's op-timeout even at a slow initial adaptive rate; the AIMD bucket ramps up over batches. Override in a concrete connector if a vendor tolerates larger per-call parent runs.
Tests connectivity to the external system.
The company integration entity with connection credentials
User context for authorization
Connection test result with success/failure and message
ProtectedTransformCORRECTION (IMPROVE build): per-record transform that strips Salesforce's
attributes metadata blob (e.g. { type, url }) from every record. This is a
SANCTIONED removal declared in ExcludedSourceKeys, NOT a silent drop — the
attributes key is vendor envelope noise, not customer data, so dropping it can
never lose a custom column. Every OTHER source field flows through untouched, so
the full-record pass-through contract (custom-column capture) is preserved.
Used by both the base-fetch path (GetRecord via applyTransformPreservingKeys) and the SOQL override path (RawToExternalRecord routes through StripVendorAttributes, which reuses ExcludedSourceKeys so the two paths agree on what is removed).
Updates an existing record in Salesforce (PATCH — only changed fields).
Honors API family routing; Tooling uses /tooling/sobjects/, everything
else uses standard /sobjects/.
Upserts a record — a single idempotent create-or-update keyed by a unique
business property (e.g. email), eliminating the search-then-create race window.
Override in subclasses whose external system exposes a keyed upsert primitive.
Check SupportsUpsert before calling.
Fonteva AMS connector.
Fonteva is a Salesforce managed package (the OrderApi__ and EventApi__ namespaces), so it rides the core Salesforce REST infrastructure verbatim. This connector therefore COMPOSES on SalesforceConnector, reusing — unchanged — its:
/services/data/vXX/sobjects/),/describefield discovery,nextRecordsUrlcursor pagination,SystemModstampwatermark incremental sync,BuildCreatedResultloud-fail-on-empty-ID write contract.The Fonteva-specific layer added here is intentionally thin:
DiscoverObjectsfilters the inherited Salesforce global describe down to the managed-package namespaces (OrderApi__ / EventApi__), so the connector surfaces ONLY Fonteva AMS objects (the standard SF CRM objects are the SalesforceConnector's job).Name("Assignment") is mapped to its real sObject API name ("OrderApi__Assignment__c", read from the IO's Configuration) before every inherited Salesforce operation, and the returned records'ObjectTypeis rewritten back to the IO name for engine identity./services/apexrest/FDService/*Apex REST services are wired as an opt-in read path with the documented SearchRequest filter/fields/sort/limit query params.Discovery is the live global-describe MECHANISM (no baked catalog); the object universe is Declared (docs) + Discovered (runtime describe). Writes use the inherited generic sObject CRUD; FDService write shapes are not idiosyncratic enough to warrant overriding the sObject write path (the metadata routes all writes through the platform sObject endpoints).