Member Junction
    Preparing search index...

    PropFuel data-export file-feed connector.

    PropFuel exposes a per-tenant hourly data-export feed as a set of JSON files. There is NO public OpenAPI spec and NO describe/introspection endpoint, so this connector encodes the DISCOVERY MECHANISM, never a baked answer:

    • DiscoverObjects calls GET /dataexport/{AccountID}/list and derives each object from the [microtime]-[datatype].json filename suffix. The concrete data-type set is whatever the live listing actually contains — never a static PROPFUEL_STREAMS array.
    • DiscoverFields lists the files for a data type, downloads ONE sample file, and streams its records through the base DiscoverFieldsViaStream helper to derive the field set + data-informed soft-PK from real values — never a frozen STREAM_FIELDS catalog.

    Incremental sync uses a SYNTHETIC, FILE-LEVEL, CLIENT-SIDE cursor __file_microtime: the feed is append-only and chronologically sortable by the filename microtime prefix, so the connector resumes by tracking the max microtime seen across processed files (WatermarkService). The vendor ack endpoint is operational state and is NOT used as the read-only incremental cursor.

    READ-ONLY: the data-export feed creates/updates/deletes no source records, so all write capability getters stay false and no ack/POST/delete path is exercised.

    AccountID and the bearer Token both come from the credential (or Configuration JSON) — the AccountID is per-tenant config and is NEVER hardcoded.

    Hierarchy (View Summary)

    Index

    Constructors

    Accessors

    • get DiscoveryIsAuthoritative(): boolean

      §7 — does this connector's discovery (DiscoverObjects/DiscoverFields → IntrospectSchema) return the AUTHORITATIVE, COMPLETE gamut of objects/fields the credentials expose? Default false (safe): a connector must explicitly affirm this. Override to true ONLY when DiscoverObjects hits a real list/describe endpoint that returns EVERYTHING accessible — so an object/field absent from a refresh genuinely means the source dropped it (and may be deactivated). Leave false for stubbed discovery (returns nothing → static metadata is all we have), a cache-driven IntrospectSchema, or any partial/scoped enumeration — there, absence proves nothing and MUST NOT deactivate.

      Returns boolean

    • get MaxConcurrencyHint(): number

      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.

      Returns number

    • get MonotonicWatermark(): boolean

      The file-feed microtime IS a reliable monotonic high-water mark: PropFuel emits export files in ascending microtime order, and an updated record re-appears in a NEW (higher-microtime) file. So NewWatermarkValue (= max microtime seen) is the true global max — the engine can NARROW the next incremental to microtime > watermark instead of clearing the keyset position and re-scanning the whole stream every run. Without this, a clean sync's keyset-resume marker is cleared and every incremental re-walks the entire object (correct via content-hash, but wasteful at scale).

      Returns boolean

    • get RateLimitPolicy(): RateLimitPolicy

      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.

      Returns RateLimitPolicy

    • get SupportsBatchWrite(): boolean

      Whether this connector supports batched target writes (plan.md §7 aggressive batching).

      Returns boolean

    • get SupportsCreate(): boolean

      Whether this connector supports creating new records in the external system.

      Returns boolean

    • get SupportsDelete(): boolean

      Whether this connector supports deleting records from the external system.

      Returns boolean

    • get SupportsGet(): boolean

      Whether this connector supports reading/fetching records. Always true.

      Returns boolean

    • get SupportsListing(): boolean

      Whether this connector supports paginated listing of records.

      Returns boolean

    • get SupportsSearch(): boolean

      Whether this connector supports searching/querying records with filters.

      Returns boolean

    • get SupportsUpdate(): boolean

      Whether this connector supports updating existing records in the external system.

      Returns boolean

    • get SupportsUpsert(): boolean

      Whether this connector supports idempotent upserts (create-or-update keyed by a unique business property). Connectors override this AND Upsert to enable it.

      Returns boolean

    Methods

    • Appends 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.

      Parameters

      Returns string

    • Runs 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).

      Parameters

      Returns Record<string, unknown>

    • Builds 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.

      Parameters

      • externalID: string
      • statusCode: number
      • objectName: string

      Returns CRUDResult

    • Build the operation request body per BodyShape:

      • 'flat' → body = attributes verbatim
      • 'wrapped' → body = { [BodyKey]: attributes }
      • 'literal' → connector should have overridden the operation; fall back to flat as safety net
      • null → default to flat (most common shape)

      Parameters

      • attributes: Record<string, unknown>
      • bodyShape: string
      • bodyKey: string

      Returns unknown

    • Appends pagination parameters to a URL based on the object's PaginationType. Override in subclasses to use vendor-specific parameter names.

      Parameters

      • basePath: string
      • obj: MJIntegrationObjectEntity
      • page: number
      • offset: number
      • Optionalcursor: string
      • OptionaleffectivePageSize: number

        When provided by FetchPaginatedLoop, caps the requested page size to the remaining batch capacity. Subclasses should honor this to prevent overshoot.

      Returns string

    • 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).

      Parameters

      Returns Promise<ExternalFieldSchema[]>

    • Stage-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.

      Parameters

      • records:
            | AsyncIterable<Record<string, unknown>, any, any>
            | Iterable<Record<string, unknown>, any, any>

        A read-only sync/async iterable of source records (the caller's fetch yields them).

      • Optionalopts: { Discovery?: StreamDiscoveryOptions; Pk?: PkPickOptions; ReadOnly?: boolean }

      Returns Promise<ExternalFieldSchema[]>

    • DISCOVERY-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.

      Returns number

    • REST 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.

      Parameters

      Returns AsyncGenerator<Record<string, unknown>>

    • Source keys a connector DELIBERATELY removes in its TransformRecord override (vendor noise, a flattened parent blob, a nested child collection emitted as its own object). These are the only sanctioned drops — excluded from the applyTransformPreservingKeys re-add. Default: none. Override per-object to declare intentional removals.

      Parameters

      • _objectName: string

      Returns string[]

    • 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).

      Parameters

      • _error: unknown

      Returns number

    • Fetches records for a data type using the synthetic __file_microtime cursor.

      Mechanism: load the stored cursor (max microtime processed), list the live files, select the files of this data type whose microtime exceeds the cursor (ascending microtime order), and download each, emitting every record with full-record pass-through. The max microtime seen across the processed files is persisted as the new cursor — ONLY on full-batch success, so a partial failure leaves the cursor unchanged and the next run resumes from the same point.

      Parameters

      Returns Promise<FetchBatchResult>

    • 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.

      Returns ActionGeneratorConfig

    • Returns suggested default field mappings for an external object to MJ entity. Override in subclasses to provide intelligent defaults.

      Parameters

      • _objectName: string

        Name of the external object

      • _entityName: string

        Name of the target MJ entity

      Returns DefaultFieldMapping[]

      Array of default field mappings (empty by default)

    • Returns the integration objects and their fields that this connector supports, for use by the ActionMetadataGenerator. This is static metadata that does NOT require a live connection — it describes the connector's known object model.

      Override in subclasses to provide connector-specific objects/fields. Returns an empty array by default (no action generation available).

      Returns IntegrationObjectInfo[]

    • Executes an HTTP request via fetch. Parses JSON when the response is JSON, else returns the raw text body. The concrete connector owns the transport seam so tests can override it.

      Parameters

      • _auth: PropFuelAuthContext
      • url: string
      • method: string
      • headers: Record<string, string>
      • Optionalbody: unknown

      Returns Promise<RESTResponse>

    • The download response for a data-export file is a JSON array of records (no envelope). When a vendor wraps records under a key, that key is honored; otherwise a root array / single object is normalized to an array.

      Parameters

      • rawBody: unknown
      • responseDataKey: string

      Returns Record<string, unknown>[]

    • 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.

      Parameters

      Returns ExternalRecord

    • KEYSET / no-watermark resume hint: every PropFuel object is a data-export file stream ordered by the filename microtime prefix, so 'microtime' is the stable, monotonic ordering key for all of them (the docs-provable feed object and every runtime-discovered data-type stream alike).

      Parameters

      • _objectName: string

      Returns string

    • Substitute 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.

      Parameters

      • path: string
      • externalID: string
      • idLocation: string

      Returns string

    • Per-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.

      Returns number

    • Optional per-record CUSTOMIZATION hook called between NormalizeResponse (vendor envelope-stripping) and ToExternalRecord (composite-PK assembly).

      Distinct from NormalizeResponse — that strips the vendor's response envelope to expose individual records. This hook is for vendor-specific record-level customization the standard pipeline shouldn't carry: nested-field flattening, empty-string→null coercion for date columns, computed fields, removing vendor metadata blobs (e.g. Salesforce 'attributes'), etc.

      Default is identity (no transform). Override only when a concrete connector needs vendor-specific shape changes.

      Parameters

      Returns Record<string, unknown>

    • 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.

      Parameters

      Returns Promise<CRUDResult>