Imperative Definition
defineEntity takes the same options as the decorators and registers identical metadata, checked the same way bar one foreign-key case. Nothing is decorated, so no decorator syntax reaches your build.
Reach for it when:
- Your transformer implements no decorators, like Oxc (Vite 8’s own).
- You are in a NestJS app, which must keep
experimentalDecoratorson for its own DI. That rules out UQL’s standard decorators in the same project. - You run the CLI on plain
node. Decorators are not erasable syntax, so auql.config.tsthat imports decorated entities needsbunornode --import tsx. AdefineEntityentity file is erasable, so type stripping alone loads it. - You generate entities at runtime, or want to leave domain classes unannotated.
- You are writing JavaScript, where a decorator is a
SyntaxErrorunless Bun or a transpiler gets to the file first. AdefineEntitycall just runs.
Two forms, one registry (@Entity itself calls defineEntity):
| Form | Use it when |
|---|---|
defineEntity(Class, opts) |
The whole shape is known: the options are checked against the properties the class declares. |
defineField(Class, 'name', opts) and friends |
The shape arrives column by column, or is only known at runtime. |
Using defineEntity
Section titled “Using defineEntity”import { v7 as uuidv7 } from 'uuid';import { defineEntity } from 'uql-orm';
export class User { id?: string; name?: string; email?: string;}
export class Post { id?: number; title?: string; authorId?: string; author?: User; publishedAt?: Date;}
defineEntity(User, { fields: { id: { type: 'uuid', isId: true, onInsert: uuidv7 }, name: { type: String, index: true }, email: { type: String, unique: true, comment: 'User login email' }, },});
defineEntity(Post, { fields: { id: { type: Number, isId: true }, title: { type: String, nullable: false }, authorId: { references: () => User }, publishedAt: { type: Date, nullable: true }, }, relations: { author: { cardinality: 'm1', entity: () => User }, }, indexes: [{ columns: (post) => [post.title, post.authorId], unique: true }], filters: { published: { condition: { publishedAt: { $ne: null } }, default: false }, },});Every entry of either form has a decorator equivalent:
| Key | Decorator equivalent | Notes |
|---|---|---|
name |
@Entity({ name }) |
Custom table name. Defaults to the class name, so name it explicitly if your build minifies. |
fields |
@Field / @Id |
Same field options; mark the primary key with isId: true instead of @Id. |
relations |
@OneToOne, @OneToMany, @ManyToOne, @ManyToMany |
Same relation options, plus cardinality: '11', '1m', 'm1', or 'mm'. |
indexes |
@Index |
{ columns: (post) => [post.title], name?, unique?, type?, where? }, see Indexes. |
hooks |
Hook decorators | Maps each lifecycle event to the methods it runs: { beforeInsert: (post) => [post.stamp] }. |
filters |
@Filter |
Same filter options: { condition, default?, security?, onMissing? }. |
extends |
class Child extends Base |
The base to inherit fields, relations, hooks and filters from. See Inheritance. |
Two things the decorators check go unchecked here. A foreign key declared with references and no type resolves its column type from the referenced primary key under both APIs, but only @Field also checks the property’s own type against that key, so authorId?: number pointing at a uuid key compiles here and not there. And where @Id refuses a key the type level cannot name, isId: true does not: brand an unconventional or composite key yourself, or every by-id method is typed against the wrong column.
Incremental registration
Section titled “Incremental registration”For dynamic schemas, register piece by piece with defineField, defineId, defineRelation, defineIndex, defineFilter, and defineHook, then call defineEntity last. It validates the metadata (fields present, exactly one primary key) and finalizes the entity:
import { v7 as uuidv7 } from 'uuid';import { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, type HookContext,} from 'uql-orm';
class Article { id?: string; title?: string; authorId?: string; author?: User; createdAt?: Date; publishedAt?: Date;
stamp(_ctx: HookContext): void { this.createdAt = new Date(); }}
defineId(Article, 'id', { type: 'uuid', onInsert: uuidv7 });defineField(Article, 'title', { type: String, nullable: false });defineField(Article, 'createdAt', { type: Date });defineField(Article, 'publishedAt', { type: Date, nullable: true });defineRelation(Article, 'author', { cardinality: 'm1', entity: () => User });defineIndex(Article, { columns: (article) => [article.title], unique: true });defineFilter(Article, 'published', { condition: { publishedAt: { $ne: null } }, default: false,});defineHook(Article, 'stamp', 'beforeInsert');defineEntity(Article, { name: 'articles' });Both APIs write to the same metadata registry, so you can mix styles within one project, and everything downstream (querying, migrations, the HTTP transport) behaves identically.
A schema defined at runtime
Section titled “A schema defined at runtime”When the shape is data rather than source (a CMS content type an admin creates, a tenant whose columns are rows in a table), the same call registers it and sync({ entity }) gives it a table. See Runtime Schemas.