UNPKG

@themost/data

Version:
903 lines (875 loc) 31.7 kB
/** * @license * MOST Web Framework 2.0 Codename Blueshift * Copyright (c) 2017, THEMOST LP All rights reserved * * Use of this source code is governed by an BSD-3-Clause license that can be * found in the LICENSE file at https://themost.io/license */ /// var sprintf = require('sprintf').sprintf; var _ = require('lodash'); var SequentialEventEmitter = require("@themost/common/emitter").SequentialEventEmitter; var LangUtils = require("@themost/common/utils").LangUtils; var AbstractClassError = require('@themost/common/errors').AbstractClassError; var AbstractMethodError = require('@themost/common/errors').AbstractMethodError; var types = { }; /** * @classdesc Represents an abstract data connector to a database * @description * <p> There are several data adapters for connections to common database engines: </p> <ul> <li>MOST Web Framework MySQL Adapter for connecting with MySQL Database Server <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-mysql</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"MySQL Data Adapter", "invariantName": "mysql", "type":"most-data-mysql" } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ ... { "name":"development", "invariantName":"mysql", "default":true, "options": { "host":"localhost", "port":3306, "user":"user", "password":"password", "database":"test" } } ... ] </code></pre> </li> <li>MOST Web Framework MSSQL Adapter for connecting with Microsoft SQL Database Server <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-mssql</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"MSSQL Data Adapter", "invariantName": "mssql", "type":"most-data-mssql" } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ ... { "name":"development", "invariantName":"mssql", "default":true, "options": { "server":"localhost", "user":"user", "password":"password", "database":"test" } } ... ] </code></pre> </li> <li>MOST Web Framework PostgreSQL Adapter for connecting with PostgreSQL Database Server <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-pg</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"PostgreSQL Data Adapter", "invariantName": "postgres", "type":"most-data-pg" } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ ... { "name":"development", "invariantName":"postgres", "default":true, "options": { "host":"localhost", "post":5432, "user":"user", "password":"password", "database":"db" } } ... ] </code></pre> </li> <li>MOST Web Framework Oracle Adapter for connecting with Oracle Database Server <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-oracle</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"Oracle Data Adapter", "invariantName": "oracle", "type":"most-data-oracle" } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ ... { "name":"development", "invariantName":"oracle", "default":true, "options": { "host":"localhost", "port":1521, "user":"user", "password":"password", "service":"orcl", "schema":"PUBLIC" } } ... ] </code></pre> </li> <li>MOST Web Framework SQLite Adapter for connecting with Sqlite Databases <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-sqlite</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"SQLite Data Adapter", "invariantName": "sqlite", "type":"most-data-sqlite" } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ ... { "name":"development", "invariantName":"sqlite", "default":true, "options": { database:"db/local.db" } } ... ] </code></pre> </li> <li>MOST Web Framework Data Pool Adapter for connection pooling <p>Install the data adapter:<p> <pre class="prettyprint"><code>npm install most-data-pool</code></pre> <p>Append the adapter type in application configuration (app.json#adapterTypes):<p> <pre class="prettyprint"><code> ... "adapterTypes": [ ... { "name":"Pool Data Adapter", "invariantName": "pool", "type":"most-data-pool" } { "name":"...", "invariantName": "...", "type":"..." } ... ] </code></pre> <p>Register an adapter in application configuration (app.json#adapters):<p> <pre class="prettyprint"><code> adapters: [ { "name":"development", "invariantName":"...", "default":false, "options": { "server":"localhost", "user":"user", "password":"password", "database":"test" } }, { "name":"development_with_pool", "invariantName":"pool", "default":true, "options": { "adapter":"development" } } ... ] </code></pre> </li> </ul> * @class * @constructor * @param {*} options - The database connection options * @abstract * @property {*} rawConnection - Gets or sets the native database connection * @property {*} options - Gets or sets the database connection options */ function DataAdapter(options) { if (this.constructor === DataAdapter.prototype.constructor) { throw new AbstractClassError(); } this.rawConnection=null; this.options = options; } // noinspection JSUnusedLocalSymbols /** * Opens the underlying database connection * @param {Function} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.open = function(callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Closes the underlying database connection * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.close = function(callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Executes the given query against the underlying database. * @param {string|*} query - A string or a query expression to execute. * @param {*} values - An object which represents the named parameters that are going to used during query parsing * @param {Function} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. The second argument will contain the result. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.execute = function(query, values, callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Produces a new identity value for the given entity and attribute. * @param {string} entity - A string that represents the target entity name * @param {string} attribute - A string that represents the target attribute name * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. The second argument will contain the result. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.selectIdentity = function(entity, attribute , callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * Begins a transactional operation and executes the given function * @param {Function} fn - The function to execute * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. The second argument will contain the result. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.executeInTransaction = function(fn, callback) { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * A helper method for creating a database view if the current data adapter supports views * @param {string} name - A string that represents the name of the view to be created * @param {QueryExpression|*} query - A query expression that represents the database view * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @abstract */ // eslint-disable-next-line no-unused-vars DataAdapter.prototype.createView = function(name, query, callback) { throw new AbstractMethodError(); }; /** * @classdesc Represents the event arguments of a data model listener. * @class * @constructor * @property {DataModel|*} model - Represents the underlying model. * @property {DataObject|*} target - Represents the underlying data object. * @property {Number|*} state - Represents the operation state (Update, Insert, Delete). * @property {DataQueryable|*} emitter - Represents the event emitter, normally a DataQueryable object instance. * @property {*} query - Represents the underlying query expression. This property may be null. * @property {DataObject|*} previous - Represents the underlying data object. */ function DataEventArgs() { // } /** * @classdesc Represents the main data context. * @class * @augments SequentialEventEmitter * @constructor * @abstract */ function DataContext() { DataContext.super_.bind(this)(); //throw abstract class error if (this.constructor === DataContext.prototype.constructor) { throw new AbstractClassError(); } /** * @property db * @description Gets the current database adapter * @type {DataAdapter} * @memberOf DataContext# */ Object.defineProperty(this, 'db', { get : function() { return null; }, configurable : true, enumerable:false }); } // noinspection JSUnusedLocalSymbols /** * Gets a data model based on the given data context * @param name {string} A string that represents the model to be loaded. * @returns {DataModel} * @abstract */ // eslint-disable-next-line no-unused-vars DataContext.prototype.model = function(name) { throw new AbstractMethodError(); }; /** * Gets an instance of DataConfiguration class which is associated with this data context * @returns {ConfigurationBase} * @abstract */ DataContext.prototype.getConfiguration = function() { throw new AbstractMethodError(); }; // noinspection JSUnusedLocalSymbols /** * @param {Function} callback * @abstract */ // eslint-disable-next-line no-unused-vars DataContext.prototype.finalize = function(callback) { throw new AbstractMethodError(); }; LangUtils.inherits(DataContext, SequentialEventEmitter); /** * @classdesc Represents a data model's listener * @class * @constructor * @abstract */ function DataEventListener() { //do nothing } /** * Occurs before executing a data operation. The event arguments contain the query that is going to be executed. * @param {DataEventArgs} e - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeExecute = function(e, cb) { return cb(); }; /** * Occurs after executing a data operation. The event arguments contain the executed query. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterExecute = function(event, cb) { return cb(); }; /** * Occurs before creating or updating a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeSave = function(event, cb) { return cb(); }; /** * Occurs after creating or updating a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterSave = function(event, cb) { return cb(); }; /** * Occurs before removing a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. * @returns {DataEventListener} */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.beforeRemove = function(event, cb) { return cb(); }; /** * Occurs after removing a data object. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterRemove = function(event, cb) { return cb(); }; /** * Occurs after upgrading a data model. * @param {DataEventArgs} event - An object that represents the event arguments passed to this operation. * @param {Function} cb - A callback function that should be called at the end of this operation. The first argument may be an error if any occurred. */ // eslint-disable-next-line no-unused-vars DataEventListener.prototype.afterUpgrade = function(event, cb) { return cb(); }; var DateTimeRegex = /^(\d{4})(?:-?W(\d+)(?:-?(\d+)D?)?|(?:-(\d+))?-(\d+))(?:[T ](\d+):(\d+)(?::(\d+)(?:\.(\d+))?)?)?(?:Z(-?\d*))?$/g; var BooleanTrueRegex = /^true$/ig; var BooleanFalseRegex = /^false$/ig; /* var NullRegex = /^null$/ig; var UndefinedRegex = /^undefined$/ig; */ var IntegerRegex =/^[-+]?\d+$/g; var FloatRegex =/^[+-]?\d+(\.\d+)?$/g; /** * Represents a model migration scheme against data adapters * @class * @constructor * @ignore */ function DataModelMigration() { /** * Gets an array that contains the definition of fields that are going to be added * @type {Array} */ this.add = []; /** * Gets an array that contains a collection of constraints which are going to be added * @type {Array} */ this.constraints = []; /** * Gets an array that contains a collection of indexes which are going to be added or updated * @type {Array} */ this.indexes = []; /** * Gets an array that contains the definition of fields that are going to be deleted * @type {Array} */ this.remove = []; /** * Gets an array that contains the definition of fields that are going to be changed * @type {Array} */ this.change = []; /** * Gets or sets a string that contains the internal version of this migration. This property cannot be null. * @type {string} */ this.version = '0.0'; /** * Gets or sets a string that represents a short description of this migration * @type {string} */ this.description = null; /** * Gets or sets a string that represents the adapter that is going to be migrated through this operation. * This property cannot be null. */ this.appliesTo = null; /** * Gets or sets a string that represents the model that is going to be migrated through this operation. * This property may be null. */ this.model = null; } /** * @classdesc DataAssociationMapping class describes the association between two models. * <p> * An association between two models is described in field attributes. For example * model Order may have an association with model Party (Person or Organization) through the field Order.customer: * </p> <pre class="prettyprint"><code> { "name": "Order", "fields": [ ... { "name": "customer", "title": "Customer", "description": "Party placing the order.", "type": "Party" } ...] } </code></pre> <p> This association is equivalent with the following DataAssociationMapping instance: </p> <pre class="prettyprint"><code> "mapping": { "cascade": "null", "associationType": "association", "select": [], "childField": "customer", "childModel": "Order", "parentField": "id", "parentModel": "Party" } </code></pre> <p> The above association mapping was auto-generated from the field definition of Order.customer where the field type (Party) actually defines the association between these models. </p> <p> Another example of an association between two models is a many-to-many association. User model has a many-to-many association (for user groups) with Group model: </p> <pre class="prettyprint"><code> { "name": "User", "fields": [ ... { "name": "groups", "title": "User Groups", "description": "A collection of groups where user belongs.", "type": "Group", "expandable": true, "mapping": { "associationAdapter": "GroupMembers", "parentModel": "Group", "parentField": "id", "childModel": "User", "childField": "id", "associationType": "junction", "cascade": "delete" } } ...] } </code></pre> <p>This association may also be defined in Group model:</p> <pre class="prettyprint"><code> { "name": "Group", "fields": [ ... { "name": "members", "title": "Group Members", "description": "Contains the collection of group members (users or groups).", "type": "Account", "many":true } ...] } </code></pre> * * @class * @property {string} associationAdapter - Gets or sets the association database object * @property {string} parentModel - Gets or sets the parent model name * @property {string} childModel - Gets or sets the child model name * @property {string} parentField - Gets or sets the parent field name * @property {string} childField - Gets or sets the child field name * @property {string} associationObjectField - Gets or sets the name of the parent field as it is defined in association adapter. This attribute is optional but it is required for many-to-many associations where parent and child model are the same. * @property {string} associationValueField - Gets or sets the name of the child field as it is defined in association adapter. This attribute is optional but it is required for many-to-many associations where parent and child model are the same. * @property {string} refersTo - Gets or sets the parent property where this association refers to * @property {string} parentLabel - Gets or sets the parent field that is going to be used as label for this association * @property {string} cascade - Gets or sets the action that occurs when parent item is going to be deleted (all|none|null|delete). The default value is 'none'. * @property {string} associationType - Gets or sets the type of this association (junction|association). The default value is 'association'. * @property {Array<DataModelPrivilege>} privilege - Gets or sets a collection of privileges which are going to be attached in a many-to-many association * @property {string[]} select - Gets or sets an array of fields to select from associated model. If this property is empty then all associated model fields will be selected. * @property {*} options - Gets or sets a set of default options which are going to be used while expanding results based on this data association. * @param {*=} obj - An object that contains relation mapping attributes * @constructor */ function DataAssociationMapping(obj) { this.cascade = 'none'; this.associationType = 'association'; //this.select = []; if (typeof obj === 'object') { _.assign(this, obj); } } /** * @class * @constructor * @property {string} name - Gets or sets the internal name of this field. * @property {string} property - Gets or sets the property name for this field. * @property {string} title - Gets or sets the title of this field. * @property {boolean} nullable - Gets or sets a boolean that indicates whether field is nullable or not. * @property {string} type - Gets or sets the type of this field. * @property {boolean} primary - Gets or sets a boolean that indicates whether field is primary key or not. * @property {boolean} many - Gets or sets a boolean that indicates whether field defines an one-to-many relationship between models. * @property {boolean} model - Gets or sets the parent model of this field. * @property {*} value - Gets or sets the default value of this field. * @property {*} calculation - Gets or sets the calculated value of this field. * @property {boolean} readonly - Gets or sets a boolean which indicates whether a field is readonly. * @property {boolean} editable - Gets or sets a boolean which indicates whether a field is available for edit. The default value is true. * @property {DataAssociationMapping} mapping - Get or sets a relation mapping for this field. * @property {string} coltype - Gets or sets a string that indicates the data field's column type. This attribute is used in data view definition * @property {boolean} expandable - Get or sets whether the current field defines an association mapping and the associated data object(s) must be included while getting data. * @property {string} section - Gets or sets the section where the field belongs. * @property {boolean} nested - Gets or sets a boolean which indicates whether this field allows object(s) to be nested and updatable during an insert or update operation * @property {string} description - Gets or sets a short description for this field. * @property {string} help - Gets or sets a short help for this field. * @property {string} appearance - Gets or sets the appearance template of this field, if any. * @property {{type:string,custom:string,minValue:*,maxValue:*,minLength:number,maxLength:number,pattern:string,patternMessage:string}|*} validation - Gets or sets data validation attributes. * @property {*} options - Gets or sets the available options for this field. * @property {boolean} virtual - Gets or sets a boolean that indicates whether this field is a view only field or not. * @property {boolean} indexed - Gets or sets a boolean which indicates whether this field will be indexed for searching items. The default value is false. */ function DataField() { this.nullable = true; this.primary = false; this.indexed = false; this.readonly = false; this.expandable = false; this.virtual = false; this.editable = true; } // noinspection JSUnusedGlobalSymbols DataField.prototype.getName = function() { return this.property || this.name; }; /** * @class * @constructor * @property {string} name - Gets or sets a short description for this listener * @property {string} type - Gets or sets a string which is the path of the module that exports this listener. * @property {boolean} disabled - Gets or sets a boolean value that indicates whether this listener is disabled or not. The default value is false. * @description * <p> * A data model uses event listeners as triggers which are automatically executed after data operations. * Those listeners are defined in [eventListeners] section of a model's schema. * </p> * <pre class="prettyprint"> *<code> * { * ... * "fields": [ ... ], * ... * "eventListeners": [ * { "name":"Update Listener", "type":"/app/controllers/an-update-listener.js" }, * { "name":"Another Update Listener", "type":"module-a/lib/listener" } * ] * ... * } *</code> * </pre> * @example * // A simple DataEventListener that sends a message to sales users after new order was arrived. * var web = require("most-web"); exports.afterSave = function(event, callback) { //exit if state is other than [Insert] if (event.state != 1) { return callback() } //initialize web mailer var mm = require("most-web-mailer"), context = event.model.context; //send new order mail template by passing new item data mm.mailer(context).to("sales@example.com") .cc("supervisor@example.com") .subject("New Order") .template("new-order").send(event.target, function(err) { if (err) { return web.common.log(err); } return callback(); }); }; * */ function DataModelEventListener() { } /** * An enumeration of tha available privilege types * @enum */ var PrivilegeType = { /** * Self Privilege (self). * @type {string} */ Self: "self", /** * Parent Privilege (parent) * @type {string} */ Parent: "parent", /** * Item Privilege (child) * @type {string} */ Item: "item", /** * Global Privilege (global) * @type {string} */ Global: "global" }; /** * @classdesc Represents a privilege which is defined in a data model and it may be given in users and groups * @class * @constructor * @property {PermissionMask} mask - Gets or sets the set of permissions which may be given with this privilege. * @property {PrivilegeType|string} type - Gets or sets the type of this privilege (global|parent|item|self). * @property {string} filter - Gets or sets a filter expression which is going to be used for self privileges. * The defined set of permissions are automatically assigned if the requested objects fulfill filter criteria. * (e.g. read-write permissions for a user's associated person through the following expression:"user eq me()") * @property {string} account - Gets or sets a wildcard (*) expression for global privileges only. * The defined set of permissions are automatically assigned to all users (e.g. read permissions for all users) */ function DataModelPrivilege() { } /** * Represents a query result when this query uses paging parameters. * @class * @property {number} total - The total number of records * @property {number} skip - The number of skipped records * @property {Array} value - An array of objects which represents the query results. * @constructor */ function DataResultSet() { this.total = 0; this.skip = 0; this.value = []; } /** * @abstract * @constructor * @ignore */ function DataContextEmitter() { if (this.constructor === DataContextEmitter.prototype.constructor) { throw new AbstractClassError(); } } /** * @abstract */ DataContextEmitter.prototype.ensureContext = function() { throw new AbstractMethodError(); }; /** * An enumeration of the available data object states * @enum {number} */ var DataObjectState = { /** * Insert State (1) */ Insert:1, /** * Update State (2) */ Update:2, /** * Delete State (4) */ Delete:4 }; /** * An enumeration of the available data caching types * @enum {string} */ var DataCachingType = { /** * Data will never be cached (none) */ None: 'none', /** * Data will always be cached (always) */ Always: 'always', /** * Data will conditionally be cached (conditional) */ Conditional: 'conditional' }; types.PrivilegeType = PrivilegeType; types.DataObjectState = DataObjectState; types.DataCachingType = DataCachingType; types.DataAdapter = DataAdapter; types.DataContext = DataContext; types.DataContextEmitter = DataContextEmitter; types.DataEventArgs = DataEventArgs; types.DataEventListener = DataEventListener; types.DataModelMigration = DataModelMigration; types.DataAssociationMapping = DataAssociationMapping; types.DataField=DataField; types.DataResultSet=DataResultSet; types.DataModelEventListener=DataModelEventListener; types.DataModelPrivilege=DataModelPrivilege; // noinspection JSUnusedGlobalSymbols types.parsers = { parseInteger: function(val) { if (_.isNil(val)) return 0; else if (typeof val === 'number') return val; else if (typeof val === 'string') { if (val.match(IntegerRegex) || val.match(FloatRegex)) { return parseInt(val, 10); } else if (val.match(BooleanTrueRegex)) return 1; else if (val.match(BooleanFalseRegex)) return 0; } else if (typeof val === 'boolean') return val===true ? 1 : 0; else { return parseInt(val) || 0; } }, parseCounter: function(val) { return types.parsers.parseInteger(val); }, parseFloat: function(val) { if (_.isNil(val)) return 0; else if (typeof val === 'number') return val; else if (typeof val === 'string') { if (val.match(IntegerRegex) || val.match(FloatRegex)) { return parseFloat(val); } else if (val.match(BooleanTrueRegex)) return 1; } else if (typeof val === 'boolean') return val===true ? 1 : 0; else { return parseFloat(val); } }, parseNumber: function(val) { return types.parsers.parseFloat(val); }, parseDateTime: function(val) { if (_.isNil(val)) return null; if (val instanceof Date) return val; if (typeof val === 'string') { if (val.match(DateTimeRegex)) return new Date(Date.parse(val)); } else if (typeof val === 'number') { return new Date(val); } return null; }, parseDate: function(val) { var res = types.parsers.parseDateTime(val); if (res instanceof Date) { res.setHours(0,0,0,0); return res; } return res; }, parseBoolean: function(val) { return (types.parsers.parseInteger(val)!==0); }, parseText: function(val) { if (_.isNil(val)) return val; else if (typeof val === 'string') { return val; } else { return val.toString(); } } }; module.exports = types;