name: podverse-orm-patterns description: Common patterns for the podverse-orm package version: 1.0.0
Podverse ORM Development Patterns
This skill provides quick reference for common patterns used in the podverse-orm package.
Monorepo Context
- ORM package location:
packages/orm/ - Database migrations (canonical):
infra/k8s/base/ops/source/database/linear-migrations/(see docs/operations/LINEAR-MIGRATIONS.md; legacy TypeORM migration paths in this skill may still appear in older snippets) - Helper packages (from
packages/helpers*/):@podverse/helpers,@podverse/helpers-validation,@podverse/helpers-config
Key Dependencies
| Package | Purpose |
|---|---|
| Helper packages | Types, DTOs, validation, config |
typeorm | ORM framework |
Patterns
Entity Definition
// packages/orm/src/entities/Podcast.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { Episode } from './Episode';
@Entity('podcast')
export class Podcast {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column({ nullable: true })
description?: string;
@Column({ unique: true })
feedUrl: string;
@Column({ nullable: true })
imageUrl?: string;
@OneToMany(() => Episode, (episode) => episode.podcast)
episodes: Episode[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Service Pattern
// packages/orm/src/services/PodcastService.ts
import { getRepository } from 'typeorm';
import { Podcast } from '../entities/Podcast';
export const PodcastService = {
async getById(id: string): Promise<Podcast | null> {
const repo = getRepository(Podcast);
return repo.findOne({ where: { id } });
},
async getByFeedUrl(feedUrl: string): Promise<Podcast | null> {
const repo = getRepository(Podcast);
return repo.findOne({ where: { feedUrl } });
},
async create(data: Partial<Podcast>): Promise<Podcast> {
const repo = getRepository(Podcast);
const podcast = repo.create(data);
return repo.save(podcast);
},
async update(id: string, data: Partial<Podcast>): Promise<Podcast | null> {
const repo = getRepository(Podcast);
await repo.update(id, data);
return this.getById(id);
},
async delete(id: string): Promise<void> {
const repo = getRepository(Podcast);
await repo.delete(id);
},
};
varchar lengths (constants vs inline)
- SQL: Keep explicit
VARCHAR(n)in linear migration files; TypeScript cannot be imported there. - TypeScript: When the same semantic max length appears in more than one place (e.g. multiple entities or
entity + API validation), define domain-named numeric constants under
packages/orm/src/lib/(example:feedLifecycleLimits.ts), export them frompackages/orm/src/index.ts, and reference them in@Column({ length: ... })and in apps (e.g. Joi.max(...)). Document in the lib file which migration defines the width. - Inline literals are fine for column widths that are unique to one entity and not duplicated elsewhere.
Query Builder Pattern
// For complex queries
async findWithFilters(filters: PodcastFilters): Promise<Podcast[]> {
const repo = getRepository(Podcast)
const qb = repo.createQueryBuilder('podcast')
if (filters.searchTerm) {
qb.where('podcast.title ILIKE :term', { term: `%${filters.searchTerm}%` })
}
if (filters.category) {
qb.andWhere('podcast.category = :category', { category: filters.category })
}
qb.orderBy('podcast.createdAt', 'DESC')
.skip(filters.offset || 0)
.take(filters.limit || 20)
return qb.getMany()
}
Relations Pattern
// Loading relations
async getByIdWithEpisodes(id: string): Promise<Podcast | null> {
const repo = getRepository(Podcast)
return repo.findOne({
where: { id },
relations: ['episodes']
})
}
// Eager vs Lazy loading
@OneToMany(() => Episode, (episode) => episode.podcast, { eager: false })
episodes: Episode[]
Migration Patterns
Migrations are located in infra/database/main/migrations/.
Creating a Migration
# Generate migration from entity changes
npm run typeorm migration:generate -- -n MigrationName
# Create empty migration
npm run typeorm migration:create -- -n MigrationName
Migration Structure
// infra/database/main/migrations/YYYYMMDDHHMMSS-AddPodcastCategory.ts
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPodcastCategory1234567890123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'podcast',
new TableColumn({
name: 'category',
type: 'varchar',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('podcast', 'category');
}
}
File Structure
packages/orm/
├── src/
│ ├── entities/ # TypeORM entities
│ ├── services/ # Data access services
│ ├── subscribers/ # Entity subscribers
│ └── index.ts # Public exports
├── package.json
└── tsconfig.json
infra/database/main/
├── migrations/ # TypeORM migrations
└── seeds/ # Seed data (if any)
Best Practices
- Always use services: Don't access repositories directly from controllers
- Use transactions: For operations that modify multiple entities
- Index properly: Add indexes for frequently queried columns
- Validate at entity level: Use class-validator decorators when appropriate
- Use DTOs: Transform entities to DTOs before sending to clients
Related Skills
- API Patterns - Using ORM in controllers
- Global Patterns - Monorepo conventions