UNPKG

@apantle/awsome-factory-associator

Version:

Provides a syntax to define factories with any kind of association.

498 lines (448 loc) 12.5 kB
const Promise = require('bluebird'); const Collection = require('./Collection'); const utils = require('./utils'); const _ = require('lodash'); const debug = require('debug'); class Definition { constructor(factory, name, model) { const creatingFromCopy = factory instanceof Definition; const defaultRetries = 1; this.creationRetries = parseInt(process.env.AFA_RETRIES, 10); if (!this.creationRetries) { this.creationRetries = typeof sails === 'undefined' ? defaultRetries : _.get(sails, 'config.factory.creationRetries', defaultRetries); } if (creatingFromCopy) { const templateDefinition = factory; this.factory = templateDefinition.factory; this.name = templateDefinition.name; this.model = templateDefinition.model; this.setConfigFromDefinition(templateDefinition); } else { this.factory = factory; this.name = name; this.setNewConfig(); if (model) { this.model = model; } } } setNewConfig() { this.config = { attr: {}, assoc: {}, assocMany: {}, assocAfter: {}, assocManyAfter: {} }; } setConfigFromDefinition(definition) { this.config = utils.clone(definition.config); } addOptionsToConfig(options) { if (!this.model) { throw new Error( 'No model defined for factory ' + this.name + '. Make sure to pass a valid model as second parameter in definition' ); } for (const attributeName in options) { const isAttribute = this.model.attributes ? this.model.attributes[attributeName] : this.model.rawAttributes[attributeName]; const value = options[attributeName]; const isSave = attributeName == '$'; if (isAttribute) { this.attr(attributeName, value); } else if (isSave) { this.saveAs = value; delete options[attributeName]; } else { this.setCorrespondingAssociation(attributeName, value); } } } getCreationData(saved) { const json = {}; for (const attrib in this.config.attr) { const value = utils.getIfSavedAtttribute(saved, this.config.attr[attrib]); if (value instanceof Collection) { json[attrib] = value.getNext(); } else if (typeof value === 'function') { json[attrib] = value(); } else { json[attrib] = value; } } return json; } getValidAssociation(as, type) { if (!this.model) { throw new Error( 'No model defined for factory ' + this.name + '. Make sure to pass a valid model as second parameter in definition' ); } const associationSequelizeOptions = this.model.associations[as]; if (!associationSequelizeOptions) { throw new Error( 'Invalid "as" name "' + as + '" used in factory ' + this.name + ' definition' ); } if (type && associationSequelizeOptions.associationType != type) { throw new Error( 'Invalid association type ' + associationSequelizeOptions.associationType + ' for ' + as + ' in factory ' + this.name + ' definition. Make sure you are using the correct option.' ); } return associationSequelizeOptions; } setCorrespondingAssociation(as, options) { const associationSequelizeOptions = this.getValidAssociation(as); switch (associationSequelizeOptions.associationType) { case 'BelongsTo': this.assoc(as, null, options, associationSequelizeOptions); break; case 'BelongsToMany': this.assocMany(as, null, options, associationSequelizeOptions); break; case 'HasOne': this.assocAfter(as, null, options, associationSequelizeOptions); break; case 'HasMany': this.assocManyAfter(as, null, options, associationSequelizeOptions); break; default: throw new Error( 'No valid association type found for ' + associationSequelizeOptions.associationType ); } return associationSequelizeOptions; } generateBelongsTo(method, saved) { const idsForMainCreation = {}; return new Promise.mapSeries( _.values(this.config.assoc), (belongsToConfig) => { return utils .generateFromOptionsObject( method, this.factory, belongsToConfig.options, belongsToConfig, saved ) .then((idAssociatedModel) => { idsForMainCreation[belongsToConfig.foreignKey] = idAssociatedModel; return; }); } ).then(() => { return idsForMainCreation; }); } generateBelongsToMany(method, createdModel, saved) { return new Promise.mapSeries( _.values(this.config.assocMany), (belongsToManyConfig) => { if (!Array.isArray(belongsToManyConfig.options)) { belongsToManyConfig.options = utils.getArrayOfOptionsFromObject( belongsToManyConfig.options ); } return Promise.mapSeries( belongsToManyConfig.options, (optionsObject) => { return utils.generateFromOptionsObject( method, this.factory, optionsObject, belongsToManyConfig, saved ); } ).then((associatedModelsIds) => { const functionName = 'set' + belongsToManyConfig.plural; return createdModel[functionName](associatedModelsIds); //TODO en modelo debe de estar distinto el nombre para saber el plural }); } ); } generateHasOne(method, createdModel, saved) { return new Promise.mapSeries( _.values(this.config.assocAfter), (hasOneConfig) => { return utils.generateFromOptionsObjectAfter( method, this.factory, hasOneConfig.options, hasOneConfig, createdModel, saved ); } ); } generateHasMany(method, createdModel, saved) { return new Promise.mapSeries( _.values(this.config.assocManyAfter), (hasManyConfig) => { if (!Array.isArray(hasManyConfig.options)) { hasManyConfig.options = utils.getArrayOfOptionsFromObject( hasManyConfig.options ); } return Promise.mapSeries(hasManyConfig.options, (optionsObject) => { return utils.generateFromOptionsObjectAfter( method, this.factory, optionsObject, hasManyConfig, createdModel, saved ); }); } ); } /** * Used in definition */ parent(parentDefinition) { parentDefinition = this.factory.definitions[parentDefinition]; if (!parentDefinition) { throw new Error('No factory defined for name ' + name + ' of parent'); } this.autoIncrement = parentDefinition.autoIncrement; this.model = parentDefinition.model; this.setConfigFromDefinition(parentDefinition); return this; } attr(name, value, options) { const isAutoIncrement = options && options.auto_increment; if (isAutoIncrement) { const collection = new Collection(value, options.auto_increment); this.config.attr[name] = collection; } else { this.config.attr[name] = value; } return this; } assoc(as, factoryName, options, associationSequelizeOptions) { return this._configureAssociationWithFK( { as, factoryName, options }, 'BelongsTo', 'assoc', associationSequelizeOptions ); } assocMany(as, factoryName, options, associationSequelizeOptions) { return this._configureAssociation( { as, factoryName, options }, { association: 'BelongsToMany', targetConfig: 'assocMany', keyTargetSet: 'plural', sequelizeOptionsSelector: 'options.name.plural' }, associationSequelizeOptions ); } assocAfter(as, factoryName, options, associationSequelizeOptions) { return this._configureAssociationWithFK( { as, factoryName, options }, 'HasOne', 'assocAfter', associationSequelizeOptions ); } assocManyAfter(as, factoryName, options, associationSequelizeOptions) { return this._configureAssociationWithFK( { as, factoryName, options }, 'HasMany', 'assocManyAfter', associationSequelizeOptions ); } /** * @private */ _configureAssociationWithFK( { as, factoryName, options }, association, targetConfig, associationSequelizeOptions ) { return this._configureAssociation( { as, factoryName, options }, { association, targetConfig, keyTargetSet: 'foreignKey', sequelizeOptionsSelector: 'foreignKey' }, associationSequelizeOptions ); } /** * * @param as * @param factoryName * @param options * @param association i.e. BelongsToMany * @param targetConfig i.e. assocMany * @param keyTargetSet i.e. plural * @param sequelizeOptionsSelector i.e. options.name.plural * @param associationSequelizeOptions * @private */ _configureAssociation( { as, factoryName, options }, { association, targetConfig, keyTargetSet, sequelizeOptionsSelector }, associationSequelizeOptions ) { associationSequelizeOptions = associationSequelizeOptions || this.getValidAssociation(as, association); const factory = factoryName || (this.config[targetConfig][as] ? this.config[targetConfig][as].factoryName : null); const sequelizeOptionsValue = _.get( associationSequelizeOptions, sequelizeOptionsSelector ); this.config[targetConfig][as] = { factoryName: factory, options: options, as: as, [keyTargetSet]: sequelizeOptionsValue }; return this; } create(saved) { return new Promise( function(resolve, reject) { this.retryCreate(saved).then(resolve).catch(reject); }.bind(this) ); } async retryCreate(saved) { let tries = 0; let out; let reason; while (tries < this.creationRetries && !out) { try { out = await this._doCreate(saved); } catch (err) { reason = err; tries++; if (err.name === 'SequelizeUniqueConstraintError') { debug('awsome-factory')( `factory retry ${tries}: ${err.parent.detail}` ); } } } if (!out) { debug('awsome-factory')(reason); throw reason; } return out; } _doCreate(saved) { const isRootCreation = !saved; let createdModel; if (isRootCreation) { saved = {}; } return this.generateBelongsTo('create', saved) .then((idsBelongsTo) => { const creationData = this.getCreationData(saved); _.defaults(creationData, idsBelongsTo); return this.model.create(creationData); }) .then((created) => { createdModel = created; if (isRootCreation) { saved.root = createdModel; } return this.generateBelongsToMany('create', createdModel, saved); }) .then(() => { return this.generateHasOne('create', createdModel, saved); }) .then(() => { return this.generateHasMany('create', createdModel, saved); }) .then(() => { return createdModel.reload({ include: [ { all: true, required: false } ] }); }) .then((modelPopulated) => { if (this.saveAs) { saved[this.saveAs] = modelPopulated; } if (isRootCreation) { modelPopulated.$ = saved; } return modelPopulated; }); } build() { if (!this.model) { throw new Error( 'No model defined for factory ' + this.name + '. Make sure to pass a valid model as second parameter in definition' ); } const json = this.getCreationData(); return new Promise((resolve) => { return resolve(this.model.build(json)); }); } } module.exports = Definition;