> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Imperative Definition

> Define entities without decorators using defineEntity, with the same options as the decorator API.

Source: https://uql-orm.dev/entities/imperative

`defineEntity` takes the same options as the [decorators](https://uql-orm.dev/entities/basic.md) and registers identical metadata, with the same checks except for 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 `experimentalDecorators` on 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 a `uql.config.ts` that imports decorated entities needs `bun` or `node --import tsx`. A `defineEntity` entity 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 `SyntaxError` unless Bun or a transpiler gets to the file first. A `defineEntity` call just runs.

Both forms write to the same 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](https://uql-orm.dev/entities/runtime.md). |

## Using `defineEntity`

```ts
import { defineEntity } from 'uql-orm';
import { v7 as uuidv7 } from 'uuid';

export class User {
  id!: string;
  name?: string | null;
  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 },
    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,
      references: (post) => post.authorId,
    },
  },
  indexes: [{ columns: (post) => [post.title, post.authorId], unique: true }],
  filters: {
    published: { where: { 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](https://uql-orm.dev/entities/basic.md#if-your-build-minifies-name-the-table). |
| `fields` | `@Field` / `@Id` | Same [field options](https://uql-orm.dev/entities/basic.md#field-options); mark the primary key with `isId: true` instead of `@Id`. |
| `relations` | `@OneToOne`, `@OneToMany`, `@ManyToOne`, `@ManyToMany` | Same [relation options](https://uql-orm.dev/entities/relations.md), plus `cardinality`: `'11'`, `'1m'`, `'m1'`, or `'mm'`. |
| `indexes` | `@Index` | `{ columns: (post) => [post.title], name?, unique?, type?, where? }`, see [Indexes](https://uql-orm.dev/entities/indexes.md). |
| `triggers` | `@Trigger` | Same [trigger options](https://uql-orm.dev/entities/triggers.md): `[{ on: 'afterUpdate', of: (post) => [post.status], run }]`. |
| `hooks` | Hook decorators | Maps each [lifecycle event](https://uql-orm.dev/entities/lifecycle-hooks.md) to the methods it runs: `{ beforeInsert: (post) => [post.stamp] }`. |
| `filters` | `@Filter` | Same [filter options](https://uql-orm.dev/querying/filters.md#defining-a-filter): `{ where, default?, security?, onMissing? }`. |
| `extends` | `class Child extends Base` | The base to inherit fields, relations, hooks and filters from. See [Inheritance](https://uql-orm.dev/entities/inheritance.md#naming-a-base-you-cannot-extend). |

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](https://uql-orm.dev/entities/basic.md#naming-the-key), `isId: true` does not: brand an unconventional or composite key yourself, or every by-id method is typed against the wrong column.

## Incremental registration

For dynamic schemas, register piece by piece with `defineField`, `defineId`, `defineRelation`, `defineIndex`, `defineTrigger`, `defineFilter`, and `defineHook`, then call `defineEntity` last. It validates the metadata (fields present, exactly one primary key) and finalizes the entity:

```ts
import {
  defineEntity,
  defineField,
  defineFilter,
  defineHook,
  defineId,
  defineIndex,
  defineRelation,
  type HookContext,
} from 'uql-orm';
import { v7 as uuidv7 } from 'uuid';

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 });
defineField(Article, 'authorId', { references: () => User });
defineRelation(Article, 'author', {
  cardinality: 'm1',
  entity: () => User,
  references: (article) => article.authorId,
});
defineIndex(Article, { columns: (article) => [article.title], unique: true });
defineFilter(Article, 'published', {
  where: { 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](https://uql-orm.dev/http.md)) behaves identically.

## 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](https://uql-orm.dev/entities/runtime.md).
