Skip to content

Imperative (Decorator-free) Definition

defineEntity registers the exact same metadata as the decorators, from the same options, checked by the same types - with one exception: a foreign key declared with references and no type has its column resolved from the referenced primary key either way, but only @Field can also check the property against that key’s type. The options reach defineEntity through a mapped type, which leaves the referenced entity nowhere to be read from. It needs no decorator support in your build at all, which matters where that is awkward: Oxc (Vite 8’s own transformer) implements no decorators, and NestJS apps must keep experimentalDecorators on for their own DI, which rules out UQL’s decorators in the same project. Also the natural choice for generating entities at runtime, or for domain classes you would rather leave unannotated.

The configuration object mirrors the decorator API one to one:

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: ['title', 'authorId'], unique: true }],
filters: {
published: { condition: { publishedAt: { $ne: null } }, default: false },
},
});

Every entry maps directly to a decorator:

Key Decorator equivalent Notes
fields @Field / @Id Same field options; mark the primary key with isId: true instead of @Id.
relations @OneToOne@ManyToMany Same relation options, plus cardinality: '11', '1m', 'm1', or 'mm'.
indexes @Index { columns, name?, unique?, type?, where? }, see Indexes.
hooks @BeforeInsert(), … Maps each lifecycle event to method names, e.g. { beforeInsert: ['stamp'] }.
filters @Filter Same filter options: { condition, default?, security?, onMissing? }.

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: ['title'], unique: true });
defineFilter(Article, 'published', {
condition: { publishedAt: { $ne: null } },
default: false,
});
defineHook(Article, 'stamp', 'beforeInsert');
defineEntity(Article, { name: 'articles' });