lib/Definition.js
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;