Catalog Features and Flows
Developer Resources Catalog Catalog Features and Flows Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Catalog module.
Use this page for the catalog domain: what it does for users, admins, workers and systems, and how each flow behaves from start to finish.
Source Type Files or Docs What Was Extracted Technical design catalog-slug.service.ts, catalog-search.service.ts, catalog.constants.ts, workers/catalog-job-state.tsSlug ownership, search form, job state machine, advisory lock API admin/{category,brand,brand-series,job}/*.controller.ts, customer/**/*.controller.ts, bulk controllersRoute surface, permissions, rate limits Backend catalog-category-{write,admin,bulk,tree}.service.ts, catalog-brand*.service.ts, catalog-import-*.ts, catalog-export.service.tsBusiness rules, transactions, side effects Schema packages/db/src/schema/catalog/*.tsConstraints, partial indexes, slug tables Jobs packages/jobs/src/index.tsQueue contracts, payloads
Field Value Module catalogSubmodule category (tree), brand, brand series, tag (newer), jobs (import/export) Primary user value The store's taxonomy — categories, brands and brand series — with permanent, honest URLs and a unified search Actors Guest, admin, worker/system Main entry points /api/admin/catalog/{categories,brands,brand-series,jobs}, /api/mobile/catalog/{categories,brands,series,search}, /api/admin/catalog/tagsMain outputs Tree/list/detail responses, per-item bulk results, CSV imports/exports, activity records, cache invalidations Related docs API , Backend
Actor Can Do Cannot Do Auth Requirement Notes Guest Browse categories (root list, detail, breadcrumbs), brands, series, search, suggestions, tags Admin mutations, see hidden/deleted entities None (@Public()) Hidden/deleted → 404 on detail routes; rate limits PUBLIC_READ 60/min, PUBLIC_SEARCH 60/min, PUBLIC_SUGGEST 300/min Admin Full CRUD + bulk + reorder + tree ops + import/export jobs for all three entities, tag CRUD Delete a category with active children, delete a brand with active series, permanently delete anything Admin JWT + Categories_* / Brands_* / BrandSeries_* / Catalog_* / Tags_* Every mutation writes activity + invalidates cache; category tree mutations serialise on an advisory lock Worker/system Run import/export jobs, sweep stalled jobs — BullMQ worker Lease-based claim; cooperative cancellation
Capability Surface Actor Route/Trigger State Read State Written Linked API Section List siblings Admin Admin GET /api/admin/catalog/categoriesOne tree level — API §3 Full tree Admin Admin GET /api/admin/catalog/categories/treeNon-deleted rows — API §3 Category detail/breadcrumbs Admin Admin GET /:publicId(/:breadcrumbs)Row + ancestors — API §3 Create/update/move/visibility/delete/restore category Admin Admin POST/PATCH/DELETE on /api/admin/catalog/categoriesRow + parent Category rows, slug rows API §3 Bulk category ops + reorder Admin Admin POST /bulk/*, PATCH /reorderRows Rows + display_order API §3.6–3.7 Brand CRUD + bulk + reorder Admin Admin /api/admin/catalog/brandsRows Rows, slugs API §4 Series CRUD + bulk + reorder Admin Admin /api/admin/catalog/brand-seriesRows + brand Rows, slugs API §5 Import/export jobs Admin/worker Admin → system /api/admin/catalog/jobsJob rows Job rows, CSV API §6 Category/brand/series browsing Storefront Guest /api/mobile/catalog/{categories,brands,series}Visible rows — API §7–9 Search + suggestions Storefront Guest /api/mobile/catalog/searchVisible rows — API §10 Tag CRUD + listing Admin/guest Both /api/admin/catalog/tags, /api/mobile/catalog/tagsTag rows Tag rows Products API §4
A guest opens the storefront. The frontend fetches root categories, then drills into a category detail by slug and renders breadcrumbs. Category visibility is inherited: a visible category under a hidden or deleted ancestor is unreachable.
Branch Condition Behavior Error/Result Retired slug Slug owned by the entity, not current 200 + canonicalSlug — frontend redirects No 3xx from the API Hidden/deleted entity Not visible on storefront 404 on both slug branches CATEGORY_NOT_FOUND-styleParent hidden Visible child under hidden parent Child unreachable everywhere 404 / excluded Tag filter resolves empty Unknown tag slugs Empty result, never the whole catalogue Empty data
Search covers category, brand and brand series with pg_trgm: the % operator bounds candidates in WHERE (index-accelerated), similarity() ranks in ORDER BY — never in WHERE, which would force a full scan. A term under 2 characters is "no search" → unfiltered visible list. Category visibility is enforced inside the SQL (hidden/deleted ancestors block); series visibility requires a visible brand.
Permission Categories_CREATE, ADMIN_WRITE 10/min. Inside the transaction: advisory tree lock first, parent resolve + depth check (max 6), slug generation against the ownership table, insert + claim slug.
The only parent-mutating route: PATCH /:publicId/move. Guards: cycle prevention (cannot move under own descendant), depth cap for the whole subtree, parent liveness. parent_id and depth are rewritten in one statement .
Delete requires no active children (409 CATEGORY_HAS_ACTIVE_CHILDREN); bulk delete processes deepest-first so parents and children can go together. Restore requires a live parent. Slugs are never released by delete, so restore never fails on a taken slug.
CSV/XLSX import (25 MB, 50,000 rows) validates every row before writing anything; a category file's internal parent graph must be acyclic. Export caps at 50,000 matching rows. Import submit re-checks the entity-specific create permission (Categories_CREATE / Brands_CREATE / BrandSeries_CREATE) beyond route-level Catalog_CREATE.
Entity From Event/Action To Guard Condition Side Effects category_slugcurrent rename retired Demote BEFORE promote (partial unique index non-deferrable) retired_at stamped; re-adoption restores original URLany *_slug any soft delete (unchanged) Slugs never released on delete Restore can never fail on a slug
Entity From Event/Action To Guard Condition Side Effects catalog_jobqueuedclaim processingstatus = 'queued' OR processing past TTLstarted_at stampedcatalog_jobprocessingcomplete completedTerminal write matches zero rows → throws CATALOG_JOB_NOT_PROCESSING finished_at in same UPDATEcatalog_jobqueued/processingcancel cancelledNot terminal Cooperative abort for running imports
Flow DB Writes Cache Effects Jobs Realtime Analytics Notifications Category create/move/delete/restore Category + slug rows catalog_category + search — — — — Brand/series writes Rows + slugs catalog_brand / catalog_series + search — — — — Reorder display_orderowning domain — — — — Import Entity rows + catalog_job owning domain catalog.import_entities— — — Export catalog_job + CSV— catalog.export_entities— — — All mutations Row + activity record domain tags + Redis patterns — — — —
Scenario Trigger User/System Experience Recovery Source Concurrent tree moves Two moves cross Both validate against a snapshot Advisory lock serialises — second waits catalog.constants.tsImport validation failure Any row invalid / cycle Job failed, nothing written Fix file, resubmit catalog-import.service.tsImport cancelled mid-run Admin cancels Cooperative abort, rollback Resubmit catalog-job-state.tsWorker died mid-run Crash Job stuck processing Sweep fails it after 15 min TTL catalog-job-sweep.*Slug generation exhausted 100 attempts taken 409 CATALOG_SLUG_GENERATION_FAILED Rename source catalog-slug.service.ts
Actor capability diagram — §3/§4.
High-level module flow — §6.1/§6.2 admin flows.
Sequence diagram per major flow — §5.1.
State machine diagram — §7.2 (jobs), §7.1 (slugs).
Data side-effect diagram — §9.
Error branch diagram — §10.
Feature Minor Behavior Actor Trigger User/System Result Backend Side Effect Source Category list Sibling listing with deletedOnly Admin GET /categories?deletedOnly=trueRestore workflow Partial index idx_category_parent_order catalog-category-admin.service.tsTree 2,000-node cap Admin GET /tree400 CATALOG_TREE_TOO_LARGE In-app bound, not truncation Move Re-root (parentId: null) Admin PATCH /moveMoves to root One-statement subtree rewrite catalog-category-tree.service.tsRename Undo Admin Rename back Original URL restored Re-adoption of owned slug catalog-slug.service.tsBulk delete Deepest-first ordering Admin POST /bulk/deleteParent+child batch works Sorted by depth desc catalog-category-bulk.service.tsBulk restore Shallowest-first Admin POST /bulk/restoreParent-first ordering Sorted by depth asc Reorder Exact sibling set Admin PATCH /reorder409 on mismatch Set equality check Import Slug-based parents Admin POST /jobs/importNothing written on any error All-or-nothing catalog-import.service.tsExport Formula-injection escape Admin Export download ' prefix on = + - @ cellsCSV escaping catalog-export.service.tsSearch Term < 2 chars Guest ?search=abUnfiltered list "No search" catalog-search.service.tsSearch Cursor stability Guest Any page Stable order ORDER BY ... id tiebreakSlug routes Retired slug Guest Old URL 200 + canonicalSlug Ownership table lookup catalog-slug.service.ts
Rule Business Reason Actor Impact Enforced In API Impact Backend Impact Tests Slug never re-hands to a different entity Old links must stay honest Guest URLs never repoint One UNIQUE over current+retired Storefront resolves any owned slug Slug ownership tables catalog-slug.service.spec.tsDepth capped at 6 Tree stays navigable Admin gets 409 on deep create/move DB CHECK + service Create/move/restore CHECK chk_category_depth_within_cap probe Delete blocked while active children No orphaned visibility Admin must delete children first Service + RESTRICT Delete/bulk delete ON DELETE RESTRICTprobe Hard delete unsupported Delete is always recoverable Nothing permanently removed RESTRICT FKs No hard-delete route FK 23503 probe Visibility inherited Hidden parent hides subtree Guest cannot reach hidden subtree Service + SQL Storefront 404 computeAncestorChainVisiblespec One error code per status Clients branch on status Deterministic errors Review fix PAGINATION_LIMIT_TOO_LARGE = 400 everywhereBadRequestException — Import requires entity-specific create Route-level superset token must not mass-create 403 without entity permission Service Import submit assertCanCreateEntityspec
Product Decision User Benefit Engineering Benefit Alternative Tradeoff Risk Slugs in ownership tables Permanent URLs Unrepresentable re-pointing Slug column + history table More tables — Advisory lock on tree writes Consistent tree Serialised rare writes Application-level checks Throughput on writes Accepted (rare) Soft delete + RESTRICT everywhere Recoverable deletes No cascade surprises Hard delete Storage grows Retention policy Trigram % in WHERE Fast fuzzy search Index-accelerated candidates similarity() in WHEREFull scan if misused Documented rule
Flow Edge Case Trigger Expected Behavior User/System Feedback Source Create Slug collision Name already used Auto-suffix (-2, …) up to 100 attempts 409 after exhaustion catalog-slug.service.tsRename Slug owned by self Rename back Re-adopt, original URL 200 Move Cycle Move under descendant 409 CATEGORY_CIRCULAR_HIERARCHY Move Concurrent Two cross moves Serialised; second sees committed state 200/409 advisory lock Bulk Duplicate ids [X, X, Y]Counted once 409 only if > 100 unique dedupeAndCapBulkIdsReorder Partial set Missing sibling 409 CATALOG_REORDER_INVALID_ITEM Import File-internal cycle parentSlug graph cyclicJob fails, nothing written CATALOG_IMPORT_CYCLE_DETECTEDKahn's algorithm Import Retry Worker error before final attempt Released to queued Real retry lease claim Export Zero rows Empty match Header-only CSV, completed Success Slug resolve No current slug Corrupt state Not found 404 service invariant
Flow Reads Writes Cache Jobs/Events Response Fields Category list/tree category, category_slug— catalog_category — ResponseDto + tree nodes Category write Row + parent + slug ownership category, category_sluginvalidate activity ResponseDto Search category/brand/brand_series + slugs— catalog:search — items + pagination Import Rows, slug ownership Entity + slug + catalog_job invalidate outbox → catalog.import_entities job Export Entity rows catalog_job + CSV— outbox → catalog.export_entities job