UNPKG

90.7 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2020. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// Turning on strict for this file breaks lots of test cases;
7// disabling strict for this file
8/* eslint-disable strict */
9
10/*!
11 * Module dependencies
12 */
13const ModelBuilder = require('./model-builder.js').ModelBuilder;
14const ModelDefinition = require('./model-definition.js');
15const RelationDefinition = require('./relation-definition.js');
16const OberserverMixin = require('./observer');
17const jutil = require('./jutil');
18const utils = require('./utils');
19const ModelBaseClass = require('./model.js');
20const DataAccessObject = require('./dao.js');
21const defineScope = require('./scope.js').defineScope;
22const EventEmitter = require('events').EventEmitter;
23const util = require('util');
24const assert = require('assert');
25const async = require('async');
26const traverse = require('traverse');
27const g = require('strong-globalize')();
28const juggler = require('..');
29const deprecated = require('depd')('loopback-datasource-juggler');
30const Transaction = require('loopback-connector').Transaction;
31const pascalCase = require('change-case').pascalCase;
32const camelCase = require('change-case').camelCase;
33
34if (process.env.DEBUG === 'loopback') {
35 // For back-compatibility
36 process.env.DEBUG = 'loopback:*';
37}
38const debug = require('debug')('loopback:datasource');
39
40/*!
41 * Export public API
42 */
43exports.DataSource = DataSource;
44
45/*!
46 * Helpers
47 */
48const slice = Array.prototype.slice;
49
50/**
51 * LoopBack models can manipulate data via the DataSource object.
52 * Attaching a `DataSource` to a `Model` adds instance methods and static methods to the `Model`.
53 *
54 * Define a data source to persist model data.
55 * To create a DataSource programmatically, call `createDataSource()` on the LoopBack object; for example:
56 * ```js
57 * var oracle = loopback.createDataSource({
58 * connector: 'oracle',
59 * host: '111.22.333.44',
60 * database: 'MYDB',
61 * username: 'username',
62 * password: 'password'
63 * });
64 * ```
65 *
66 * All classes in single dataSource share same the connector type and
67 * one database connection.
68 *
69 * For example, the following creates a DataSource, and waits for a connection callback.
70 *
71 * ```
72 * var dataSource = new DataSource('mysql', { database: 'myapp_test' });
73 * dataSource.define(...);
74 * dataSource.on('connected', function () {
75 * // work with database
76 * });
77 * ```
78 * @class DataSource
79 * @param {String} [name] Optional name for datasource.
80 * @options {Object} settings Database-specific settings to establish connection (settings depend on specific connector).
81 * The table below lists a typical set for a relational database.
82 * @property {String} connector Database connector to use. For any supported connector, can be any of:
83 *
84 * - The connector module from `require(connectorName)`.
85 * - The full name of the connector module, such as 'loopback-connector-oracle'.
86 * - The short name of the connector module, such as 'oracle'.
87 * - A local module under `./connectors/` folder.
88 * @property {String} host Database server host name.
89 * @property {String} port Database server port number.
90 * @property {String} username Database user name.
91 * @property {String} password Database password.
92 * @property {String} database Name of the database to use.
93 * @property {Boolean} debug Display debugging information. Default is false.
94 *
95 * The constructor allows the following styles:
96 *
97 * 1. new DataSource(dataSourceName, settings). For example:
98 * - new DataSource('myDataSource', {connector: 'memory'});
99 * - new DataSource('myDataSource', {name: 'myDataSource', connector: 'memory'});
100 * - new DataSource('myDataSource', {name: 'anotherDataSource', connector: 'memory'});
101 *
102 * 2. new DataSource(settings). For example:
103 * - new DataSource({name: 'myDataSource', connector: 'memory'});
104 * - new DataSource({connector: 'memory'});
105 *
106 * 3. new DataSource(connectorModule, settings). For example:
107 * - new DataSource(connectorModule, {name: 'myDataSource})
108 * - new DataSource(connectorModule)
109 */
110function DataSource(name, settings, modelBuilder) {
111 if (!(this instanceof DataSource)) {
112 return new DataSource(name, settings);
113 }
114
115 // Check if the settings object is passed as the first argument
116 if (typeof name === 'object' && settings === undefined) {
117 settings = name;
118 name = undefined;
119 }
120
121 // Check if the first argument is a URL
122 if (typeof name === 'string' && name.indexOf('://') !== -1) {
123 name = utils.parseSettings(name);
124 }
125
126 // Check if the settings is in the form of URL string
127 if (typeof settings === 'string' && settings.indexOf('://') !== -1) {
128 settings = utils.parseSettings(settings);
129 }
130
131 // Shallow-clone the settings so that updates to `ds.settings`
132 // do not modify the original object provided via constructor arguments
133 if (settings) settings = Object.assign({}, settings);
134 // It's possible to provide settings object via the "name" arg too!
135 if (typeof name === 'object') name = Object.assign({}, name);
136
137 this.modelBuilder = modelBuilder || new ModelBuilder();
138 this.models = this.modelBuilder.models;
139 this.definitions = this.modelBuilder.definitions;
140 this.juggler = juggler;
141 this._queuedInvocations = 0;
142
143 // operation metadata
144 // Initialize it before calling setup as the connector might register operations
145 this._operations = {};
146
147 this.setup(name, settings);
148
149 this._setupConnector();
150
151 // connector
152 const connector = this.connector;
153
154 // DataAccessObject - connector defined or supply the default
155 const dao = (connector && connector.DataAccessObject) || this.constructor.DataAccessObject;
156 this.DataAccessObject = function() {
157 };
158
159 // define DataAccessObject methods
160 Object.keys(dao).forEach(function(name) {
161 const fn = dao[name];
162 this.DataAccessObject[name] = fn;
163
164 if (typeof fn === 'function') {
165 this.defineOperation(name, {
166 accepts: fn.accepts,
167 'returns': fn.returns,
168 http: fn.http,
169 remoteEnabled: fn.shared ? true : false,
170 scope: this.DataAccessObject,
171 fnName: name,
172 });
173 }
174 }.bind(this));
175
176 // define DataAccessObject.prototype methods
177 Object.keys(dao.prototype || []).forEach(function(name) {
178 const fn = dao.prototype[name];
179 this.DataAccessObject.prototype[name] = fn;
180 if (typeof fn === 'function') {
181 this.defineOperation(name, {
182 prototype: true,
183 accepts: fn.accepts,
184 'returns': fn.returns,
185 http: fn.http,
186 remoteEnabled: fn.shared ? true : false,
187 scope: this.DataAccessObject.prototype,
188 fnName: name,
189 });
190 }
191 }.bind(this));
192}
193
194util.inherits(DataSource, EventEmitter);
195
196// allow child classes to supply a data access object
197DataSource.DataAccessObject = DataAccessObject;
198
199/**
200 * Global maximum number of event listeners
201 */
202DataSource.DEFAULT_MAX_OFFLINE_REQUESTS = 16;
203
204/**
205 * Set up the connector instance for backward compatibility with JugglingDB schema/adapter
206 * @private
207 */
208DataSource.prototype._setupConnector = function() {
209 this.connector = this.connector || this.adapter; // The legacy JugglingDB adapter will set up `adapter` property
210 this.adapter = this.connector; // Keep the adapter as an alias to connector
211 if (this.connector) {
212 if (!this.connector.dataSource) {
213 // Set up the dataSource if the connector doesn't do so
214 this.connector.dataSource = this;
215 }
216 const dataSource = this;
217
218 // Set max listeners to a default/configured value
219 dataSource.setMaxListeners(dataSource.getMaxOfflineRequests());
220
221 this.connector.log = function(query, start) {
222 dataSource.log(query, start);
223 };
224
225 this.connector.logger = function(query) {
226 const t1 = Date.now();
227 const log = this.log;
228 return function(q) {
229 log(q || query, t1);
230 };
231 };
232 // Configure the connector instance to mix in observer functions
233 jutil.mixin(this.connector, OberserverMixin);
234 }
235};
236
237// List possible connector module names
238function connectorModuleNames(name) {
239 const names = []; // Check the name as is
240 if (!name.match(/^\//)) {
241 names.push('./connectors/' + name); // Check built-in connectors
242 if (name.indexOf('loopback-connector-') !== 0) {
243 names.push('loopback-connector-' + name); // Try loopback-connector-<name>
244 }
245 }
246 // Only try the short name if the connector is not from StrongLoop
247 if (['mongodb', 'oracle', 'mysql', 'postgresql', 'mssql', 'rest', 'soap', 'db2', 'cloudant']
248 .indexOf(name) === -1) {
249 names.push(name);
250 }
251 return names;
252}
253
254// testable with DI
255function tryModules(names, loader) {
256 let mod;
257 loader = loader || require;
258 for (let m = 0; m < names.length; m++) {
259 try {
260 mod = loader(names[m]);
261 } catch (e) {
262 const notFound = e.code === 'MODULE_NOT_FOUND' &&
263 e.message && e.message.indexOf(names[m]) > 0;
264
265 if (notFound) {
266 debug('Module %s not found, will try another candidate.', names[m]);
267 continue;
268 }
269
270 debug('Cannot load connector %s: %s', names[m], e.stack || e);
271 throw e;
272 }
273 if (mod) {
274 break;
275 }
276 }
277 return mod;
278}
279
280/*!
281 * Resolve a connector by name
282 * @param name The connector name
283 * @returns {*}
284 * @private
285 */
286DataSource._resolveConnector = function(name, loader) {
287 const names = connectorModuleNames(name);
288 const connector = tryModules(names, loader);
289 let error = null;
290 if (!connector) {
291 error = g.f('\nWARNING: {{LoopBack}} connector "%s" is not installed ' +
292 'as any of the following modules:\n\n %s\n\nTo fix, run:\n\n {{npm install %s --save}}\n',
293 name, names.join('\n'), names[names.length - 1]);
294 }
295 return {
296 connector: connector,
297 error: error,
298 };
299};
300
301/**
302 * Connect to the data source.
303 * If no callback is provided, it will return a Promise.
304 * Emits the 'connect' event.
305 * @param callback
306 * @returns {Promise}
307 * @emits connected
308 */
309DataSource.prototype.connect = function(callback) {
310 callback = callback || utils.createPromiseCallback();
311 const self = this;
312 if (this.connected) {
313 // The data source is already connected, return immediately
314 process.nextTick(callback);
315 return callback.promise;
316 }
317 if (typeof this.connector.connect !== 'function') {
318 // Connector doesn't have the connect function
319 // Assume no connect is needed
320 self.connected = true;
321 self.connecting = false;
322 process.nextTick(function() {
323 self.emit('connected');
324 callback();
325 });
326 return callback.promise;
327 }
328
329 // Queue the callback
330 this.pendingConnectCallbacks = this.pendingConnectCallbacks || [];
331 this.pendingConnectCallbacks.push(callback);
332
333 // The connect is already in progress
334 if (this.connecting) return callback.promise;
335 this.connector.connect(function(err, result) {
336 self.connecting = false;
337 if (!err) self.connected = true;
338 const cbs = self.pendingConnectCallbacks;
339 self.pendingConnectCallbacks = [];
340 if (!err) {
341 self.emit('connected');
342 } else {
343 self.emit('error', err);
344 }
345 // Invoke all pending callbacks
346 async.each(cbs, function(cb, done) {
347 try {
348 cb(err);
349 } catch (e) {
350 // Ignore error to make sure all callbacks are invoked
351 debug('Uncaught error raised by connect callback function: ', e);
352 } finally {
353 done();
354 }
355 }, function(err) {
356 if (err) throw err; // It should not happen
357 });
358 });
359
360 // Set connecting flag to be `true` so that the connector knows there is
361 // a connect in progress. The change of `connecting` should happen immediately
362 // after the connect request is sent
363 this.connecting = true;
364 return callback.promise;
365};
366
367/**
368 * Set up the data source. The following styles are supported:
369 * ```js
370 * ds.setup('myDataSource', {connector: 'memory'}); // ds.name -> 'myDataSource'
371 * ds.setup('myDataSource', {name: 'myDataSource', connector: 'memory'}); // ds.name -> 'myDataSource'
372 * ds.setup('myDataSource', {name: 'anotherDataSource', connector: 'memory'}); // ds.name -> 'myDataSource' and a warning will be issued
373 * ds.setup({name: 'myDataSource', connector: 'memory'}); // ds.name -> 'myDataSource'
374 * ds.setup({connector: 'memory'}); // ds.name -> 'memory'
375 * ```
376 * @param {String} dsName The name of the datasource. If not set, use
377 * `settings.name`
378 * @param {Object} settings The settings
379 * @returns {*}
380 * @private
381 */
382DataSource.prototype.setup = function(dsName, settings) {
383 const dataSource = this;
384 let connector;
385
386 // First argument is an `object`
387 if (dsName && typeof dsName === 'object') {
388 if (settings === undefined) {
389 // setup({name: 'myDataSource', connector: 'memory'})
390 settings = dsName;
391 dsName = undefined;
392 } else {
393 // setup(connector, {name: 'myDataSource', host: 'localhost'})
394 connector = dsName;
395 dsName = undefined;
396 }
397 }
398
399 if (typeof dsName !== 'string') {
400 dsName = undefined;
401 }
402
403 if (typeof settings === 'object') {
404 if (settings.initialize) {
405 // Settings is the resolved connector instance
406 connector = settings;
407 // Set settings to undefined to avoid confusion
408 settings = undefined;
409 } else if (settings.connector) {
410 // Use `connector`
411 connector = settings.connector;
412 } else if (settings.adapter) {
413 // `adapter` as alias for `connector`
414 connector = settings.adapter;
415 }
416 }
417
418 // just save everything we get
419 this.settings = settings || {};
420
421 this.settings.debug = this.settings.debug || debug.enabled;
422
423 if (this.settings.debug) {
424 debug('Settings: %j', this.settings);
425 }
426
427 if (typeof settings === 'object' && typeof settings.name === 'string' &&
428 typeof dsName === 'string' && dsName !== settings.name) {
429 // setup('myDataSource', {name: 'anotherDataSource', connector: 'memory'});
430 // ds.name -> 'myDataSource' and a warning will be issued
431 console.warn(
432 'A datasource is created with name %j, which is different from the name in settings (%j). ' +
433 'Please adjust your configuration to ensure these names match.',
434 dsName, settings.name,
435 );
436 }
437
438 // Disconnected by default
439 this.connected = false;
440 this.connecting = false;
441 this.initialized = false;
442
443 this.name = dsName || (typeof this.settings.name === 'string' && this.settings.name);
444
445 let connectorName;
446 if (typeof connector === 'string') {
447 // Connector needs to be resolved by name
448 connectorName = connector;
449 connector = undefined;
450 } else if ((typeof connector === 'object') && connector) {
451 connectorName = connector.name;
452 } else {
453 connectorName = dsName;
454 }
455 if (!this.name) {
456 // Fall back to connector name
457 this.name = connectorName;
458 }
459
460 if ((!connector) && connectorName) {
461 // The connector has not been resolved
462 const result = DataSource._resolveConnector(connectorName);
463 connector = result.connector;
464 if (!connector) {
465 console.error(result.error);
466 this.emit('error', new Error(result.error));
467 return;
468 }
469 }
470
471 if (connector) {
472 const postInit = function postInit(err, result) {
473 this._setupConnector();
474 // we have an connector now?
475 if (!this.connector) {
476 throw new Error(g.f('Connector is not defined correctly: ' +
477 'it should create `{{connector}}` member of dataSource'));
478 }
479 if (!err) {
480 this.initialized = true;
481 this.emit('initialized');
482 }
483 debug('Connector is initialized for dataSource %s', this.name);
484 // If `result` is set to `false` explicitly, the connection will be
485 // lazily established
486 if (!this.settings.lazyConnect) {
487 this.connected = (!err) && (result !== false); // Connected now
488 }
489 if (this.connected) {
490 debug('DataSource %s is now connected to %s', this.name, this.connector.name);
491 this.emit('connected');
492 } else if (err) {
493 // The connection fails, let's report it and hope it will be recovered in the next call
494 // Reset the connecting to `false`
495 this.connecting = false;
496 if (this._queuedInvocations) {
497 // Another operation is already waiting for connect() result,
498 // let them handle the connection error.
499 debug('Connection fails: %s\nIt will be retried for the next request.', err);
500 } else {
501 g.error('Connection fails: %s\nIt will be retried for the next request.', err);
502 this.emit('error', err);
503 }
504 } else {
505 // Either lazyConnect or connector initialize() defers the connection
506 debug('DataSource %s will be connected to connector %s', this.name,
507 this.connector.name);
508 }
509 }.bind(this);
510
511 try {
512 if ('function' === typeof connector.initialize) {
513 // Call the async initialize method
514 debug('Initializing connector %s', connector.name);
515 connector.initialize(this, postInit);
516 } else if ('function' === typeof connector) {
517 // Use the connector constructor directly
518 this.connector = new connector(this.settings);
519 postInit();
520 }
521 } catch (err) {
522 if (err.message) {
523 err.message = 'Cannot initialize connector ' +
524 JSON.stringify(connectorName) + ': ' +
525 err.message;
526 }
527 throw err;
528 }
529 }
530};
531
532function isModelClass(cls) {
533 if (!cls) {
534 return false;
535 }
536 return cls.prototype instanceof ModelBaseClass;
537}
538
539DataSource.relationTypes = Object.keys(RelationDefinition.RelationTypes);
540
541function isModelDataSourceAttached(model) {
542 return model && (!model.settings.unresolved) && (model.dataSource instanceof DataSource);
543}
544
545/*!
546 * Define scopes for the model class from the scopes object. See
547 * [scopes](./Model-definition-JSON-file.html#scopes) for more information on
548 * scopes and valid options objects.
549 * @param {Object} modelClass - The model class that corresponds to the model
550 * definition that will be enhanced by the provided scopes.
551 * @param {Object} scopes A key-value collection of names and their object
552 * definitions
553 * @property options The options defined on the scope object.
554 */
555DataSource.prototype.defineScopes = function(modelClass, scopes) {
556 if (scopes) {
557 for (const s in scopes) {
558 defineScope(modelClass, modelClass, s, scopes[s], {}, scopes[s].options);
559 }
560 }
561};
562
563/*!
564 * Define relations for the model class from the relations object. See
565 * [relations](./Model-definition-JSON-file.html#relations) for more information.
566 * @param {Object} modelClass - The model class that corresponds to the model
567 * definition that will be enhanced by the provided relations.
568 * @param {Object} relations A key-value collection of relation names and their
569 * object definitions.
570 */
571DataSource.prototype.defineRelations = function(modelClass, relations) {
572 const self = this;
573
574 // Wait for target/through models to be attached before setting up the relation
575 const deferRelationSetup = function(relationName, relation, targetModel, throughModel) {
576 if (!isModelDataSourceAttached(targetModel)) {
577 targetModel.once('dataAccessConfigured', function(targetModel) {
578 // Check if the through model doesn't exist or resolved
579 if (!throughModel || isModelDataSourceAttached(throughModel)) {
580 // The target model is resolved
581 const params = traverse(relation).clone();
582 params.as = relationName;
583 params.model = targetModel;
584 if (throughModel) {
585 params.through = throughModel;
586 }
587 modelClass[relation.type].call(modelClass, relationName, params);
588 }
589 });
590 }
591
592 if (throughModel && !isModelDataSourceAttached(throughModel)) {
593 // Set up a listener to the through model
594 throughModel.once('dataAccessConfigured', function(throughModel) {
595 if (isModelDataSourceAttached(targetModel)) {
596 // The target model is resolved
597 const params = traverse(relation).clone();
598 params.as = relationName;
599 params.model = targetModel;
600 params.through = throughModel;
601 modelClass[relation.type].call(modelClass, relationName, params);
602 }
603 });
604 }
605 };
606
607 // Set up the relations
608 if (relations) {
609 Object.keys(relations).forEach(function(relationName) {
610 let targetModel;
611 const r = relations[relationName];
612
613 validateRelation(relationName, r);
614
615 if (r.model) {
616 targetModel = isModelClass(r.model) ? r.model : self.getModel(r.model, true);
617 }
618
619 let throughModel = null;
620 if (r.through) {
621 throughModel = isModelClass(r.through) ? r.through : self.getModel(r.through, true);
622 }
623
624 if ((targetModel && !isModelDataSourceAttached(targetModel)) ||
625 (throughModel && !isModelDataSourceAttached(throughModel))) {
626 // Create a listener to defer the relation set up
627 deferRelationSetup(relationName, r, targetModel, throughModel);
628 } else {
629 // The target model is resolved
630 const params = traverse(r).clone();
631 params.as = relationName;
632 params.model = targetModel;
633 if (throughModel) {
634 params.through = throughModel;
635 }
636 modelClass[r.type].call(modelClass, relationName, params);
637 }
638 });
639 }
640};
641
642function validateRelation(relationName, relation) {
643 const rn = relationName;
644 const r = relation;
645 let msg, code;
646
647 assert(DataSource.relationTypes.indexOf(r.type) !== -1, 'Invalid relation type: ' + r.type);
648 assert(isValidRelationName(rn), 'Invalid relation name: ' + rn);
649
650 // VALIDATION ERRORS
651
652 // non polymorphic belongsTo relations should have `model` defined
653 if (!r.polymorphic && r.type === 'belongsTo' && !r.model) {
654 msg = g.f('%s relation: %s requires param `model`', r.type, rn);
655 code = 'BELONGS_TO_MISSING_MODEL';
656 }
657 // polymorphic belongsTo relations should not have `model` defined
658 if (r.polymorphic && r.type === 'belongsTo' && r.model) {
659 msg = g.f('{{polymorphic}} %s relation: %s does not expect param `model`', r.type, rn);
660 code = 'POLYMORPHIC_BELONGS_TO_MODEL';
661 }
662 // polymorphic relations other than belongsTo should have `model` defined
663 if (r.polymorphic && r.type !== 'belongsTo' && !r.model) {
664 msg = g.f('{{polymorphic}} %s relation: %s requires param `model`', r.type, rn);
665 code = 'POLYMORPHIC_NOT_BELONGS_TO_MISSING_MODEL';
666 }
667 // polymorphic relations should provide both discriminator and foreignKey or none
668 if (r.polymorphic && r.polymorphic.foreignKey && !r.polymorphic.discriminator) {
669 msg = g.f('{{polymorphic}} %s relation: %s requires param `polymorphic.discriminator` ' +
670 'when param `polymorphic.foreignKey` is provided', r.type, rn);
671 code = 'POLYMORPHIC_MISSING_DISCRIMINATOR';
672 }
673 // polymorphic relations should provide both discriminator and foreignKey or none
674 if (r.polymorphic && r.polymorphic.discriminator && !r.polymorphic.foreignKey) {
675 msg = g.f('{{polymorphic}} %s relation: %s requires param `polymorphic.foreignKey` ' +
676 'when param `polymorphic.discriminator` is provided', r.type, rn);
677 code = 'POLYMORPHIC_MISSING_FOREIGN_KEY';
678 }
679 // polymorphic relations should not provide polymorphic.as when using custom foreignKey/discriminator
680 if (r.polymorphic && r.polymorphic.as && r.polymorphic.foreignKey) {
681 msg = g.f('{{polymorphic}} %s relation: %s does not expect param `polymorphic.as` ' +
682 'when defing custom `foreignKey`/`discriminator` ', r.type, rn);
683 code = 'POLYMORPHIC_EXTRANEOUS_AS';
684 }
685 // polymorphic relations should not provide polymorphic.as when using custom foreignKey/discriminator
686 if (r.polymorphic && r.polymorphic.selector && r.polymorphic.foreignKey) {
687 msg = g.f('{{polymorphic}} %s relation: %s does not expect param `polymorphic.selector` ' +
688 'when defing custom `foreignKey`/`discriminator` ', r.type, rn);
689 code = 'POLYMORPHIC_EXTRANEOUS_SELECTOR';
690 }
691
692 if (msg) {
693 const error = new Error(msg);
694 error.details = {code: code, rType: r.type, rName: rn};
695 throw error;
696 }
697
698 // DEPRECATION WARNINGS
699 if (r.polymorphic && r.polymorphic.as) {
700 deprecated(g.f('WARNING: {{polymorphic}} %s relation: %s uses keyword `polymorphic.as` which will ' +
701 'be DEPRECATED in LoopBack.next, refer to this doc for replacement solutions ' +
702 '(https://loopback.io/doc/en/lb3/Polymorphic-relations.html#deprecated-polymorphic-as)',
703 r.type, rn), r.type);
704 }
705}
706
707function isValidRelationName(relationName) {
708 const invalidRelationNames = ['trigger'];
709 return invalidRelationNames.indexOf(relationName) === -1;
710}
711
712/*!
713 * Set up the data access functions from the data source. Each data source will
714 * expose a data access object (DAO), which will be mixed into the modelClass.
715 * @param {Model} modelClass The model class that will receive DAO mixins.
716 * @param {Object} settings The settings object; typically allows any settings
717 * that would be valid for a typical Model object.
718 */
719DataSource.prototype.setupDataAccess = function(modelClass, settings) {
720 if (this.connector) {
721 // Check if the id property should be generated
722 const idName = modelClass.definition.idName();
723 const idProp = modelClass.definition.rawProperties[idName];
724 if (idProp && idProp.generated && idProp.useDefaultIdType !== false && this.connector.getDefaultIdType) {
725 // Set the default id type from connector's ability
726 const idType = this.connector.getDefaultIdType() || String;
727 idProp.type = idType;
728 modelClass.definition.rawProperties[idName].type = idType;
729 modelClass.definition.properties[idName].type = idType;
730 }
731 if (this.connector.define) {
732 // pass control to connector
733 this.connector.define({
734 model: modelClass,
735 properties: modelClass.definition.properties,
736 settings: settings,
737 });
738 }
739 }
740
741 // add data access objects
742 this.mixin(modelClass);
743
744 // define relations from LDL (options.relations)
745 const relations = settings.relationships || settings.relations;
746 this.defineRelations(modelClass, relations);
747
748 // Emit the dataAccessConfigured event to indicate all the methods for data
749 // access have been mixed into the model class
750 modelClass.emit('dataAccessConfigured', modelClass);
751
752 // define scopes from LDL (options.relations)
753 const scopes = settings.scopes || {};
754 this.defineScopes(modelClass, scopes);
755};
756
757/**
758 * Define a model class. Returns newly created model object.
759 * The first (String) argument specifying the model name is required.
760 * You can provide one or two JSON object arguments, to provide configuration options.
761 * See [Model definition reference](http://docs.strongloop.com/display/DOC/Model+definition+reference) for details.
762 *
763 * Simple example:
764 * ```
765 * var User = dataSource.createModel('User', {
766 * email: String,
767 * password: String,
768 * birthDate: Date,
769 * activated: Boolean
770 * });
771 * ```
772 * More advanced example
773 * ```
774 * var User = dataSource.createModel('User', {
775 * email: { type: String, limit: 150, index: true },
776 * password: { type: String, limit: 50 },
777 * birthDate: Date,
778 * registrationDate: {type: Date, default: function () { return new Date }},
779 * activated: { type: Boolean, default: false }
780 * });
781 * ```
782 * You can also define an ACL when you create a new data source with the `DataSource.create()` method. For example:
783 *
784 * ```js
785 * var Customer = ds.createModel('Customer', {
786 * name: {
787 * type: String,
788 * acls: [
789 * {principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
790 * {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
791 * ]
792 * }
793 * }, {
794 * acls: [
795 * {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
796 * ]
797 * });
798 * ```
799 *
800 * @param {String} className Name of the model to create.
801 * @param {Object} properties Hash of model properties in format `{property: Type, property2: Type2, ...}` or `{property: {type: Type}, property2: {type: Type2}, ...}`
802 * @options {Object} properties Other configuration options. This corresponds to the options key in the config object.
803 * @param {Object} settings A settings object that would typically be used for Model objects.
804 */
805
806DataSource.prototype.createModel =
807DataSource.prototype.define = function defineClass(className, properties, settings) {
808 const args = slice.call(arguments);
809
810 if (!className) {
811 throw new Error(g.f('Class name required'));
812 }
813 if (args.length === 1) {
814 properties = {};
815 args.push(properties);
816 }
817 if (args.length === 2) {
818 settings = {};
819 args.push(settings);
820 }
821
822 properties = properties || {};
823 settings = settings || {};
824
825 if (this.isRelational()) {
826 // Set the strict mode to be true for relational DBs by default
827 if (settings.strict === undefined || settings.strict === null) {
828 settings.strict = true;
829 }
830 if (settings.strict === false) {
831 console.warn("WARNING: relational database doesn't support {strict: false} mode." +
832 ` {strict: true} mode will be set for model ${className} instead.`);
833 settings.strict = true;
834 }
835 }
836
837 const modelClass = this.modelBuilder.define(className, properties, settings);
838 modelClass.dataSource = this;
839
840 if (settings.unresolved) {
841 return modelClass;
842 }
843
844 this.setupDataAccess(modelClass, settings);
845 modelClass.emit('dataSourceAttached', modelClass);
846
847 return modelClass;
848};
849
850/**
851 * Remove a model from the registry.
852 *
853 * @param {String} modelName
854 */
855DataSource.prototype.deleteModelByName = function(modelName) {
856 this.modelBuilder.deleteModelByName(modelName);
857 delete this.connector._models[modelName];
858};
859
860/**
861 * Remove all models from the registry, but keep the connector instance
862 * (including the pool of database connections).
863 */
864DataSource.prototype.deleteAllModels = function() {
865 for (const m in this.modelBuilder.models) {
866 this.deleteModelByName(m);
867 }
868};
869
870/**
871 * Mixin DataAccessObject methods.
872 *
873 * @param {Function} ModelCtor The model constructor
874 * @private
875 */
876
877DataSource.prototype.mixin = function(ModelCtor) {
878 const ops = this.operations();
879 const DAO = this.DataAccessObject;
880
881 // mixin DAO
882 jutil.mixin(ModelCtor, DAO, {proxyFunctions: true, override: true});
883
884 // decorate operations as alias functions
885 Object.keys(ops).forEach(function(name) {
886 const op = ops[name];
887 let scope;
888
889 if (op.enabled) {
890 scope = op.prototype ? ModelCtor.prototype : ModelCtor;
891 // var sfn = scope[name] = function () {
892 // op.scope[op.fnName].apply(self, arguments);
893 // }
894 Object.keys(op)
895 .filter(function(key) {
896 // filter out the following keys
897 return ~[
898 'scope',
899 'fnName',
900 'prototype',
901 ].indexOf(key);
902 })
903 .forEach(function(key) {
904 if (typeof op[key] !== 'undefined') {
905 op.scope[op.fnName][key] = op[key];
906 }
907 });
908 }
909 });
910};
911
912/*! Method will be deprecated in LoopBack.next
913*/
914/**
915 * See [ModelBuilder.getModel](http://apidocs.strongloop.com/loopback-datasource-juggler/#modelbuilder-prototype-getmodel)
916 * for details.
917 */
918DataSource.prototype.getModel = function(name, forceCreate) {
919 return this.modelBuilder.getModel(name, forceCreate);
920};
921
922/*! Method will be deprecated in LoopBack.next
923*/
924/**
925 * See ModelBuilder.getModelDefinition
926 * See [ModelBuilder.getModelDefinition](http://apidocs.strongloop.com/loopback-datasource-juggler/#modelbuilder-prototype-getmodeldefinition)
927 * for details.
928 */
929DataSource.prototype.getModelDefinition = function(name) {
930 return this.modelBuilder.getModelDefinition(name);
931};
932
933/*! Method will be deprecated in LoopBack.next
934*/
935/**
936 * Get the data source types collection.
937 * @returns {String[]} The data source type array.
938 * For example, ['db', 'nosql', 'mongodb'] would be represent a datasource of
939 * type 'db', with a subtype of 'nosql', and would use the 'mongodb' connector.
940 *
941 * Alternatively, ['rest'] would be a different type altogether, and would have
942 * no subtype.
943 */
944DataSource.prototype.getTypes = function() {
945 const getTypes = this.connector && this.connector.getTypes;
946 let types = getTypes && getTypes() || [];
947 if (typeof types === 'string') {
948 types = types.split(/[\s,\/]+/);
949 }
950 return types;
951};
952
953/**
954 * Check the data source supports the specified types.
955 * @param {String|String[]} types Type name or an array of type names.
956 * @returns {Boolean} true if all types are supported by the data source
957 */
958DataSource.prototype.supportTypes = function(types) {
959 const supportedTypes = this.getTypes();
960 if (Array.isArray(types)) {
961 // Check each of the types
962 for (let i = 0; i < types.length; i++) {
963 if (supportedTypes.indexOf(types[i]) === -1) {
964 // Not supported
965 return false;
966 }
967 }
968 return true;
969 } else {
970 // The types is a string
971 return supportedTypes.indexOf(types) !== -1;
972 }
973};
974
975/*! In future versions, this will not maintain a strict 1:1 relationship between datasources and model classes
976* Moving forward, we will allow a model to be attached to a datasource. The model itself becomes a template.
977*/
978/**
979 * Attach an existing model to a data source.
980 * This will mixin all of the data access object functions (DAO) into your
981 * modelClass definition.
982 * @param {Function} modelClass The model constructor that will be enhanced by
983 * DAO mixins.
984 */
985
986DataSource.prototype.attach = function(modelClass) {
987 if (modelClass.dataSource === this) {
988 // Already attached to the data source
989 return modelClass;
990 }
991
992 if (modelClass.modelBuilder !== this.modelBuilder) {
993 this.modelBuilder.definitions[modelClass.modelName] = modelClass.definition;
994 this.modelBuilder.models[modelClass.modelName] = modelClass;
995 // reset the modelBuilder
996 modelClass.modelBuilder = this.modelBuilder;
997 }
998
999 // redefine the dataSource
1000 modelClass.dataSource = this;
1001
1002 this.setupDataAccess(modelClass, modelClass.settings);
1003 modelClass.emit('dataSourceAttached', modelClass);
1004 return modelClass;
1005};
1006
1007/*! Method will be deprecated in LoopBack.next
1008*/
1009/**
1010 * Define a property with name `prop` on a target `model`. See
1011 * [Properties](./Model-definition-JSON-file.html#properties) for more information
1012 * regarding valid options for `params`.
1013 * @param {String} model Name of model
1014 * @param {String} prop Name of property
1015 * @param {Property} params Property settings
1016 */
1017DataSource.prototype.defineProperty = function(model, prop, params) {
1018 this.modelBuilder.defineProperty(model, prop, params);
1019
1020 const resolvedProp = this.getModelDefinition(model).properties[prop];
1021 if (this.connector && this.connector.defineProperty) {
1022 this.connector.defineProperty(model, prop, resolvedProp);
1023 }
1024};
1025
1026/**
1027 * Drop schema objects such as tables, indexes, views, triggers, etc that correspond
1028 * to model definitions attached to this DataSource instance, specified by the `models` parameter.
1029 *
1030 * **WARNING**: In many situations, this will destroy data! `autoupdate()` will attempt to preserve
1031 * data while updating the schema on your target DataSource, but this is not guaranteed to be safe.
1032 *
1033 * Please check the documentation for your specific connector(s) for a detailed breakdown of
1034 * behaviors for automigrate!
1035 *
1036 * @param {String|String[]} [models] Model(s) to migrate. If not present, apply to all models.
1037 * @param {Function} [callback] Callback function. Optional.
1038 *
1039 */
1040DataSource.prototype.automigrate = function(models, cb) {
1041 this.freeze();
1042
1043 if ((!cb) && ('function' === typeof models)) {
1044 cb = models;
1045 models = undefined;
1046 }
1047
1048 cb = cb || utils.createPromiseCallback();
1049
1050 if (!this.connector.automigrate) {
1051 // NOOP
1052 process.nextTick(cb);
1053 return cb.promise;
1054 }
1055
1056 // First argument is a model name
1057 if ('string' === typeof models) {
1058 models = [models];
1059 }
1060
1061 const attachedModels = this.connector._models;
1062
1063 if (attachedModels && typeof attachedModels === 'object') {
1064 models = models || Object.keys(attachedModels);
1065
1066 if (models.length === 0) {
1067 process.nextTick(cb);
1068 return cb.promise;
1069 }
1070
1071 const invalidModels = models.filter(function(m) {
1072 return !(m in attachedModels);
1073 });
1074
1075 if (invalidModels.length) {
1076 process.nextTick(function() {
1077 cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
1078 invalidModels.join(' '))));
1079 });
1080 return cb.promise;
1081 }
1082 }
1083
1084 const args = [models, cb];
1085 args.callee = this.automigrate;
1086 const queued = this.ready(this, args);
1087 if (queued) {
1088 // waiting to connect
1089 return cb.promise;
1090 }
1091
1092 this.connector.automigrate(models, cb);
1093 return cb.promise;
1094};
1095
1096/**
1097 * Update existing database tables.
1098 * This method applies only to database connectors.
1099 *
1100 * **WARNING**: `autoupdate()` will attempt to preserve data while updating the
1101 * schema on your target DataSource, but this is not guaranteed to be safe.
1102 *
1103 * Please check the documentation for your specific connector(s) for a detailed breakdown of
1104 * behaviors for automigrate!*
1105 *
1106 * @param {String|String[]} [models] Model(s) to migrate. If not present, apply to all models.
1107 * @param {Function} [cb] The callback function
1108 */
1109DataSource.prototype.autoupdate = function(models, cb) {
1110 this.freeze();
1111
1112 if ((!cb) && ('function' === typeof models)) {
1113 cb = models;
1114 models = undefined;
1115 }
1116
1117 cb = cb || utils.createPromiseCallback();
1118
1119 if (!this.connector.autoupdate) {
1120 // NOOP
1121 process.nextTick(cb);
1122 return cb.promise;
1123 }
1124
1125 // First argument is a model name
1126 if ('string' === typeof models) {
1127 models = [models];
1128 }
1129
1130 const attachedModels = this.connector._models;
1131
1132 if (attachedModels && typeof attachedModels === 'object') {
1133 models = models || Object.keys(attachedModels);
1134
1135 if (models.length === 0) {
1136 process.nextTick(cb);
1137 return cb.promise;
1138 }
1139
1140 const invalidModels = models.filter(function(m) {
1141 return !(m in attachedModels);
1142 });
1143
1144 if (invalidModels.length) {
1145 process.nextTick(function() {
1146 cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
1147 invalidModels.join(' '))));
1148 });
1149 return cb.promise;
1150 }
1151 }
1152
1153 const args = [models, cb];
1154 args.callee = this.autoupdate;
1155 const queued = this.ready(this, args);
1156 if (queued) {
1157 // waiting to connect
1158 return cb.promise;
1159 }
1160
1161 this.connector.autoupdate(models, cb);
1162 return cb.promise;
1163};
1164
1165/**
1166 * Discover existing database tables.
1167 * This method returns an array of model objects, including {type, name, onwer}
1168 *
1169 * @options {Object} options Discovery options. See below.
1170 * @param {Function} Callback function. Optional.
1171 * @property {String} owner/schema The owner or schema to discover from.
1172 * @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
1173 * @property {Boolean} views If true, include views; if false, only tables.
1174 * @property {Number} limit Page size
1175 * @property {Number} offset Starting index
1176 * @returns {ModelDefinition[]}
1177 */
1178DataSource.prototype.discoverModelDefinitions = function(options, cb) {
1179 this.freeze();
1180
1181 if (cb === undefined && typeof options === 'function') {
1182 cb = options;
1183 options = {};
1184 }
1185 options = options || {};
1186 cb = cb || utils.createPromiseCallback();
1187
1188 if (this.connector.discoverModelDefinitions) {
1189 this.connector.discoverModelDefinitions(options, cb);
1190 } else if (cb) {
1191 process.nextTick(cb);
1192 }
1193 return cb.promise;
1194};
1195
1196/*! Method will be completely removed in LoopBack.next
1197*/
1198/**
1199 * The synchronous version of discoverModelDefinitions.
1200 * @options {Object} options The options
1201 * @property {Boolean} all If true, discover all models; if false, discover only models owned by the current user.
1202 * @property {Boolean} views If true, nclude views; if false, only tables.
1203 * @property {Number} limit Page size
1204 * @property {Number} offset Starting index
1205 * @returns {ModelDefinition[]}
1206 */
1207DataSource.prototype.discoverModelDefinitionsSync = function(options) {
1208 this.freeze();
1209 if (this.connector.discoverModelDefinitionsSync) {
1210 return this.connector.discoverModelDefinitionsSync(options);
1211 }
1212 return null;
1213};
1214
1215/**
1216 * Discover properties for a given model.
1217 *
1218 * Callback function return value is an object that can have the following properties:
1219 *
1220 *| Key | Type | Description |
1221 *|-----|------|-------------|
1222 *|owner | String | Database owner or schema|
1223 *|tableName | String | Table/view name|
1224 *|columnName | String | Column name|
1225 *|dataType | String | Data type|
1226 *|dataLength | Number | Data length|
1227 *|dataPrecision | Number | Numeric data precision|
1228 *|dataScale |Number | Numeric data scale|
1229 *|nullable |Boolean | If true, then the data can be null|
1230 * See [Properties](./Model-definition-JSON-file.html#properties) for more
1231 * details on the Property return type.
1232 * @param {String} modelName The table/view name
1233 * @options {Object} options The options
1234 * @property {String} owner|schema The database owner or schema
1235 * @param {Function} cb Callback function. Optional
1236 * @callback cb
1237 * @returns {Promise} A promise that returns an array of Properties (Property[])
1238 *
1239 */
1240DataSource.prototype.discoverModelProperties = function(modelName, options, cb) {
1241 this.freeze();
1242
1243 if (cb === undefined && typeof options === 'function') {
1244 cb = options;
1245 options = {};
1246 }
1247 options = options || {};
1248 cb = cb || utils.createPromiseCallback();
1249
1250 if (this.connector.discoverModelProperties) {
1251 this.connector.discoverModelProperties(modelName, options, cb);
1252 } else if (cb) {
1253 process.nextTick(cb);
1254 }
1255 return cb.promise;
1256};
1257
1258/*! Method will be completely removed in LoopBack.next
1259*/
1260/**
1261 * The synchronous version of discoverModelProperties
1262 * @param {String} modelName The table/view name
1263 * @param {Object} options The options
1264 * @returns {*}
1265 */
1266DataSource.prototype.discoverModelPropertiesSync = function(modelName, options) {
1267 this.freeze();
1268 if (this.connector.discoverModelPropertiesSync) {
1269 return this.connector.discoverModelPropertiesSync(modelName, options);
1270 }
1271 return null;
1272};
1273
1274/**
1275 * Discover primary keys for a given owner/modelName.
1276 * Callback function return value is an object that can have the following properties:
1277 *
1278 *| Key | Type | Description |
1279 *|-----|------|-------------|
1280 *| owner |String | Table schema or owner (may be null). Owner defaults to current user.
1281 *| tableName |String| Table name
1282 *| columnName |String| Column name
1283 *| 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).
1284 *| pkName |String| Primary key name (may be null)
1285 * See [ID Properties](./Model-definition-JSON-file.html#id-properties) for more
1286 * information.
1287 * @param {String} modelName The model name
1288 * @options {Object} options The options
1289 * @property {String} owner|schema The database owner or schema
1290 * @param {Function} [cb] The callback function
1291 * @returns {Promise} A promise with an array of Primary Keys (Property[])
1292 */
1293DataSource.prototype.discoverPrimaryKeys = function(modelName, options, cb) {
1294 this.freeze();
1295
1296 if (cb === undefined && typeof options === 'function') {
1297 cb = options;
1298 options = {};
1299 }
1300 options = options || {};
1301 cb = cb || utils.createPromiseCallback();
1302
1303 if (this.connector.discoverPrimaryKeys) {
1304 this.connector.discoverPrimaryKeys(modelName, options, cb);
1305 } else if (cb) {
1306 process.nextTick(cb);
1307 }
1308 return cb.promise;
1309};
1310
1311/*! Method will be completely removed in LoopBack.next
1312*/
1313/**
1314 * The synchronous version of discoverPrimaryKeys
1315 * @param {String} modelName The model name
1316 * @options {Object} options The options
1317 * @property {String} owner|schema The database owner or schema
1318 * @returns {*}
1319 */
1320DataSource.prototype.discoverPrimaryKeysSync = function(modelName, options) {
1321 this.freeze();
1322 if (this.connector.discoverPrimaryKeysSync) {
1323 return this.connector.discoverPrimaryKeysSync(modelName, options);
1324 }
1325 return null;
1326};
1327
1328/**
1329 * Discover foreign keys for a given owner/modelName
1330 *
1331 * Callback function return value is an object that can have the following properties:
1332 *
1333 *| Key | Type | Description |
1334 *|-----|------|-------------|
1335 *|fkOwner |String | Foreign key table schema (may be null)
1336 *|fkName |String | Foreign key name (may be null)
1337 *|fkTableName |String | Foreign key table name
1338 *|fkColumnName |String | Foreign key column name
1339 *|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).
1340 *|pkOwner |String | Primary key table schema being imported (may be null)
1341 *|pkName |String | Primary key name (may be null)
1342 *|pkTableName |String | Primary key table name being imported
1343 *|pkColumnName |String | Primary key column name being imported
1344 * See [Relations](/Model-definition-JSON-file.html#relations) for more
1345 * information.
1346 * @param {String} modelName The model name
1347 * @options {Object} options The options
1348 * @property {String} owner|schema The database owner or schema
1349 * @param {Function} [cb] The callback function
1350 * @returns {Promise} A Promise with an array of foreign key relations.
1351 *
1352 */
1353DataSource.prototype.discoverForeignKeys = function(modelName, options, cb) {
1354 this.freeze();
1355
1356 if (cb === undefined && typeof options === 'function') {
1357 cb = options;
1358 options = {};
1359 }
1360 options = options || {};
1361 cb = cb || utils.createPromiseCallback();
1362
1363 if (this.connector.discoverForeignKeys) {
1364 this.connector.discoverForeignKeys(modelName, options, cb);
1365 } else if (cb) {
1366 process.nextTick(cb);
1367 }
1368 return cb.promise;
1369};
1370
1371/*! Method will be completely removed in LoopBack.next
1372*/
1373/**
1374 * The synchronous version of discoverForeignKeys
1375 *
1376 * @param {String} modelName The model name
1377 * @param {Object} options The options
1378 * @returns {*}
1379 */
1380DataSource.prototype.discoverForeignKeysSync = function(modelName, options) {
1381 this.freeze();
1382 if (this.connector.discoverForeignKeysSync) {
1383 return this.connector.discoverForeignKeysSync(modelName, options);
1384 }
1385 return null;
1386};
1387
1388/**
1389 * Retrieves a description of the foreign key columns that reference the given table's primary key columns
1390 * (the foreign keys exported by a table), ordered by fkTableOwner, fkTableName, and keySeq.
1391 *
1392 * Callback function return value is an object that can have the following properties:
1393 *
1394 *| Key | Type | Description |
1395 *|-----|------|-------------|
1396 *|fkOwner |String | Foreign key table schema (may be null)
1397 *|fkName |String | Foreign key name (may be null)
1398 *|fkTableName |String | Foreign key table name
1399 *|fkColumnName |String | Foreign key column name
1400 *|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).
1401 *|pkOwner |String | Primary key table schema being imported (may be null)
1402 *|pkName |String | Primary key name (may be null)
1403 *|pkTableName |String | Primary key table name being imported
1404 *|pkColumnName |String | Primary key column name being imported
1405 * See [Relations](/Model-definition-JSON-file.html#relations) for more
1406 * information.
1407 *
1408 * @param {String} modelName The model name
1409 * @options {Object} options The options
1410 * @property {String} owner|schema The database owner or schema
1411 * @param {Function} [cb] The callback function
1412 * @returns {Promise} A Promise with an array of exported foreign key relations.
1413 */
1414DataSource.prototype.discoverExportedForeignKeys = function(modelName, options, cb) {
1415 this.freeze();
1416
1417 if (cb === undefined && typeof options === 'function') {
1418 cb = options;
1419 options = {};
1420 }
1421 options = options || {};
1422 cb = cb || utils.createPromiseCallback();
1423
1424 if (this.connector.discoverExportedForeignKeys) {
1425 this.connector.discoverExportedForeignKeys(modelName, options, cb);
1426 } else if (cb) {
1427 process.nextTick(cb);
1428 }
1429 return cb.promise;
1430};
1431
1432/*! Method will be completely removed in LoopBack.next
1433*/
1434/**
1435 * The synchronous version of discoverExportedForeignKeys
1436 * @param {String} modelName The model name
1437 * @param {Object} options The options
1438 * @returns {*}
1439 */
1440DataSource.prototype.discoverExportedForeignKeysSync = function(modelName, options) {
1441 this.freeze();
1442 if (this.connector.discoverExportedForeignKeysSync) {
1443 return this.connector.discoverExportedForeignKeysSync(modelName, options);
1444 }
1445 return null;
1446};
1447
1448function capitalize(str) {
1449 if (!str) {
1450 return str;
1451 }
1452 return str.charAt(0).toUpperCase() + ((str.length > 1) ? str.slice(1).toLowerCase() : '');
1453}
1454
1455/**
1456 * Renames db column names with different naming conventions:
1457 * camelCase for property names as it's LB default naming convention for properties,
1458 * or keep the name the same if needed.
1459 *
1460 * @param {*} name name defined in database
1461 * @param {*} caseFunction optional. A function to convert the name into different case.
1462 */
1463function fromDBName(name, caseFunction) {
1464 if (!name) {
1465 return name;
1466 }
1467 if (typeof caseFunction === 'function') {
1468 return caseFunction(name);
1469 }
1470 return name;
1471}
1472
1473/**
1474 * Discover one schema from the given model without following the relations.
1475 **Example schema from oracle connector:**
1476 *
1477 * ```js
1478 * {
1479 * "name": "Product",
1480 * "options": {
1481 * "idInjection": false,
1482 * "oracle": {
1483 * "schema": "BLACKPOOL",
1484 * "table": "PRODUCT"
1485 * }
1486 * },
1487 * "properties": {
1488 * "id": {
1489 * "type": "String",
1490 * "required": true,
1491 * "length": 20,
1492 * "id": 1,
1493 * "oracle": {
1494 * "columnName": "ID",
1495 * "dataType": "VARCHAR2",
1496 * "dataLength": 20,
1497 * "nullable": "N"
1498 * }
1499 * },
1500 * "name": {
1501 * "type": "String",
1502 * "required": false,
1503 * "length": 64,
1504 * "oracle": {
1505 * "columnName": "NAME",
1506 * "dataType": "VARCHAR2",
1507 * "dataLength": 64,
1508 * "nullable": "Y"
1509 * }
1510 * },
1511 * ...
1512 * "fireModes": {
1513 * "type": "String",
1514 * "required": false,
1515 * "length": 64,
1516 * "oracle": {
1517 * "columnName": "FIRE_MODES",
1518 * "dataType": "VARCHAR2",
1519 * "dataLength": 64,
1520 * "nullable": "Y"
1521 * }
1522 * }
1523 * }
1524 * }
1525 * ```
1526 *
1527 * @param {String} tableName The name of the table to discover.
1528 * @options {Object} [options] An options object typically used for Relations.
1529 * See [Relations](./Model-definition-JSON-file.html#relations) for more information.
1530 * @param {Function} [cb] The callback function
1531 * @returns {Promise} A promise object that resolves to a single schema.
1532 */
1533DataSource.prototype.discoverSchema = function(tableName, options, cb) {
1534 options = options || {};
1535
1536 if (!cb && 'function' === typeof options) {
1537 cb = options;
1538 options = {};
1539 }
1540 options.visited = {};
1541 options.relations = false;
1542
1543 cb = cb || utils.createPromiseCallback();
1544
1545 this.discoverSchemas(tableName, options, function(err, schemas) {
1546 if (err || !schemas) {
1547 cb && cb(err, schemas);
1548 return;
1549 }
1550 for (const s in schemas) {
1551 cb && cb(null, schemas[s]);
1552 return;
1553 }
1554 });
1555 return cb.promise;
1556};
1557
1558/**
1559 * Discover schema from a given tableName/viewName.
1560 *
1561 * @param {String} tableName The table name.
1562 * @options {Object} [options] Options; see below.
1563 * @property {String} owner|schema Database owner or schema name.
1564 * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
1565 * @property {Boolean} all True if all owners are included; false otherwise.
1566 * @property {Boolean} views True if views are included; false otherwise.
1567 * @param {Function} [cb] The callback function
1568 * @returns {Promise} A promise object that resolves to an array of schemas.
1569 */
1570DataSource.prototype.discoverSchemas = function(tableName, options, cb) {
1571 options = options || {};
1572
1573 if (!cb && 'function' === typeof options) {
1574 cb = options;
1575 options = {};
1576 }
1577 cb = cb || utils.createPromiseCallback();
1578
1579 const self = this;
1580 const dbType = this.connector.name;
1581
1582 let nameMapper;
1583 const disableCamelCase = !!options.disableCamelCase;
1584
1585 if (options.nameMapper === null) {
1586 // No mapping
1587 nameMapper = function(type, name) {
1588 return name;
1589 };
1590 } else if (typeof options.nameMapper === 'function') {
1591 // Custom name mapper
1592 nameMapper = options.nameMapper;
1593 } else {
1594 // Default name mapper
1595 nameMapper = function mapName(type, name) {
1596 if (type === 'table' || type === 'model') {
1597 return fromDBName(name, pascalCase);
1598 } else if (type == 'fk') {
1599 if (disableCamelCase) {
1600 return fromDBName(name + 'Rel');
1601 } else {
1602 return fromDBName(name + 'Rel', camelCase);
1603 }
1604 } else {
1605 if (disableCamelCase) {
1606 return fromDBName(name);
1607 } else {
1608 return fromDBName(name, camelCase);
1609 }
1610 }
1611 };
1612 }
1613
1614 if (this.connector.discoverSchemas) {
1615 // Delegate to the connector implementation
1616 this.connector.discoverSchemas(tableName, options, cb);
1617 return cb.promise;
1618 }
1619
1620 const tasks = [
1621 this.discoverModelProperties.bind(this, tableName, options),
1622 this.discoverPrimaryKeys.bind(this, tableName, options)];
1623
1624 const followingRelations = options.associations || options.relations;
1625 if (followingRelations) {
1626 tasks.push(this.discoverForeignKeys.bind(this, tableName, options));
1627 }
1628
1629 async.parallel(tasks, function(err, results) {
1630 if (err) {
1631 cb(err);
1632 return cb.promise;
1633 }
1634
1635 const columns = results[0];
1636 if (!columns || columns.length === 0) {
1637 cb(new Error(g.f('Table \'%s\' does not exist.', tableName)));
1638 return cb.promise;
1639 }
1640
1641 // Handle primary keys
1642 const primaryKeys = results[1] || [];
1643 const pks = {};
1644 primaryKeys.forEach(function(pk) {
1645 pks[pk.columnName] = pk.keySeq;
1646 });
1647
1648 if (self.settings.debug) {
1649 debug('Primary keys: ', pks);
1650 }
1651
1652 const schema = {
1653 name: nameMapper('table', tableName),
1654 options: {
1655 idInjection: false, // DO NOT add id property
1656 },
1657 properties: {},
1658 };
1659
1660 schema.options[dbType] = {
1661 schema: columns[0].owner,
1662 table: tableName,
1663 };
1664
1665 columns.forEach(function(item) {
1666 const propName = nameMapper('column', item.columnName);
1667 schema.properties[propName] = {
1668 type: item.type,
1669 required: (item.nullable === 'N' || item.nullable === 'NO' ||
1670 item.nullable === 0 || item.nullable === false),
1671 length: item.dataLength,
1672 precision: item.dataPrecision,
1673 scale: item.dataScale,
1674 };
1675
1676 if (pks[item.columnName]) {
1677 schema.properties[propName].id = pks[item.columnName];
1678 }
1679 const dbSpecific = schema.properties[propName][dbType] = {
1680 columnName: item.columnName,
1681 dataType: item.dataType,
1682 dataLength: item.dataLength,
1683 dataPrecision: item.dataPrecision,
1684 dataScale: item.dataScale,
1685 nullable: item.nullable,
1686 };
1687 // merge connector-specific properties
1688 if (item[dbType]) {
1689 for (const k in item[dbType]) {
1690 dbSpecific[k] = item[dbType][k];
1691 }
1692 }
1693 });
1694
1695 // Add current modelName to the visited tables
1696 options.visited = options.visited || {};
1697 const schemaKey = columns[0].owner + '.' + tableName;
1698 if (!options.visited.hasOwnProperty(schemaKey)) {
1699 if (self.settings.debug) {
1700 debug('Adding schema for ' + schemaKey);
1701 }
1702 options.visited[schemaKey] = schema;
1703 }
1704
1705 const otherTables = {};
1706 if (followingRelations) {
1707 // Handle foreign keys
1708 const fks = {};
1709 const foreignKeys = results[2] || [];
1710 foreignKeys.forEach(function(fk) {
1711 const fkInfo = {
1712 keySeq: fk.keySeq,
1713 owner: fk.pkOwner,
1714 tableName: fk.pkTableName,
1715 columnName: fk.pkColumnName,
1716 };
1717 if (fks[fk.fkName]) {
1718 fks[fk.fkName].push(fkInfo);
1719 } else {
1720 fks[fk.fkName] = [fkInfo];
1721 }
1722 });
1723
1724 if (self.settings.debug) {
1725 debug('Foreign keys: ', fks);
1726 }
1727
1728 schema.options.relations = {};
1729 foreignKeys.forEach(function(fk) {
1730 const propName = nameMapper('fk', (fk.fkName || fk.pkTableName));
1731 schema.options.relations[propName] = {
1732 model: nameMapper('table', fk.pkTableName),
1733 type: 'belongsTo',
1734 foreignKey: nameMapper('column', fk.fkColumnName),
1735 };
1736
1737 const key = fk.pkOwner + '.' + fk.pkTableName;
1738 if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) {
1739 otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName};
1740 }
1741 });
1742 }
1743
1744 if (Object.keys(otherTables).length === 0) {
1745 cb(null, options.visited);
1746 } else {
1747 const moreTasks = [];
1748 for (const t in otherTables) {
1749 if (self.settings.debug) {
1750 debug('Discovering related schema for ' + schemaKey);
1751 }
1752 const newOptions = {};
1753 for (const key in options) {
1754 newOptions[key] = options[key];
1755 }
1756 newOptions.owner = otherTables[t].owner;
1757
1758 moreTasks.push(DataSource.prototype.discoverSchemas.bind(self, otherTables[t].tableName, newOptions));
1759 }
1760 async.parallel(moreTasks, function(err, results) {
1761 const result = results && results[0];
1762 cb(err, result);
1763 });
1764 }
1765 });
1766 return cb.promise;
1767};
1768
1769/*! Method will be completely removed in LoopBack.next
1770*/
1771/**
1772 * Discover schema from a given table/view synchronously
1773 * @param {String} modelName The model name
1774 * @options {Object} [options] Options; see below.
1775 * @property {String} owner|schema Database owner or schema name.
1776 * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
1777 * @property {Boolean} all True if all owners are included; false otherwise.
1778 * @property {Boolean} views True if views are included; false otherwise.
1779 * @returns {Array<Object>} An array of schema definition objects.
1780 */
1781DataSource.prototype.discoverSchemasSync = function(modelName, options) {
1782 const self = this;
1783 const dbType = this.connector.name;
1784
1785 const columns = this.discoverModelPropertiesSync(modelName, options);
1786 if (!columns || columns.length === 0) {
1787 return [];
1788 }
1789 const disableCamelCase = !!options.disableCamelCase;
1790
1791 const nameMapper = options.nameMapper || function mapName(type, name) {
1792 if (type === 'table' || type === 'model') {
1793 return fromDBName(name, pascalCase);
1794 } else {
1795 if (disableCamelCase) {
1796 return fromDBName(name);
1797 } else {
1798 return fromDBName(name, camelCase);
1799 }
1800 }
1801 };
1802
1803 // Handle primary keys
1804 const primaryKeys = this.discoverPrimaryKeysSync(modelName, options);
1805 const pks = {};
1806 primaryKeys.forEach(function(pk) {
1807 pks[pk.columnName] = pk.keySeq;
1808 });
1809
1810 if (self.settings.debug) {
1811 debug('Primary keys: ', pks);
1812 }
1813
1814 const schema = {
1815 name: nameMapper('table', modelName),
1816 options: {
1817 idInjection: false, // DO NOT add id property
1818 },
1819 properties: {},
1820 };
1821
1822 schema.options[dbType] = {
1823 schema: columns.length > 0 && columns[0].owner,
1824 table: modelName,
1825 };
1826
1827 columns.forEach(function(item) {
1828 const i = item;
1829
1830 const propName = nameMapper('column', item.columnName);
1831 schema.properties[propName] = {
1832 type: item.type,
1833 required: (item.nullable === 'N'),
1834 length: item.dataLength,
1835 precision: item.dataPrecision,
1836 scale: item.dataScale,
1837 };
1838
1839 if (pks[item.columnName]) {
1840 schema.properties[propName].id = pks[item.columnName];
1841 }
1842 schema.properties[propName][dbType] = {
1843 columnName: i.columnName,
1844 dataType: i.dataType,
1845 dataLength: i.dataLength,
1846 dataPrecision: item.dataPrecision,
1847 dataScale: item.dataScale,
1848 nullable: i.nullable,
1849 };
1850 });
1851
1852 // Add current modelName to the visited tables
1853 options.visited = options.visited || {};
1854 const schemaKey = columns[0].owner + '.' + modelName;
1855 if (!options.visited.hasOwnProperty(schemaKey)) {
1856 if (self.settings.debug) {
1857 debug('Adding schema for ' + schemaKey);
1858 }
1859 options.visited[schemaKey] = schema;
1860 }
1861
1862 const otherTables = {};
1863 const followingRelations = options.associations || options.relations;
1864 if (followingRelations) {
1865 // Handle foreign keys
1866 const fks = {};
1867 const foreignKeys = this.discoverForeignKeysSync(modelName, options);
1868 foreignKeys.forEach(function(fk) {
1869 const fkInfo = {
1870 keySeq: fk.keySeq,
1871 owner: fk.pkOwner,
1872 tableName: fk.pkTableName,
1873 columnName: fk.pkColumnName,
1874 };
1875 if (fks[fk.fkName]) {
1876 fks[fk.fkName].push(fkInfo);
1877 } else {
1878 fks[fk.fkName] = [fkInfo];
1879 }
1880 });
1881
1882 if (self.settings.debug) {
1883 debug('Foreign keys: ', fks);
1884 }
1885
1886 schema.options.relations = {};
1887 foreignKeys.forEach(function(fk) {
1888 const propName = nameMapper('column', fk.pkTableName);
1889 schema.options.relations[propName] = {
1890 model: nameMapper('table', fk.pkTableName),
1891 type: 'belongsTo',
1892 foreignKey: nameMapper('column', fk.fkColumnName),
1893 };
1894
1895 const key = fk.pkOwner + '.' + fk.pkTableName;
1896 if (!options.visited.hasOwnProperty(key) && !otherTables.hasOwnProperty(key)) {
1897 otherTables[key] = {owner: fk.pkOwner, tableName: fk.pkTableName};
1898 }
1899 });
1900 }
1901
1902 if (Object.keys(otherTables).length === 0) {
1903 return options.visited;
1904 } else {
1905 const moreTasks = [];
1906 for (const t in otherTables) {
1907 if (self.settings.debug) {
1908 debug('Discovering related schema for ' + schemaKey);
1909 }
1910 const newOptions = {};
1911 for (const key in options) {
1912 newOptions[key] = options[key];
1913 }
1914 newOptions.owner = otherTables[t].owner;
1915 self.discoverSchemasSync(otherTables[t].tableName, newOptions);
1916 }
1917 return options.visited;
1918 }
1919};
1920
1921/**
1922 * Discover and build models from the specified owner/modelName.
1923 *
1924 * @param {String} modelName The model name.
1925 * @options {Object} [options] Options; see below.
1926 * @property {String} owner|schema Database owner or schema name.
1927 * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
1928 * @property {Boolean} all True if all owners are included; false otherwise.
1929 * @property {Boolean} views True if views are included; false otherwise.
1930 * @param {Function} [cb] The callback function
1931 * @returns {Promise} A Promise object that resolves with a map of model
1932 * constructors, keyed by model name
1933 */
1934DataSource.prototype.discoverAndBuildModels = function(modelName, options, cb) {
1935 const self = this;
1936 options = options || {};
1937 this.discoverSchemas(modelName, options, function(err, schemas) {
1938 if (err) {
1939 cb && cb(err, schemas);
1940 return;
1941 }
1942
1943 const schemaList = [];
1944 for (const s in schemas) {
1945 const schema = schemas[s];
1946 if (options.base) {
1947 schema.options = schema.options || {};
1948 schema.options.base = options.base;
1949 }
1950 schemaList.push(schema);
1951 }
1952
1953 const models = self.modelBuilder.buildModels(schemaList,
1954 self.createModel.bind(self));
1955
1956 cb && cb(err, models);
1957 });
1958};
1959
1960/*! Method will be completely removed in LoopBack.next
1961*/
1962/**
1963 * Discover and build models from the given owner/modelName synchronously.
1964 * @param {String} modelName The model name.
1965 * @options {Object} [options] Options; see below.
1966 * @property {String} owner|schema Database owner or schema name.
1967 * @property {Boolean} relations True if relations (primary key/foreign key) are navigated; false otherwise.
1968 * @property {Boolean} all True if all owners are included; false otherwise.
1969 * @property {Boolean} views True if views are included; false otherwise.
1970
1971 * @param {String} modelName The model name
1972 * @param {Object} [options] The options
1973 * @returns {Object} A map of model constructors, keyed by model name
1974 */
1975DataSource.prototype.discoverAndBuildModelsSync = function(modelName, options) {
1976 options = options || {};
1977 const schemas = this.discoverSchemasSync(modelName, options);
1978
1979 const schemaList = [];
1980 for (const s in schemas) {
1981 const schema = schemas[s];
1982 if (options.base) {
1983 schema.options = schema.options || {};
1984 schema.options.base = options.base;
1985 }
1986 schemaList.push(schema);
1987 }
1988
1989 const models = this.modelBuilder.buildModels(schemaList,
1990 this.createModel.bind(this));
1991
1992 return models;
1993};
1994
1995/**
1996 * Introspect a JSON object and build a model class
1997 * @param {String} name Name of the model
1998 * @param {Object} json The json object representing a model instance
1999 * @param {Object} options Options
2000 * @returns {Model} A Model class
2001 */
2002DataSource.prototype.buildModelFromInstance = function(name, json, options) {
2003 // Introspect the JSON document to generate a schema
2004 const schema = ModelBuilder.introspect(json);
2005
2006 // Create a model for the generated schema
2007 return this.createModel(name, schema, options);
2008};
2009
2010/**
2011 * Check whether or not migrations are required for the database schema to match
2012 * the Model definitions attached to the DataSource.
2013 * Note: This method applies only to SQL connectors.
2014 * @param {String|String[]} [models] A model name or an array of model names. If not present, apply to all models.
2015 * @returns {Boolean} Whether or not migrations are required.
2016 */
2017DataSource.prototype.isActual = function(models, cb) {
2018 this.freeze();
2019 if (this.connector.isActual) {
2020 this.connector.isActual(models, cb);
2021 } else {
2022 if ((!cb) && ('function' === typeof models)) {
2023 cb = models;
2024 models = undefined;
2025 }
2026 if (cb) {
2027 process.nextTick(function() {
2028 cb(null, true);
2029 });
2030 }
2031 }
2032};
2033
2034/**
2035 * Log benchmarked message. Do not redefine this method, if you need to grab
2036 * schema logs, use `dataSource.on('log', ...)` emitter event
2037 *
2038 * @private used by connectors
2039 */
2040DataSource.prototype.log = function(sql, t) {
2041 debug(sql, t);
2042 this.emit('log', sql, t);
2043};
2044
2045/**
2046 * Freeze dataSource. Behavior depends on connector
2047 * To continuously add artifacts to datasource until it is frozen, but it is not really used in loopback.
2048 */
2049DataSource.prototype.freeze = function freeze() {
2050 if (!this.connector) {
2051 throw new Error(g.f('The connector has not been initialized.'));
2052 }
2053 if (this.connector.freezeDataSource) {
2054 this.connector.freezeDataSource();
2055 }
2056 if (this.connector.freezeSchema) {
2057 this.connector.freezeSchema();
2058 }
2059};
2060
2061/**
2062 * Return table name for specified `modelName`.
2063 * @param {String} modelName The model name.
2064 * @returns {String} The table name.
2065 */
2066DataSource.prototype.tableName = function(modelName) {
2067 return this.getModelDefinition(modelName).tableName(this.connector.name);
2068};
2069
2070/**
2071 * Return column name for specified modelName and propertyName
2072 * @param {String} modelName The model name
2073 * @param {String} propertyName The property name
2074 * @returns {String} columnName The column name.
2075 */
2076DataSource.prototype.columnName = function(modelName, propertyName) {
2077 return this.getModelDefinition(modelName).columnName(this.connector.name, propertyName);
2078};
2079
2080/**
2081 * Return column metadata for specified modelName and propertyName
2082 * @param {String} modelName The model name
2083 * @param {String} propertyName The property name
2084 * @returns {Object} column metadata
2085 */
2086DataSource.prototype.columnMetadata = function(modelName, propertyName) {
2087 return this.getModelDefinition(modelName).columnMetadata(this.connector.name, propertyName);
2088};
2089
2090/**
2091 * Return column names for specified modelName
2092 * @param {String} modelName The model name
2093 * @returns {String[]} column names
2094 */
2095DataSource.prototype.columnNames = function(modelName) {
2096 return this.getModelDefinition(modelName).columnNames(this.connector.name);
2097};
2098
2099/**
2100 * @deprecated
2101 * Function to get the id column name of the target model
2102 *
2103 * For SQL connectors, use `SqlConnector.prototype.column(ModelDefinition.prototype.idName())`.
2104 * For NoSQL connectors, use `Connector.prototype.idMapping(ModelDefinition.prototype.idName())`
2105 * instead.
2106 *
2107 * @param {String} modelName The model name
2108 * @returns {String} columnName for ID
2109 */
2110DataSource.prototype.idColumnName = function(modelName) {
2111 return this.getModelDefinition(modelName).idColumnName(this.connector.name);
2112};
2113
2114/**
2115 * Find the ID property name
2116 * @param {String} modelName The model name
2117 * @returns {String} property name for ID
2118 */
2119DataSource.prototype.idName = function(modelName) {
2120 if (!this.getModelDefinition(modelName).idName) {
2121 g.error('No {{id}} name %s', this.getModelDefinition(modelName));
2122 }
2123 return this.getModelDefinition(modelName).idName();
2124};
2125
2126/**
2127 * Find the ID property names sorted by the index
2128 * @param {String} modelName The model name
2129 * @returns {String[]} property names for IDs
2130 */
2131DataSource.prototype.idNames = function(modelName) {
2132 return this.getModelDefinition(modelName).idNames();
2133};
2134
2135/**
2136 * Find the id property definition
2137 * @param {String} modelName The model name
2138 * @returns {Object} The id property definition
2139 */
2140DataSource.prototype.idProperty = function(modelName) {
2141 const def = this.getModelDefinition(modelName);
2142 const idProps = def && def.ids();
2143 return idProps && idProps[0] && idProps[0].property;
2144};
2145
2146/**
2147 * Define foreign key to another model
2148 * @param {String} className The model name that owns the key
2149 * @param {String} key Name of key field
2150 * @param {String} foreignClassName The foreign model name
2151 * @param {String} pkName (optional) primary key used for foreignKey
2152 */
2153DataSource.prototype.defineForeignKey = function defineForeignKey(className, key, foreignClassName, pkName) {
2154 let pkType = null;
2155 const foreignModel = this.getModelDefinition(foreignClassName);
2156 pkName = pkName || foreignModel && foreignModel.idName();
2157 if (pkName) {
2158 pkType = foreignModel.properties[pkName].type;
2159 }
2160 const model = this.getModelDefinition(className);
2161 if (model.properties[key]) {
2162 if (pkType) {
2163 // Reset the type of the foreign key
2164 model.rawProperties[key].type = model.properties[key].type = pkType;
2165 }
2166 return;
2167 }
2168
2169 const fkDef = {type: pkType};
2170 const foreignMeta = this.columnMetadata(foreignClassName, pkName);
2171 if (foreignMeta && (foreignMeta.dataType || foreignMeta.dataLength)) {
2172 fkDef[this.connector.name] = {};
2173 if (foreignMeta.dataType) {
2174 fkDef[this.connector.name].dataType = foreignMeta.dataType;
2175 }
2176 if (foreignMeta.dataLength) {
2177 fkDef[this.connector.name].dataLength = foreignMeta.dataLength;
2178 }
2179 }
2180 if (this.connector.defineForeignKey) {
2181 const cb = function(err, keyType) {
2182 if (err) throw err;
2183 fkDef.type = keyType || pkType;
2184 // Add the foreign key property to the data source _models
2185 this.defineProperty(className, key, fkDef);
2186 }.bind(this);
2187 switch (this.connector.defineForeignKey.length) {
2188 case 4:
2189 this.connector.defineForeignKey(className, key, foreignClassName, cb);
2190 break;
2191 default:
2192 case 3:
2193 this.connector.defineForeignKey(className, key, cb);
2194 break;
2195 }
2196 } else {
2197 // Add the foreign key property to the data source _models
2198 this.defineProperty(className, key, fkDef);
2199 }
2200};
2201
2202/**
2203 * Close connection to the DataSource.
2204 * @param {Function} [cb] The callback function. Optional.
2205 */
2206DataSource.prototype.disconnect = function disconnect(cb) {
2207 cb = cb || utils.createPromiseCallback();
2208 const self = this;
2209 if (this.connected && (typeof this.connector.disconnect === 'function')) {
2210 this.connector.disconnect(function(err, result) {
2211 self.connected = false;
2212 cb && cb(err, result);
2213 });
2214 } else {
2215 process.nextTick(function() {
2216 self.connected = false;
2217 cb && cb();
2218 });
2219 }
2220 return cb.promise;
2221};
2222
2223/**
2224 * An alias for `disconnect` to make the datasource a LB4 life-cycle observer.
2225 * Please note that we are intentionally not providing a `start` method,
2226 * because the logic for establishing connection(s) is more complex
2227 * and usually started immediately from the datasoure constructor.
2228 */
2229DataSource.prototype.stop = DataSource.prototype.disconnect;
2230
2231/**
2232 * Copy the model from Master.
2233 * @param {Function} Master The model constructor
2234 * @returns {Model} A copy of the Model object constructor
2235 *
2236 * @private
2237 */
2238DataSource.prototype.copyModel = function copyModel(Master) {
2239 const dataSource = this;
2240 const className = Master.modelName;
2241 const md = Master.modelBuilder.getModelDefinition(className);
2242 const Slave = function SlaveModel() {
2243 Master.apply(this, [].slice.call(arguments));
2244 };
2245
2246 util.inherits(Slave, Master);
2247
2248 // Delegating static properties
2249 Slave.__proto__ = Master;
2250
2251 hiddenProperty(Slave, 'dataSource', dataSource);
2252 hiddenProperty(Slave, 'modelName', className);
2253 hiddenProperty(Slave, 'relations', Master.relations);
2254
2255 if (!(className in dataSource.modelBuilder.models)) {
2256 // store class in model pool
2257 dataSource.modelBuilder.models[className] = Slave;
2258 dataSource.modelBuilder.definitions[className] =
2259 new ModelDefinition(dataSource.modelBuilder, md.name, md.properties, md.settings);
2260
2261 if ((!dataSource.isTransaction) && dataSource.connector && dataSource.connector.define) {
2262 dataSource.connector.define({
2263 model: Slave,
2264 properties: md.properties,
2265 settings: md.settings,
2266 });
2267 }
2268 }
2269
2270 return Slave;
2271};
2272
2273/**
2274 * Run a transaction against the DataSource.
2275 *
2276 * This method can be used in different ways based on the passed arguments and
2277 * type of underlying data source:
2278 *
2279 * If no `execute()` function is provided and the underlying DataSource is a
2280 * database that supports transactions, a Promise is returned that resolves to
2281 * an EventEmitter representing the transaction once it is ready.
2282 * `transaction.models` can be used to receive versions of the DataSource's
2283 * model classes which are bound to the created transaction, so that all their
2284 * database methods automatically use the transaction. At the end of all
2285 * database transactions, `transaction.commit()` can be called to commit the
2286 * transactions, or `transaction.rollback()` to roll them back.
2287 *
2288 * If no `execute()` function is provided on a transient or memory DataSource,
2289 * the EventEmitter representing the transaction is returned immediately. For
2290 * backward compatibility, this object also supports `transaction.exec()`
2291 * instead of `transaction.commit()`, and calling `transaction.rollback()` is
2292 * not required on such transient and memory DataSource instances.
2293 *
2294 * If an `execute()` function is provided, then it is called as soon as the
2295 * transaction is ready, receiving `transaction.models` as its first
2296 * argument. `transaction.commit()` and `transaction.rollback()` are then
2297 * automatically called at the end of `execute()`, based on whether exceptions
2298 * happen during execution or not. If no callback is provided to be called at
2299 * the end of the execution, a Promise object is returned that is resolved or
2300 * rejected as soon as the execution is completed, and the transaction is
2301 * committed or rolled back.
2302 *
2303 * @param {Function} execute The execute function, called with (models). Note
2304 * that the instances in `models` are bound to the created transaction, and
2305 * are therefore not identical with the models in `app.models`, but slaves
2306 * thereof (optional).
2307 * @options {Object} options The options to be passed to `beginTransaction()`
2308 * when creating the transaction on database sources (optional).
2309 * @param {Function} cb Callback called with (err)
2310 * @returns {Promise | EventEmitter}
2311 */
2312DataSource.prototype.transaction = function(execute, options, cb) {
2313 if (cb === undefined && typeof options === 'function') {
2314 cb = options;
2315 options = {};
2316 } else {
2317 options = options || {};
2318 }
2319
2320 const dataSource = this;
2321 const transaction = new EventEmitter();
2322
2323 for (const p in dataSource) {
2324 transaction[p] = dataSource[p];
2325 }
2326
2327 transaction.isTransaction = true;
2328 transaction.origin = dataSource;
2329 transaction.connected = false;
2330 transaction.connecting = false;
2331
2332 // Don't allow creating transactions on a transaction data-source:
2333 transaction.transaction = function() {
2334 throw new Error(g.f('Nesting transactions is not supported'));
2335 };
2336
2337 // Create a blank pool for the slave models bound to this transaction.
2338 const modelBuilder = new ModelBuilder();
2339 const slaveModels = modelBuilder.models;
2340 transaction.modelBuilder = modelBuilder;
2341 transaction.models = slaveModels;
2342 transaction.definitions = modelBuilder.definitions;
2343
2344 // For performance reasons, use a getter per model and only call copyModel()
2345 // for the models that are actually used. These getters are then replaced
2346 // with the actual values on first use.
2347 const masterModels = dataSource.modelBuilder.models;
2348 Object.keys(masterModels).forEach(function(name) {
2349 Object.defineProperty(slaveModels, name, {
2350 enumerable: true,
2351 configurable: true,
2352 get: function() {
2353 // Delete getter so copyModel() can redefine slaveModels[name].
2354 // NOTE: No need to set the new value as copyModel() takes care of it.
2355 delete slaveModels[name];
2356 return dataSource.copyModel.call(transaction, masterModels[name]);
2357 },
2358 });
2359 });
2360
2361 let done = function(err) {
2362 if (err) {
2363 transaction.rollback(function(error) {
2364 cb(err || error);
2365 });
2366 } else {
2367 transaction.commit(cb);
2368 }
2369 // Make sure cb() isn't called twice, e.g. if `execute` returns a
2370 // thenable object and also calls the passed `cb` function.
2371 done = function() {};
2372 };
2373
2374 function handleExecute() {
2375 if (execute) {
2376 cb = cb || utils.createPromiseCallback();
2377 try {
2378 const result = execute(slaveModels, done);
2379 if (result && typeof result.then === 'function') {
2380 result.then(function() { done(); }, done);
2381 }
2382 } catch (err) {
2383 done(err);
2384 }
2385 return cb.promise;
2386 } else if (cb) {
2387 cb(null, transaction);
2388 } else {
2389 return transaction;
2390 }
2391 }
2392
2393 function transactionCreated(err, tx) {
2394 if (err) {
2395 cb(err);
2396 } else {
2397 // Expose transaction on the created transaction dataSource so it can be
2398 // retrieved again in determineOptions() in dao.js, as well as referenced
2399 // in transaction.commit() and transaction.rollback() below.
2400 transaction.currentTransaction = tx;
2401
2402 // Some connectors like Postgresql expose loobpack-connector as a property on the tx
2403 if (!tx.observe && tx.connector) {
2404 tx = tx.connector;
2405 }
2406 // Handle timeout and pass it on as an error.
2407 tx.observe('timeout', function(context, next) {
2408 const err = new Error(g.f('Transaction is rolled back due to timeout'));
2409 err.code = 'TRANSACTION_TIMEOUT';
2410 // Pass on the error to next(), so that the final 'timeout' observer in
2411 // loopback-connector does not trigger a rollback by itself that we
2412 // can't get a callback for when it's finished.
2413 next(err);
2414 // Call done(err) after, to execute the rollback here and reject the
2415 // promise with the error when it's completed.
2416 done(err);
2417 });
2418 handleExecute();
2419 }
2420 }
2421
2422 function ensureTransaction(transaction, cb) {
2423 if (!transaction) {
2424 process.nextTick(function() {
2425 cb(new Error(g.f(
2426 'Transaction is not ready, wait for the returned promise to resolve',
2427 )));
2428 });
2429 }
2430 return transaction;
2431 }
2432
2433 const connector = dataSource.connector;
2434 if (connector.transaction) {
2435 // Create a transient or memory source transaction.
2436 transaction.connector = connector.transaction();
2437 transaction.commit =
2438 transaction.exec = function(cb) {
2439 this.connector.exec(cb);
2440 };
2441 transaction.rollback = function(cb) {
2442 // No need to do anything here.
2443 cb();
2444 };
2445 return handleExecute();
2446 } else if (connector.beginTransaction) {
2447 // Create a database source transaction.
2448 transaction.exec =
2449 transaction.commit = function(cb) {
2450 ensureTransaction(this.currentTransaction, cb).commit(cb);
2451 };
2452 transaction.rollback = function(cb) {
2453 ensureTransaction(this.currentTransaction, cb).rollback(cb);
2454 };
2455 // Always use callback / promise due to the use of beginTransaction()
2456 cb = cb || utils.createPromiseCallback();
2457 Transaction.begin(connector, options, transactionCreated);
2458 return cb.promise;
2459 } else {
2460 throw new Error(g.f('DataSource does not support transactions'));
2461 }
2462};
2463
2464/**
2465 * Enable remote access to a data source operation. Each [connector](#connector) has its own set of set
2466 * remotely enabled and disabled operations. To list the operations, call `dataSource.operations()`.
2467 * @param {String} operation The operation name
2468 */
2469
2470DataSource.prototype.enableRemote = function(operation) {
2471 const op = this.getOperation(operation);
2472 if (op) {
2473 op.remoteEnabled = true;
2474 } else {
2475 throw new Error(g.f('%s is not provided by the attached connector', operation));
2476 }
2477};
2478
2479/**
2480 * Disable remote access to a data source operation. Each [connector](#connector) has its own set of set enabled
2481 * and disabled operations. To list the operations, call `dataSource.operations()`.
2482 *
2483 *```js
2484 * var oracle = loopback.createDataSource({
2485 * connector: require('loopback-connector-oracle'),
2486 * host: '...',
2487 * ...
2488 * });
2489 * oracle.disableRemote('destroyAll');
2490 * ```
2491 * **Notes:**
2492 *
2493 * - Disabled operations will not be added to attached models.
2494 * - Disabling the remoting for a method only affects client access (it will still be available from server models).
2495 * - Data sources must enable / disable operations before attaching or creating models.
2496 * @param {String} operation The operation name
2497 */
2498
2499DataSource.prototype.disableRemote = function(operation) {
2500 const op = this.getOperation(operation);
2501 if (op) {
2502 op.remoteEnabled = false;
2503 } else {
2504 throw new Error(g.f('%s is not provided by the attached connector', operation));
2505 }
2506};
2507
2508/**
2509 * Get an operation's metadata.
2510 * @param {String} operation The operation name
2511 */
2512
2513DataSource.prototype.getOperation = function(operation) {
2514 const ops = this.operations();
2515 const opKeys = Object.keys(ops);
2516
2517 for (let i = 0; i < opKeys.length; i++) {
2518 const op = ops[opKeys[i]];
2519
2520 if (op.name === operation) {
2521 return op;
2522 }
2523 }
2524};
2525
2526/**
2527 * Return JSON object describing all operations.
2528 *
2529 * Example return value:
2530 * ```js
2531 * {
2532 * find: {
2533 * remoteEnabled: true,
2534 * accepts: [...],
2535 * returns: [...]
2536 * enabled: true
2537 * },
2538 * save: {
2539 * remoteEnabled: true,
2540 * prototype: true,
2541 * accepts: [...],
2542 * returns: [...],
2543 * enabled: true
2544 * },
2545 * ...
2546 * }
2547 * ```
2548 */
2549DataSource.prototype.operations = function() {
2550 return this._operations;
2551};
2552
2553/**
2554 * Define an operation to the data source
2555 * @param {String} name The operation name
2556 * @param {Object} options The options
2557 * @param {Function} fn The function
2558 */
2559DataSource.prototype.defineOperation = function(name, options, fn) {
2560 options.fn = fn;
2561 options.name = name;
2562 this._operations[name] = options;
2563};
2564
2565/**
2566 * Check if the backend is a relational DB
2567 * @returns {Boolean}
2568 */
2569DataSource.prototype.isRelational = function() {
2570 return this.connector && this.connector.relational;
2571};
2572
2573/**
2574 * Check if the data source is connected. If not, the method invocation will be
2575 * deferred and queued.
2576 *
2577 *
2578 * @param {Object} obj Receiver for the method call
2579 * @param {Object} args Arguments passing to the method call
2580 * @returns - a Boolean value to indicate if the method invocation is deferred.
2581 * false: The datasource is already connected
2582 * - true: The datasource is yet to be connected
2583 */
2584DataSource.prototype.queueInvocation = DataSource.prototype.ready =
2585function(obj, args) {
2586 const self = this;
2587 debug('Datasource %s: connected=%s connecting=%s', this.name,
2588 this.connected, this.connecting);
2589 if (this.connected) {
2590 // Connected
2591 return false;
2592 }
2593
2594 this._queuedInvocations++;
2595
2596 const method = args.callee;
2597 // Set up a callback after the connection is established to continue the method call
2598
2599 let onConnected = null, onError = null, timeoutHandle = null;
2600 onConnected = function() {
2601 debug('Datasource %s is now connected - executing method %s', self.name, method.name);
2602 this._queuedInvocations--;
2603 // Remove the error handler
2604 self.removeListener('error', onError);
2605 if (timeoutHandle) {
2606 clearTimeout(timeoutHandle);
2607 }
2608 const params = [].slice.call(args);
2609 try {
2610 method.apply(obj, params);
2611 } catch (err) {
2612 // Catch the exception and report it via callback
2613 const cb = params.pop();
2614 if (typeof cb === 'function') {
2615 process.nextTick(function() {
2616 cb(err);
2617 });
2618 } else {
2619 throw err;
2620 }
2621 }
2622 };
2623 onError = function(err) {
2624 debug('Datasource %s fails to connect - aborting method %s', self.name, method.name);
2625 this._queuedInvocations--;
2626 // Remove the connected listener
2627 self.removeListener('connected', onConnected);
2628 if (timeoutHandle) {
2629 clearTimeout(timeoutHandle);
2630 }
2631 const params = [].slice.call(args);
2632 const cb = params.pop();
2633 if (typeof cb === 'function') {
2634 process.nextTick(function() {
2635 cb(err);
2636 });
2637 }
2638 };
2639 this.once('connected', onConnected);
2640 this.once('error', onError);
2641
2642 // Set up a timeout to cancel the invocation
2643 const timeout = this.settings.connectionTimeout || 5000;
2644 timeoutHandle = setTimeout(function() {
2645 debug('Datasource %s fails to connect due to timeout - aborting method %s',
2646 self.name, method.name);
2647 this._queuedInvocations--;
2648 self.connecting = false;
2649 self.removeListener('error', onError);
2650 self.removeListener('connected', onConnected);
2651 const params = [].slice.call(args);
2652 const cb = params.pop();
2653 if (typeof cb === 'function') {
2654 cb(new Error(g.f('Timeout in connecting after %s ms', timeout)));
2655 }
2656 }, timeout);
2657
2658 if (!this.connecting) {
2659 debug('Connecting datasource %s to connector %s', this.name, this.connector.name);
2660 // When no callback is provided to `connect()`, it returns a Promise.
2661 // We are not handling that promise and thus UnhandledPromiseRejection
2662 // warning is triggered when the connection could not be established.
2663 // We are avoiding this problem by providing a no-op callback.
2664 this.connect(() => {});
2665 }
2666 return true;
2667};
2668
2669/**
2670 * Ping the underlying connector to test the connections
2671 * @param {Function} [cb] Callback function
2672 */
2673DataSource.prototype.ping = function(cb) {
2674 cb = cb || utils.createPromiseCallback();
2675 const self = this;
2676 if (self.connector.ping) {
2677 this.connector.ping(cb);
2678 } else if (self.connector.discoverModelProperties) {
2679 self.discoverModelProperties('dummy', {}, cb);
2680 } else {
2681 process.nextTick(function() {
2682 const err = self.connected ? null : new Error(g.f('Not connected'));
2683 cb(err);
2684 });
2685 }
2686 return cb.promise;
2687};
2688
2689/**
2690 * Execute an arbitrary command. The commands are connector specific,
2691 * please refer to the documentation of your connector for more details.
2692 *
2693 * @param [...params] Array The command and its arguments, e.g. an SQL query.
2694 * @returns Promise A promise of the result
2695 */
2696DataSource.prototype.execute = function(...params) {
2697 if (!this.connector) {
2698 return Promise.reject(errorNotImplemented(
2699 `DataSource "${this.name}" is missing a connector to execute the command.`,
2700 ));
2701 }
2702
2703 if (!this.connector.execute) {
2704 return Promise.reject(new errorNotImplemented(
2705 `The connector "${this.connector.name}" used by dataSource "${this.name}" ` +
2706 'does not implement "execute()" API.',
2707 ));
2708 }
2709
2710 return new Promise((resolve, reject) => {
2711 this.connector.execute(...params, onExecuted);
2712 function onExecuted(err, result) {
2713 if (err) return reject(err);
2714 if (arguments.length > 2) {
2715 result = Array.prototype.slice.call(arguments, 1);
2716 }
2717 resolve(result);
2718 }
2719 });
2720
2721 function errorNotImplemented(msg) {
2722 const err = new Error(msg);
2723 err.code = 'NOT_IMPLEMENTED';
2724 return err;
2725 }
2726};
2727
2728/**
2729 * Begin a new Transaction.
2730 *
2731 *
2732 * @param [options] Options {isolationLevel: '...', timeout: 1000}
2733 * @returns Promise A promise which resolves to a Transaction object
2734 */
2735DataSource.prototype.beginTransaction = function(options) {
2736 return Transaction.begin(this.connector, options);
2737};
2738
2739/**
2740 * Get the maximum number of event listeners
2741 */
2742DataSource.prototype.getMaxOfflineRequests = function() {
2743 // Set max listeners to a default value
2744 // Override this default value with a datasource setting
2745 // 'maxOfflineRequests' from an application's datasources.json
2746
2747 let maxOfflineRequests = DataSource.DEFAULT_MAX_OFFLINE_REQUESTS;
2748 if (
2749 this.settings &&
2750 this.settings.maxOfflineRequests
2751 ) {
2752 if (typeof this.settings.maxOfflineRequests !== 'number')
2753 throw new Error('maxOfflineRequests must be a number');
2754
2755 maxOfflineRequests = this.settings.maxOfflineRequests;
2756 }
2757 return maxOfflineRequests;
2758};
2759
2760/*! The hidden property call is too expensive so it is not used that much
2761*/
2762/**
2763 * Define a hidden property
2764 * It is an utility to define a property to the Object with info flags
2765 * @param {Object} obj The property owner
2766 * @param {String} key The property name
2767 * @param {Mixed} value The default value
2768 */
2769function hiddenProperty(obj, key, value) {
2770 Object.defineProperty(obj, key, {
2771 writable: false,
2772 enumerable: false,
2773 configurable: false,
2774 value: value,
2775 });
2776}
2777
2778/**
2779 * Define readonly property on object
2780 *
2781 * @param {Object} obj The property owner
2782 * @param {String} key The property name
2783 * @param {Mixed} value The default value
2784 */
2785function defineReadonlyProp(obj, key, value) {
2786 Object.defineProperty(obj, key, {
2787 writable: false,
2788 enumerable: true,
2789 configurable: true,
2790 value: value,
2791 });
2792}
2793
2794// Carry over a few properties/methods from the ModelBuilder as some tests use them
2795DataSource.Text = ModelBuilder.Text;
2796DataSource.JSON = ModelBuilder.JSON;
2797DataSource.Any = ModelBuilder.Any;