/** * SourcedEntryService — drizzle-bound entry point for the * `catalog_sourced_entries` store. * * One row per sourced entity, written at `discover()` time. The row carries * provenance, lifecycle markers, and the canonical local copy of the * indexed projection (so the read service and the thin-content synthesizer * can dispatch without round-tripping the search index). * * Three primitives: * * - `upsertSourcedEntry` — called from `sync.ts` for every projection; * idempotent on (entity_module, entity_id) and on * (source_kind, source_connection_id, source_ref). * - `readSourcedEntry` — point-read by Voyant-side identity. Returns * null for owned entities (which have no row here). * - `createReadProvenance` — factory that composes `readSourcedEntry` * with vertical-specific owned-checkers to produce a unified * `readProvenance(db, entity_module, entity_id)` that returns one of: * `{ kind: "owned" }`, `{ kind: "sourced", ... }`, or `null`. * * The factory pattern keeps this package neutral — it doesn't know how to * read the products / cruises / hotels owned tables. Each vertical * registers its owned-checker once when wiring its content service (Phase * D and beyond). * * See `docs/architecture/catalog-sourced-content.md` §2.5. */ import type { AnyDrizzleDb } from "@voyantjs/db"; import type { CatalogProjection } from "../adapter/contract.js"; import type { Provenance } from "../provenance.js"; import { type SelectCatalogSourcedEntry, type SourcedEntryStatus } from "../schema-sourced-entries.js"; /** * Result of a `readProvenance` call. Three shapes: * * - `{ kind: "owned" }` — entity exists in the vertical's owned * table; no sourced-entry row. * - `{ kind: "sourced", ... }` — entity is sourced; carries the durable * provenance row + entry id. * - `null` — entity not found in either store. * * Callers pattern-match on `kind` to dispatch owned-vs-sourced reads. */ export type ProvenanceReadResult = { kind: "owned"; provenance: Provenance; } | { kind: "sourced"; provenance: Provenance; entry_id: string; status: SourcedEntryStatus; projection: Record; projection_etag: string | null; projection_seen_at: Date; first_seen_at: Date; last_seen_at: Date; }; /** * Vertical-specific owned-checker. Returns `true` iff the entity exists in * the vertical's owned table (i.e. its id matches a row that has no source * link). Implementations are tiny per-vertical helpers and live in the * vertical package — the catalog plane doesn't import them directly. */ export type OwnedChecker = (db: AnyDrizzleDb, entityId: string) => Promise; /** * Read one sourced-entry row by Voyant-side identity. Returns `null` for * entities that aren't in the sourced-entry store — owned entities, or * sourced entities the deployment hasn't yet discovered. */ export declare function readSourcedEntry(db: AnyDrizzleDb, entityModule: string, entityId: string): Promise; /** * Build a unified `readProvenance(db, entity_module, entity_id)` against a * registry of vertical-specific owned-checkers. The returned function: * * 1. Calls the vertical's owned-checker. If it returns `true`, the entity * is owned — return `{ kind: "owned", provenance: ... }` without * touching the sourced-entry table. * 2. Otherwise, look up the sourced-entry row. If found, return * `{ kind: "sourced", ... }`. * 3. If neither, return `null`. * * Verticals not in `ownedCheckers` skip the owned check (treated as * sourced-only). This is intentional: not every vertical has an owned * counterpart for every sourced entity. */ export declare function createReadProvenance(options: { ownedCheckers?: ReadonlyMap; }): (db: AnyDrizzleDb, entityModule: string, entityId: string) => Promise; /** * Input for `upsertSourcedEntry`. Accepts a `CatalogProjection` (the shape * `discover()` emits) plus optional metadata the adapter chose not to put * on the projection (etag, freshness override). */ export interface UpsertSourcedEntryInput { /** The projection emitted by `adapter.discover()`. */ projection: CatalogProjection; /** * Optional ETag-style marker for the projection itself. Distinct from * the content cache's etag — this one stamps the indexed projection. */ projectionEtag?: string; /** * When the upstream said this projection was last sourced. Defaults to * `provenance.last_sourced_at` if set, otherwise `new Date()`. */ lastSourcedAt?: Date; /** * Optional override for the lifecycle status. Withdrawal sweepers set * this to `"withdrawn"` for rows the upstream stopped emitting. */ status?: SourcedEntryStatus; } /** * Upsert a sourced-entry row. Idempotent on `(entity_module, entity_id)` * — repeated calls update `projection`, `projection_etag`, * `projection_seen_at`, `last_seen_at`, `last_sourced_at`, * `source_freshness`, and `updated_at`. The first-seen timestamp is * preserved. * * Owned projections are rejected — `provenance.source_kind === "owned"` * has no place in the sourced-entry store. Callers in `sync.ts` should * already filter these out, but this guard makes the invariant explicit. */ export declare function upsertSourcedEntry(db: AnyDrizzleDb, input: UpsertSourcedEntryInput): Promise; /** * Mark a sourced-entry row as withdrawn (the upstream stopped emitting * it). Used by the periodic withdrawal sweeper or by drift events of kind * `entity_archived`. Does not delete the row — withdrawals are auditable. */ export declare function markSourcedEntryWithdrawn(db: AnyDrizzleDb, entityModule: string, entityId: string): Promise; /** * Mark active sourced rows missing from a successful full-source discovery pass * as withdrawn. Callers should invoke this only after an adapter completed its * projection stream; failed refreshes must leave existing rows untouched. */ export declare function markMissingSourcedEntriesWithdrawn(db: AnyDrizzleDb, input: { entityModule: string; sourceKind: string; sourceConnectionId?: string | null; seenEntityIds: ReadonlySet; }): Promise; //# sourceMappingURL=sourced-entry-service.d.ts.map