Skip to main content
The @ooneex/entity component is the foundation for domain models in the Ooneex framework. It ships the IEntity interface and the EntityClassType constructor type that every entity satisfies, so repositories, the container, and the database layer can refer to entities in a uniform, type-safe way. The package itself is intentionally tiny and dependency-free; the actual column mapping is done with TypeORM decorators on classes you generate with the CLI.

Why this component

  • One contract for every entity. Each entity exposes a string id through IEntity, so repositories and DI can treat any model uniformly.
  • Type-safe class references. EntityClassType types a constructor that produces an IEntity, letting you pass entity classes around (DI tokens, repository factories) without any.
  • TypeORM-native mapping. Columns, primary keys, timestamps, and relations are declared with standard TypeORM decorators — nothing proprietary to learn.
  • Zero runtime dependencies. The package contributes only types; it adds nothing to your bundle at runtime.
  • Generator-driven. ooneex entity:create scaffolds a ready-to-edit entity plus its test file and registers it in the module.

How it works

The package surface is two declarations:
ExportKindPurpose
IEntityinterfaceBase shape for every entity — requires a string id.
EntityClassTypetypeConstructor type new (...args: any[]) => IEntity for passing entity classes by reference.
export interface IEntity {
  id: string;
}

export type EntityClassType = new (...args: any[]) => IEntity;
An entity is a TypeORM class whose properties map to columns. You declare the table with @Entity, the primary key with @PrimaryColumn, plain columns with @Column, and the audit timestamps with @CreateDateColumn, @UpdateDateColumn, and @DeleteDateColumn. The id is generated up front so an instance always satisfies IEntity before it is persisted.

Usage

A minimal entity needs an id to satisfy IEntity:
import type { IEntity } from "@ooneex/entity";

export class UserEntity implements IEntity {
  id = "";
  name = "";
  email = "";
}
In practice entities are TypeORM classes. Declare the table and map each property to a column:
import { random } from "@ooneex/utils/random";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm";

@Entity({
  name: "users",
})
export class UserEntity {
  @PrimaryColumn({ name: "id", type: "varchar", length: 25 })
  public id: string = random.nanoid(25);

  @Column({ name: "email", type: "varchar", length: 255 })
  public email!: string;

  @Column({ name: "name", type: "varchar", length: 100, nullable: true })
  public name?: string | null;

  @CreateDateColumn({ name: "created_at", nullable: true })
  public createdAt?: Date | null;

  @UpdateDateColumn({ name: "updated_at", nullable: true })
  public updatedAt?: Date | null;

  @DeleteDateColumn({ name: "deleted_at", nullable: true })
  public deletedAt?: Date | null;
}
EntityClassType lets helpers accept any entity class as a value:
import type { EntityClassType } from "@ooneex/entity";

const register = (entityClass: EntityClassType): void => {
  // pass the class to a repository factory, DI token, etc.
  const instance = new entityClass();
  console.log(instance.id);
};

register(UserEntity);
Relations use the standard TypeORM decorators (@ManyToOne, @OneToMany, @ManyToMany, @OneToOne) alongside the columns.

Best practices

  • Always give entities a string id. Satisfy IEntity by generating the id at construction (for example with random.nanoid(25)), so an instance is valid before it is saved.
  • Map every property explicitly. Pass name, type, and length to @Column so the database schema is predictable rather than inferred.
  • Mark nullable columns nullable: true and type them | null. Keep the TypeScript type and the database constraint in sync.
  • Use the audit columns. Keep @CreateDateColumn, @UpdateDateColumn, and @DeleteDateColumn for created/updated tracking and soft deletes.
  • Reference classes with EntityClassType. When passing an entity class around, type it as EntityClassType instead of Function or any.
  • Generate, then trim. Scaffold with entity:create and remove the columns you do not need rather than starting from a blank file.

CLI command

Scaffold an entity class and its test file with the generator. It writes the class under modules/<module>/src/entities/<Name>Entity.ts, a spec under modules/<module>/tests/entities/<Name>Entity.spec.ts, and registers the class in the module’s entities array.
# Interactive: prompts for the name
ooneex entity:create

# Provide the name
ooneex entity:create --name=User

# Target a module and set the table name
ooneex entity:create --name=User --module=auth --table-name=users
OptionDescriptionDefault
--nameEntity class name. Normalized to PascalCase; the Entity suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--table-nameDatabase table name.snake_case plural of the name (e.g. Userusers)
--overrideOverwrite an existing entity without prompting.false
The generated entity comes pre-populated with common columns (locking, blocking, visibility, locale, and audit timestamps) ready for you to keep, adjust, or remove:
import type { LocaleType } from "@ooneex/translation";
import { random } from "@ooneex/utils/random";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm";

@Entity({
  name: "users",
})
export class UserEntity {
  @PrimaryColumn({ name: "id", type: "varchar", length: 20, nullable: false })
  id: string = random.id();

  @Column({
    name: "is_locked",
    type: "boolean",
    default: false,
    nullable: true,
  })
  isLocked?: boolean | null;

  @Column({ name: "locked_at", type: "timestamptz", nullable: true })
  lockedAt?: Date | null;

  @Column({
    name: "is_blocked",
    type: "boolean",
    default: false,
    nullable: true,
  })
  isBlocked?: boolean | null;

  @Column({ name: "blocked_at", type: "timestamptz", nullable: true })
  blockedAt?: Date | null;

  @Column({ name: "block_reason", type: "text", nullable: true })
  blockReason?: string | null;

  @Column({ name: "is_public", type: "boolean", default: true, nullable: true })
  isPublic?: boolean | null;

  @Column({ name: "lang", type: "varchar", length: 10, nullable: true })
  lang?: LocaleType | null;

  @CreateDateColumn({ name: "created_at", nullable: true })
  createdAt?: Date | null;

  @UpdateDateColumn({ name: "updated_at", nullable: true })
  updatedAt?: Date | null;

  @DeleteDateColumn({ name: "deleted_at", nullable: true })
  deletedAt?: Date | null;
}
See entity:create for the full command reference.

Use with Claude and Codex

The generator ships a matching entity:create skill. It runs the scaffold and then guides your AI agent through completing the entity — adding the columns and relations the model needs, removing the scaffolded ones that do not apply, and finishing the test file. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a User entity with email and name columns.
For example, the prompt above maps to entity:create --name=User, then adds email and name columns and trims the scaffolded ones.