// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-datasource-juggler // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; const assert = require('assert'); const util = require('util'); const EventEmitter = require('events').EventEmitter; const traverse = require('traverse'); const ModelBaseClass = require('./model'); const ModelBuilder = require('./model-builder'); /** * Model definition */ module.exports = ModelDefinition; /** * Constructor for ModelDefinition * @param {ModelBuilder} modelBuilder A model builder instance * @param {String|Object} name The model name or the schema object * @param {Object} properties The model properties, optional * @param {Object} settings The model settings, optional * @returns {ModelDefinition} * @constructor * */ function ModelDefinition(modelBuilder, name, properties, settings) { if (!(this instanceof ModelDefinition)) { // Allow to call ModelDefinition without new return new ModelDefinition(modelBuilder, name, properties, settings); } this.modelBuilder = modelBuilder || ModelBuilder.defaultInstance; assert(name, 'name is missing'); if (arguments.length === 2 && typeof name === 'object') { const schema = name; this.name = schema.name; this.rawProperties = schema.properties || {}; // Keep the raw property definitions this.settings = schema.settings || {}; } else { assert(typeof name === 'string', 'name must be a string'); this.name = name; this.rawProperties = properties || {}; // Keep the raw property definitions this.settings = settings || {}; } this.relations = []; this.properties = null; this.build(); } util.inherits(ModelDefinition, EventEmitter); // Set up types require('./types')(ModelDefinition); /** * Return table name for specified `modelName` * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb' */ ModelDefinition.prototype.tableName = function(connectorType) { const settings = this.settings; if (settings[connectorType]) { return settings[connectorType].table || settings[connectorType].tableName || this.name; } else { return this.name; } }; /** * Return column name for specified modelName and propertyName * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb' * @param propertyName The property name * @returns {String} columnName */ ModelDefinition.prototype.columnName = function(connectorType, propertyName) { if (!propertyName) { return propertyName; } this.build(); const property = this.properties[propertyName]; if (property && property[connectorType]) { return property[connectorType].column || property[connectorType].columnName || propertyName; } else { return propertyName; } }; /** * Return column metadata for specified modelName and propertyName * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb' * @param propertyName The property name * @returns {Object} column metadata */ ModelDefinition.prototype.columnMetadata = function(connectorType, propertyName) { if (!propertyName) { return propertyName; } this.build(); const property = this.properties[propertyName]; if (property && property[connectorType]) { return property[connectorType]; } else { return null; } }; /** * Return column names for specified modelName * @param {String} connectorType The connector type, such as 'oracle' or 'mongodb' * @returns {String[]} column names */ ModelDefinition.prototype.columnNames = function(connectorType) { this.build(); const props = this.properties; const cols = []; for (const p in props) { if (props[p][connectorType]) { cols.push(props[p][connectorType].column || props[p][connectorType].columnName || p); } else { cols.push(p); } } return cols; }; /** * Find the ID properties sorted by the index * @returns {Object[]} property name/index for IDs */ ModelDefinition.prototype.ids = function() { if (this._ids) { return this._ids; } const ids = []; this.build(); const props = this.properties; for (const key in props) { let id = props[key].id; if (!id) { continue; } if (typeof id !== 'number') { id = 1; } ids.push({name: key, id: id, property: props[key]}); } ids.sort(function(a, b) { return a.id - b.id; }); this._ids = ids; return ids; }; /** * Find the ID column name * @param {String} modelName The model name * @returns {String} columnName for ID */ ModelDefinition.prototype.idColumnName = function(connectorType) { return this.columnName(connectorType, this.idName()); }; /** * Find the ID property name * @returns {String} property name for ID */ ModelDefinition.prototype.idName = function() { const id = this.ids()[0]; if (this.properties.id && this.properties.id.id) { return 'id'; } else { return id && id.name; } }; /** * Find the ID property names sorted by the index * @returns {String[]} property names for IDs */ ModelDefinition.prototype.idNames = function() { const ids = this.ids(); const names = ids.map(function(id) { return id.name; }); return names; }; /** * * @returns {{}} */ ModelDefinition.prototype.indexes = function() { this.build(); const indexes = {}; if (this.settings.indexes) { for (const i in this.settings.indexes) { indexes[i] = this.settings.indexes[i]; } } for (const p in this.properties) { if (this.properties[p].index) { indexes[p + '_index'] = this.properties[p].index; } } return indexes; }; /** * Build a model definition * @param {Boolean} force Forcing rebuild */ ModelDefinition.prototype.build = function(forceRebuild) { if (forceRebuild) { this.properties = null; this.relations = []; this._ids = null; this.json = null; } if (this.properties) { return this.properties; } this.properties = {}; for (const p in this.rawProperties) { const prop = this.rawProperties[p]; const type = this.modelBuilder.resolveType(prop); if (typeof type === 'string') { this.relations.push({ source: this.name, target: type, type: Array.isArray(prop) ? 'hasMany' : 'belongsTo', as: p, }); } else { const typeDef = { type: type, }; if (typeof prop === 'object' && prop !== null) { for (const a in prop) { // Skip the type property but don't delete it Model.extend() shares same instances of the properties from the base class if (a !== 'type') { typeDef[a] = prop[a]; } } } this.properties[p] = typeDef; } } return this.properties; }; /** * Define a property * @param {String} propertyName The property name * @param {Object} propertyDefinition The property definition */ ModelDefinition.prototype.defineProperty = function(propertyName, propertyDefinition) { this.rawProperties[propertyName] = propertyDefinition; this.build(true); }; function isModelClass(cls) { if (!cls) { return false; } return cls.prototype instanceof ModelBaseClass; } ModelDefinition.prototype.toJSON = function(forceRebuild) { if (forceRebuild) { this.json = null; } if (this.json) { return this.json; } const json = { name: this.name, properties: {}, settings: this.settings, }; this.build(forceRebuild); const mapper = function(val) { if (val === undefined || val === null) { return val; } if ('function' === typeof val.toJSON) { // The value has its own toJSON() object return val.toJSON(); } if ('function' === typeof val) { if (isModelClass(val)) { if (val.settings && val.settings.anonymous) { return val.definition && val.definition.toJSON().properties; } else { return val.modelName; } } return val.name; } else { return val; } }; for (const p in this.properties) { json.properties[p] = traverse(this.properties[p]).map(mapper); } this.json = json; return json; }; ModelDefinition.prototype.hasPK = function() { return this.ids().length > 0; };