Model Metadata
Read back what a model declares — table name, columns, relations, scopes — from your own code, without instantiating a repository.
What It Is For
defineModel stores a model's configuration in a registry as well as returning the class. MetadataStorage is the read side of that registry, and it is what the ORM itself uses internally — Repository reads your column definitions through it rather than off the class.
Reach for it when you are writing code that has to work across models it does not know about at compile time: an admin UI that renders a form per model, a schema inventory or audit script, a seed generator, a linter that checks every table has a primary key.
import { MetadataStorage } from "stabilize-orm";import { User } from "./models/user";
MetadataStorage.getTableName(User); // "users"Object.keys(MetadataStorage.getColumns(User)); // ["id", "email", "nationalId"]
MetadataStorage.isVersioned(User); // falseMetadataStorage.getSoftDeleteField(User); // "deletedAt", or nullMetadataStorage.getTimestamps(User); // the timestamps config, or {} if noneThe Methods
Every method takes the model class — the value defineModel returned — and is static.
| Method | Returns |
|---|---|
getModelMetadata(model) | the whole ModelConfig, or undefined |
getTableName(model) | the table name, or "" |
getColumns(model) | Record<string, ColumnConfig> keyed by property name |
getRelations(model) | Record<string, RelationConfig> keyed by relation property |
getScopes(model) | a record of scope name to scope function |
getValidators(model) | Record<string, string[]> — see the warning below |
getSoftDeleteField(model) | the soft-delete property name, or null |
isVersioned(model) | boolean |
getTimestamps(model) | a TimestampsConfig, or {} |
getModelByTableName(tableName) | the model class, or undefined |
setModelMetadata(model, config) | void — usually called by defineModel, not by you |
Absent Models Do Not Throw
Nothing here raises on an unregistered model. Each getter falls back to a neutral value, which is convenient for the ORM internally and dangerous in a script that expects to be told it made a mistake:
class NotAModel {}
MetadataStorage.getTableName(NotAModel); // "" — not undefinedMetadataStorage.getColumns(NotAModel); // {}MetadataStorage.getRelations(NotAModel); // {}MetadataStorage.isVersioned(NotAModel); // falseMetadataStorage.getSoftDeleteField(NotAModel); // nullMetadataStorage.getModelMetadata(NotAModel); // undefined — the one real signalNote the asymmetry: getTableName answers "" and isVersioned answers false for a model that was never defined, so an empty string or a false is ambiguous between “declared this way” and “never registered”. When that distinction matters, test getModelMetadata(model) !== undefined instead.
import { MetadataStorage } from "stabilize-orm";
function isRegistered(model: Function): boolean { return MetadataStorage.getModelMetadata(model) !== undefined;}
if (!isRegistered(User)) { throw new Error("User model was never registered — check the import path");}getValidators Is Narrower Than It Sounds
It reports required and unique, and nothing else
The name suggests every rule a column carries, but the method walks the columns and emits only two rule names. A column declared with minLength, maxLength, pattern or customValidator shows up as an empty array — so a form generated from this output silently drops those constraints.
// id: { type: DataTypes.INTEGER, required: true, unique: true }// email: { type: DataTypes.STRING, minLength: 3, pattern: /@/ }
MetadataStorage.getValidators(User);// {// id: ["required", "unique"],// email: [], // minLength and pattern are not reported// }Read the column configs directly if you need the rest — getColumns(model) carries the full ColumnConfig, including those fields. Enforcement at write time is unchanged; this only affects what the metadata reports. See Validation.
Property Names, Not Column Names
getColumns and getSoftDeleteField are keyed by the property name you wrote in defineModel. If a column is mapped to a different database name, the key is still the property, and the mapping lives inside the value:
export const User = defineModel({ tableName: "users", columns: { id: { type: DataTypes.INTEGER, name: "user_id", required: true }, },});
MetadataStorage.getSoftDeleteField(User); // the property name, not the DB columnObject.keys(MetadataStorage.getColumns(User)); // ["id"]
// The database name is on the value when overridden:MetadataStorage.getColumns(User).id.name; // "user_id"A repository resolves the mapping for you — this matters only when you are emitting SQL or building a schema report yourself, where a property name in a query will not match a renamed column.
Why It Survives a Duplicate ORM Copy
The registry is stored on globalThis under a Symbol.for(...) key rather than in a module-local variable, and setModelMetadata additionally mirrors the configuration onto static properties of the class. Both exist for the same failure mode: if the ORM ends up loaded twice — a duplicated dependency, mixed CJS and ESM resolution, a CLI process plus your app — two module instances would otherwise each hold their own registry, and models registered by one would look undefined to the other.
The globalThis registry is the authoritative copy. getModelMetadata consults it first and only falls back to the static mirror, which is written best-effort: a frozen class, or one whose columns is a getter without a setter, will not take the mirror and will not throw over it. Metadata is still available through the registry in that case.
Finding a Model by Table
getModelByTableName scans the registry — it is a linear search, so it suits startup work rather than a per-request lookup. Table names are not enforced unique, and the first registration wins if two models claim one.
const model = MetadataStorage.getModelByTableName("users");if (model) { const repo = orm.getRepository(model as any); await repo.find().execute(orm.client);}