Domain Model
@nx/searchowns no PostgreSQL tables. Its "domain" is the set of Typesense collections — denormalized read projections of source tables in thepublic,pricing, andinventoryschemas. Schema definitions live insrc/configurations/<entity>.collection.ts(shared shapes inschema-fragments.ts).
1. Full ERD
CDC source tables (left) projected into Typesense collections (right). Solid = direct doc source; dashed = cascade-only.
2. Entities (Collections)
One block per collection. Each is a flat-ish Typesense document; i18n
{ en, vi }is flattened tofield.en/field.vi.queryBylists the searchable text fields; facetable fields are filterable.
organizers
| Property | Value |
|---|---|
| Source | public.Organizer (direct) |
| Config | src/configurations/organizer.collection.ts |
| Doc id | Organizer.id |
| queryBy | name.en, name.vi, description.en, description.vi, slug, identifier |
| Relations | parent (organizers, hydration) |
| Embedding | optional |
merchants
| Property | Value |
|---|---|
| Source | public.Merchant (direct) |
| Config | src/configurations/merchant.collection.ts |
| Doc id | Merchant.id |
| queryBy | name.en, name.vi, description.en, description.vi, slug, identifier |
| Relations | organizer (native), parent (merchants, hydration) |
categories
| Property | Value |
|---|---|
| Source | public.Category (direct) |
| Config | src/configurations/category.collection.ts |
| Doc id | Category.id |
| queryBy | name.en, name.vi, description.en, description.vi |
| Relations | merchant (native) |
devices
| Property | Value |
|---|---|
| Source | public.Device (direct) |
| Config | src/configurations/device.collection.ts |
| Doc id | Device.id |
| queryBy | name.*, identifier, code |
| Relations | organizer (native), merchant (native) |
sale-channels
| Property | Value |
|---|---|
| Source | public.SaleChannel (direct) |
| Config | src/configurations/sale-channel.collection.ts |
| Doc id | SaleChannel.id |
| queryBy | name.en, name.vi, slug, identifier |
| Relations | merchant (native), parent (sale-channels, hydration) |
products
| Property | Value |
|---|---|
| Source | public.Product + public.ProductInfo (i18n) — both direct (info is partial update) |
| Cascade | ProductCategory → categoryIds[] |
| Config | src/configurations/product.collection.ts (+ product-info-mapper) |
| Doc id | Product.id |
| queryBy | identifier, slug, info.name.{en,vi}, info.description.{en,vi}, merchant_name.{en,vi}, organizer_name.{en,vi}, sale_channel_names |
| Facets | status, merchantId (ref merchants.id), categoryIds[], parentId, denormalized parent names |
| Relations | merchant (native), parent (products, hydration) |
| Embedding | embedding field from [identifier, slug] |
product-variants
| Property | Value |
|---|---|
| Source | public.ProductVariant (direct) + ProductVariantInfo |
| Cascade | ProductCategory (categoryIds), MetaLink (metaLinks), FareSet/Fare (fareSet, defaultPrice), ProductBundler (comboItems) |
| Config | src/configurations/product-variant.collection.ts |
| Doc id | ProductVariant.id |
| Relations | product (native), merchant (native) |
inventories
| Property | Value |
|---|---|
| Source | inventory.InventoryStock (direct, doc grain) |
| Cascade | InventoryItem, InventoryLocation, InventoryIdentifier, Material |
| Config | src/configurations/inventory.collection.ts |
| Doc id | InventoryStock.id (== inventoryStockId) |
| queryBy | item.name.{en,vi}, location.name.{en,vi}, identifiers.value, stock.lotNumber, stock.serialNumber, item.slug, item.description.{en,vi} |
| Structure | nested item / location / stock groups; identifiers[] (scheme-agnostic); flags[] (derived booleans) |
| Notes | flags.isExpired / isExpiringSoon go stale — refreshed by periodic reindex or query-time range |
3. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
Document id always equals the source row id | Mapper extractDocumentId / idField |
| Soft-deleted source row ⇒ document absent | Mapper returns null → CDCService deletes |
| Out-of-order CDC events never overwrite newer state | LSN guard (source_lsn) + deleted_at compare |
products.categoryIds reflects all ProductCategory rows | Cascade fan-out + enrichment (not the row mapper) |
inventories doc is one-per-InventoryStock, item/location denormalized | Cascade from InventoryItem/InventoryLocation |
Typesense reference fields must be top-level | Schema places merchantId, inventoryItemId, etc. at top level |
4. Soft-delete Behavior
| Behavior | Detail |
|---|---|
| Source of truth | PostgreSQL deletedAt (owned by source services) |
| Search effect | Mapper detects deletedAt != null → emits delete to Typesense |
| Read default | Soft-deleted rows have no document, so they never appear in results |
| Hard-delete | A Debezium d op also deletes the document |
| Restore | A subsequent non-deleted CDC event re-upserts the document |