Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
508dd3f
docs: add postgres snowflake migration spec
Innei May 1, 2026
28e533f
docs: expand postgres migration schema spec
Innei May 1, 2026
bcdaf76
docs(spec): pin Phase 0 decisions for postgres migration
Innei May 1, 2026
14f5baf
feat(shared/id): add Snowflake generator and EntityId primitives
Innei May 1, 2026
038ffa7
feat(database): add PostgreSQL schema, drizzle provider, migration ta…
Innei May 2, 2026
4a534fb
feat(repository): add base repository and category/topic/page pilots
Innei May 2, 2026
973ed84
feat(repository): add post/note/comment/reader repositories
Innei May 2, 2026
c9ff3a2
docs(handoff): record postgres migration session-1 state
Innei May 2, 2026
b13c35f
feat(repository): add 17 more PG repositories covering ops/ai/auxilia…
Innei May 2, 2026
7c738dc
feat(repository): add search, ai-agent, auth repositories
Innei May 2, 2026
ad4adf9
feat(migration): add Mongo-to-Postgres data migration CLI
Innei May 2, 2026
0204684
docs(handoff): update postgres migration state after second push
Innei May 2, 2026
2f27ecb
feat(migration): fix Mongo→PG refType normalization, add BasePgCrudFa…
Innei May 2, 2026
8e824b0
feat(modules): cut over project + topic to PostgreSQL via BasePgCrudF…
Innei May 2, 2026
3f5cb54
feat(subscribe): cut over SubscribeService and controller to PostgreSQL
Innei May 2, 2026
8207a94
docs(handoff): record wave 1 cutover progress (project, topic, subscr…
Innei May 2, 2026
8a6a11a
feat(say): cut over SayService and controller to PostgreSQL
Innei May 2, 2026
c3f08ab
docs(handoff): record say cutover; update wave 1 module plan
Innei May 2, 2026
ca19fbe
feat(link): cut over Link/LinkAvatar/Crud controllers to PostgreSQL
Innei May 2, 2026
ade5b07
docs(handoff): record link cutover in wave 1 plan
Innei May 2, 2026
c9c6ef7
feat(snippet): cut over Snippet/Serverless to PostgreSQL
Innei May 2, 2026
26687ac
docs(handoff): record session-4 decisions and snippet cutover
Innei May 2, 2026
be52459
feat(repository): add wave 2 batch methods to content repos
Innei May 2, 2026
69f2814
docs(handoff): add wave 2 Pass B execution playbook
Innei May 2, 2026
394ad3d
feat(repository): add wave 2 Pass A.5 prep methods
Innei May 2, 2026
d5e582b
feat(content): cut over post/note/page/comment/category/recently/draf…
Innei May 2, 2026
e2f7076
feat(ops): cut over ai/activity/analyze/configs/cron/file/meta-preset…
Innei May 2, 2026
3e4e62a
feat(auth): switch auth tables to text PKs and swap Better Auth to dr…
Innei May 2, 2026
845f2a3
chore(migration): remove Mongoose runtime and ship PG-only stack
Innei May 2, 2026
66901e1
fix(modules): wire cross-module repository imports for PG cutover (pa…
Innei May 2, 2026
1b88234
fix(modules): wire DI graph for PG-only Nest boot
Innei May 2, 2026
3e05159
fix(test): align translation-consistency spec with sourceModifiedAt r…
Innei May 2, 2026
adcde6a
chore(deps): add tsx devDep for Mongo→PG migration CLI
Innei May 2, 2026
787360d
Merge remote-tracking branch 'origin/master' into codex/postgresql-sn…
Innei May 2, 2026
a113e38
feat(comment-image-upload): port master refactor onto PG stack
Innei May 2, 2026
34789e7
fix(security): redact connection URLs in migration CLI logs
Innei May 2, 2026
d4914c3
fix(ci): approve cpu-features/protobufjs/ssh2 build scripts (deny)
Innei May 3, 2026
39b8c4a
fix(pg): bug fixes uncovered by docker smoke + restore CI tests
Innei May 3, 2026
2b844a7
fix(ci): set MIGRATIONS_DIR for bundle server smoke
Innei May 3, 2026
b9823fb
refactor(api): drop legacy mongoose-shape mappers, unify ref_type to …
Innei May 3, 2026
b5b382e
fix: codex review
Innei May 3, 2026
536f1df
fix(pg): restore migrated endpoint behavior
Innei May 3, 2026
c7a2abf
feat(migration): implement legacy JSONB normalization and worker ID r…
Innei May 3, 2026
4f7058a
feat: add consumer contracts for recently, say, and topic lists
Innei May 3, 2026
55befe5
refactor(core): post-PG migration cleanup & code simplification
Innei May 3, 2026
e480cff
feat: refactor repository and service structures to separate type def…
Innei May 3, 2026
fc886d8
feat(pg): migrate business ids to text snowflakes
Innei May 3, 2026
d3ddc90
fix(comment): hydrate parent preview, ref summary, and migrate reader_id
Innei May 4, 2026
66e6093
fix(migration): translate credential accounts.account_id to snowflake…
Innei May 4, 2026
ac728d2
feat(migration): drop orphan rows and add reader_id FK constraints
Innei May 4, 2026
d785a35
fix(ai): cascade-clean ai_summaries/insights/agent_conversations on c…
Innei May 4, 2026
228eb92
docs: update project documentation for PostgreSQL era and Yohaku fron…
Innei May 4, 2026
4cbfa8e
feat(api-client): expose CommentParentPreview and bump to 4.0.0-next.0
Innei May 4, 2026
21ede5d
feat(migration): update environment variables for PostgreSQL and add …
Innei May 4, 2026
a691eb6
feat(api-client): expose RecentlyRefSummary for attachRef hydration
Innei May 4, 2026
8b4000c
feat(api-client): type CommentAnchor/CommentRef and harmonize images …
Innei May 4, 2026
dcc694e
feat(api-client): make CommentModel.mail optional, add SDK shape cont…
Innei May 4, 2026
cbca4ff
feat(api-client): SDK shape contract for note/page/category, drop Not…
Innei May 4, 2026
5a1c44b
update
Innei May 4, 2026
9151a55
feat(note): enhance note handling with lexical summaries and year fil…
Innei May 4, 2026
8b8aa2c
fix(pg): restore lossy joined fields after mongo-to-pg cutover
Innei May 4, 2026
a5331d7
fix(migration): coerce BSON Double(NaN) to NULL for api_keys integers
Innei May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
161 changes: 161 additions & 0 deletions .claude/skills/mx-pg-controller-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
---
name: mx-pg-controller-migration
description: Use when verifying and porting an mx-core controller (Post/Note/Page/Comment/Category/etc.) after the MongoDB→PostgreSQL cutover, or when its data shape no longer matches what api-client and admin-vue3 expect. Triggers on "校验 controller"、"check controller"、"迁移 controller"、"修复迁移后的接口"、"data missing after PG migration"、"related/category 字段丢了" and similar.
---

# mx-core PG Cutover · Controller Verification & Downstream Sync

## Repos in scope (paths assume worktree root)

| Layer | Path | Concern |
| --- | --- | --- |
| Server | `apps/core/src/modules/<mod>/` | controller / service / repository correctness |
| SDK | `packages/api-client/{models,controllers}/` | type definitions must match server response |
| Dashboard | `/Users/innei/git/innei-repo/admin-vue3/apps/admin/src/{models,api,views/manage-<mod>}/` | consumer code reads new field names |

**No server-side back-compat shim.** If a field rename is correct on PG, propagate it through SDK and dashboard. The user has explicitly opted out of legacy aliases.

**Data completeness IS a bug.** A migration that compiles but silently drops `related`, `category`, or any joined value is broken. Always cross-check what the **old mongoose pipeline emitted** against what the new repository emits.

## Workflow (run in this order)

### 1 · Snapshot the migration delta

Identify the PG cutover commit for the module and diff against the last Mongo-era commit. The mx-core history convention:

```bash
# Last canonical pre-PG commit (refactor: comment module ...)
PRE_PG=58983aef
# PG cutover for content modules
PG_CUT=d5e582ba

git show $PRE_PG:apps/core/src/modules/<mod>/<mod>.controller.ts | head -200
git show $PRE_PG:apps/core/src/modules/<mod>/<mod>.model.ts
git log --oneline $PRE_PG..HEAD -- apps/core/src/modules/<mod>/
```

Look for: `autopopulate`, `aggregate(...$lookup, $project)`, `BaseModel`/`WriteBaseModel` virtuals, `count: { read, like }`, `pin`, `created`, `modified`. Any of these are likely lossy after migration.

### 2 · Walk every endpoint

Read the controller end to end. For each route, ask:

1. **Field names**: does the response shape still include what the dashboard / front-end consumes? (See mapping table below.)
2. **Joined data**: did mongoose emit a `category`, `related`, `topic`, `ref`, … via `populate`/`$lookup`? If yes, does the new repository attach it? Use `attach<Foo>` helpers; never trust that the controller's `(doc as any).related` is populated — the repo is the source of truth.
3. **Aggregate-pipeline order**: old code often did `$project (select)` BEFORE `$lookup`, so `$lookup`-injected fields survived `select` filtering. The PG port frequently inverts that order and silently drops joined fields. **Check `select`-style projections in the controller** — they must whitelist or unconditionally preserve joined fields.
4. **Dead JSON-string parsing**: code like `if (typeof doc.meta === 'string') doc.meta = JSON.safeParse(...)` is dead under `jsonb`. Delete it.
5. **Redundant aliases**: `related: body.relatedId as any` style props that the service no longer reads. Delete.
6. **Cross-cutting enums** (`DraftRefType`, `CollectionRefTypes`, `CommentRefType`, `RecentlyRefTypes`): commit `b9823fb6` unified `ref_type` to **singular lowercase** (`'post' | 'note' | 'page' | 'recently'`). The PG SQL UPDATE migrated existing rows. The dashboard's local enum copies still hold legacy plural/PascalCase values and MUST be re-checked when verifying any module that touches drafts, comments, recently, file-references or ai-translations.

### 3 · Field rename map (Mongo → PG, after snake-case → camel-case round-trip)

| Mongo field | PG field | Notes |
| --- | --- | --- |
| `_id` | *(removed)* | Only `id` (Snowflake bigint as string) exists |
| `created` | `createdAt` | server returns `created_at`, SDK camelcases |
| `modified` | `modifiedAt` | nullable |
| `pin` (Date or null) | `pinAt` | nullable |
| `count: { read, like }` | `readCount`, `likeCount` | flat int columns |
| `commentsIndex`, `allowComment` | *(usually removed)* | check the PG schema; `posts/notes/pages` no longer have them, only `recentlies` does |
| populated `category` / `related` | computed via repo `attach*` | not in the row, must be loaded explicitly |

When in doubt, read `apps/core/src/database/schema/*.ts` — it is authoritative.

### 4 · Fix the server (apps/core)

Typical patches:

- **Repository**: extend `<Mod>Row` with optional joined fields (`category?`, `related?`); add a private `attach<Foo>(rows)` that does **one batched query**, never per-row N+1; wire it into `findById` / `findBySlug` / `find<...>` / `list`.
- **Controller**: when applying `select` whitelisting, force-include joined keys that are not addressable by the query string (`selected.add('id'); selected.add('category')`). Document why with a brief comment.
- **Service / controller**: drop redundant aliases; fold legacy input fields (`created`, `pin`) into their PG counterparts in the write path (the comment in `post.service.ts` after commit `536f1df9` is the reference pattern).

Run, scoped to the module:

```bash
pnpm -C apps/core exec tsc --noEmit
pnpm -C apps/core exec eslint src/modules/<mod>/
```

### 5 · Sync the SDK (packages/api-client)

The SDK type IS the contract. It must reflect the actual server payload after `snake_case → camelCase`.

For each renamed field:

1. Update `models/<mod>.ts` — usually means *not* extending the legacy `TextBaseModel` (which still has `created`/`modified`); flatten the model with PG names instead. Keep `BaseModel`/`TextBaseModel` untouched until the matching module is also being migrated, to avoid touching unrelated SDK types.
2. Grep for `Pick<<Mod>Model, …>` across the SDK — `models/category.ts`, `models/aggregate.ts`, `controllers/<mod>.ts`, `controllers/search.ts` — and rename the picked keys.
3. Update `<Mod>ListOptions.sortBy` literal unions in `controllers/<mod>.ts` to the PG names.

```bash
pnpm -C packages/api-client exec tsc --noEmit # ignore the TS6.0 deprecation noise
```

### 6 · Sync the dashboard (admin-vue3)

The dashboard uses its **own** model copies under `apps/admin/src/models/<mod>.ts` (not the api-client types). Both must be updated.

1. Rewrite `apps/admin/src/models/<mod>.ts` and any cross-module `Pick<...>` (e.g. `models/category.ts → PickedPostModelInCategoryChildren`).
2. Update views under `apps/admin/src/views/manage-<mod>/`:
- Table column `key`s (used by `n-data-table`'s sorter)
- `select` query strings sent to the server
- All `row.<oldField>` reads → `row.<newField>` (search for `row.created`, `row.modified`, `row.pin`, `row.count`, `commentsIndex`, `allowComment`)
3. The reactive form state may keep boolean toggles (`pin: boolean`) — don't change the type, but in `loadPublished` map `payload.pinAt → data.pin = !!payload.pinAt` so the toggle still binds.
4. Components like `<RelativeTime>` require non-null time. For `modifiedAt` (nullable) use `row.modifiedAt ?? row.createdAt`.
5. **Re-check ref-type enums.** Dashboard ships its own copies — these are out of date:
- `apps/admin/src/models/draft.ts → DraftRefType` was `'posts' | 'notes' | 'pages'`, must become `'post' | 'note' | 'page'`.
- `apps/admin/src/models/recently.ts → RecentlyRefTypes` was `'Post' | 'Note' | 'Page'`, must become `'post' | 'note' | 'page' | 'recently'`.
- Anywhere a controller verifies that touches drafts (post/note/page editor pages), recently, comments, or ai-translations: grep the dashboard for the enum, fix values, run typecheck — enum members keep the same names so call sites are unaffected.

```bash
cd /Users/innei/git/innei-repo/admin-vue3 && pnpm -C apps/admin run typecheck
```

(If pnpm version warnings appear, they're unrelated — only `tsc` errors matter.)

## Checklist (run per module)

- [ ] Read `<mod>.controller.ts`, list every route
- [ ] `git show <pre-pg>:.../<mod>.model.ts` — note virtuals, populates, count shape
- [ ] For each route, list (field rename × joined-data × dead-code) issues
- [ ] Patch repository: add `attach<Foo>` + wire into all read paths
- [ ] Patch controller: preserve joined fields under `select`; drop dead `JSON.safeParse(meta)` and redundant aliases
- [ ] `pnpm -C apps/core exec tsc --noEmit`
- [ ] Update `packages/api-client/models/<mod>.ts` + cross-references in `models/category.ts` / `models/aggregate.ts` / `controllers/{<mod>,search}.ts`
- [ ] `pnpm -C packages/api-client exec tsc --noEmit`
- [ ] Update admin-vue3 `models/<mod>.ts`, `models/category.ts`, `views/manage-<mod>/*`
- [ ] If module touches drafts/comments/recently/file-references/ai-translations: re-verify dashboard ref-type enum values are singular lowercase
- [ ] `pnpm -C apps/admin run typecheck` (in admin-vue3 worktree)
- [ ] Eyeball the diff one more time: any `row.created` / `row.pin` / `count?.read` left?

## Common bugs (caught while migrating PostController)

| Symptom | Root cause | Fix |
| --- | --- | --- |
| `related` is always `[]` on detail page | Repo's `findByCategoryAndSlug` / `findById` never call `getRelatedPosts`; controller does `(baseData as any).related ?? []` | Add `attachRelated()`, wire into all read paths |
| Joined `category` disappears after `select=...` | Old aggregate did `$lookup` AFTER `$project`; new code attaches first then filters keys | `selected.add('category')` (and `'id'`) before filtering |
| `sortBy=created` silently does nothing | Repository compares `params.sortBy === 'createdAt'`; dashboard still sends `created` | Either dashboard updates literal, or document failure mode (we chose: update dashboard) |
| `select: 'title _id created modified count pin'` returns nearly empty objects | The select string still uses Mongo names | Update select string to PG names: `'title id createdAt modifiedAt readCount likeCount pinAt'` |
| Edit form loses pin/publish state | `useParsePayloadIntoData` matches by key; reactive holds `pin`, payload has `pinAt` | Map in `loadPublished`: `postData.pin = !!postData.pinAt` |
| `JSON.safeParse(doc.meta)` branch unreachable | `meta` is `jsonb`, drizzle returns object | Delete the branch |

## Red flags — STOP and re-check

- A controller method returns `(doc as any).<something>` — the cast is hiding a missing repo attachment.
- New repository method has `await Promise.all(rows.map(r => this.attach<Foo>(r)))` shape — that's N+1; switch to a batched `attach<Foo>(rows: Row[])`.
- You're tempted to add a "back-compat alias" on the server. Don't. The user has rejected this — propagate the rename downstream instead.
- You changed `BaseModel` / `TextBaseModel` in api-client to fix a single module. Don't — that affects every module that hasn't been migrated yet. Flatten the single model instead.
- A dashboard column's sorter `key` doesn't match a real PG field (e.g. `key: 'count.read'`). Sorting is broken — pick a real key (`'readCount'`) or remove sortability.

## Reference: tools used during PostController pass

```bash
# Find every consumer of a model in the dashboard
grep -rn "<Mod>Model\b" /Users/innei/git/innei-repo/admin-vue3/apps/admin/src --include="*.ts" --include="*.tsx" --include="*.vue"

# Find dashboard accesses to old fields scoped to one module's views
grep -rn "row\.created\|row\.modified\|row\.pin\b\|row\.count\." \
/Users/innei/git/innei-repo/admin-vue3/apps/admin/src/views/manage-<mod> 2>/dev/null

# Reference fix commit for write-side input mapping (created→createdAt, pin→pinAt)
git show 536f1df9 -- apps/core/src/modules/post/post.service.ts
```
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ ENCRYPT_ENABLE=false
CDN_CACHE_HEADER=true
FORCE_CACHE_HEADER=false

# CUSTOM MONGO CONNECTION
MONGO_CONNECTION=
# PostgreSQL
PG_URL=

# Throttle
THROTTLE_TTL=10
Expand Down
48 changes: 41 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,20 @@ jobs:
needs: quality
env:
REDISMS_DISABLE_POSTINSTALL: 1
MONGOMS_DISABLE_POSTINSTALL: 1
services:
mongodb:
image: mongo
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: mx
POSTGRES_PASSWORD: mx
POSTGRES_DB: mx_core
ports:
- 27017:27017
- 5432:5432
options: >-
--health-cmd "pg_isready -U mx -d mx_core"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
Expand All @@ -98,17 +106,37 @@ jobs:
run: npm run bundle
- name: Test Bundle Server
run: bash scripts/workflow/test-server.sh
env:
SNOWFLAKE_WORKER_ID: 1
PG_HOST: 127.0.0.1
PG_PORT: 5432
PG_USER: mx
PG_PASSWORD: mx
PG_DATABASE: mx_core
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
JWT_SECRET: test-bundle-server-jwt-secret
MIGRATIONS_DIR: ${{ github.workspace }}/apps/core/src/database/migrations

test:
name: Test
timeout-minutes: 10
runs-on: ubuntu-latest
needs: quality
services:
mongodb:
image: mongo
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: mx
POSTGRES_PASSWORD: mx
POSTGRES_DB: mx_core
ports:
- 27017:27017
- 5432:5432
options: >-
--health-cmd "pg_isready -U mx -d mx_core"
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
Expand All @@ -129,3 +157,9 @@ jobs:
env:
CI: true
REDIS_BINARY_PATH: /usr/bin/redis-server
SNOWFLAKE_WORKER_ID: 1
PG_HOST: 127.0.0.1
PG_PORT: 5432
PG_USER: mx
PG_PASSWORD: mx
PG_DATABASE: mx_core
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# compiled output
/dist
node_modules
.pnpm-store/

# Logs
logs
Expand Down Expand Up @@ -53,4 +54,9 @@ dist
dev/

.eslintcache
.superpowers
.superpowers

# local docker smoke overrides — never commit
docker-compose.smoke.yml

.pnpm-store/
35 changes: 17 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

MX Space is a personal blog server application built with NestJS, MongoDB, and Redis. This is a monorepo containing the core server application and related packages. The main application is located in `apps/core/`.
MX Space is a personal blog server application (AI-powered headless CMS) built with NestJS, PostgreSQL, and Redis. This is a monorepo containing the core server application and related packages. The main application is located in `apps/core/`.

## Related Projects

- **Dashboard (admin-vue3)**: `../admin-vue3` — 后台管理面板,Vue 3 项目
- **Frontend (Shiroi)**: `../Shiroi` — 主站前端 (Next.js)
- **haklex**: `../haklex` (standalone) / `../Shiroi/haklex` (original host) — Rich editor packages (`@haklex/*`)
- **Frontend (Yohaku)**: `../Yohaku` — 主站前端 (Next.js)
- **haklex**: `../haklex` (standalone) — Rich editor packages (`@haklex/*`)

### Lexical Content Processing

Expand Down Expand Up @@ -68,14 +68,14 @@ pnpm -C apps/core run test:watch
**API Route Prefix**: The `@ApiController()` decorator adds `/api/v{version}` prefix in production but no prefix in development. This allows direct access during development.

**Processors**: Infrastructure services organized in `processors/`:
- `database/` - MongoDB connection and model registration
- `database/` - PostgreSQL connection (Drizzle ORM), repository registry, base repository class
- `redis/` - Redis caching and pub/sub
- `gateway/` - WebSocket gateways for real-time features
- `helper/` - Utility services (email, image, JWT, etc.)
- `helper/` - Utility services (email, image, JWT, Lexical, etc.)

**Database Models**: Uses Mongoose with TypeGoose. All models extend a base with `_id`, `created`, `updated` fields.
**Database**: Uses PostgreSQL 16+ with Drizzle ORM. Schema definitions in `src/database/schema/`. Drizzle SQL migrations in `src/database/migrations/`. IDs are Snowflake `bigint` (serialized as strings at API boundaries). Repositories extend `BaseRepository` and are registered via `repository.tokens.ts`.

**Authentication**: JWT-based with decorators `@Auth()` for route protection and `@CurrentUser()` for accessing the authenticated user.
**Authentication**: Better Auth-based session management with decorators `@Auth()` for route protection and `@CurrentUser()` for accessing the authenticated user. Supports password, OAuth, Passkey, and API key (`x-api-key` header).

## API Response Rules

Expand All @@ -89,21 +89,17 @@ pnpm -C apps/core run test:watch

## Testing

Uses Vitest with in-memory MongoDB and Redis.
Uses Vitest with PostgreSQL testcontainers (`@testcontainers/postgresql`) and Redis memory server.

### E2E Test Pattern
Use `createE2EApp` helper from `test/helper/create-e2e-app.ts`:
Use `createE2EApp` helper from `test/helper/create-e2e-app.ts`. Tests requiring PostgreSQL use `startPgTestContainer()` from `test/helper/pg-testcontainer.ts`.
```typescript
import { createE2EApp } from 'test/helper/create-e2e-app'

const proxy = createE2EApp({
imports: [...],
controllers: [MyController],
providers: [...],
models: [MyModel],
pourData: async (modelMap) => {
// Insert test data
const model = modelMap.get(MyModel)!.model
await model.create({ ... })
}
})

it('should work', async () => {
Expand All @@ -112,14 +108,17 @@ it('should work', async () => {
})
```

### Test Mocks
### Test Helpers
- `test/helper/pg-testcontainer.ts` - Ephemeral PostgreSQL 17 container per test run
- `test/helper/pg-repository-mock.ts` - Repository mock utilities
- `test/helper/redis-mock.helper.ts` - Redis mock
- `test/helper/create-mock-global-module.ts` - Global module mocking
- `test/mock/modules/` - Module-level mocks (auth, redis, gateway)
- `test/mock/processors/` - Processor mocks (email, event)
- `test/helper/` - Test utilities (db-mock, redis-mock)

## Database Migrations

Migration scripts in `src/migration/version/` are version-based and run automatically on startup when needed.
Database migrations use Drizzle Kit. SQL migration files live in `src/database/migrations/` (e.g. `0000_initial.sql`). Historical data migrations from the MongoDB era are in `src/migration/postgres-data-migration/`.

## Configuration

Expand Down
Loading
Loading