zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Start

From TypeORMSupported

TypeORM's Active Record and Data Mapper patterns both assume entity instances with behaviour. zmdb has neither, so the migration is a rewrite of the data layer's shape, not a find-and-replace.

Entity class → entity interface#

// TypeORM
@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn() id: number;
  @Column({ unique: true }) email: string;
  @CreateDateColumn() createdAt: Date;
}
// zmdb
import type { HasDefault, PrimaryKey, Serial, Sql, Table, Unique } from '@zmdb/core/tags';

export interface User extends Table<'users'> {
  id: number & Sql<'integer'> & Serial & PrimaryKey;
  email: string & Sql<'text'> & Unique;
  createdAt: Date & Sql<'timestamp'> & HasDefault;
}

The decorators become intersection tags on the property, and the class becomes an interface. The differences that will bite during a port:

@CreateDateColumn / @UpdateDateColumn have no equivalent. createdAt is HasDefault plus a SET DEFAULT now() in the DDL; updatedAt is either a database trigger or a value you set in a lifecycle hook — explicitly, in your code, where you can test it. See Timestamp defaults.

Repository#

TypeORM's Repository<T> is the closest thing in either library, so this part maps cleanly:

TypeORMzmdb
repo.findOneBy({ id })repo.findById(id)
repo.find({ where: { age: MoreThan(18) } })repo.find({ age: { gt: 18 } })
repo.find({ relations: ['posts'] })repo.findAll({ populate: ['posts'] })
repo.save(entity)repo.create(dto) or repo.update(id, patch)
repo.remove(entity)repo.delete(id)
repo.createQueryBuilder()createQueryCompiler(dialect)

save() splitting into create and update is deliberate: save decides insert-versus-update from whether the id is set, which is exactly the ambiguity that produces accidental inserts.

Active Record goes away#

User.find(), user.save(), user.remove() have no equivalent. Rows are plain objects with no methods. See Why fetched rows are inert.

Relations#

@ManyToOne / @OneToMany / @JoinTable become entries in a relations map:

import type { ManyToOne, OneToMany } from '@zmdb/core/tags';

export interface Post extends Table<'posts'> {
  authorId: number & Sql<'integer'> & References<'users.id'>;
  author?: User & ManyToOne<'users', 'authorId'>;
  comments?: Comment[] & OneToMany<'comments', 'postId'>;
}

The tag names the target table and join column. The declared property type carries the cardinality: User & … is to-one, while Comment[] & … is to-many.

Keep relation properties optional. Entity<T> omits them, and a returned row contains the relation only when it was requested. populate: ['author'] checks the name against the declaration and batches the query from the same tag.

eager: true has no equivalent — that is lazy loading with the switch flipped, and both are excluded. Ask for what you want with populate. See Loading Strategies.

cascade: true has no application-level equivalent: zmdb does not walk an object graph and persist or remove related rows. Database ON DELETE and ON UPDATE actions are supported through the declaration — see Cascading.

Migrations#

TypeORM's migration:generate diffs entities against the live database. zmdb diffs the declarations against a committed snapshot file, and never reads the database to work out what to do:

const ops = diff(JSON.parse(readFileSync('migrations/snapshot.json', 'utf8')), snapshot([schemaOf<User>(), schemaOf<Post>()]));

That means generation works offline and in CI, and the snapshot is a reviewable artefact in the diff. A separate library workflow can now read the live catalog and emit declarations, and detectDrift() reports the two directions. The check command has not landed; see pull.

synchronize: true has no equivalent#

Emitting DDL directly from the declarations is push, and it is a script you run knowingly, not a config flag that runs at boot.

Connection / DataSource#

new DataSource({...}).initialize() becomes a Driver:

const driver: Driver = { execute: q => pool.query(q.text, [...q.parameters]).then(r => r.rows) };

Pooling, retries and TLS are your pool's job, not the ORM's. See Writing a Driver.

Things TypeORM has that zmdb does not#

---

See also: Repository · Migrations · Anti-patterns