Skip to main content

Recipes

UUID primary keys in migrations

uuidDefault(engine) returns a dialect-aware Knex.Raw default expression — gen_random_uuid() on Postgres (PG13+, or the pgcrypto extension on older versions) and (UUID()) on MySQL. It throws UnsupportedUuidDefaultDialectError on any other dialect.

import { uuidDefault } from '@quik/database';

export class CreateOrders_20260101093000 {
public async up(schema, engine) {
return schema.createTable('Order', (table) => {
table.uuid('id').primary().defaultTo(uuidDefault(engine));
});
}
}

Altering an existing enum column

alterEnumColumn(engine, options) changes the allowed value set of an existing enum column using a dialect-appropriate strategy: Postgres native enum types are recreated and cast across (nativeType: true), non-native Postgres enum columns (Knex's default, CHECK-constraint-backed) have their constraint replaced, and MySQL columns are altered directly with MODIFY COLUMN ... ENUM(...). It throws UnsupportedEnumAlterationDialectError on any other dialect.

import { alterEnumColumn } from '@quik/database';

export class WidenOrderStatus_20260101093100 {
public async up(schema, engine) {
await alterEnumColumn(engine, {
table: 'Order',
column: 'status',
values: ['pending', 'shipped', 'delivered', 'cancelled'],
nativeType: true,
typeName: 'order_status'
});
}
}

Postgres schema-scoped modules

Scope a module's tables and migrations to a Postgres schema instead of the default public schema:

import { MigrationStore } from '@quik/database';

MigrationStore.setSchema('billing', 'billing');
@Model({ name: 'Invoice', schema: 'billing' })
export class Invoice extends QModel { /* ... */ }

Joining tables outside a model's declared relations

QRepository.joinQuery(resultEntity) covers queries that join tables not declared as @Relations and/or return a shape other than the model itself. resultEntity can be any plain @Entity()-decorated class — rows are mapped the same way countBy() maps custom-shaped results, so unrecognized fields don't trigger default-filling or validation errors.

import { Entity, Fields, QEntity } from '@quik/entity';
import { Conditions, QRepository, QWhereBuilder } from '@quik/database';

@Entity('ActiveMembershipRow')
class ActiveMembershipRow extends QEntity {
@Fields.Integer()
public id: number;
}

class ProjectTeamMemberRepository extends QRepository<ProjectTeamMember> {
async findActiveMembership(projectId: number, userId: number) {
return this.joinQuery(ActiveMembershipRow)
.join('ProjectTeam', 'ProjectTeam.id', 'ProjectTeamMember.projectTeamId')
.where(QWhereBuilder.New()
.Equal('ProjectTeam.projectId', projectId)
.And(Conditions.Equal('ProjectTeamMember.userId', userId)))
.select('ProjectTeamMember.*')
.first(); // or .get() for all matching rows
}
}

Field names passed to .where()/.select()/.groupBy()/.orderBy() should be fully qualified as table.column when the query spans more than one table. Field<TModel>(table, field) builds that string with the field name checked against the model's actual fields at compile time. Besides .join(), .leftJoin()/.rightJoin()/.fullOuterJoin() are also available, each accepting (table, first, second), (table, first, operator, second), or a Knex join callback for multi-condition ON clauses.

Generating a migration from model changes

migration:generate <name> --module <module> diffs registered @Model() classes' column definitions against the db.migrations.snapshotFile snapshot (not the live database) and writes a migration file with the resulting up()/down() calls, then updates the snapshot.

migration:generate create-order --module core

The snapshot is global, not scoped per module — running the command for one module still picks up schema changes in models belonging to any other module. All relevant model files must be imported/registered first, same as getModels(). Generated migrations are best-effort: column type/nullable/default changes render as .alter() (subject to Knex's own .alter() limitations), enum value-set changes route through alterEnumColumn() instead, and dropped models/columns generate dropTable/dropColumn — review before running. Indexes, foreign keys, and primary key changes beyond a plain .alter() aren't covered yet, and there's no apply-directly/auto-sync mode.

Listing registered models

import { getModelNames, getModels } from '@quik/database';

getModelNames(); // ['User', 'billing.Invoice', ...]
getModels(); // full EntityMetadata for each registered model

Only classes decorated with @Model()/@BaseModel() are returned — plain @Entity()-decorated DTOs and query parameter classes are excluded.