---
title: Category
tags: [data-model, mongoose, postgres]
aliases: [Category Model, Taxonomy, ICategory]
---

# Category

Hierarchical taxonomy node used by [[PurchaseRequest]] and [[RequestTemplate]]. Each row is bilingual (`name` in the local language, `nameEn` in English), supports parent/child via `parentId`, has an icon and a display `order`, and an `isActive` toggle for soft-removal.

> [!note] Source
> `backend/src/models/Category.ts:15` — schema definition
> `backend/src/models/Category.ts:64` — model export
> `backend/src/services/marketplace/categoryStore.ts:89` — Postgres runtime bootstrap and duplicate cleanup
> `backend/src/db/schema/category.ts:88` — Drizzle active normalized-name unique index

## Schema

| Field | Type | Required | Default | Validation | Index | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `name` | String | yes | — | trim | yes | Local language name. |
| `nameEn` | String | yes | — | trim | unique | English name. |
| `description` | String | no | — | trim | — | Description. |
| `icon` | String | no | — | trim | — | Icon identifier / URL. |
| `isActive` | Boolean | no | `true` | — | yes | Active flag. |
| `parentId` | ObjectId → [[Category]] | no | `null` | — | yes | Parent category (`null` for top level). |
| `order` | Number | no | `0` | — | — | Display order. |
| `createdAt` | Date | auto | — | — | — | Mongoose timestamp. |
| `updatedAt` | Date | auto | — | — | — | Mongoose timestamp. |

## Virtuals

None defined.

## Indexes

Defined at `backend/src/models/Category.ts:55-62`:

- `{ name: 1 }`
- `{ nameEn: 1 }`, unique
- `{ isActive: 1 }`
- `{ parentId: 1 }`

Postgres runtime and Drizzle additionally enforce:

- `categories_legacy_object_id_uq`: unique Mongo bridge id for idempotent backfill/upsert.
- `categories_active_name_norm_uq`: unique active category display label using `lower(btrim(name)) WHERE is_active = true`.
- Existing duplicate active PG rows are deactivated before the unique index is created; purchase-request category references and child category parents are repointed to the kept row.

## Pre/Post Hooks

None declared.

## Instance Methods

None defined.

## Static Methods

None defined.

## Relationships

- **References**: [[Category]] (self, via `parentId`).
- **Referenced by**: [[PurchaseRequest]] (`categoryId`), [[RequestTemplate]] (`categoryId`).

## State Transitions

No status field — only the `isActive` boolean for soft-disable.

```mermaid
stateDiagram-v2
    [*] --> active
    active --> inactive : admin disables
    inactive --> active : admin re-enables
```

## Common Queries

```ts
// Top-level categories; runtime store dedupes active rows by normalized display name.
Category.find({ parentId: null, isActive: true }).sort({ order: 1 });

// Children of a category
Category.find({ parentId, isActive: true }).sort({ order: 1 });

// Bilingual search
Category.find({ $or: [{ name: regex }, { nameEn: regex }], isActive: true });

// Full tree (basic, two-level)
const roots = await Category.find({ parentId: null }).sort({ order: 1 });
const children = await Category.find({ parentId: { $in: roots.map(r => r._id) } });
```

Related: [[PurchaseRequest]], [[RequestTemplate]].
