loopback-datasource-juggler
Version:
LoopBack DataSource Juggler
1,485 lines (1,338 loc) • 93.3 kB
JavaScript
// Copyright IBM Corp. 2013,2020. 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
// Turning on strict for this file breaks lots of test cases;
// disabling strict for this file
/* eslint-disable strict */
/*!
* Module dependencies
*/
const ModelBuilder = require('./model-builder.js').ModelBuilder;
const ModelDefinition = require('./model-definition.js');
const RelationDefinition = require('./relation-definition.js');
const OberserverMixin = require('./observer');
const jutil = require('./jutil');
const utils = require('./utils');
const ModelBaseClass = require('./model.js');
const DataAccessObject = require('./dao.js');
const defineScope = require('./scope.js').defineScope;
const EventEmitter = require('events').EventEmitter;
const util = require('util');
const assert = require('assert');
const async = require('async');
const traverse = require('neotraverse/legacy');
const g = require('strong-globalize')();
const juggler = require('..');
const deprecated = require('depd')('loopback-datasource-juggler');
const Transaction = require('loopback-connector').Transaction;
const pascalCase = require('change-case').pascalCase;
const camelCase = require('change-case').camelCase;
if (process.env.DEBUG === 'loopback') {
// For back-compatibility
process.env.DEBUG = 'loopback:*';
}
const debug = require('debug')('loopback:datasource');
/*!
* Export public API
*/
exports.DataSource = DataSource;
/*!
* Helpers
*/
const slice = Array.prototype.slice;
/**
* LoopBack models can manipulate data via the DataSource object.
* Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`.
*
* Define a data source to persist model data.
* To create a DataSource programmatically, call `createDataSource()` on the LoopBack object; for example:
* ```js
* var oracle = loopback.createDataSource({
* connector: 'oracle',
* host: '111.22.333.44',
* database: 'MYDB',
* username: 'username',
* password: 'password'
* });
* ```
*
* All classes in single dataSource share same the connector type and
* one database connection.
*
* For example, the following creates a DataSource, and waits for a connection callback.
*
* ```
* var dataSource = new DataSource('mysql', { database: 'myapp_test' });
* dataSource.define(...);
* dataSource.on('connected', function () {
* // work with database
* });
* ```
* @class DataSource
* @param {String} [name] Optional name for datasource.
* @options {Object} settings Database-specific settings to establish connection (settings depend on specific connector).
* The table below lists a typical set for a relational database.
* @property {String} connector Database connector to use. For any supported connector, can be any of:
*
* - The connector module from `require(connectorName)`.
* - The full name of the connector module, such as 'loopback-connector-oracle'.
* - The short name of the connector module, such as 'oracle'.
* - A local module under `./connectors/` folder.
* @property {String} host Database server host name.
* @property {String} port Database server port number.
* @property {String} username Database user name.
* @property {String} password Database password.
* @property {String} database Name of the database to use.
* @property {Boolean} debug Display debugging information. Default is false.
*
* The constructor allows the following styles:
*
* 1. new DataSource(dataSourceName, settings). For example:
* - new DataSource('myDataSource', {connector: 'memory'});
* - new DataSource('myDataSource', {name: 'myDataSource', connector: 'memory'});
* - new DataSource('myDataSource', {name: 'anotherDataSource', connector: 'memory'});
*
* 2. new DataSource(settings). For example:
* - new DataSource({name: 'myDataSource', connector: 'memory'});
* - new DataSource({connector: 'memory'});
*
* 3. new DataSource(connectorModule, settings). For example:
* - new DataSource(connectorModule, {name: 'myDataSource})
* - new DataSource(connectorModule)
*/
function DataSource(name, settings, modelBuilder) {
if (!(this instanceof DataSource)) {
return new DataSource(name, settings);
}
// Check if the settings object is passed as the first argument
if (typeof name === 'object' && settings === undefined) {
settings = name;
name = undefined;
}
// Check if the first argument is a URL
if (typeof name === 'string' && name.indexOf('://') !== -1) {
name = utils.parseSettings(name);
}
// Check if the settings is in the form of URL string
if (typeof settings === 'string' && settings.indexOf('://') !== -1) {
settings = utils.parseSettings(settings);
}
// Shallow-clone the settings so that updates to `ds.settings`
// do not modify the original object provided via constructor arguments
if (settings) settings = Object.assign({}, settings);
// It's possible to provide settings object via the "name" arg too!
if (typeof name === 'object') name = Object.assign({}, name);
this.modelBuilder = modelBuilder || new ModelBuilder();
this.models = this.modelBuilder.models;
this.definitions = this.modelBuilder.definitions;
this.juggler = juggler;
this._queuedInvocations = 0;
// operation metadata
// Initialize it before calling setup as the connector might register operations
this._operations = {};
this.setup(name, settings);
this._setupConnector();
// connector
const connector = this.connector;
// DataAccessObject - connector defined or supply the default
const dao = (connector && connector.DataAccessObject) || this.constructor.DataAccessObject;
this.DataAccessObject = function() {
};
// define DataAccessObject methods
Object.keys(dao).forEach(function(name) {
const fn = dao[name];
this.DataAccessObject[name] = fn;
if (typeof fn === 'function') {
this.defineOperation(name, {
accepts: fn.accepts,
'returns': fn.returns,
http: fn.http,
remoteEnabled: fn.shared ? true : false,
scope: this.DataAccessObject,
fnName: name,
});
}
}.bind(this));
// define DataAccessObject.prototype methods
Object.keys(dao.prototype || []).forEach(function(name) {
const fn = dao.prototype[name];
this.DataAccessObject.prototype[name] = fn;
if (typeof fn === 'function') {
this.defineOperation(name, {
prototype: true,
accepts: fn.accepts,
'returns': fn.returns,
http: fn.http,
remoteEnabled: fn.shared ? true : false,
scope: this.DataAccessObject.prototype,
fnName: name,
});
}
}.bind(this));
}
util.inherits(DataSource, EventEmitter);
// allow child classes to supply a data access object
DataSource.DataAccessObject = DataAccessObject;
/**
* Global maximum number of event listeners
*/
DataSource.DEFAULT_MAX_OFFLINE_REQUESTS = 16;
/**
* Set up the connector instance for backward compatibility with JugglingDB schema/adapter
* @private
*/
DataSource.prototype._setupConnector = function() {
this.connector = this.connector || this.adapter; // The legacy JugglingDB adapter will set up `adapter` property
this.adapter = this.connector; // Keep the adapter as an alias to connector
if (this.connector) {
if (!this.connector.dataSource) {
// Set up the dataSource if the connector doesn't do so
this.connector.dataSource = this;
}
const dataSource = this;
// Set max listeners to a default/configured value
dataSource.setMaxListeners(dataSource.getMaxOfflineRequests());
this.connector.log = function(query, start) {
dataSource.log(query, start);
};
this.connector.logger = function(query) {
const t1 = Date.now();
const log = this.log;
return function(q) {
log(q || query, t1);
};
};
// Configure the connector instance to mix in observer functions
jutil.mixin(this.connector, OberserverMixin);
}
};
// List possible connector module names
function connectorModuleNames(name) {
const names = []; // Check the name as is
if (!name.match(/^\//)) {
names.push('./connectors/' + name); // Check built-in connectors
if (name.indexOf('loopback-connector-') !== 0) {
names.push('loopback-connector-' + name); // Try loopback-connector-<name>
}
}
// Only try the short name if the connector is not from StrongLoop
if (['mongodb', 'oracle', 'mysql', 'postgresql', 'mssql', 'rest', 'soap', 'db2', 'cloudant']
.indexOf(name) === -1) {
names.push(name);
}
return names;
}
// testable with DI
function tryModules(names, loader) {
let mod;
loader = loader || require;
for (let m = 0; m < names.length; m++) {
try {
mod = loader(names[m]);
} catch (e) {
const notFound = e.code === 'MODULE_NOT_FOUND' &&
e.message && e.message.indexOf(names[m]) > 0;
if (notFound) {
debug('Module %s not found, will try another candidate.', names[m]);
continue;
}
debug('Cannot load connector %s: %s', names[m], e.stack || e);
throw e;
}
if (mod) {
break;
}
}
return mod;
}
/*!
* Resolve a connector by name
* @param name The connector name
* @returns {*}
* @private
*/
DataSource._resolveConnector = function(name, loader) {
const names = connectorModuleNames(name);
const connector = tryModules(names, loader);
let error = null;
if (!connector) {
error = g.f('\nWARNING: {{LoopBack}} connector "%s" is not installed ' +
'as any of the following modules:\n\n %s\n\nTo fix, run:\n\n {{npm install %s --save}}\n',
name, names.join('\n'), names[names.length - 1]);
}
return {
connector: connector,
error: error,
};
};
/**
* Connect to the data source.
* If no callback is provided, it will return a Promise.
* Emits the 'connect' event.
* @param callback
* @returns {Promise}
* @emits connected
*/
DataSource.prototype.connect = function(callback) {
callback = callback || utils.createPromiseCallback();
const self = this;
if (this.connected) {
// The data source is already connected, return immediately
process.nextTick(callback);
return callback.promise;
}
if (typeof this.connector.connect !== 'function') {
// Connector doesn't have the connect function
// Assume no connect is needed
self.connected = true;
self.connecting = false;
process.nextTick(function() {
self.emit('connected');
callback();
});
return callback.promise;
}
// Queue the callback
this.pendingConnectCallbacks = this.pendingConnectCallbacks || [];
this.pendingConnectCallbacks.push(callback);
// The connect is already in progress
if (this.connecting) return callback.promise;
this.connector.connect(function(err, result) {
self.connecting = false;
if (!err) self.connected = true;
const cbs = self.pendingConnectCallbacks;
self.pendingConnectCallbacks = [];
if (!err) {
self.emit('connected');
} else {
self.emit('error', err);
}
// Invoke all pending callbacks
async.each(cbs, function(cb, done) {
try {
cb(err);
} catch (e) {
// Ignore error to make sure all callbacks are invoked
debug('Uncaught error raised by connect callback function: ', e);
} finally {
done();
}
}, function(err) {
if (err) throw err; // It should not happen
});
});
// Set connecting flag to be `true` so that the connector knows there is
// a connect in progress. The change of `connecting` should happen immediately
// after the connect request is sent
this.connecting = true;
return callback.promise;
};
/**
* Set up the data source. The following styles are supported:
* ```js
* ds.setup('myDataSource', {connector: 'memory'}); // ds.name -> 'myDataSource'
* ds.setup('myDataSource', {name: 'myDataSource', connector: 'memory'}); // ds.name -> 'myDataSource'
* ds.setup('myDataSource', {name: 'anotherDataSource', connector: 'memory'}); // ds.name -> 'myDataSource' and a warning will be issued
* ds.setup({name: 'myDataSource', connector: 'memory'}); // ds.name -> 'myDataSource'
* ds.setup({connector: 'memory'}); // ds.name -> 'memory'
* ```
* @param {String} dsName The name of the datasource. If not set, use
* `settings.name`
* @param {Object} settings The settings
* @returns {*}
* @private
*/
DataSource.prototype.setup = function(dsName, settings) {
const dataSource = this;
let connector;
// First argument is an `object`
if (dsName && typeof dsName === 'object') {
if (settings === undefined) {
// setup({name: 'myDataSource', connector: 'memory'})
settings = dsName;
dsName = undefined;
} else {
// setup(connector, {name: 'myDataSource', host: 'localhost'})
connector = dsName;
dsName = undefined;
}
}
if (typeof dsName !== 'string') {
dsName = undefined;
}
if (typeof settings === 'object') {
if (settings.initialize) {
// Settings is the resolved connector instance
connector = settings;
// Set settings to undefined to avoid confusion
settings = undefined;
} else if (settings.connector) {
// Use `connector`
connector = settings.connector;
} else if (settings.adapter) {
// `adapter` as alias for `connector`
connector = settings.adapter;
}
}
// just save everything we get
this.settings = settings || {};
this.settings.debug = this.settings.debug || debug.enabled;
if (this.settings.debug) {
debug('Settings: %j', this.settings);
}
if (typeof settings === 'object' && typeof settings.name === 'string' &&
typeof dsName === 'string' && dsName !== settings.name) {
// setup('myDataSource', {name: 'anotherDataSource', connector: 'memory'});
// ds.name -> 'myDataSource' and a warning will be issued
console.warn(
'A datasource is created with name %j, which is different from the name in settings (%j). ' +
'Please adjust your configuration to ensure these names match.',
dsName, settings.name,
);
}
// Disconnected by default
this.connected = false;
this.connecting = false;
this.initialized = false;
this.name = dsName || (typeof this.settings.name === 'string' && this.settings.name);
let connectorName;
if (typeof connector === 'string') {
// Connector needs to be resolved by name
connectorName = connector;
connector = undefined;
} else if ((typeof connector === 'object') && connector) {
connectorName = connector.name;
} else {
connectorName = dsName;
}
if (!this.name) {
// Fall back to connector name
this.name = connectorName;
}
if ((!connector) && connectorName) {
// The connector has not been resolved
const result = DataSource._resolveConnector(connectorName);
connector = result.connector;
if (!connector) {
console.error(result.error);
this.emit('error', new Error(result.error));
return;
}
}
if (connector) {
const postInit = function postInit(err, result) {
this._setupConnector();
// we have an connector now?
if (!this.connector) {
throw new Error(g.f('Connector is not defined correctly: ' +
'it should create `{{connector}}` member of dataSource'));
}
if (!err) {
this.initialized = true;
this.emit('initialized');
}
debug('Connector is initialized for dataSource %s', this.name);
// If `result` is set to `false` explicitly, the connection will be
// lazily established
if (!this.settings.lazyConnect) {
this.connected = (!err) && (result !== false); // Connected now
}
if (this.connected) {
debug('DataSource %s is now connected to %s', this.name, this.connector.name);
this.emit('connected');
} else if (err) {
// The connection fails, let's report it and hope it will be recovered in the next call
// Reset the connecting to `false`
this.connecting = false;
if (this._queuedInvocations) {
// Another operation is already waiting for connect() result,
// let them handle the connection error.
debug('Connection fails: %s\nIt will be retried for the next request.', err);
} else {
g.error('Connection fails: %s\nIt will be retried for the next request.', err);
if (settings.catchFailure) {
try {
this.emit('error', err);
} catch (error) {
console.log(error);
}
} else {
this.emit('error', err);
}
}
} else {
// Either lazyConnect or connector initialize() defers the connection
debug('DataSource %s will be connected to connector %s', this.name,
this.connector.name);
}
}.bind(this);
try {
if ('function' === typeof connector.initialize) {
// Call the async initialize method
debug('Initializing connector %s', connector.name);
connector.initialize(this, postInit);
} else if ('function' === typeof connector) {
// Use the connector constructor directly
this.connector = new connector(this.settings);
postInit();
}
} catch (err) {
if (err.message) {
err.message = 'Cannot initialize connector ' +
JSON.stringify(connectorName) + ': ' +
err.message;
}
throw err;
}
}
};
function isModelClass(cls) {
if (!cls) {
return false;
}
return cls.prototype instanceof ModelBaseClass;
}
DataSource.relationTypes = Object.keys(RelationDefinition.RelationTypes);
function isModelDataSourceAttached(model) {
return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource);
}
/*!
* Define scopes for the model class from the scopes object. See
* [scopes](./Model-definition-JSON-file.html#scopes) for more information on
* scopes and valid options objects.
* @param {Object} modelClass - The model class that corresponds to the model
* definition that will be enhanced by the provided scopes.
* @param {Object} scopes A key-value collection of names and their object
* definitions
* @property options The options defined on the scope object.
*/
DataSource.prototype.defineScopes = function(modelClass, scopes) {
if (scopes) {
for (const s in scopes) {
defineScope(modelClass, modelClass, s, scopes[s], {}, scopes[s].options);
}
}
};
/*!
* Define relations for the model class from the relations object. See
* [relations](./Model-definition-JSON-file.html#relations) for more information.
* @param {Object} modelClass - The model class that corresponds to the model
* definition that will be enhanced by the provided relations.
* @param {Object} relations A key-value collection of relation names and their
* object definitions.
*/
DataSource.prototype.defineRelations = function(modelClass, relations) {
const self = this;
// Wait for target/through models to be attached before setting up the relation
const deferRelationSetup = function(relationName, relation, targetModel, throughModel) {
if (!isModelDataSourceAttached(targetModel)) {
targetModel.once('dataAccessConfigured', function(targetModel) {
// Check if the through model doesn't exist or resolved
if (!throughModel || isModelDataSourceAttached(throughModel)) {
// The target model is resolved
const params = traverse(relation).clone();
params.as = relationName;
params.model = targetModel;
if (throughModel) {
params.through = throughModel;
}
modelClass[relation.type].call(modelClass, relationName, params);
}
});
}
if (throughModel && !isModelDataSourceAttached(throughModel)) {
// Set up a listener to the through model
throughModel.once('dataAccessConfigured', function(throughModel) {
if (isModelDataSourceAttached(targetModel)) {
// The target model is resolved
const params = traverse(relation).clone();
params.as = relationName;
params.model = targetModel;
params.through = throughModel;
modelClass[relation.type].call(modelClass, relationName, params);
}
});
}
};
// Set up the relations
if (relations) {
Object.keys(relations).forEach(function(relationName) {
let targetModel;
const r = relations[relationName];
validateRelation(relationName, r);
if (r.model) {
targetModel = isModelClass(r.model) ? r.model : self.getModel(r.model, true);
}
let throughModel = null;
if (r.through) {
throughModel = isModelClass(r.through) ? r.through : self.getModel(r.through, true);
}
if ((targetModel && !isModelDataSourceAttached(targetModel)) ||
(throughModel && !isModelDataSourceAttached(throughModel))) {
// Create a listener to defer the relation set up
deferRelationSetup(relationName, r, targetModel, throughModel);
} else {
// The target model is resolved
const params = traverse(r).clone();
params.as = relationName;
params.model = targetModel;
if (throughModel) {
params.through = throughModel;
}
modelClass[r.type].call(modelClass, relationName, params);
}
});
}
};
function validateRelation(relationName, relation) {
const rn = relationName;
const r = relation;
let msg, code;
assert(DataSource.relationTypes.indexOf(r.type) !== -1, 'Invalid relation type: ' + r.type);
assert(isValidRelationName(rn), 'Invalid relation name: ' + rn);
// VALIDATION ERRORS
// non polymorphic belongsTo relations should have `model` defined
if (!r.polymorphic && r.type === 'belongsTo' && !r.model) {
msg = g.f('%s relation: %s requires param `model`', r.type, rn);
code = 'BELONGS_TO_MISSING_MODEL';
}
// polymorphic belongsTo relations should not have `model` defined
if (r.polymorphic && r.type === 'belongsTo' && r.model) {
msg = g.f('{{polymorphic}} %s relation: %s does not expect param `model`', r.type, rn);
code = 'POLYMORPHIC_BELONGS_TO_MODEL';
}
// polymorphic relations other than belongsTo should have `model` defined
if (r.polymorphic && r.type !== 'belongsTo' && !r.model) {
msg = g.f('{{polymorphic}} %s relation: %s requires param `model`', r.type, rn);
code = 'POLYMORPHIC_NOT_BELONGS_TO_MISSING_MODEL';
}
// polymorphic relations should provide both discriminator and foreignKey or none
if (r.polymorphic && r.polymorphic.foreignKey && !r.polymorphic.discriminator) {
msg = g.f('{{polymorphic}} %s relation: %s requires param `polymorphic.discriminator` ' +
'when param `polymorphic.foreignKey` is provided', r.type, rn);
code = 'POLYMORPHIC_MISSING_DISCRIMINATOR';
}
// polymorphic relations should provide both discriminator and foreignKey or none
if (r.polymorphic && r.polymorphic.discriminator && !r.polymorphic.foreignKey) {
msg = g.f('{{polymorphic}} %s relation: %s requires param `polymorphic.foreignKey` ' +
'when param `polymorphic.discriminator` is provided', r.type, rn);
code = 'POLYMORPHIC_MISSING_FOREIGN_KEY';
}
// polymorphic relations should not provide polymorphic.as when using custom foreignKey/discriminator
if (r.polymorphic && r.polymorphic.as && r.polymorphic.foreignKey) {
msg = g.f('{{polymorphic}} %s relation: %s does not expect param `polymorphic.as` ' +
'when defing custom `foreignKey`/`discriminator` ', r.type, rn);
code = 'POLYMORPHIC_EXTRANEOUS_AS';
}
// polymorphic relations should not provide polymorphic.as when using custom foreignKey/discriminator
if (r.polymorphic && r.polymorphic.selector && r.polymorphic.foreignKey) {
msg = g.f('{{polymorphic}} %s relation: %s does not expect param `polymorphic.selector` ' +
'when defing custom `foreignKey`/`discriminator` ', r.type, rn);
code = 'POLYMORPHIC_EXTRANEOUS_SELECTOR';
}
if (msg) {
const error = new Error(msg);
error.details = {code: code, rType: r.type, rName: rn};
throw error;
}
// DEPRECATION WARNINGS
if (r.polymorphic && r.polymorphic.as) {
deprecated(g.f('WARNING: {{polymorphic}} %s relation: %s uses keyword `polymorphic.as` which will ' +
'be DEPRECATED in LoopBack.next, refer to this doc for replacement solutions ' +
'(https://loopback.io/doc/en/lb3/Polymorphic-relations.html#deprecated-polymorphic-as)',
r.type, rn), r.type);
}
}
function isValidRelationName(relationName) {
const invalidRelationNames = ['trigger'];
return invalidRelationNames.indexOf(relationName) === -1;
}
/*!
* Set up the data access functions from the data source. Each data source will
* expose a data access object (DAO), which will be mixed into the modelClass.
* @param {Model} modelClass The model class that will receive DAO mixins.
* @param {Object} settings The settings object; typically allows any settings
* that would be valid for a typical Model object.
*/
DataSource.prototype.setupDataAccess = function(modelClass, settings) {
if (this.connector) {
// Check if the id property should be generated
const idName = modelClass.definition.idName();
const idProp = modelClass.definition.rawProperties[idName];
if (idProp && idProp.generated && idProp.useDefaultIdType !== false && this.connector.getDefaultIdType) {
// Set the default id type from connector's ability
const idType = this.connector.getDefaultIdType() || String;
idProp.type = idType;
modelClass.definition.rawProperties[idName].type = idType;
modelClass.definition.properties[idName].type = idType;
}
if (this.connector.define) {
// pass control to connector
this.connector.define({
model: modelClass,
properties: modelClass.definition.properties,
settings: settings,
});
}
}
// add data access objects
this.mixin(modelClass);
// define relations from LDL (options.relations)
const relations = settings.relationships || settings.relations;
this.defineRelations(modelClass, relations);
// Emit the dataAccessConfigured event to indicate all the methods for data
// access have been mixed into the model class
modelClass.emit('dataAccessConfigured', modelClass);
// define scopes from LDL (options.relations)
const scopes = settings.scopes || {};
this.defineScopes(modelClass, scopes);
};
/**
* Define a model class. Returns newly created model object.
* The first (String) argument specifying the model name is required.
* You can provide one or two JSON object arguments, to provide configuration options.
* See [Model definition reference](http://docs.strongloop.com/display/DOC/Model+definition+reference) for details.
*
* Simple example:
* ```
* var User = dataSource.createModel('User', {
* email: String,
* password: String,
* birthDate: Date,
* activated: Boolean
* });
* ```
* More advanced example
* ```
* var User = dataSource.createModel('User', {
* email: { type: String, limit: 150, index: true },
* password: { type: String, limit: 50 },
* birthDate: Date,
* registrationDate: {type: Date, default: function () { return new Date }},
* activated: { type: Boolean, default: false }
* });
* ```
* You can also define an ACL when you create a new data source with the `DataSource.create()` method. For example:
*
* ```js
* var Customer = ds.createModel('Customer', {
* name: {
* type: String,
* acls: [
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
* ]
* }
* }, {
* acls: [
* {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
* ]
* });
* ```
*
* @param {String} className Name of the model to create.
* @param {Object} properties Hash of model properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
* @options {Object} properties Other configuration options. This corresponds to the options key in the config object.
* @param {Object} settings A settings object that would typically be used for Model objects.
*/
DataSource.prototype.createModel =
DataSource.prototype.define = function defineClass(className, properties, settings) {
const args = slice.call(arguments);
if (!className) {
throw new Error(g.f('Class name required'));
}
if (args.length === 1) {
properties = {};
args.push(properties);
}
if (args.length === 2) {
settings = {};
args.push(settings);
}
properties = properties || {};
settings = settings || {};
if (this.isRelational()) {
// Set the strict mode to be true for relational DBs by default
if (settings.strict === undefined || settings.strict === null) {
settings.strict = true;
}
if (settings.strict === false) {
console.warn("WARNING: relational database doesn't support {strict: false} mode." +
` {strict: true} mode will be set for model ${className} instead.`);
settings.strict = true;
}
}
const modelClass = this.modelBuilder.define(className, properties, settings);
modelClass.dataSource = this;
if (settings.unresolved) {
return modelClass;
}
this.setupDataAccess(modelClass, settings);
modelClass.emit('dataSourceAttached', modelClass);
return modelClass;
};
/**
* Remove a model from the registry.
*
* @param {String} modelName
*/
DataSource.prototype.deleteModelByName = function(modelName) {
this.modelBuilder.deleteModelByName(modelName);
delete this.connector._models[modelName];
};
/**
* Remove all models from the registry, but keep the connector instance
* (including the pool of database connections).
*/
DataSource.prototype.deleteAllModels = function() {
for (const m in this.modelBuilder.models) {
this.deleteModelByName(m);
}
};
/**
* Mixin DataAccessObject methods.
*
* @param {Function} ModelCtor The model constructor
* @private
*/
DataSource.prototype.mixin = function(ModelCtor) {
const ops = this.operations();
const DAO = this.DataAccessObject;
// mixin DAO
jutil.mixin(ModelCtor, DAO, {proxyFunctions: true, override: true});
// decorate operations as alias functions
Object.keys(ops).forEach(function(name) {
const op = ops[name];
let scope;
if (op.enabled) {
scope = op.prototype ? ModelCtor.prototype : ModelCtor;
// var sfn = scope[name] = function () {
// op.scope[op.fnName].apply(self, arguments);
// }
Object.keys(op)
.filter(function(key) {
// filter out the following keys
return ~[
'scope',
'fnName',
'prototype',
].indexOf(key);
})
.forEach(function(key) {
if (typeof op[key] !== 'undefined') {
op.scope[op.fnName][key] = op[key];
}
});
}
});
};
/*! Method will be deprecated in LoopBack.next
*/
/**
* See [ModelBuilder.getModel](http://apidocs.strongloop.com/loopback-datasource-juggler/#modelbuilder-prototype-getmodel)
* for details.
*/
DataSource.prototype.getModel = function(name, forceCreate) {
return this.modelBuilder.getModel(name, forceCreate);
};
/*! Method will be deprecated in LoopBack.next
*/
/**
* See ModelBuilder.getModelDefinition
* See [ModelBuilder.getModelDefinition](http://apidocs.strongloop.com/loopback-datasource-juggler/#modelbuilder-prototype-getmodeldefinition)
* for details.
*/
DataSource.prototype.getModelDefinition = function(name) {
return this.modelBuilder.getModelDefinition(name);
};
/*! Method will be deprecated in LoopBack.next
*/
/**
* Get the data source types collection.
* @returns {String[]} The data source type array.
* For example, ['db', 'nosql', 'mongodb'] would be represent a datasource of
* type 'db', with a subtype of 'nosql', and would use the 'mongodb' connector.
*
* Alternatively, ['rest'] would be a different type altogether, and would have
* no subtype.
*/
DataSource.prototype.getTypes = function() {
const getTypes = this.connector && this.connector.getTypes;
let types = getTypes && getTypes() || [];
if (typeof types === 'string') {
types = types.split(/[\s,\/]+/);
}
return types;
};
/**
* Check the data source supports the specified types.
* @param {String|String[]} types Type name or an array of type names.
* @returns {Boolean} true if all types are supported by the data source
*/
DataSource.prototype.supportTypes = function(types) {
const supportedTypes = this.getTypes();
if (Array.isArray(types)) {
// Check each of the types
for (let i = 0; i < types.length; i++) {
if (supportedTypes.indexOf(types[i]) === -1) {
// Not supported
return false;
}
}
return true;
} else {
// The types is a string
return supportedTypes.indexOf(types) !== -1;
}
};
/*! In future versions, this will not maintain a strict 1:1 relationship between datasources and model classes
* Moving forward, we will allow a model to be attached to a datasource. The model itself becomes a template.
*/
/**
* Attach an existing model to a data source.
* This will mixin all of the data access object functions (DAO) into your
* modelClass definition.
* @param {Function} modelClass The model constructor that will be enhanced by
* DAO mixins.
*/
DataSource.prototype.attach = function(modelClass) {
if (modelClass.dataSource === this) {
// Already attached to the data source
return modelClass;
}
if (modelClass.modelBuilder !== this.modelBuilder) {
this.modelBuilder.definitions[modelClass.modelName] = modelClass.definition;
this.modelBuilder.models[modelClass.modelName] = modelClass;
// reset the modelBuilder
modelClass.modelBuilder = this.modelBuilder;
}
// redefine the dataSource
modelClass.dataSource = this;
this.setupDataAccess(modelClass, modelClass.settings);
modelClass.emit('dataSourceAttached', modelClass);
return modelClass;
};
/*! Method will be deprecated in LoopBack.next
*/
/**
* Define a property with name `prop` on a target `model`. See
* [Properties](./Model-definition-JSON-file.html#properties) for more information
* regarding valid options for `params`.
* @param {String} model Name of model
* @param {String} prop Name of property
* @param {Property} params Property settings
*/
DataSource.prototype.defineProperty = function(model, prop, params) {
this.modelBuilder.defineProperty(model, prop, params);
const resolvedProp = this.getModelDefinition(model).properties[prop];
if (this.connector && this.connector.defineProperty) {
this.connector.defineProperty(model, prop, resolvedProp);
}
};
/**
* Drop schema objects such as tables, indexes, views, triggers, etc that correspond
* to model definitions attached to this DataSource instance, specified by the `models` parameter.
*
* **WARNING**: In many situations, this will destroy data! `autoupdate()` will attempt to preserve
* data while updating the schema on your target DataSource, but this is not guaranteed to be safe.
*
* Please check the documentation for your specific connector(s) for a detailed breakdown of
* behaviors for automigrate!
*
* @param {String|String[]} [models] Model(s) to migrate. If not present, apply to all models.
* @param {Function} [callback] Callback function. Optional.
*
*/
DataSource.prototype.automigrate = function(models, cb) {
this.freeze();
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
cb = cb || utils.createPromiseCallback();
if (!this.connector.automigrate) {
// NOOP
process.nextTick(cb);
return cb.promise;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
const attachedModels = this.connector._models;
if (attachedModels && typeof attachedModels === 'object') {
models = models || Object.keys(attachedModels);
if (models.length === 0) {
process.nextTick(cb);
return cb.promise;
}
const invalidModels = models.filter(function(m) {
return !(m in attachedModels);
});
if (invalidModels.length) {
process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
return cb.promise;
}
}
const args = [models, cb];
args.callee = this.automigrate;
const queued = this.ready(this, args);
if (queued) {
// waiting to connect
return cb.promise;
}
this.connector.automigrate(models, cb);
return cb.promise;
};
/**
* Update existing database tables.
* This method applies only to database connectors.
*
* **WARNING**: `autoupdate()` will attempt to preserve data while updating the
* schema on your target DataSource, but this is not guaranteed to be safe.
*
* Please check the documentation for your specific connector(s) for a detailed breakdown of
* behaviors for automigrate!*
*
* @param {String|String[]} [models] Model(s) to migrate. If not present, apply to all models.
* @param {Function} [cb] The callback function
*/
DataSource.prototype.autoupdate = function(models, cb) {
this.freeze();
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
cb = cb || utils.createPromiseCallback();
if (!this.connector.autoupdate) {
// NOOP
process.nextTick(cb);
return cb.promise;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
const attachedModels = this.connector._models;
if (attachedModels && typeof attachedModels === 'object') {
models = models || Object.keys(attachedModels);
if (models.length === 0) {
process.nextTick(cb);
return cb.promise;
}
const invalidModels = models.filter(function(m) {
return !(m in attachedModels);
});
if (invalidModels.length) {
process.nextTick(function() {
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
invalidModels.join(' '))));
});
return cb.promise;
}
}
const args = [models, cb];
args.callee = this.autoupdate;
const queued = this.ready(this, args);
if (queued) {
// waiting to connect
return cb.promise;
}
this.connector.autoupdate(models, cb);
return cb.promise;
};
/**
* Discover existing database tables.
* This method returns an array of model objects, including {type, name, onwer}
*
* @options {Object} options Discovery options. See below.
* @param {Function} Callback function. Optional.
* @property {String} owner/schema The owner or schema to discover from.
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
* @property {Boolean} views If true, include views; if false, only tables.
* @property {Number} limit Page size
* @property {Number} offset Starting index
* @returns {ModelDefinition[]}
*/
DataSource.prototype.discoverModelDefinitions = function(options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverModelDefinitions) {
this.connector.discoverModelDefinitions(options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/*! Method will be completely removed in LoopBack.next
*/
/**
* The synchronous version of discoverModelDefinitions.
* @options {Object} options The options
* @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
* @property {Boolean} views If true, nclude views; if false, only tables.
* @property {Number} limit Page size
* @property {Number} offset Starting index
* @returns {ModelDefinition[]}
*/
DataSource.prototype.discoverModelDefinitionsSync = function(options) {
this.freeze();
if (this.connector.discoverModelDefinitionsSync) {
return this.connector.discoverModelDefinitionsSync(options);
}
return null;
};
/**
* Discover properties for a given model.
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|owner | String | Database owner or schema|
*|tableName | String | Table/view name|
*|columnName | String | Column name|
*|dataType | String | Data type|
*|dataLength | Number | Data length|
*|dataPrecision | Number | Numeric data precision|
*|dataScale |Number | Numeric data scale|
*|nullable |Boolean | If true, then the data can be null|
* See [Properties](./Model-definition-JSON-file.html#properties) for more
* details on the Property return type.
* @param {String} modelName The table/view name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} cb Callback function. Optional
* @callback cb
* @returns {Promise} A promise that returns an array of Properties (Property[])
*
*/
DataSource.prototype.discoverModelProperties = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverModelProperties) {
this.connector.discoverModelProperties(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/*! Method will be completely removed in LoopBack.next
*/
/**
* The synchronous version of discoverModelProperties
* @param {String} modelName The table/view name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverModelPropertiesSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverModelPropertiesSync) {
return this.connector.discoverModelPropertiesSync(modelName, options);
}
return null;
};
/**
* Discover primary keys for a given owner/modelName.
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*| owner |String | Table schema or owner (may be null). Owner defaults to current user.
*| tableName |String| Table name
*| columnName |String| Column name
*| keySeq |Number| Sequence number within primary key (1 indicates the first column in the primary key; 2 indicates the second column in the primary key).
*| pkName |String| Primary key name (may be null)
* See [ID Properties](./Model-definition-JSON-file.html#id-properties) for more
* information.
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
* @returns {Promise} A promise with an array of Primary Keys (Property[])
*/
DataSource.prototype.discoverPrimaryKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverPrimaryKeys) {
this.connector.discoverPrimaryKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/**
* Discover unique keys for a given modelName.
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*| owner |String | Table schema or owner (may be null). Owner defaults to current user.
*| tableName |String| Table name
*| columnName |String| Column name
* See [ID Properties](./Model-definition-JSON-file.html#id-properties) for more
* information.
* @param {String} modelName The model name
* @options {Object} options The options
* @param {Function} [cb] The callback function
* @returns {Promise} A promise with an array of Primary Keys (Property[])
*/
DataSource.prototype.discoverUniqueKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverUniqueKeys) {
this.connector.discoverUniqueKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/*! Method will be completely removed in LoopBack.next
*/
/**
* The synchronous version of discoverPrimaryKeys
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @returns {*}
*/
DataSource.prototype.discoverPrimaryKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverPrimaryKeysSync) {
return this.connector.discoverPrimaryKeysSync(modelName, options);
}
return null;
};
/**
* Discover foreign keys for a given owner/modelName
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|fkOwner |String | Foreign key table schema (may be null)
*|fkName |String | Foreign key name (may be null)
*|fkTableName |String | Foreign key table name
*|fkColumnName |String | Foreign key column name
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
*|pkOwner |String | Primary key table schema being imported (may be null)
*|pkName |String | Primary key name (may be null)
*|pkTableName |String | Primary key table name being imported
*|pkColumnName |String | Primary key column name being imported
* See [Relations](/Model-definition-JSON-file.html#relations) for more
* information.
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
* @returns {Promise} A Promise with an array of foreign key relations.
*
*/
DataSource.prototype.discoverForeignKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverForeignKeys) {
this.connector.discoverForeignKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/*! Method will be completely removed in LoopBack.next
*/
/**
* The synchronous version of discoverForeignKeys
*
* @param {String} modelName The model name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverForeignKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverForeignKeysSync) {
return this.connector.discoverForeignKeysSync(modelName, options);
}
return null;
};
/**
* Retrieves a description of the foreign key columns that reference the given table's primary key columns
* (the foreign keys exported by a table), ordered by fkTableOwner, fkTableName, and keySeq.
*
* Callback function return value is an object that can have the following properties:
*
*| Key | Type | Description |
*|-----|------|-------------|
*|fkOwner |String | Foreign key table schema (may be null)
*|fkName |String | Foreign key name (may be null)
*|fkTableName |String | Foreign key table name
*|fkColumnName |String | Foreign key column name
*|keySeq |Number | Sequence number within a foreign key( a value of 1 represents the first column of the foreign key, a value of 2 would represent the second column within the foreign key).
*|pkOwner |String | Primary key table schema being imported (may be null)
*|pkName |String | Primary key name (may be null)
*|pkTableName |String | Primary key table name being imported
*|pkColumnName |String | Primary key column name being imported
* See [Relations](/Model-definition-JSON-file.html#relations) for more
* information.
*
* @param {String} modelName The model name
* @options {Object} options The options
* @property {String} owner|schema The database owner or schema
* @param {Function} [cb] The callback function
* @returns {Promise} A Promise with an array of exported foreign key relations.
*/
DataSource.prototype.discoverExportedForeignKeys = function(modelName, options, cb) {
this.freeze();
if (cb === undefined && typeof options === 'function') {
cb = options;
options = {};
}
options = options || {};
cb = cb || utils.createPromiseCallback();
if (this.connector.discoverExportedForeignKeys) {
this.connector.discoverExportedForeignKeys(modelName, options, cb);
} else if (cb) {
process.nextTick(cb);
}
return cb.promise;
};
/*! Method will be completely removed in LoopBack.next
*/
/**
* The synchronous version of discoverExportedForeignKeys
* @param {String} modelName The model name
* @param {Object} options The options
* @returns {*}
*/
DataSource.prototype.discoverExportedForeignKeysSync = function(modelName, options) {
this.freeze();
if (this.connector.discoverExportedForeignKeysSync) {
return this.connec