UNPKG

@adonisjs/lucid

Version:

SQL ORM built on top of Active Record pattern

423 lines (422 loc) 14 kB
/* * @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; } } }