mevn-orm
Version:
simple ORM for express js
330 lines (329 loc) • 13.6 kB
TypeScript
import type { Knex } from 'knex';
import { BelongsToRelation, HasManyRelation, HasOneRelation } from './relation.js';
type Row = Record<string, unknown>;
/**
* Paginated query result returned by {@link Model.paginate}.
*
* @typeParam T - Model instance type contained in `data`.
*/
interface PaginatedResult<T extends Model> {
/** Model instances for the current page. */
data: ModelCollection<T>;
/** Total rows matching the scoped query across all pages. */
total: number;
/** Requested page size. */
per_page: number;
/** Current page number (1-based). */
current_page: number;
/** Next page number, or `null` on the last page. */
next_page: number | null;
/** Previous page number, or `null` on the first page. */
prev_page: number | null;
/** Total number of pages. */
last_page: number;
}
/**
* Array subclass returned by {@link Model.all} and {@link Model.paginate}.
*
* Extends `Array` so it remains compatible with array operations while adding
* {@link ModelCollection.toArray | toArray()} for API serialisation.
*
* @typeParam T - Model instance type stored in the collection.
*/
declare class ModelCollection<T extends Model> extends Array<T> {
/**
* Serialises every model in the collection to a plain object.
*
* @returns Array of serialised records (respects each model's `hidden` fields).
*/
toArray(): Row[];
}
/**
* ActiveRecord-style base model backed by Knex.
*
* Extend this class for each database table. Table names are inferred from the
* class name unless overridden via `override table`. Use static methods for
* queries and instance methods for row-level persistence.
*
* @example
* ```ts
* class User extends Model {
* override fillable = ['name', 'email', 'password']
* override hidden = ['password']
* }
*
* const user = await User.create({ name: 'Jane', email: 'jane@example.com' })
* const page = await User.orderBy('name').paginate(10)
* ```
*/
declare class Model {
#private;
[key: string]: any;
/**
* Resolved database table name for this model class.
*
* Honours subclass `override table` declarations.
*/
static get currentTable(): string;
/**
* Active scoped Knex query built by `where()`, `orderBy()`, and other chain methods.
*
* Consumed and reset by terminal methods such as `first()`, `all()`, and `paginate()`.
*/
static currentQuery: Knex.QueryBuilder<Row, Row[]> | undefined;
/**
* Resolves the database table name for this model class.
*
* Instantiates the subclass to read its `table` property, so explicit
* `override table` values are honoured on static query paths.
*
* @returns Resolved table name.
*/
static resolveTable(this: typeof Model): string;
/**
* Returns the active scoped query, initialising one against the model table when absent.
*
* Used internally by chain methods like `orderBy()` when called without a prior `where()`.
*/
static ensureCurrentQuery(this: typeof Model): Knex.QueryBuilder<Row, Row[]>;
/** Attributes allowed through {@link Model.save | save()} mass assignment. */
fillable: string[];
/** Attributes excluded from {@link Model.toArray | toArray()} and stripped after reads. */
hidden: string[];
/** Snake_case singular name derived from the class name (used for default foreign keys). */
modelName: string;
/** Database table this model maps to. Override when inference does not match your schema. */
table: string;
/** Primary key value, set after insert or load. */
id?: number;
/**
* Creates a model instance from a database row or plain object.
*
* @param properties - Initial attribute values.
*/
constructor(properties?: Row);
/**
* Inserts the current model using {@link Model.fillable | fillable} attributes and reloads it.
*
* @returns This instance with database-assigned fields (including `id`) populated.
*/
save(): Promise<this>;
/**
* Updates the current row by primary key and returns a refreshed model instance.
*
* @param properties - Columns and values to update.
* @returns Refreshed instance with updated attributes.
* @throws When the instance has no `id`.
*/
update(properties: Row): Promise<this>;
/**
* Deletes the current row by primary key.
*
* @throws When the instance has no `id`.
*/
delete(): Promise<void>;
/**
* Bulk-updates rows in the model table.
*
* When preceded by {@link Model.where | where()}, only scoped rows are updated.
* The query scope is reset after execution.
*
* @param properties - Columns and values to update.
* @returns Number of rows updated.
*/
static update(properties: Row): Promise<number | undefined>;
/**
* Bulk-deletes rows in the model table.
*
* When preceded by {@link Model.where | where()}, only scoped rows are deleted.
* The query scope is reset after execution.
*
* @returns Number of rows deleted.
*/
static destroy(): Promise<number | undefined>;
/**
* Finds a single model by primary key.
*
* @param id - Primary key value.
* @param columns - Columns to select (default `'*'`).
* @returns Model instance, or `null` when not found. Preserves the derived class type.
*/
static find<T extends typeof Model>(this: T, id: number | string, columns?: string | string[]): Promise<InstanceType<T> | null>;
/**
* Finds a model by primary key or throws when it does not exist.
*
* @param id - Primary key value.
* @param columns - Columns to select (default `'*'`).
* @returns Model instance. Preserves the derived class type.
* @throws When no row matches the given id.
*/
static findOrFail<T extends typeof Model>(this: T, id: number | string, columns?: string | string[]): Promise<InstanceType<T>>;
/**
* Inserts a row and returns the created model instance.
*
* @param properties - Column values to insert.
* @returns Created model with `hidden` fields stripped. Preserves the derived class type.
*/
static create<T extends typeof Model>(this: T, properties: Row): Promise<InstanceType<T>>;
/**
* Inserts multiple rows sequentially and returns created model instances.
*
* @param properties - Array of column value objects to insert.
* @returns Created model instances in insertion order.
*/
static createMany<T extends typeof Model>(this: T, properties: Row[]): Promise<InstanceType<T>[]>;
/**
* Returns the first row matching `attributes`, or creates one with merged values.
*
* @param attributes - Lookup conditions.
* @param values - Additional values used only when creating a new row.
* @returns Existing or newly created model instance.
*/
static firstOrCreate<T extends typeof Model>(this: T, attributes: Row, values?: Row): Promise<InstanceType<T>>;
/**
* Starts a scoped query with a `where` clause.
*
* Chain further constraints (`orderBy`, `limit`, …) then call a terminal method
* (`first`, `all`, `paginate`, `count`, `update`, `destroy`).
*
* @param conditions - Equality conditions passed to Knex `where`.
* @returns Model constructor for chaining.
*/
static where<T extends typeof Model>(this: T, conditions?: Row): T;
/**
* Appends an `orderBy` clause to the current scoped query.
*
* @param column - Column to sort by.
* @param direction - Sort direction (`'asc'` or `'desc'`). Defaults to `'asc'`.
* @returns Model constructor for chaining.
*/
static orderBy<T extends typeof Model>(this: T, column: string, direction?: 'asc' | 'desc'): T;
/**
* Appends a `limit` clause to the current scoped query.
*
* @param count - Maximum number of rows to return.
* @returns Model constructor for chaining.
*/
static limit<T extends typeof Model>(this: T, count: number): T;
/**
* Appends an `offset` clause to the current scoped query.
*
* @param count - Number of rows to skip (commonly paired with {@link Model.limit | limit()}).
* @returns Model constructor for chaining.
*/
static offset<T extends typeof Model>(this: T, count: number): T;
/**
* Returns the first model matching the current scope.
*
* When no scope is active, returns the first row in the table.
* The query scope is reset after execution.
*
* @param columns - Columns to select (default `'*'`).
* @returns First matching model, or `null` when none found.
*/
static first<T extends typeof Model>(this: T, columns?: string | string[]): Promise<InstanceType<T> | null>;
/**
* Returns all models matching the current scope.
*
* When no scope is active, returns every row in the table.
* The query scope is reset after execution.
*
* @param columns - Columns to select (default `'*'`).
* @returns {@link ModelCollection} of matching models.
*/
static all<T extends typeof Model>(this: T, columns?: string | string[]): Promise<ModelCollection<InstanceType<T>>>;
/**
* Returns a paginated result set for the current scope.
*
* Runs a count query and a data query against the scoped builder.
* The query scope is reset after execution.
*
* @param perPage - Items per page (default `15`).
* @param page - Page number, 1-based (default `1`).
* @param columns - Columns to select (default `'*'`).
* @returns Paginated data and metadata.
*
* @example
* ```ts
* const result = await Post.where({ published: true }).orderBy('created_at', 'desc').paginate(10, 2)
* ```
*/
static paginate<T extends typeof Model>(this: T, perPage?: number, page?: number, columns?: string | string[]): Promise<PaginatedResult<InstanceType<T>>>;
/**
* Returns a row count for the current scope.
*
* When no scope is active, counts all rows in the table.
* The query scope is reset after execution.
*
* @param column - Column to count (default `'*'` for all rows).
* @returns Matching row count.
*/
static count(this: typeof Model, column?: string): Promise<number>;
/**
* Serialises the model to a plain object for API responses.
*
* Excludes ORM internals (`fillable`, `hidden`, `modelName`, `table`) and
* any attributes listed in {@link Model.hidden | hidden}.
*
* @returns Plain data object safe to return from HTTP handlers.
*/
toArray(): Row;
/**
* Removes internal and hidden fields from a model instance in place.
*
* @param model - Model object to strip.
* @param keepInternalState - When `true`, retains `fillable` and `hidden` keys.
* @returns The same object reference with keys removed.
*/
stripColumns<T extends Record<string, unknown>>(model: T, keepInternalState?: boolean): T;
}
interface Model {
/**
* Defines a one-to-one relationship to another model.
*
* @param Related - Related model class constructor.
* @param localKey - Parent key value (defaults to `this.id`).
* @param foreignKey - Foreign key column on the related table (defaults to `{modelName}_id`).
* @returns Lazy {@link HasOneRelation} — await directly or chain `where()` before executing.
*
* @example
* ```ts
* const profile = await user.profile()
* const active = await user.profile().where({ active: true }).first()
* ```
*/
hasOne<T extends typeof Model>(Related: T, localKey?: number | string, foreignKey?: string): HasOneRelation<InstanceType<T>>;
/**
* Defines a one-to-many relationship to another model.
*
* @param Related - Related model class constructor.
* @param localKey - Parent key value (defaults to `this.id`).
* @param foreignKey - Foreign key column on the related table (defaults to `{modelName}_id`).
* @returns Lazy {@link HasManyRelation} — await for all rows or call `.get()` / `.first()`.
*
* @example
* ```ts
* const posts = await user.posts()
* const drafts = await user.posts().where({ status: 'draft' }).get()
* ```
*/
hasMany<T extends typeof Model>(Related: T, localKey?: number | string, foreignKey?: string): HasManyRelation<InstanceType<T>>;
/**
* Defines an inverse belongs-to relationship to a parent model.
*
* @param Related - Parent model class constructor.
* @param foreignKey - Foreign key column on this model (defaults to `{relatedModelName}_id`).
* @param ownerKey - Primary key column on the parent table (defaults to `'id'`).
* @returns Lazy {@link BelongsToRelation} — await directly or chain `where()` before executing.
*
* @example
* ```ts
* const author = await post.author()
* ```
*/
belongsTo<T extends typeof Model>(Related: T, foreignKey?: string, ownerKey?: string): BelongsToRelation<InstanceType<T>>;
}
export { Model, ModelCollection };
export type { PaginatedResult };
export { HasOneRelation, HasManyRelation, BelongsToRelation, Relation } from './relation.js';
export { DB, getDB, configure, createKnexConfig, configureDatabase, setMigrationConfig, getMigrationConfig, makeMigration, migrateLatest, migrateRollback, migrateCurrentVersion, migrateList, } from './config.js';