Skip to content

modernx-admin — Ingest delta detection (computeDelta)

Ingest (lib/ingest/) is what feeds the automations engine (see Automations engine entity model) its TriggerEvents in the first place: as Magento data lands in the warehouse, ingest decides which rows are new or changed and enqueues the matching trigger. Until 2026-07-27 that decision logic lived only inside upsertOrders, bespoke to orders. It’s now extracted into a reusable, domain-agnostic helper so the other six domains that already populate the warehouse silently (customers, products, creditmemos, invoices, shipments, quotes) have a mechanism to plug into once their own trigger types exist.

computeDelta — pure delta classification

Section titled “computeDelta — pure delta classification”

lib/ingest/delta.ts exports:

computeDelta<T, Key>(
prior: Map<Key, T>,
incoming: T[],
opts: { keyOf: (row: T) => Key; watched: Array<{ field: keyof T; triggerType: string }> },
): { created: T[]; changed: Array<{ row: T; triggerType: string }> }
  • created — incoming rows whose keyOf result is absent from prior.
  • changed — one entry per watched field whose prior value differs from the incoming value, for every incoming row that is present in prior. A row with two watched fields that both changed yields two entries; callers decide whether to enqueue one event per entry or dedupe upstream (orders only watches one field today, so this doesn’t come up yet).
  • Classification is decided solely from the prior snapshot map — never from whether a row is still present in some other store — because callers delete-then-insert every row in the batch before calling this, which makes table presence useless for telling “new” from “re-ingested”. A key present in prior is never misclassified as created, even when the incoming row shares no object identity with anything in prior.
  • Zero dependencies on Db/drizzle/automation types — pure and unit-testable without a DB fixture.

lib/ingest/delta-config.ts exports a DomainDeltaConfig<T, Key> type (key extractor, createdTriggerType, watched list — the shape computeDelta’s opts expects, plus the created-trigger type) and a domainDeltaConfigs map with exactly one populated entry so far:

domainDeltaConfigs.order = {
keyOf: (row) => row.sourceOrderId,
createdTriggerType: ORDER_PLACED_TRIGGER,
watched: [{ field: 'status', triggerType: ORDER_STATUS_CHANGED_TRIGGER }],
}

Adding a new domain (customer, product, …) once it has real trigger types is one more entry here — computeDelta itself never changes.

upsertOrders (lib/ingest/upsert.ts) now builds its prior map (keyed by sourceOrderId, holding the prior status) from a read inside the same transaction as the delete-then-insert, then calls computeDelta(prior, orders, domainDeltaConfigs.order) to derive newOrders/changed, replacing the old bespoke priorById filter logic. Everything downstream of that call is unchanged:

  • Same post-commit, failure-isolated block — a computeDelta/enqueue error is caught and logged, never fails the already-committed ingest batch.
  • Same hasActive(...) gate per trigger type before building events.
  • Same event shapes, including the entityKey = `${sourceOrderId}:${status}` composite key for status-changed events.

No other upsert function (upsertCustomers/Products/Creditmemos/Invoices/ Shipments/Quotes), applyTombstones, or processBatch changed — those six domains still populate the warehouse without detecting deltas or enqueuing anything, same as before this change.

This is scaffolding only, same posture as the engine’s entity-model generalization: no new trigger types are registered, and order behavior is byte-identical to before (every existing order-trigger and status-changed test in upsert.test.ts passes unmodified). It’s bead A4 of the same trigger-breadth arc that produced the DomainEntity/entityLoaders registry — later beads (B1 customer, B2 product, B3 order-lifecycle) add a domain’s trigger-type constants and its domainDeltaConfigs entry, then wire its upsert function through computeDelta the same way orders is wired here.

See also Scan-trigger infrastructure for the counterpart mechanism covering the three trigger types that can’t be delta-detected on an ingest push at all (cart-abandoned, spending-threshold-crossed, inactive-customer) because they’re states an entity ages into rather than a change that arrives on a batch.

Source: modernx-admin PR #391.