@adonisjs/lucid
Version:
SQL ORM built on top of Active Record pattern
423 lines (422 loc) • 14 kB
JavaScript
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { FactoryContext } from './factory_context.js';
/**
* Factory builder exposes the API to create/persist factory model instances.
*/
export class FactoryBuilder {
factory;
options;
viaRelation;
/**
* Relationships to setup. Do note: It is possible to load one relationship
* twice. A practical use case is to apply different states. For example:
*
* Make user with "3 active posts" and "2 draft posts"
*/
withRelations = [];
/**
* An array of callbacks to execute before persisting the model instance
*/
tapCallbacks = [];
/**
* Belongs to relationships are treated different, since they are
* persisted before the parent model
*/
withBelongsToRelations = [];
/**
* The current index. Updated by `makeMany` and `createMany`
*/
currentIndex = 0;
/**
* Custom attributes to pass to model merge method
*/
attributes = {};
/**
* Custom attributes to pass to relationship merge methods
*/
recursiveAttributes = {};
/**
* States to apply. One state can be applied only once and hence
* a set is used.
*/
appliedStates = new Set();
/**
* Custom context passed using `useCtx` method. It not defined, we will
* create one inline inside `create` and `make` methods
*/
ctx;
/**
* Pivot attributes for a many to many relationship
*/
attributesForPivotTable;
/**
* Instead of relying on the `FactoryModelContract`, we rely on the
* `FactoryModel`, since it exposes certain API's required for
* the runtime operations and those API's are not exposed
* on the interface to keep the API clean
*/
constructor(factory, options,
/**
* The relationship via which this factory builder was
* created
*/ viaRelation) {
this.factory = factory;
this.options = options;
this.viaRelation = viaRelation;
}
/**
* Access the parent relationship for which the model instance
* is created
*/
get parent() {
return this.viaRelation ? this.viaRelation.parent : undefined;
}
/**
* Returns factory state
*/
async getCtx(isStubbed, withTransaction) {
if (withTransaction === false) {
return new FactoryContext(isStubbed, undefined);
}
const client = this.factory.model.$adapter.modelConstructorClient(this.factory.model, this.options);
const trx = await client.transaction();
return new FactoryContext(isStubbed, trx);
}
/**
* Returns attributes to merge for a given index
*/
getMergeAttributes(index) {
const attributes = Array.isArray(this.attributes) ? this.attributes[index] : this.attributes;
const recursiveAttributes = Array.isArray(this.recursiveAttributes)
? this.recursiveAttributes[index]
: this.recursiveAttributes;
return {
...recursiveAttributes,
...attributes,
};
}
/**
* Returns a new model instance with filled attributes
*/
async getModelInstance(ctx) {
const modelAttributes = await this.factory.define(ctx);
const modelInstance = this.factory.newUpModelInstance(modelAttributes, ctx, this.factory.model, this);
this.factory.mergeAttributes(modelInstance, this.getMergeAttributes(this.currentIndex), ctx, this);
return modelInstance;
}
/**
* Apply states by invoking state callback
*/
async applyStates(modelInstance, ctx) {
for (let state of this.appliedStates) {
await this.factory.getState(state)(modelInstance, ctx, this);
}
}
/**
* Invoke tap callbacks
*/
invokeTapCallback(modelInstance, ctx) {
this.tapCallbacks.forEach((callback) => callback(modelInstance, ctx, this));
}
/**
* Compile factory by instantiating model instance, applying merge
* attributes, apply state
*/
async compile(ctx) {
try {
/**
* Newup the model instance
*/
const modelInstance = await this.getModelInstance(ctx);
/**
* Apply state
*/
await this.applyStates(modelInstance, ctx);
/**
* Invoke tap callbacks as the last step
*/
this.invokeTapCallback(modelInstance, ctx);
/**
* Pass pivot attributes to the relationship instance
*/
if (this.viaRelation && this.viaRelation.pivotAttributes) {
this.viaRelation.pivotAttributes(this.attributesForPivotTable || {});
}
return modelInstance;
}
catch (error) {
if (!this.ctx && ctx.$trx) {
await ctx.$trx.rollback();
}
throw error;
}
}
/**
* Makes relationship instances. Call [[createRelation]] to
* also persist them.
*/
async makeRelations(modelInstance, ctx) {
for (let { name, count, callback } of this.withBelongsToRelations) {
const relation = this.factory.getRelation(name);
await relation
.useCtx(ctx)
.merge(this.recursiveAttributes)
.make(modelInstance, callback, count);
}
for (let { name, count, callback } of this.withRelations) {
const relation = this.factory.getRelation(name);
await relation
.useCtx(ctx)
.merge(this.recursiveAttributes)
.make(modelInstance, callback, count);
}
}
/**
* Makes and persists relationship instances
*/
async createRelations(modelInstance, ctx, cycle) {
const relationships = cycle === 'before' ? this.withBelongsToRelations : this.withRelations;
for (let { name, count, callback } of relationships) {
const relation = this.factory.getRelation(name);
await relation
.useCtx(ctx)
.merge(this.recursiveAttributes)
.create(modelInstance, callback, count);
}
}
/**
* Persist the model instance along with its relationships
*/
async persistModelInstance(modelInstance, ctx) {
/**
* Fire the after "make" hook. There is no before make hook
*/
await this.factory.hooks.runner('after:make').run(this, modelInstance, ctx);
/**
* Fire the before "create" hook
*/
await this.factory.hooks.runner('before:create').run(this, modelInstance, ctx);
/**
* Sharing transaction with the model
*/
modelInstance.$trx = ctx.$trx;
/**
* Create belongs to relationships before calling the save method. Even though
* we can update the foriegn key after the initial insert call, we avoid it
* for cases, where FK is a not nullable.
*/
await this.createRelations(modelInstance, ctx, 'before');
/**
* Persist model instance
*/
await modelInstance.save();
/**
* Create relationships that are meant to be created after the parent
* row. Basically all types of relationships except belongsTo
*/
await this.createRelations(modelInstance, ctx, 'after');
/**
* Fire after hook before the transaction is committed, so that
* hook can run db operations using the same transaction
*/
await this.factory.hooks.runner('after:create').run(this, modelInstance, ctx);
}
/**
* Define custom database connection
*/
connection(connection) {
this.options = this.options || {};
this.options.connection = connection;
return this;
}
/**
* Define custom query client
*/
client(client) {
this.options = this.options || {};
this.options.client = client;
return this;
}
/**
* Define custom context. Usually called by the relationships
* to share the parent context with relationship factory
*/
useCtx(ctx) {
this.ctx = ctx;
return this;
}
/**
* Load relationship
*/
with(name, count, callback) {
const relation = this.factory.getRelation(name);
if (relation.relation.type === 'belongsTo') {
this.withBelongsToRelations.push({ name, count, callback: callback });
return this;
}
this.withRelations.push({ name, count, callback: callback });
return this;
}
/**
* Apply one or more states. Multiple calls to apply a single
* state will be ignored
*/
apply(...states) {
states.forEach((state) => this.appliedStates.add(state));
return this;
}
/**
* Fill custom set of attributes. They are passed down to the newUp
* method of the factory
*/
merge(attributes) {
this.attributes = attributes;
return this;
}
/**
* Merge custom set of attributes with the correct factory builder
* model and all of its relationships as well
*/
mergeRecursive(attributes) {
this.recursiveAttributes = attributes;
return this;
}
/**
* Define pivot attributes when persisting a many to many
* relationship. Results in a noop, when not called
* for a many to many relationship
*/
pivotAttributes(attributes) {
this.attributesForPivotTable = attributes;
return this;
}
/**
* Tap into the persistence layer of factory builder. Allows one
* to modify the model instance just before it is persisted
* to the database
*/
tap(callback) {
this.tapCallbacks.push(callback);
return this;
}
/**
* Make model instance. Relationships are not processed with the make function.
*/
async make() {
const ctx = this.ctx || (await this.getCtx(false, false));
const modelInstance = await this.compile(ctx);
await this.factory.hooks.runner('after:make').run(this, modelInstance, ctx);
return modelInstance;
}
/**
* Create many of the factory model instances
*/
async makeMany(count) {
let modelInstances = [];
const counter = new Array(count).fill(0).map((_, i) => i);
for (let index of counter) {
this.currentIndex = index;
modelInstances.push(await this.make());
}
return modelInstances;
}
/**
* Returns a model instance without persisting it to the database.
* Relationships are still loaded and states are also applied.
*/
async makeStubbed() {
const ctx = this.ctx || (await this.getCtx(true, false));
const modelInstance = await this.compile(ctx);
await this.factory.hooks.runner('after:make').run(this, modelInstance, ctx);
await this.factory.hooks.runner('before:makeStubbed').run(this, modelInstance, ctx);
const id = modelInstance.$primaryKeyValue || this.factory.manager.getNextId(modelInstance);
modelInstance[this.factory.model.primaryKey] = id;
/**
* Make relationships. The relationships will be not persisted
*/
await this.makeRelations(modelInstance, ctx);
/**
* Fire the after hook
*/
await this.factory.hooks.runner('after:makeStubbed').run(this, modelInstance, ctx);
return modelInstance;
}
/**
* Create many of model factory instances
*/
async makeStubbedMany(count) {
let modelInstances = [];
const counter = new Array(count).fill(0).map((_, i) => i);
for (let index of counter) {
this.currentIndex = index;
modelInstances.push(await this.makeStubbed());
}
return modelInstances;
}
/**
* Similar to make, but also persists the model instance to the
* database.
*/
async create() {
/**
* Use pre-defined ctx or create a new one
*/
const ctx = this.ctx || (await this.getCtx(false, true));
/**
* Compile a model instance
*/
const modelInstance = await this.compile(ctx);
try {
await this.persistModelInstance(modelInstance, ctx);
if (!this.ctx && ctx.$trx) {
await ctx.$trx.commit();
}
return modelInstance;
}
catch (error) {
if (!this.ctx && ctx.$trx) {
await ctx.$trx.rollback();
}
throw error;
}
}
/**
* Create and persist many of factory model instances
*/
async createMany(count) {
let modelInstances = [];
/**
* Use pre-defined ctx or create a new one
*/
const ctx = this.ctx || (await this.getCtx(false, true));
const counter = new Array(count).fill(0).map((_, i) => i);
try {
for (let index of counter) {
this.currentIndex = index;
/**
* Compile a model instance
*/
const modelInstance = await this.compile(ctx);
await this.persistModelInstance(modelInstance, ctx);
modelInstances.push(modelInstance);
}
if (!this.ctx && ctx.$trx) {
await ctx.$trx.commit();
}
return modelInstances;
}
catch (error) {
if (!this.ctx && ctx.$trx) {
await ctx.$trx.rollback();
}
throw error;
}
}
}