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.