Docs/Guides/Database

Database

@veap/database provides a Knex-backed database connection layer and an Active Record-style model API. It supports PostgreSQL and SQLite through its documented configuration, with…

4 min read

@veap/database provides a Knex-backed database connection layer and an Active Record-style model API. It supports PostgreSQL and SQLite through its documented configuration, with MySQL support available when the application installs the mysql2 driver.

Guides#

Core principles#

  • Initialize the database before resolving models or services that query it.
  • Use schema migrations to version structural changes.
  • Validate external input and constrain data with database constraints.
  • Use transactions for multi-step operations that must succeed or fail together.
  • Keep the connection and model layer on the server.

Overview#

@veap/database is a Knex-backed Active Record layer. Its root export re-exports model, connection, query builder, scopes, casts, relations, migration helpers, providers, factories and seeders. A dedicated @veap/database/migrations and @veap/database/relations subpath is declared in package.json.

Initialize a standalone connection#

ts
import { initDatabase } from "@veap/database";

initDatabase({
  client: "better-sqlite3",
  connection: { filename: "./data/app.sqlite" },
  useNullAsDefault: true,
});

initDatabase() accepts DatabaseConfig. The implementation maps driver: "postgres" | "sqlite" | "mysql" to pg, better-sqlite3, or mysql2 when client is absent. The default client is pg; useNullAsDefault defaults to true. Install the appropriate driver in your application. getKnex() throws if no connection was initialized, and dbClient is a dynamic proxy for Knex access.

For PostgreSQL in production, review TLS settings in src/connection.ts and explicitly configure the deployment's certificate and verification requirements. The code allows SSL to be disabled through sslmode=disable; avoid insecure no-verify settings in production.

Define models#

The base class declares static table, static primaryKey = "id", static autoUuid = true, static timestamps = true, static casts, static softDeletes, static hidden, static fillable, static guarded, and static globalScopes. Use table, not tableName.

ts
import { Model, hasMany, belongsTo } from "@veap/database";
import type { HasMany, BelongsTo } from "@veap/database";

export class Author extends Model {
  static override table = "authors";
  declare id: string;
  declare name: string;

  posts(): HasMany<Post> {
    return hasMany(this, Post, "author_id");
  }
}

export class Post extends Model {
  static override table = "posts";
  declare id: string;
  declare author_id: string;
  declare title: string;

  author(): BelongsTo<Author> {
    return belongsTo(this, Author, "author_id");
  }
}

The exact attribute typing is model-defined. The model layer provides create/save/fill operations, casts, UUID generation by default, timestamp handling, hidden fields during JSON serialization, and soft-delete support when enabled on the model.

Querying#

ts
const posts = await Post.query()
  .where("status", "published")
  .orderBy("created_at", "desc")
  .with("author")
  .get();

const first = await Post.query().where("slug", "hello-world").first();
const page = await Post.query().paginate(1, 20);

The query builder source includes firstOrNew, firstOrCreate, updateOrCreate, find, findOrFail, count, exists, pluck, paginate, bulk update, delete, forceDelete, restore, increment, decrement, and toSQL. Check the TypeScript declarations for complete overloads and relation constraints.

Transactions#

ts
import { transaction } from "@veap/database";

await transaction(async () => {
  const author = await Author.create({ name: "Ada" });
  await Post.create({ title: "Notes", author_id: author.id });
});

transaction() uses AsyncLocalStorage to bind the current Knex transaction to the async execution context. Nested calls reuse the active transaction by default; pass { savepoint: true } to create a nested savepoint. Errors thrown from the callback cause Knex to roll back.

Migrations#

The migration runner takes a scope and migration definitions with name, up(db, schema), and optional down(db, schema). It tracks completed migrations in a migrations table by scope and runs a pending batch inside a transaction. rollbackMigrations(scope, migrations) rolls back the latest batch in reverse order when corresponding down functions are present.

Do not rely on a migration provider to run when no database connection exists; the runner returns without doing work when getKnex() cannot resolve a connection. Test migrations against the database engine you deploy because SQL and schema-builder behavior can vary by driver.

Relations#

The relations entrypoint exports belongsTo, hasMany, hasOne, belongsToMany, and polymorphic helpers such as morphTo, morphMany, morphOne, and morphToMany. Eager-load relations with .with(...) where supported by the query builder.

veap
Preparing0%