diff --git a/index.js b/index.js new file mode 100644 index 00000000..5c3f74de --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +exports.Connector = require('./lib/connector'); +exports.SqlConnector = require('./lib/sql'); diff --git a/lib/connector.js b/lib/connector.js new file mode 100644 index 00000000..54d42273 --- /dev/null +++ b/lib/connector.js @@ -0,0 +1,172 @@ +module.exports = Connector; + +/** + * Base class for LooopBack connector. This is more a collection of useful + * methods for connectors than a super class + * @constructor + */ +function Connector(name, settings) { + this._models = {}; + this.name = name; + this.settings = settings || {}; +} + +/** + * Set the relational property to indicate the backend is a relational DB + * @type {boolean} + */ +Connector.prototype.relational = false; + +/** + * Get types associated with the connector + * @returns {String[]} The types for the connector + */ +Connector.prototype.getTypes = function() { + return ['db', 'nosql']; +}; + +/** + * Get the default data type for ID + * @returns {Function} The default type for ID + */ +Connector.prototype.getDefaultIdType = function() { + return String; +}; + +/** + * Get the metadata for the connector + * @returns {Object} The metadata object + * @property {String} type The type for the backend + * @property {Function} defaultIdType The default id type + * @property {Boolean} [isRelational] If the connector represents a relational database + * @property {Object} schemaForSettings The schema for settings object + */ +Connector.prototype.getMedadata = function () { + if (!this._metadata) { + this._metadata = { + types: this.getTypes(), + defaultIdType: this.getDefaultIdType(), + isRelational: this.isRelational || (this.getTypes().indexOf('rdbms') !== -1), + schemaForSettings: {} + }; + } + return this._metadata; +}; + +/** + * Execute a command with given parameters + * @param {String} command The command such as SQL + * @param {Object[]} [params] An array of parameters + * @param {Function} [callback] The callback function + */ +Connector.prototype.execute = function (command, params, callback) { + /*jshint unused:false */ + throw new Error('query method should be declared in connector'); +}; + +/** + * Look up the data source by model name + * @param {String} model The model name + * @returns {DataSource} The data source + */ +Connector.prototype.getDataSource = function (model) { + var m = this._models[model]; + if (!m) { + console.trace('Model not found: ' + model); + } + return m && m.model.dataSource; +}; + +/** + * Get the id property name + * @param {String} model The model name + * @returns {String} The id property name + */ +Connector.prototype.idName = function (model) { + return this.getDataSource(model).idName(model); +}; + +/** + * Get the id property names + * @param {String} model The model name + * @returns {[String]} The id property names + */ +Connector.prototype.idNames = function (model) { + return this.getDataSource(model).idNames(model); +}; + +/** + * Get the id index (sequence number, starting from 1) + * @param {String} model The model name + * @param {String} prop The property name + * @returns {Number} The id index, undefined if the property is not part + * of the primary key + */ +Connector.prototype.id = function (model, prop) { + var p = this._models[model].properties[prop]; + if (!p) { + console.trace('Property not found: ' + model + '.' + prop); + } + return p.id; +}; + +/** + * Hook to be called by DataSource for defining a model + * @param {Object} modelDefinition The model definition + */ +Connector.prototype.define = function (modelDefinition) { + if (!modelDefinition.settings) { + modelDefinition.settings = {}; + } + this._models[modelDefinition.model.modelName] = modelDefinition; +}; + +/** + * Hook to be called by DataSource for defining a model property + * @param {String} model The model name + * @param {String} propertyName The property name + * @param {Object} propertyDefinition The object for property metadata + */ +Connector.prototype.defineProperty = function (model, propertyName, propertyDefinition) { + this._models[model].properties[propertyName] = propertyDefinition; +}; + +/** + * Disconnect from the connector + */ +Connector.prototype.disconnect = function disconnect(cb) { + // NO-OP + if (cb) process.nextTick(cb); +}; + +/** + * Get the id value for the given model + * @param {String} model The model name + * @param {Object} data The model instance data + * @returns {*} The id value + * + */ +Connector.prototype.getIdValue = function (model, data) { + return data && data[this.idName(model)]; +}; + +/** + * Set the id value for the given model + * @param {String} model The model name + * @param {Object} data The model instance data + * @param {*} value The id value + * + */ +Connector.prototype.setIdValue = function (model, data, value) { + if (data) { + data[this.idName(model)] = value; + } +}; + +Connector.prototype.getType = function () { + return this.type; +}; + + + + diff --git a/lib/sql.js b/lib/sql.js new file mode 100644 index 00000000..dcad57ab --- /dev/null +++ b/lib/sql.js @@ -0,0 +1,402 @@ +var util = require('util'); +var async = require('async'); +var assert = require('assert'); +var Connector = require('./connector'); + +module.exports = SqlConnector; + +/** + * Base class for connectors that are backed by relational databases/SQL + * @class + */ +function SqlConnector() { + Connector.apply(this, [].slice.call(arguments)); +} + +util.inherits(SqlConnector, Connector); + +/** + * Set the relational property to indicate the backend is a relational DB + * @type {boolean} + */ +SqlConnector.prototype.relational = true; + +/** + * Get types associated with the connector + * Returns {String[]} The types for the connector + */ +SqlConnector.prototype.getTypes = function() { + return ['db', 'rdbms', 'sql']; +}; + +/*! + * Get the default data type for ID + * Returns {Function} + */ +SqlConnector.prototype.getDefaultIdType = function() { + return Number; +}; + +SqlConnector.prototype.query = function () { + throw new Error('query method should be declared in connector'); +}; + +SqlConnector.prototype.command = function (sql, params, callback) { + return this.query(sql, params, callback); +}; + +SqlConnector.prototype.queryOne = function (sql, callback) { + return this.query(sql, function (err, data) { + if (err) { + return callback(err); + } + callback(err, data && data[0]); + }); +}; + +/** + * Get the table name for a given model. + * Returns the table name (String). + * @param {String} model The model name + */ +SqlConnector.prototype.table = function (model) { + var name = this.getDataSource(model).tableName(model); + var dbName = this.dbName; + if (typeof dbName === 'function') { + name = dbName(name); + } + return name; +}; + +/** + * Get the column name for given model property + * @param {String} model The model name + * @param {String} property The property name + * @returns {String} The column name + */ +SqlConnector.prototype.column = function (model, property) { + var name = this.getDataSource(model).columnName(model, property); + var dbName = this.dbName; + if (typeof dbName === 'function') { + name = dbName(name); + } + return name; +}; + +/** + * Get the column name for given model property + * @param {String} model The model name + * @param {String} property The property name + * @returns {Object} The column metadata + */ +SqlConnector.prototype.columnMetadata = function (model, property) { + return this.getDataSource(model).columnMetadata(model, property); +}; + +/** + * Get the corresponding property name for a given column name + * @param {String} model The model name + * @param {String} column The column name + * @returns {String} The property name for a given column + */ +SqlConnector.prototype.propertyName = function (model, column) { + var props = this._models[model].properties; + for (var p in props) { + if (this.column(model, p) === column) { + return p; + } + } + return null; +}; + +/** + * Get the id column name + * @param {String} model The model name + * @returns {String} The column name + */ +SqlConnector.prototype.idColumn = function (model) { + var name = this.getDataSource(model).idColumnName(model); + var dbName = this.dbName; + if (typeof dbName === 'function') { + name = dbName(name); + } + return name; +}; + +/** + * Get the escaped id column name + * @param {String} model The model name + * @returns {String} the escaped id column name + */ +SqlConnector.prototype.idColumnEscaped = function (model) { + return this.escapeName(this.getDataSource(model).idColumnName(model)); +}; + +/** + * Escape the name for the underlying database + * @param {String} name The name + */ +SqlConnector.prototype.escapeName = function (name) { + /*jshint unused:false */ + throw new Error('escapeName method should be declared in connector'); +}; + +/** + * Get the escaped table name + * @param {String} model The model name + * @returns {String} the escaped table name + */ +SqlConnector.prototype.tableEscaped = function (model) { + return this.escapeName(this.table(model)); +}; + +/** + * Get the escaped column name for a given model property + * @param {String} model The model name + * @param {String} property The property name + * @returns {String} The escaped column name + */ +SqlConnector.prototype.columnEscaped = function (model, property) { + return this.escapeName(this.column(model, property)); +}; + +function isIdValuePresent(idValue, callback, returningNull) { + try { + assert(idValue !== null && idValue !== undefined, 'id value is required'); + return true; + } catch (err) { + process.nextTick(function () { + if(callback) callback(returningNull ? null: err); + }); + return false; + } +} +/** + * Save the model instance into the backend store + * @param {String} model The model name + * @param {Object} data The model instance data + * @param {Function} callback The callback function + */ +SqlConnector.prototype.save = function (model, data, callback) { + var idName = this.getDataSource(model).idName(model); + var idValue = data[idName]; + + if (!isIdValuePresent(idValue, callback)) { + return; + } + + idValue = this._escapeIdValue(model, idValue); + var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' + + this.toFields(model, data) + + ' WHERE ' + this.idColumnEscaped(model) + ' = ' + idValue; + + this.query(sql, function (err, result) { + if (callback) callback(err, result); + }); +}; + +/** + * Check if a model instance exists for the given id value + * @param {String} model The model name + * @param {*} id The id value + * @param {Function} callback The callback function + */ +SqlConnector.prototype.exists = function (model, id, callback) { + if (!isIdValuePresent(id, callback, true)) { + return; + } + var sql = 'SELECT 1 FROM ' + + this.tableEscaped(model) + ' WHERE ' + + this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id) + + ' LIMIT 1'; + + this.query(sql, function (err, data) { + if (!callback) return; + if (err) { + callback(err); + } else { + callback(null, data.length >= 1); + } + }); +}; + +/** + * Find a model instance by id + * @param {String} model The model name + * @param {*} id The id value + * @param {Function} callback The callback function + */ +SqlConnector.prototype.find = function find(model, id, callback) { + if (!isIdValuePresent(id, callback, true)) { + return; + } + var self = this; + var idQuery = this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id); + var sql = 'SELECT * FROM ' + + this.tableEscaped(model) + ' WHERE ' + idQuery + ' LIMIT 1'; + + this.query(sql, function (err, data) { + var result = (data && data.length >= 1) ? data[0] : null; + if (callback) callback(err, self.fromDatabase(model, result)); + }); +}; + +/** + * Delete a model instance by id value + * @param {String} model The model name + * @param {*} id The id value + * @param {Function} callback The callback function + */ +SqlConnector.prototype.delete = +SqlConnector.prototype.destroy = function destroy(model, id, callback) { + if (!isIdValuePresent(id, callback, true)) { + return; + } + var sql = 'DELETE FROM ' + this.tableEscaped(model) + ' WHERE ' + + this.idColumnEscaped(model) + ' = ' + this._escapeIdValue(model, id); + + this.command(sql, function (err, result) { + if (callback) callback(err, result); + }); +}; + +SqlConnector.prototype._escapeIdValue = function(model, idValue) { + var idProp = this.getDataSource(model).idProperty(model); + if(typeof this.toDatabase === 'function') { + return this.toDatabase(idProp, idValue); + } else { + if(idProp.type === Number) { + return idValue; + } else { + return '\'' + idValue + '\''; + } + } +}; + +/** + * Delete all model instances + * + * @param {String} model The model name + * @param {Function} callback The callback function + */ +SqlConnector.prototype.deleteAll = +SqlConnector.prototype.destroyAll = function destroyAll(model, callback) { + this.command('DELETE FROM ' + this.tableEscaped(model), function (err, result) { + if (callback) callback(err, result); + }); +}; + +/** + * Count all model instances by the where filter + * + * @param {String} model The model name + * @param {Function} callback The callback function + * @param {Object} where The where clause + */ +SqlConnector.prototype.count = function count(model, callback, where) { + var self = this; + var props = this._models[model].properties; + + this.queryOne('SELECT count(*) as cnt FROM ' + + this.tableEscaped(model) + ' ' + buildWhere(where), function (err, res) { + if (err) { + return callback(err); + } + callback(err, res && res.cnt); + }); + + function buildWhere(conds) { + var cs = []; + Object.keys(conds || {}).forEach(function (key) { + var keyEscaped = self.columnEscaped(model, key); + if (conds[key] === null) { + cs.push(keyEscaped + ' IS NULL'); + } else { + cs.push(keyEscaped + ' = ' + self.toDatabase(props[key], conds[key])); + } + }); + return cs.length ? ' WHERE ' + cs.join(' AND ') : ''; + } +}; + +/** + * Update attributes for a given model instance + * @param {String} model The model name + * @param {*} id The id value + * @param {Object} data The model data instance containing all properties to be updated + * @param {Function} cb The callback function + */ +SqlConnector.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { + if (!isIdValuePresent(id, cb)) { + return; + } + var idName = this.getDataSource(model).idName(model); + data[idName] = id; + this.save(model, data, cb); +}; + +/** + * Disconnect from the connector + */ +SqlConnector.prototype.disconnect = function disconnect() { + // No operation +}; + +/** + * Recreate the tables for the given models + * @param {[String]|String} [models] A model name or an array of model names, + * if not present, apply to all models defined in the connector + * @param {Function} [cb] The callback function + */ +SqlConnector.prototype.automigrate = function (models, cb) { + var self = this; + + if ((!cb) && ('function' === typeof models)) { + cb = models; + models = undefined; + } + // First argument is a model name + if ('string' === typeof models) { + models = [models]; + } + + models = models || Object.keys(self._models); + async.each(models, function (model, callback) { + if (model in self._models) { + self.dropTable(model, function (err) { + if (err) { + // TODO(bajtos) should we abort here and call cb(err)? + // The original code in juggler ignored the error completely + console.error(err); + } + self.createTable(model, function (err, result) { + if (err) { + console.error(err); + } + callback(err, result); + }); + }); + } + }, cb); +}; + +/** + * Drop the table for the given model from the database + * @param {String} model The model name + * @param {Function} [cb] The callback function + */ +SqlConnector.prototype.dropTable = function (model, cb) { + this.command('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb); +}; + +/** + * Create the table for the given model + * @param {String} model The model name + * @param {Function} [cb] The callback function + */ + +SqlConnector.prototype.createTable = function (model, cb) { + this.command('CREATE TABLE ' + this.tableEscaped(model) + + ' (\n ' + this.propertiesSQL(model) + '\n)', cb); +}; + diff --git a/package.json b/package.json index 49644196..19be599e 100644 --- a/package.json +++ b/package.json @@ -18,5 +18,8 @@ "license": { "name": "Dual MIT/StrongLoop", "url": "https://github.com/strongloop/loopback-connector/blob/master/LICENSE" + }, + "dependencies": { + "async": "^0.9.0" } }