Prisma ORM Integration
QuickModel works alongside Prisma as a type-safe DTO layer between your database and your application. Use it to coerce raw Prisma results, validate create/update inputs, implement repository patterns, and derive computed fields — all without adding Prisma-specific code to your model definitions.
Key Patterns
| Pattern | QuickModel API |
|---|---|
| Map Prisma row to DTO | new UserRecordDto(prismaRow) — strips _count, _prisma* |
| Validate create input | qCheckRules(new CreateUserDto(formData)) |
| Bulk seed / import | UserRecordDto.createMany(seedArray) |
| Repository abstraction | repo.create(dto) → dto.$qToInterface() → prisma.user.create() |
| Partial update | existing.$qCopy({ score: 100 }) → prisma.user.update({ data: ... }) |
| Derived field | @QComputed() get label() — included in $qSerialize() |
| DB uniqueness check | qCheckRulesAsync() with async rule hitting prisma.user.findFirst() |
Model Setup
import { QModel, Quick, QRule, QField, QGroup, QComputed } from 'quickmodel';
interface IUserRecord {
uid: string;
name: string;
email: string;
age: number;
role: string;
active: boolean;
score: number;
fullLabel?: string;
}
@Quick(
{
uid: 'string',
name: 'string',
email: 'string',
age: 'number',
role: 'string',
active: 'boolean',
score: 'number',
},
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class UserRecordDto extends QModel<IUserRecord> {
declare uid: string;
declare name: string;
@QGroup('identity')
@QField({ label: 'Email', required: true })
@QRule((val: string) => val.includes('@'), 'Invalid email')
declare email: string;
@QField({ label: 'Age' })
@QRule((val: number) => val >= 0 && val <= 120, 'Invalid age')
declare age: number;
@QGroup('identity')
@QField({ label: 'Role' })
@QRule(
(val: string) => ['admin', 'user', 'guest'].includes(val),
'Invalid role'
)
declare role: string;
declare active: boolean;
declare score: number;
@QComputed()
get fullLabel(): string {
return `[${this.role.toUpperCase()}] ${this.name} — ${this.email}`;
}
}The unknownPropertyPolicy: 'strip' handles Prisma's extra relational fields (_count, _avg, joined models) automatically.
DTO from Prisma Result
const prismaRow = await prisma.user.findUnique({
where: { uid },
include: { _count: { select: { posts: true } } },
});
const dto = new UserRecordDto(prismaRow!);
// _count → stripped ✅
// dto.fullLabel → '[USER] Alice — alice@example.com' ✅Coercion from Prisma JSON Fields
If a Prisma model returns values from a JSON column or a raw query, coercionStrategy: 'loose' handles the conversion:
const rawQuery =
await prisma.$queryRaw`SELECT uid, age::text, active::int FROM users WHERE uid = ${uid}`;
const dto = new UserRecordDto((rawQuery as object[])[0]!);
// dto.age is a number, dto.active is a boolean ✅Create Input Validation
Define a separate DTO for create inputs with stricter rules:
interface ICreateUser {
name: string;
email: string;
age: number;
role: string;
}
@Quick(
{ name: 'string', email: 'string', age: 'number', role: 'string' },
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class CreateUserDto extends QModel<ICreateUser> {
@QField({ label: 'Name', required: true })
@QRule((val: string) => val.trim().length >= 2, 'Name too short')
declare name: string;
@QField({ label: 'Email', required: true })
@QRule(
(val: string) => val.includes('@') && val.includes('.'),
'Invalid email format'
)
declare email: string;
@QField({ label: 'Age' })
@QRule((val: number) => val >= 18, 'Must be 18 or older')
declare age: number;
@QField({ label: 'Role' })
@QRule(
(val: string) => ['admin', 'user', 'guest'].includes(val),
'Invalid role'
)
declare role: string;
}
// Usage:
const dto = new CreateUserDto(formData);
const validation = qCheckRules(dto);
if (!validation.valid) throw new Error(validation.errors[0]?.message);
// Pass to Prisma:
await prisma.user.create({
data: { ...dto.$qToInterface(), uid: crypto.randomUUID() },
});Bulk Seed / Import
import seedData from './seed-users.json';
const { instances } = UserRecordDto.createMany(seedData);
await prisma.user.createMany({
data: instances.map((dto) => dto.$qToInterface()),
skipDuplicates: true,
});Repository Pattern
class UserRepository {
async create(dto: CreateUserDto): Promise<UserRecordDto> {
const validation = qCheckRules(dto);
if (!validation.valid) throw new Error(validation.errors[0]?.message!);
const row = await prisma.user.create({
data: {
...dto.$qToInterface(),
uid: crypto.randomUUID(),
active: true,
score: 0,
},
});
return new UserRecordDto(row);
}
async findById(uid: string): Promise<UserRecordDto | null> {
const row = await prisma.user.findUnique({ where: { uid } });
return row ? new UserRecordDto(row) : null;
}
async findAll(): Promise<UserRecordDto[]> {
const rows = await prisma.user.findMany();
const { instances } = UserRecordDto.createMany(rows);
return instances;
}
}Partial Updates with $qCopy()
const existing = new UserRecordDto(
await prisma.user.findUniqueOrThrow({ where: { uid } })
);
// Apply only the fields that changed:
const updated = existing.$qCopy({ score: 100, role: 'admin' });
if (updated.$qIsDirty()) {
await prisma.user.update({
where: { uid: updated.uid },
data: { score: updated.score, role: updated.role },
});
}Derived Fields with @QComputed
Computed fields are included in $qSerialize() — useful for Prisma-based API responses:
interface IPostRecord {
pid: string;
title: string;
body: string;
authorId: string;
published: boolean;
views: number;
excerpt?: string;
}
@Quick(
{
pid: 'string',
title: 'string',
body: 'string',
authorId: 'string',
published: 'boolean',
views: 'number',
},
{ unknownPropertyPolicy: 'strip', coercionStrategy: 'loose' }
)
class PostRecordDto extends QModel<IPostRecord> {
declare pid: string;
declare title: string;
declare body: string;
declare authorId: string;
declare published: boolean;
declare views: number;
@QComputed()
get excerpt(): string {
return this.body.length > 100
? `${this.body.slice(0, 100)}…`
: this.body;
}
}
const post = new PostRecordDto(
await prisma.post.findUniqueOrThrow({ where: { pid } })
);
return post.$qSerialize(); // includes excerpt ✅DB Uniqueness Validation
import { qCheckRulesAsync } from 'quickmodel/forms';
class CreateUserWithUniquenessDto extends CreateUserDto {}
async function createUserSafe(input: object) {
const dto = new CreateUserWithUniquenessDto(input);
const result = await qCheckRulesAsync(dto, {
asyncRules: {
email: [
async (val) => {
const exists = await prisma.user.findFirst({
where: { email: val as string },
});
return !exists; // false = validation error
},
],
},
});
if (!result.valid) {
throw new Error(`Email already taken`);
}
return prisma.user.create({
data: {
...dto.$qToInterface(),
uid: crypto.randomUUID(),
active: true,
score: 0,
},
});
}Migration from class-transformer + class-validator
| class-transformer / class-validator | QuickModel |
|---|---|
@IsEmail() | @QRule((v) => v.includes('@'), '...') |
@Min(18) | @QRule((v) => v >= 18, '...') |
@IsEnum(Role) | @QRule((v) => Object.values(Role).includes(v), '...') |
plainToClass(User, data) | new UserDto(data) |
validateOrReject(instance) | qCheckRules(instance) |
@Exclude() on extra props | { unknownPropertyPolicy: 'strip' } |
@Type(() => Number) | coercionStrategy: 'loose' |
Schema integration with getSchema('prisma') and fromSchema('prisma', ...)
Export a Prisma model block from a QModel
Import quickmodel/schema once, then call getSchema('prisma') to get a Prisma model block string:
import 'quickmodel/schema';
import { QModel, Quick } from 'quickmodel';
interface IUser {
name: string;
email: string;
createdAt: Date;
score: number;
}
@Quick({ createdAt: Date, score: Number })
class User extends QModel<IUser> {
declare name: string;
declare email: string;
declare createdAt: Date;
declare score: number;
}
const prismaModel = User.getSchema('prisma');
/*
model User {
name String
email String
createdAt DateTime
score Float
}
*/Scaffold a QModel from an existing Prisma model
fromSchema('prisma', ...) converts a Prisma model block string into a full QModel class definition. This is scaffolding — the result is source code to save as a .ts file:
import 'quickmodel/schema';
const prismaModel = `
model Product {
id Int @id @default(autoincrement())
name String
price Float
createdAt DateTime @default(now())
inStock Boolean @default(true)
}
`;
const code = QModel.fromSchema('prisma', prismaModel, 'Product');
// → TypeScript source:
// interface IProduct { id: number; name: string; price: number; createdAt: Date; inStock: boolean; }
// @Quick({ id: Number, price: Number, createdAt: Date, inStock: Boolean })
// class Product extends QModel<IProduct> { ... }
// Save to disk:
// fs.writeFileSync('src/models/product.model.ts', code);See also
- Schema Generation — full
getSchema()reference - JSON Schema Integration — portable schema format
- TypeScript Schema Integration — interface export