Catalog Backend Documentation
Developer Resources Catalog Catalog Backend Documentation Backend architecture, data model, services, cache, queues, runtime rules, and operational behavior for the Catalog module.
Area Files Inspected Verified Details Module wiring catalog.module.ts, catalog-{admin,shared,worker}.module.ts, customer/catalog-customer.module.tsAggregate + leaf composition, shared services Controllers admin/{category,brand,brand-series,job}/*.controller.ts, bulk controllers, customer/**/*.controller.tsRoute ownership, guards, status codes, registration order Services catalog-slug.service.ts, catalog-search.service.ts, catalog-category-{write,admin,bulk,tree}.service.ts, catalog-brand*.service.ts, catalog-import-*.ts, catalog-export.service.tsTransactions, locks, slug ordering, all-or-nothing import DTOs dto/*.ts under each leafValidation and defaults Schema packages/db/src/schema/catalog/*.tsCHECKs, partial indexes, slug tables Jobs packages/jobs/src/index.ts, workers/*Queue contracts, lease claim, sweep Cache cache-invalidation.tags.tsDomains, patterns, TTL
Category tree (adjacency list + depth, max 6), brand and brand series CRUD, bulk and reorder.
Slug ownership for all three entities (current + retired under one unique).
Storefront reads under /api/mobile/catalog/* and search over all three entities.
Catalog import/export jobs with lease-based claims and cooperative cancellation.
The tag entity (flat facet taxonomy).
Product membership (product_tag_link lives in products; tag never references products).
The outbox (generic infra), the money representation, the product domain.
The movement of tag-related filtering — products consumes tags, catalog does not know products exist.
Concern Source of Truth Notes Runtime state PostgreSQL (category, brand, brand_series, *_slug, catalog_job) Slug history Ownership tables — unique across current and retired Tree legality DB CHECKs (depth, root consistency) + advisory lock Job state catalog_job row + lease claim
Module Type Path Controllers Providers Exports Responsibility CatalogModuleAggregate catalog.module.tsNone — Leaf modules Composes admin/customer/worker CatalogAdminModuleAggregate catalog-admin.module.tsNone — Leaf modules Composes the four admin leaves CatalogCategoryAdminModuleLeaf admin/category/CatalogCategoryAdminController, CatalogCategoryBulkControllerServices Services Category surface CatalogBrandAdminModuleLeaf admin/brand/CatalogBrandAdminController, CatalogBrandBulkControllerServices Services Brand surface CatalogBrandSeriesAdminModuleLeaf admin/brand-series/+ bulk controller Services Services Series surface CatalogJobAdminModuleLeaf admin/job/CatalogJobAdminControllerService — Jobs surface CatalogCustomerModuleAggregate customer/None — Leaves Composes the storefront leaves CatalogCategoryTreeCustomerModuleLeaf customer/category-tree/CatalogCategoryTreeCustomerControllerCatalogCategoryTreeCustomerServiceService Storefront navigation tree, one statement CatalogSharedModuleLeaf shared/None CatalogSlugService, CatalogSearchServiceBoth Shared domain services CatalogWorkerModuleLeaf catalog-worker.module.tsNone Processors/handlers — Import/export + sweep
Bulk controllers are registered before their :publicId siblings — both mount the same base path and Nest matches in registration order, so bulk/reorder never get swallowed as UUIDs (verified live: unauthenticated bulk → 401, not 400).
apps/api/src/modules/catalog/
catalog.module.ts
catalog-admin.module.ts
catalog-shared.module.ts
catalog-worker.module.ts
admin/
category/ catalog-category-{admin,bulk,write,tree}.service.ts + controllers + dto/
brand/ catalog-brand-{admin,bulk}.service.ts + controllers + dto/
brand-series/ catalog-brand-series-{admin,bulk}.service.ts + controllers + dto/
job/ catalog-job-admin.{controller,service}.ts + dto/
customer/
category/ category-tree/ brand/ brand-series/ search/ tag/
shared/
catalog-slug.service.ts # slug generation, ownership, rename, resolve
catalog-search.service.ts # pg_trgm search
catalog.constants.ts # lock key, caps, TTLs
catalog-bulk.util.ts # dedupeAndCapBulkIds
catalog-activity-actions.ts # typed activity literals
catalog-db-executor.type.ts
import-export/
catalog-import-{parser,row,service}.ts
catalog-export.service.ts
workers/
catalog-import.processor.ts catalog-export.processor.ts
catalog-job-state.ts catalog-job-sweep.{processor,scheduler}.ts
Key files:
File Purpose Key Exports Notes shared/catalog-slug.service.tsSlug ownership for all three entities CatalogSlugServiceRename is demote-then-promote, one transaction shared/catalog-search.service.tsShared trigram search CatalogSearchService% bounds, similarity() ranksshared/catalog.constants.tsTunables with why-comments CATALOG_TREE_LOCK_KEY, capsadmin/category/catalog-category-tree.service.tsTree mechanics CatalogCategoryTreeServiceLock, cycle checks, one-statement move customer/category-tree/catalog-category-tree-customer.service.tsStorefront navigation tree CatalogCategoryTreeCustomerServiceONE UNION ALL statement so roots and children come from one snapshot; caps enforced in SQL, never a silent truncation import-export/catalog-import.service.tsImport orchestration CatalogImportServiceAll-or-nothing, terminal write in same tx workers/catalog-job-state.tsJob transitions claimCatalogJob, completeImportJob, …Zero-row guard throws
packages/db/src/schema/catalog/
category.ts brand.ts brand-series.ts catalog-job.ts enums.ts tag.ts
Table Purpose Key points categoryTree node parent_id + depth; 6 CHECKs; 8 indexes incl. trgmcategory_slugSlug ownership UNIQUE(slug) across current+retired; partial UNIQUE WHERE is_current; RESTRICT brandFlat list CHECKs: order non-negative, alt-requires-url; trgm indexes brand_slugSlug ownership same shape as category_slug brand_seriesSeries under brand brand_id FK RESTRICT; trgm indexesbrand_series_slugSlug ownership same shape catalog_jobJob rows 6 CHECKs; status enum; lease claim tagFlat facet slug unique among live rows only; no history table
Constraint Table What it prevents chk_category_depth_within_capcategorydepth outside 0..6 chk_category_depth_matches_parentcategoryparent row claiming root depth chk_category_display_order_non_negativecategorynegative order chk_category_no_self_parentcategoryself-parenting chk_category_image_alt_requires_urlcategoryalt without url chk_*_slug_retired_consistentslug tables is_current=false without retired_atchk_*_slug_formatslug tables non-route-safe slug uq_*_slug_current (partial unique)slug tables two current slugs per owner chk_catalog_job_row_counts_sanecatalog_jobfailed ≤ processed ≤ total chk_catalog_job_total_rows_knowncatalog_jobcompleted without total_rows chk_catalog_job_terminal_has_finished_atcatalog_jobterminal status without finished_at chk_catalog_job_timestamps_orderedcatalog_jobfinished_at < started_atchk_catalog_job_source_file_matches_kindcatalog_jobimport without source file chk_catalog_job_result_matches_statuscatalog_jobresult URL except on completed export all FKs all ON DELETE RESTRICT — hard delete unsupported
Method Called By Reads Writes Side Effects Errors generateUniqueSlug()create/update paths ownership table — — CATALOG_SLUG_GENERATION_FAILEDclaimSlug()create paths — slug row — — rename()update paths current slug demote + promote — *_SLUG_ALREADY_EXISTSresolve()storefront ownership table — — null → 404 by caller
Ordering note: demote-then-promote is not negotiable — uq_*_slug_current is a partial unique index and can never be DEFERRABLE.
Owns lock acquisition (pg_advisory_xact_lock, key 4_812_001, first statement in tx), cycle detection (recursive CTEs with CYCLE clauses), depth validation for subtree moves, and the one-statement subtree move (UPDATE ... FROM rewriting parent_id + depth together — two statements fail 100% at root boundaries because the CHECK is immediate).
UNION ALL over three branches; % operator in WHERE (index-accelerated), similarity() only in ORDER BY; ORDER BY rank DESC, display_order ASC, id ASC for stable pagination; visibility enforced in SQL (ancestor CTE for categories, brand join for series); cached under catalog:search: with CACHE_TTL.STANDARD (300s); terms > 64 chars skip the cache.
Import: parse (CSV/XLSX streaming), validate every row, detect file-internal category cycles (Kahn's algorithm), then one transaction : inserts + terminal job write (completeImportJob last). Cancellation check every 500 rows inside the transaction. Export: count-guard at 50,000 rows, CSV with formula-injection escaping, result_file_url + completed in one UPDATE.
Domain Revalidation tags Redis patterns catalog_categorycatalog:categories, page:catalogcatalog:category:*, catalog:search:*catalog_brandcatalog:brands, page:catalogcatalog:brand:*, catalog:series:*, catalog:search:*catalog_brand_seriescatalog:series, page:catalogcatalog:series:*, catalog:brand:*, catalog:search:*
A brand write clears series keys (series visibility depends on the brand) and vice versa; every domain clears search. Activity logging and cache invalidation run after commit .
Queue Jobs Contract CATALOGcatalog.import_entities, catalog.export_entities{ jobPublicId, entity, sourceFileUrl } / { jobPublicId, entity, filters }
Lease-based claim (queued OR processing past 15-min TTL); retries are real (release to queued before final attempt).
Cooperative cancellation: import re-checks its job status every 500 rows; completeImportJob throws CATALOG_JOB_NOT_PROCESSING on a zero-row match.
Sweep (maintenance.sweep_stalled_catalog_jobs, every 5 min) fails stalled processing jobs; isStalled derived flag on detail.
CATALOG is registered by its own module — not in REGISTERED_QUEUES; job options set explicitly at .add() (attempts 3, exponential backoff 5s).
Admin: JwtAuthGuard + RoleGuard; permissions Categories_*, Brands_*, BrandSeries_*, Catalog_*, Tags_*; superadmin bypasses.
Import submit re-checks the entity-specific create permission (route-level Catalog_CREATE is a superset).
Rate limits: ADMIN_READ 30/min, ADMIN_WRITE 10/min, ADMIN_BULK_WRITE 5/min, ADMIN_REORDER 30/min, ADMIN_ASYNC_JOB_SUBMIT 10/hour, PUBLIC_READ 60/min, PUBLIC_SEARCH 60/min, PUBLIC_SUGGEST 300/min (guest-facing).
Storefront never exposes integer PKs; slugs are ownership-table resolved.
Catalog Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the Catalog module.
Complete API contracts for the Catalog module, including routes, auth, DTOs, responses, errors, examples, and integration notes.