import Store from './Store'
import clone from 'lodash/clone'
import times from 'lodash/times'
/**
* A class to create and use factories
*
* @class FactoryFarm
*/
class FactoryFarm {
/**
* Sets up the store, and a private property to make it apparent the store is used
* for a FactoryFarm
*
* @param {object} store the store to use under the hood
*/
constructor (store) {
this.store = store || new Store()
this.store.__usedForFactoryFarm__ = true
}
/**
* A hash of available factories. A factory is an object with a structure like:
* { name, type, attributes, relationships }.
*
* @type {object}
*/
factories = {}
/**
* A hash of singleton objects.
*
* @type {object}
*/
singletons = {}
/**
* Allows easy building of Store objects, including relationships.
* Takes parameters `attributes` and `relationships` to use for building.
*
* const batchAction = store.build('cropBatchAction')
* store.build('basilBatch', {
* arbitrary_id: 'new_id'
* zone: 'bay1',
* crop_batch_actions: [
* batchAction,
* store.build('batchAction')
* ]
* })
*
* @param {string} factoryName the name of the factory to use
* @param {object} overrideOptions overrides for the factory
* @param {number} numberOfRecords optional number of models to build
* @returns {object} instance of an Store model
*/
build (factoryName, overrideOptions = {}, numberOfRecords = 1) {
const { store, factories, singletons, _verifyFactory, _buildModel } = this
_verifyFactory(factoryName)
const { type, ...properties } = factories[factoryName]
const newModelProperties = {
/**
* Increments the id for the type based on ids already present
*
* @param {number} i the number that will be used to create an id
* @returns {number} an incremented number related to the latest id in the store
*/
id: (i) => String(store.getAll(type).length + i + 1),
...properties,
...overrideOptions
}
let identity = false
if (newModelProperties.identity) {
if (typeof newModelProperties.identity === 'string') {
identity = newModelProperties.identity
} else {
identity = factoryName
}
delete newModelProperties.identity
if (numberOfRecords === 1) {
if (singletons[identity]) return singletons[identity]
}
}
let addProperties
if (numberOfRecords > 1) {
addProperties = times(numberOfRecords, (i) => _buildModel(factoryName, newModelProperties, i))
} else {
addProperties = _buildModel(factoryName, newModelProperties)
}
const results = store.add(type, addProperties)
if (identity) {
singletons[identity] = results
}
return results
}
/**
* Creates a factory with { name, type, parent, ...attributesAndRelationships }, which can be used for
* building test data.
* The factory is named, with a set of options to use to configure it.
* - parent - use another factory as a basis for this one
* - type - the type of model to use (for use if no parent)
* - identity - whether this factory should be a singleton
* attributesAndRelationships - attributes and relationships. If properties are a function or an array of functions, they
* will be executed at runtime.
*
* @param {string} name the name to use for the factory
* @param {object} options options that can be used to configure the factory
*/
define (name, options = {}) {
const { type, parent, ...properties } = options
let factory
if (parent) {
const fromFactory = this.factories[parent]
if (!fromFactory) {
throw new Error(`Factory ${parent} does not exist`)
}
factory = {
...fromFactory,
...properties
}
} else {
factory = {
type,
...properties
}
}
this.factories[name] = factory
}
/**
* Alias for `this.store.add`
*
* @param {...any} params attributes and relationships to be added to the store
* @returns {*} object or array
*/
add = (...params) => this.store.add(...params)
/**
* Verifies that the requested factory exists
*
* @param {string} factoryName the name of the factory
* @private
*/
_verifyFactory = (factoryName) => {
const factory = this.factories[factoryName]
if (!factory) {
throw new Error(`Factory ${factoryName} does not exist`)
}
}
/**
* Builds model properties that will be used for creating models. Since factories can use
* functions to define relationships, it loops through properties and attempts to execute any functions.
*
* @param {string} factoryName the name of the factory
* @param {object} properties properties to build the object
* @param {number} index a number that can be used to build the object
* @returns {object} an object of properties to be used.
* @private
*/
_buildModel = (factoryName, properties, index = 0) => {
properties = clone(properties)
Object.keys(properties).forEach((key) => {
if (Array.isArray(properties[key])) {
properties[key] = properties[key].map((propDefinition) => {
return this._callPropertyDefinition(propDefinition, index, factoryName, properties)
})
} else {
properties[key] = this._callPropertyDefinition(properties[key], index, factoryName, properties)
}
})
return properties
}
/**
* If `definition` is a function, calls the function. Otherwise, returns the definition.
*
* @param {*} definition a property or function
* @param {number} index an index to be passed to the called function
* @param {string} factoryName the name of the factory
* @param {object} properties properties to be passed to the executed function
* @returns {*} a definition or executed function
*/
_callPropertyDefinition = (definition, index, factoryName, properties) => {
return typeof definition === 'function' ? definition.call(this, index, factoryName, properties) : definition
}
}
export default FactoryFarm