UNPKG

loopback-workspace

Version:

**⚠️ LoopBack 3 is in Maintenance LTS mode, only critical bugs and critical security fixes will be provided. (See [Module Long Term Support Policy](#module-long-term-support-policy) below.)**

188 lines (169 loc) 5.21 kB
// Copyright IBM Corp. 2015,2019. All Rights Reserved. // Node module: loopback-workspace // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; const extend = require('util')._extend; const async = require('async'); module.exports = exports = TestDataBuilder; /** * Build many Model instances in one async call. * * Usage: * ```js * // The context object to hold the created models. * // You can use `this` in mocha test instead. * var context = {}; * * var ref = TestDataBuilder.ref; * new TestDataBuilder() * .define('application', Application, { * pushSettings: { stub: { } } * }) * .define('device', Device, { * appId: ref('application.id'), * deviceType: 'android' * }) * .define('notification', Notification) * .buildTo(context, function(err) { * // test models are available as * // context.application * // context.device * // context.notification * }); * ``` * @constructor */ function TestDataBuilder() { this._definitions = []; } /** * Define a new model instance. * @param {string} name Name of the instance. * `buildTo()` will save the instance created as context[name]. * @param {constructor} Model Model class/constructor. * @param {Object.<string, Object>=} properties * Properties to set in the object. * Intelligent default values are supplied by the builder * for required properties not listed. * @return TestDataBuilder (fluent interface) */ TestDataBuilder.prototype.define = function(name, Model, properties) { this._definitions.push({ name: name, model: Model, properties: properties, }); return this; }; /** * Reference the value of a property from a model instance defined before. * @param {string} path Generally in the form '{name}.{property}', where {name} * is the name passed to `define()` and {property} is the name of * the property to use. */ TestDataBuilder.ref = function(path) { return new Reference(path); }; /** * Asynchronously build all models defined via `define()` and save them in * the supplied context object. * @param {Object.<string, Object>} context The context to object to populate. * @param {function(Error)} callback Callback. */ TestDataBuilder.prototype.buildTo = function(context, callback) { this._context = context; async.eachSeries( this._definitions, this._buildObject.bind(this), callback, ); }; TestDataBuilder.prototype._buildObject = function(definition, callback) { const defaultValues = this._gatherDefaultPropertyValues(definition.model); const values = extend(defaultValues, definition.properties || {}); const resolvedValues = this._resolveValues(values); definition.model.create(resolvedValues, function(err, result) { if (err) { console.error( 'Cannot build object %j - %s\nDetails: %j', definition, err.message, err.details, ); } else { this._context[definition.name] = result; } callback(err); }.bind(this)); }; TestDataBuilder.prototype._resolveValues = function(values) { const result = {}; for (const key in values) { let val = values[key]; if (val instanceof Reference) { val = values[key].resolveFromContext(this._context); } result[key] = val; } return result; }; let valueCounter = 0; TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) { const result = {}; Model.forEachProperty(function createDefaultPropertyValue(name) { const prop = Model.definition.properties[name]; if (!prop.required) return; switch (prop.type) { case String: let generatedString = 'a test ' + name + ' #' + (++valueCounter); // If this property has a maximum length, ensure that the generated // string is not longer than the property's max length if (prop.length) { // Chop off the front part of the string so it is equal to the length generatedString = generatedString.substring( generatedString.length - prop.length, ); } result[name] = generatedString; break; case Number: result[name] = 1230000 + (++valueCounter); break; case Date: result[name] = new Date( 2222, 12, 12, // yyyy, mm, dd 12, 12, 12, // hh, MM, ss ++valueCounter, // milliseconds ); break; case Boolean: // There isn't much choice here, is it? // Let's use "false" to encourage users to be explicit when they // require "true" to turn some flag/behaviour on result[name] = false; break; // TODO: support nested structures - array, object } }); return result; }; /** * Placeholder for values that will be resolved during build. * @param path * @constructor * @private */ function Reference(path) { this._path = path; } Reference.prototype.resolveFromContext = function(context) { const elements = this._path.split('.'); const result = elements.reduce( function(obj, prop) { return obj[prop]; }, context, ); return result; };