Factories and Seeders
Arkorm includes class-based factories and seeders for test data and local bootstrap flows.
Factories
Create a factory
import { ModelFactory } from 'arkormx'
import type { User } from '../../src/models/User'
export class UserFactory extends ModelFactory<User> {
protected definition(sequence: number) {
return {
name: `User ${sequence}`,
email: `user${sequence}@example.com`,
}
}
}When a model declares this factory through factoryClass (or setFactory()), Model.factory() injects the model constructor into the factory. The factory can therefore use a type-only model import and avoid a runtime model → factory → model cycle:
export class User extends Model {
protected static factoryClass = UserFactory
}Factories instantiated directly can call .setModel(User). Defining the legacy protected model = User property remains supported.
Use from model
User.setFactory(UserFactory)
await User.factory().create()
await User.factory(10).createMany()
await new UserFactory().setModel(User).create()Model.factory(n) is shorthand for setting how many models to build. The same is expressed explicitly with count(), and for synchronous definitions make() / makeMany() build in-memory models without persisting:
const one = User.factory().make() // single, unsaved
const many = User.factory().count(3).makeMany() // three, unsaved
const saved = await User.factory().count(3).createMany() // three, persistedmakeMany(amount?, overrides?) accepts an explicit count and per-model attribute overrides.
Inline factories
When a full factory class is unnecessary, defineFactory() builds one from a model and a definition function:
import { defineFactory } from 'arkormx'
const userFactory = defineFactory(User, (sequence) => ({
name: `User ${sequence}`,
email: `user${sequence}@example.com`,
}))
await userFactory.count(5).createMany()The returned factory supports the same chain (count, state, has, for, make/makeMany/create/createMany, and the async variants) as a class-based factory.
Async definitions
Factory definitions can perform async work, use makeAsync() when a definition or state returns a promise.
export class PostFactory extends ModelFactory<Post> {
protected model = Post
protected async definition(sequence: number) {
const user = await User.factory().create()
return {
title: `Post ${sequence}`,
userId: user.getAttribute('id'),
}
}
}
await Post.factory().makeAsync()
await Post.factory().create()
await Post.factory(10).makeManyAsync()
await Post.factory(10).createMany()Calling make() on a factory with an async definition or async state throws and points callers to makeAsync(), makeManyAsync(), create(), or createMany().
Dependent attributes
Definition values can use another factory. Arkorm creates the related model and assigns its primary key to the attribute:
export class PostFactory extends ModelFactory<Post> {
protected model = Post
protected definition(sequence: number) {
return {
title: `Post ${sequence}`,
userId: User.factory(),
userType: async (attributes) => {
const user = await User.query().find(attributes.userId)
return user?.getAttribute('type')
},
}
}
}Attribute resolver functions run in definition order and receive attributes that have already been resolved. Factory-valued attributes and async resolvers require makeAsync(), makeManyAsync(), create(), or createMany().
Factory states
Use state() for one-off transformations:
await User.factory()
.state((attributes) => ({
...attributes,
isActive: false,
}))
.create()Expose reusable states as methods on the factory:
export class UserFactory extends ModelFactory<User> {
protected model = User
protected definition(sequence: number) {
return {
name: `User ${sequence}`,
email: `user${sequence}@example.com`,
isActive: true,
}
}
public suspended() {
return this.state((attributes) => ({
...attributes,
isActive: false,
suspendedAt: new Date(),
}))
}
}
await User.factory<UserFactory>().suspended().create()States are applied in the order they are added. Explicit attributes passed to make() or create() are merged after states.
Factory callbacks
Register afterMaking and afterCreating callbacks inside configure():
export class UserFactory extends ModelFactory<User> {
protected model = User
protected configure() {
this.afterMaking((user) => {
user.setAttribute('source', 'factory')
})
this.afterCreating(async (user) => {
await AuditLog.query().create({
userId: user.getAttribute('id'),
action: 'factory-created',
})
})
}
protected definition(sequence: number) {
return {
name: `User ${sequence}`,
email: `user${sequence}@example.com`,
}
}
}configure() runs once for each factory instance. Async callbacks are supported by async factory methods; synchronous make() rejects an async afterMaking callback.
Factory relationships
Has Many and Many to Many Relationships
Use has() for has-one or has-many relations:
await User.factory().has(Post.factory(3), 'posts').create()Pivot Table Attributes
Use hasAttached() for many-to-many relations and pivot attributes:
await User.factory().hasAttached(Role.factory(2), { approved: true }, 'roles').create()Belongs To Relationships
Use for() to create or associate a belongs-to parent:
await Post.factory().for(User.factory(), 'user').create()The relationship name is optional when it can be inferred from the related model name. Pass it explicitly when the model method uses a different name.
Use recycle() to reuse existing models instead of creating another related record:
const user = await User.query().firstOrFail()
await Post.factory().for(User.factory(), 'user').recycle(user).create()Seeders
import { Seeder } from 'arkormx'
export class DatabaseSeeder extends Seeder {
async run(): Promise<void> {
await this.call(UserSeeder, RoleSeeder)
await this.call([PermissionSeeder])
}
}When a seeder calls other seeders through this.call(), the CLI reports every seeder that ran, including the root and nested seeders:
Seeded DatabaseSeeder
Seeded UserSeeder
Seeded RoleSeederRun seeders through CLI:
npx arkorm seed
npx arkorm seed DatabaseSeeder
npx arkorm seed --allSeeding with foreign-key constraints disabled
When seeding interdependent records you may need to insert rows in an order that would otherwise violate foreign keys. Wrap the work in SchemaBuilder.withoutForeignKeyConstraints() to disable enforcement for the duration (and restore it afterwards, even on failure):
import { SchemaBuilder } from 'arkormx'
export class DatabaseSeeder extends Seeder {
async run(): Promise<void> {
await SchemaBuilder.withoutForeignKeyConstraints(async () => {
await User.factory()
.hasAttached(Tenant.factory().has(Project.factory(3)), { status: 'active' }, 'tenants')
.create()
})
}
}See Toggling foreign-key constraints for details and caveats.
Package and plugin discovery
Packages can add their own discovery paths without replacing the application's configured paths.* values:
import { loadFactoriesFrom, loadModelsFrom, loadSeedersFrom, registerPaths } from 'arkormx'
loadSeedersFrom('./packages/audit/database/seeders')
loadFactoriesFrom('./packages/audit/database/factories')
loadModelsFrom('./packages/audit/src/models')
registerPaths({
seeders: './packages/billing/database/seeders',
factories: './packages/billing/database/factories',
})The focused discovery helpers are also exposed on the Arkorm class:
import { Arkorm } from 'arkormx'
Arkorm.loadSeedersFrom('./packages/audit/database/seeders')
Arkorm.loadFactoriesFrom('./packages/audit/database/factories')
Arkorm.loadModelsFrom('./packages/audit/src/models')The seed command includes seeders from the configured seeder directory plus any directories registered with loadSeedersFrom(...).
Explicit registration
If a package exposes concrete classes instead of files to scan, register them directly:
import { registerFactories, registerModels, registerSeeders } from 'arkormx'
import { AuditLogFactory } from './database/factories/AuditLogFactory'
import { AuditSeeder } from './database/seeders/AuditSeeder'
import { AuditLog } from './src/models/AuditLog'
registerSeeders(AuditSeeder)
registerModels(AuditLog)
registerFactories(AuditLogFactory)The equivalent Arkorm class API is:
import { Arkorm } from 'arkormx'
const arkorm = new Arkorm()
arkorm.registerSeeders(AuditSeeder)
arkorm.registerModels(AuditLog)
arkorm.registerFactories(AuditLogFactory)Explicit seeders can be run by name:
npx arkorm seed AuditSeeder