Skip to content
NewFrom Mongoose to SQL6 min read

Imperative Definition

defineEntity defines an entity without decorators. It takes the same options as the decorators and registers the same metadata, so the two styles mix freely in one project.

import { defineEntity } from 'uql-orm';
import { v7 as uuidv7 } from 'uuid';
export class User {
id!: string;
email?: string | null;
}
export class Post {
id!: number;
title!: string;
authorId?: string | null;
author?: User;
publishedAt?: Date | null;
}
defineEntity(User, {
fields: {
id: { type: 'uuid', isId: true, onInsert: uuidv7 },
email: { type: String, unique: true },
},
});
defineEntity(Post, {
fields: {
id: { type: Number, isId: true },
title: { type: String, nullable: false },
authorId: { references: () => User },
publishedAt: { type: Date },
},
relations: {
author: {
cardinality: 'm1',
entity: () => User,
references: (post) => post.authorId,
},
},
indexes: [{ columns: (post) => [post.title, post.authorId], unique: true }],
filters: {
published: { where: { publishedAt: { $ne: null } }, default: false },
},
});

Reach for it when decorators get in the way:

  • Your transformer does not implement them, like Oxc (Vite 8’s own).
  • You are in a NestJS app, which keeps experimentalDecorators on for its own DI, and one config cannot compile both decorator specs.
  • You run the CLI on plain node without tsx. Node’s type stripping runs no decorators, so a uql.config.ts importing decorated entities needs Bun or tsx installed (the CLI uses it when it finds it), while a defineEntity file loads as is.
  • You write JavaScript, or want to leave domain classes unannotated.

Each key matches a decorator:

Key Decorator Notes
name @Entity({ name }) Table name. Set it if your build minifies.
schema @Entity({ schema }) See multiple schemas.
fields @Field / @Id Field options, with isId: true in place of @Id.
relations @ManyToOne and the others Relation options, plus cardinality: '11', '1m', 'm1' or 'mm'.
indexes @Index { columns: (post) => [post.title], ...options }, see Indexes.
checks @Entity({ checks }) Check constraints.
triggers @Trigger Trigger options.
hooks Hook decorators Each lifecycle event and its methods: { beforeInsert: (post) => [post.stamp] }.
filters @Filter Filter options.
extends class Child extends Base A base to inherit from, see Inheritance.

When the shape arrives piece by piece, register each part with defineField, defineId, defineRelation, defineIndex, defineTrigger, defineFilter and defineHook, then call defineEntity last to validate and finish it:

import {
defineEntity,
defineField,
defineHook,
defineId,
defineRelation,
type HookContext,
} from 'uql-orm';
import { v7 as uuidv7 } from 'uuid';
class Article {
id?: string;
title?: string;
authorId?: string;
author?: User;
createdAt?: 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, 'authorId', { references: () => User });
defineRelation(Article, 'author', {
cardinality: 'm1',
entity: () => User,
references: (article) => article.authorId,
});
defineHook(Article, 'stamp', 'beforeInsert');
defineEntity(Article, { name: 'articles' });

When the shape is data, such as a CMS content type an admin creates, the same calls register it and sync({ entity }) gives it a table. See Runtime schemas.