Scaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.
79
73%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./database/typeorm-starter/SKILL.mdScaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.
npm install typeorm reflect-metadata pg
npm install -D typescript @types/node tsx
# Ensure tsconfig.json has these compiler options:
# "emitDecoratorMetadata": true
# "experimentalDecorators": true
# "strictPropertyInitialization": false (for entity columns)# Synchronize schema (development only — never use in production)
npx typeorm schema:sync
# Generate migration from entity changes
npx typeorm migration:generate -d src/data-source.ts src/migrations/Init
# Run pending migrations
npx typeorm migration:run -d src/data-source.tssrc/
data-source.ts # DataSource configuration (single source of truth)
entities/
user.entity.ts
post.entity.ts
tag.entity.ts
migrations/
1700000000000-InitialSchema.ts
subscribers/
user.subscriber.ts
repositories/
user.repository.ts
lib/
database.ts # Initialize and export DataSourceDataSource config file that is used by both the application and the CLI.@Entity, @Column, @PrimaryGeneratedColumn, etc.).dataSource.getRepository(Entity) — the old getConnection() API is removed in 0.3+.reflect-metadata once at the application entry point before any TypeORM usage.@CreateDateColumn and @UpdateDateColumn for automatic timestamps.QueryBuilder for complex queries; use repository methods for simple CRUD.src/data-source.ts)import "reflect-metadata";
import { DataSource } from "typeorm";
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5432", 10),
username: process.env.DB_USERNAME ?? "app",
password: process.env.DB_PASSWORD ?? "secret",
database: process.env.DB_DATABASE ?? "myapp",
synchronize: false, // Never true in production
logging: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],
entities: ["src/entities/**/*.entity.ts"],
migrations: ["src/migrations/**/*.ts"],
subscribers: ["src/subscribers/**/*.ts"],
});src/lib/database.ts)import { AppDataSource } from "../data-source";
export async function initializeDatabase(): Promise<void> {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
console.log("Database connection established");
}
}
export { AppDataSource };src/entities/user.entity.ts)import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
OneToOne,
Index,
} from "typeorm";
import { Post } from "./post.entity";
import { Profile } from "./profile.entity";
export enum UserRole {
USER = "user",
ADMIN = "admin",
}
@Entity("users")
export class User {
@PrimaryGeneratedColumn("uuid")
id: string;
@Index({ unique: true })
@Column({ unique: true })
email: string;
@Column()
name: string;
@Column({ select: false })
password: string;
@Column({ type: "enum", enum: UserRole, default: UserRole.USER })
role: UserRole;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
profile: Profile;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}src/entities/post.entity.ts)import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
ManyToMany,
JoinTable,
Index,
} from "typeorm";
import { User } from "./user.entity";
import { Tag } from "./tag.entity";
export enum PostStatus {
DRAFT = "draft",
PUBLISHED = "published",
ARCHIVED = "archived",
}
@Entity("posts")
export class Post {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column()
title: string;
@Column({ type: "text", nullable: true })
content: string | null;
@Column({ type: "enum", enum: PostStatus, default: PostStatus.DRAFT })
@Index()
status: PostStatus;
@ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
@Index()
author: User;
@Column()
authorId: string;
@ManyToMany(() => Tag, (tag) => tag.posts)
@JoinTable({ name: "posts_tags" })
tags: Tag[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}src/entities/tag.entity.ts)import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Post } from "./post.entity";
@Entity("tags")
export class Tag {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, (post) => post.tags)
posts: Post[];
}src/repositories/user.repository.ts)import { AppDataSource } from "../data-source";
import { User, UserRole } from "../entities/user.entity";
const userRepo = AppDataSource.getRepository(User);
export async function createUser(data: {
email: string;
name: string;
password: string;
}): Promise<User> {
const user = userRepo.create(data);
return userRepo.save(user);
}
export async function findUserByEmail(email: string): Promise<User | null> {
return userRepo.findOne({
where: { email },
relations: { profile: true },
});
}
export async function findUserWithPosts(userId: string): Promise<User | null> {
return userRepo.findOne({
where: { id: userId },
relations: { posts: true },
});
}
// Query Builder for complex queries
export async function searchUsers(params: {
query?: string;
role?: UserRole;
page: number;
limit: number;
}) {
const qb = userRepo
.createQueryBuilder("user")
.leftJoinAndSelect("user.profile", "profile")
.loadRelationCountAndMap("user.postCount", "user.posts");
if (params.query) {
qb.andWhere("(user.name ILIKE :q OR user.email ILIKE :q)", {
q: `%${params.query}%`,
});
}
if (params.role) {
qb.andWhere("user.role = :role", { role: params.role });
}
const [users, total] = await qb
.orderBy("user.createdAt", "DESC")
.skip((params.page - 1) * params.limit)
.take(params.limit)
.getManyAndCount();
return { users, total, pages: Math.ceil(total / params.limit) };
}src/subscribers/user.subscriber.ts)import {
EventSubscriber,
EntitySubscriberInterface,
InsertEvent,
UpdateEvent,
} from "typeorm";
import { User } from "../entities/user.entity";
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>): void {
if (event.entity.email) {
event.entity.email = event.entity.email.toLowerCase().trim();
}
}
beforeUpdate(event: UpdateEvent<User>): void {
if (event.entity?.email) {
event.entity.email = event.entity.email.toLowerCase().trim();
}
}
}import { AppDataSource } from "../data-source";
import { User } from "../entities/user.entity";
import { Profile } from "../entities/profile.entity";
export async function createUserWithProfile(data: {
email: string;
name: string;
password: string;
bio: string;
}): Promise<User> {
return AppDataSource.transaction(async (manager) => {
const user = manager.create(User, {
email: data.email,
name: data.name,
password: data.password,
});
await manager.save(user);
const profile = manager.create(Profile, {
bio: data.bio,
userId: user.id,
});
await manager.save(profile);
return manager.findOneOrFail(User, {
where: { id: user.id },
relations: { profile: true },
});
});
}# Generate a migration from entity changes
npx typeorm migration:generate src/migrations/MigrationName -d src/data-source.ts
# Create an empty migration (for manual SQL)
npx typeorm migration:create src/migrations/MigrationName
# Run pending migrations
npx typeorm migration:run -d src/data-source.ts
# Revert the last migration
npx typeorm migration:revert -d src/data-source.ts
# Show migration status
npx typeorm migration:show -d src/data-source.ts
# Sync schema (dev only — never in production)
npx typeorm schema:sync -d src/data-source.ts
# Drop all tables
npx typeorm schema:drop -d src/data-source.ts@nestjs/typeorm with TypeOrmModule.forRootAsync() and TypeOrmModule.forFeature([Entity]). Pair with nestjs-project-starter skill.AppDataSource.initialize() in the server bootstrap before handling requests.AppDataSource.query('TRUNCATE ... CASCADE').docker-compose-generator skill for the database service. Run migrations in a startup script or init container.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.