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,Databaseindatabase.ts. IfKyselyDbis not exported yet, add once indatabase.ts(export type KyselyDb = Kysely<Database> | Transaction<Database>) — do not redeclare per repo. - One entity per PR.
- Branch = Linear ticket
gitBranchName— notfeature/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-communicationbrands when they exist. - Flat
timestamptz→z.date().isoDate_zodonly 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.