Common patterns for the podverse-orm package
Quick reference for @podverse/orm (packages/orm/). Podverse uses TypeORM 1.x with linear SQL migrations only ā not the TypeORM CLI.
@podverse/orm ā entities, services, createORMContext, shared find-option helpersinfra/k8s/base/ops/source/database/linear-migrations/make db_regen_linear_baseline when baselines must be regenerated (see AGENTS.md / linear-baseline rule)@podverse/helpers, @podverse/helpers-validation, @podverse/helpers-configApps call createORMContext(config) from @podverse/orm, then initialize read and read-write sources separately:
import { createORMContext } from '@podverse/orm';
const orm = createORMContext(ormConfig);
await orm.dataSourceRead.initialize();
await orm.dataSourceReadWrite.initialize();
// shutdown
await orm.dataSourceRead.destroy();
await orm.dataSourceReadWrite.destroy();
getDataSourceRead() / getDataSourceReadWrite() internally.apps/management-api/src/index.ts.manager.findOne(Queue, { where: ⦠}), not string entity names.Use a DataSource instance or service base classes ā never import repository accessors from the top-level typeorm package (use dataSource.getRepository(Entity) on an initialized source).
import { getDataSourceRead } from '@podverse/orm';
import { Clip } from '@podverse/orm';
const repo = getDataSourceRead().getRepository(Clip);
await repo.findOne({ where: { id_text: idText } });
BaseManyService / BaseOneService wire repositories from context in their constructors.
TypeORM v1 requires object relations and select ā not string arrays.
import { IsNull } from 'typeorm';
import type { FindOptionsRelations, FindOptionsSelect } from '@podverse/orm';
const relations: FindOptionsRelations<Account> = {
account_profile: true,
account_membership: { account_membership_status: true },
};
const select: FindOptionsSelect<Account> = {
id: true,
id_text: true,
email: true,
};
await repo.find({
where: {
email,
deleted_at: IsNull(),
},
relations,
select,
});
Nested paths from dot strings: use findOptionsRelationsFromPaths / mergeFindOptionsRelations from @podverse/orm when converting legacy path lists.
Optional filters: do not pass undefined in where (v1 throws). Omit keys or build the object conditionally:
where: {
...(optionalName !== undefined ? { name: optionalName } : {}),
}
Do not set invalidWhereValuesBehavior to ignore null/undefined on the DataSource.
Relation-object where + nullable columns: TypeORM v1 can reject relation-object where clauses when a
joined relation includes nullable columns (for example channel.slug). Prefer matching by non-nullable relation
keys (typically id) inside relation-object where clauses. If you need nullable-relation predicates, move the
filter to QueryBuilder joins/conditions instead of relation-object where.
await getDataSourceReadWrite().transaction(async (manager) => {
const queue = await manager.findOne(Queue, { where: { id_text: queueIdText } });
await manager.save(QueueResource, partial);
});
Prefer dataSource.transaction or manager.transaction inside an existing unit of work.
For complex filters, use the repository or manager QueryBuilder with explicit aliases:
const rows = await repo
.createQueryBuilder('clip')
.innerJoin('clip.item', 'item')
.where('item.channel_id = :channelId', { channelId })
.orderBy('clip.id', 'DESC')
.getMany();
Use entity classes in findOne / update / delete ā not string table names.
.sql file under linear-migrations/ (app or management tree per table).make db_regen_linear_baseline, then make db_verify_linear_baseline).Do not use npm run typeorm migration:*, infra/database/main/migrations/, or TypeORM MigrationInterface classes in this repo.
Use SnakeNamingStrategy from @podverse/orm on every DataSource that maps entities:
import { SnakeNamingStrategy } from '@podverse/orm';
namingStrategy: new SnakeNamingStrategy(),
Relation<T> on the property; @ManyToOne / @JoinColumn with explicit name.id (number) + id_text (public string).import type { Relation } from 'typeorm';
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity('clip')
export class Clip {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', unique: true, length: NANO_ID_V2_MAX_LENGTH })
id_text!: string;
@ManyToOne('Account', (account: Account) => account.id, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'account_id' })
account!: Relation<Account>;
}
Business data access lives in classes under packages/orm/src/services/:
AccountService ā standalone class with read/write reposClipService extends BaseManyService<Clip, 'account'> ā parent-scoped CRUDpackages/orm/src/index.ts; apps import @podverse/ormControllers and workers should call services, not repositories directly.
@podverse/ormPrefer importing find-option types from the package (keeps apps aligned with the ORM version):
import type {
FindManyOptions,
FindOptionsRelations,
FindOptionsSelect,
FindOptionsWhere,
} from '@podverse/orm';
EntityManager is re-exported from packages/orm/src/lib/typeORMTypes.ts.
varchar lengthsVARCHAR(n) in linear migration files.packages/orm/src/lib/ when reused (entity + validation); see feedLifecycleLimits.ts.packages/orm/src/
āāā entities/
āāā services/
āāā lib/ # snakeNamingStrategy, findOptionsRelationsFromPaths, limits
āāā factory.ts # createORMContext
āāā context.ts # getDataSourceRead / ReadWrite
āāā index.ts
infra/k8s/base/ops/source/database/linear-migrations/
āāā app/
āāā management/