Aller au contenu

Migrate TypeORM entity → Kysely + Zod (investor)

One investor TypeORM entity per PR. Follow api-conventions.mdc § Stack Kysely investisseur and postgres/README.md. This skill complements those rules with migration workflow and schema edge cases.

referral-link.repository.ts (BRI-890) predates KyselyDb global + params inserts — new migrations use the conventions below.

Merged PRs index: reference.md.

Prerequisites

  • Foundation BRI-885 merged: ky_safePgTransaction, Database in database.ts. If KyselyDb is not exported yet, add once in database.ts (export type KyselyDb = Kysely<Database> | Transaction<Database>) — do not redeclare per repo.
  • One entity per PR.
  • Branch = Linear ticket gitBranchNamenot feature/migrate-….

Inventory before coding:

rg "from 'src/db/entities/{entity}'|{Entity}Repository|{Entity}" projects/api/src projects/api/scripts projects/api/worker projects/api/tests
rg "from 'src/db/repositories'.*{Entity}|{Entity}Repository" projects/api/src projects/api/scripts projects/api/worker projects/api/tests

Skip scripts_deprecated/. Delete unused legacy db/repositories/{entity}.repository.ts in teardown.

Workflow

- [ ] 0. Linear branch + map columns → Zod
- [ ] 1. Integration test create + read (legacy path) — minimum bar
- [ ] 2. Commit — test green on develop before migration code
- [ ] 3. XxxPgSchema + register table in database.ts
- [ ] 4. Repository — Kysely block then TypeORM block (conventions below)
- [ ] 5. Grep mutations — *WithTypeorm only if caller still in safePgTransaction
- [ ] 6. Migrate TypeORM call sites only
- [ ] 7. Entity logic → {module}/business/
- [ ] 8. Teardown entity + legacy repo + forFeature
- [ ] 9. tsc + biome + knip + re-run create+read test

Templates: reference.md.

Phase 1 — Zod schema

File: projects/api/src/__new/lib/kysely/schemas/{kebab-name}.schema.ts

See reference.md. Key points:

  • Row = z.infer<typeof row>. UUID PK → T = Row. Serial / JSONB + generated → KyselyTableFromZodType.
  • Reuse @bricks-common/api-communication brands when they exist.
  • Flat timestamptzz.date(). isoDate_zod only inside JSON blobs.

Register in database.ts investor section.

Phase 2 — Repository

File: projects/api/src/__new/modules/<module>/repository/{kebab-name}.repository.ts

import { getKysely, type KyselyDb } from 'src/__new/lib/kysely/database'

// Reads (no tx): findOneById(params: { id })
// Kysely writes: insertOneWithKysely(params: { trx: KyselyDb; row })
// TypeORM writes: insertOneWithTypeorm(params: { transac: EntityManager; row })
// In-tx reads/updates: …WithTypeorm(params: { transac, … }) / updateOneWithKysely(params: { trx, … })
Need Pattern
Read outside tx getKysely() + params: { … }
Write in ky_safePgTransaction insertOneWithKysely({ trx, row })
Write in safePgTransaction insertOneWithTypeorm({ transac, row })
Read/update in TypeORM tx …WithTypeorm(params: { transac, … })

trx = Kysely, transac = TypeORM. No dispatch wrapper.

Phase 3 — Call sites

await ky_safePgTransaction(async (trx) => {
  await XxxRepository.insertOneWithKysely({ trx, row })
})

await safePgTransaction(async (transac) => {
  await XxxRepository.insertOneWithTypeorm({ transac, row })
})

One stack per transaction — never mix Kysely and TypeORM in the same tx.

Phase 4 — Tests

Test-first: create + read on legacy path → commit green → migrate → same assertions still pass. See reference.md.

Phase 5 — Teardown

Delete entity + legacy repo; remove from index.ts, forFeature, all-entities.ts.

Validation

cd projects/api
npx tsc -p tsconfig.build.json --noEmit
npx @biomejs/biome check --write <touched files>
doppler run -p api -c dev_integration_tests -- pnpm exec vitest run --config vitest.config.integration.ts <relevant-test-files>

FAQ

Mix Kysely and TypeORM in one tx? No — ky_safePgTransaction*WithKysely, safePgTransaction*WithTypeorm.

Flyway? Only if schema changes.

Additional resources