loopback-connector-crate
Version:
Loopback Crate Connector
1,659 lines (1,524 loc) • 46.4 kB
JavaScript
/*!
* Crate connector for LoopBack
*/
var crate = require('cratejs');
var SqlConnector = require('loopback-connector').SqlConnector;
var util = require('util');
var async = require('async');
var debug = require('debug')('loopback:connector:crate');
/**
*
* Initialize the Crate connector against the given data source
*
* @param {DataSource} dataSource The loopback-datasource-juggler dataSource
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @header Crate.initialize(dataSource, [callback])
*/
exports.initialize = function initializeDataSource(dataSource, callback) {
if (!crate) {
return;
}
var dbSettings = dataSource.settings || {};
dbSettings.host = dbSettings.host || dbSettings.hostname || 'localhost';
dbSettings.port = dbSettings.port || dbSettings.port || 4200;
dbSettings.debug = dbSettings.debug || debug.enabled;
dataSource.connector = new Crate(crate, dbSettings);
dataSource.connector.dataSource = dataSource;
callback
//if (callback) {
// dataSource.connecting = true;
// dataSource.connector.connect(callback);
//}
};
/**
* Crate connector constructor
*
* @param {Crate} crate Crate node.js binding
* @options {Object} settings An object for the data source settings.
* See [node-postgres documentation](https://github.com/brianc/node-postgres/wiki/Client#parameters).
* @property {String} url URL to the database, such as 'postgres://test:mypassword@localhost:5432/devdb'.
* Other parameters can be defined as query string of the url
* @property {String} hostname The host name or ip address of the Crate DB server
* @property {Number} port The port number of the Crate DB Server
* @property {String} user The user name
* @property {String} password The password
* @property {String} database The database name
* @property {Boolean} ssl Whether to try SSL/TLS to connect to server
*
* @constructor
*/
function Crate(crate, settings) {
// this.name = 'crate';
// this._models = {};
// this.settings = settings;
this.constructor.super_.call(this, 'crate', settings);
this.clientConfig = settings.url || settings;
this.pg = new crate(settings);
this.settings = settings;
if (settings.debug) {
console.log('settings :',settings);
debug('Settings %j', settings);
}
}
// Inherit from loopback-datasource-juggler BaseSQL
require('util').inherits(Crate, SqlConnector);
Crate.prototype.debug = function () {
if (this.settings.debug) {
debug.apply(debug, arguments);
}
};
/**
* Connect to Crate
* @callback {Function} [callback] The callback after the connection is established
*/
Crate.prototype.connect = function (callback) {
var self = this;
console.log('self.clientConfig :',self.clientConfig);
self.pg = new crate(self.clientConfig);
process.nextTick(callback);
callback;
/*
, function(err, client, done) {
process.nextTick(done);
callback && callback(err, client);
});
*/
};
/**
* Execute the sql statement
*
* @param {String} sql The SQL statement
* @param {String[]} params The parameter values for the SQL statement
* @callback {Function} [callback] The callback after the SQL statement is executed
* @param {String|Error} err The error string or object
* @param {Object[]) data The result from the SQL
*/
Crate.prototype.executeSQL = function (sql, params, callback) {
var self = this;
var time = Date.now();
var log = self.log;
if (self.settings.debug) {
if (params && params.length > 0) {
self.debug('SQL: ' + sql + '\nParameters: ' + params);
} else {
self.debug('SQL: ' + sql);
}
}
self.pg.execute(sql, params, function(err, data) {
// if(err) console.error(err);
if (err && self.settings.debug) {
self.debug(err);
}
if (self.settings.debug && data) self.debug("%j", data);
// console.log(err);
if (log) log(sql, time);
//process.nextTick(done); // Release the pooled client
if (data) {
/*
// TODO: Optimize it using async map reduce
data.json = data.rows.map(function (e) {
var x = {}, i;
for (i = 0; i < data.cols.length; i += 1) {
x[data.cols[i]] = e[i];
}
return x;
});
*/
callback(null, data);
}
else callback(err, null);
});
};
/*
* Check if the connection is in progress
* @private
*/
function stillConnecting(dataSource, obj, args) {
if (dataSource.connected) return false; // Connected
var method = args.callee;
// Set up a callback after the connection is established to continue the method call
dataSource.once('connected', function () {
method.apply(obj, [].slice.call(args));
});
if (!dataSource.connecting) {
dataSource.connect();
}
return true;
}
/**
* Execute a sql statement with the given parameters
*
* @param {String} sql The SQL statement
* @param {[]} params An array of parameter values
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object[]} data The result from the SQL
*/
Crate.prototype.query = function (sql, params, callback) {
//if (stillConnecting(this.dataSource, this, arguments)) return;
if (!callback && typeof params === 'function') {
callback = params;
params = [];
}
params = params || [];
var cb = callback || function (err, result) {
};
this.executeSQL(sql, params, cb);
};
/**
* Count the number of instances for the given model
*
* @param {String} model The model name
* @param {Function} [callback] The callback function
* @param {Object} filter The filter for where
*
*/
Crate.prototype.count = function count(model, callback, filter) {
this.query('SELECT count(*) as "cnt" FROM '
+ ' ' + this.toFilter(model, filter && {where: filter}), function (err, data) {
if (err) return callback(err);
var c = data && data[0] && data[0].cnt;
callback(err, Number(c));
}.bind(this));
};
/**
* Delete instances for the given model
*
* @param {String} model The model name
* @param {Object} [where] The filter for where
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
*/
Crate.prototype.destroyAll = function destroyAll(model, where, callback) {
if (!callback && 'function' === typeof where) {
callback = where;
where = undefined;
}
var deleteBars = this.pg.Delete(model)
.where(where);
deleteBars.run(function(err, res) {
if (err) {
return callback(err, []);
}
else{
return callback(null, res);
}
});
/*
this.query('DELETE FROM '
+ ' ' + this.toFilter(model, where && {where: where}), function (err, data) {
callback && callback(err, data);
}.bind(this));
*/
};
/*!
* Categorize the properties for the given model and data
* @param {String} model The model name
* @param {Object} data The data object
* @returns {{ids: String[], idsInData: String[], nonIdsInData: String[]}}
* @private
*/
Crate.prototype._categorizeProperties = function(model, data) {
var ids = this.idNames(model);
var idsInData = ids.filter(function(key) {
return data[key] !== null && data[key] !== undefined;
});
var props = Object.keys(this._models[model].properties);
var nonIdsInData = Object.keys(data).filter(function(key) {
return props.indexOf(key) !== -1 && ids.indexOf(key) === -1 && data[key] !== undefined;
});
return {
ids: ids,
idsInData: idsInData,
nonIdsInData: nonIdsInData
};
};
Crate.prototype.mapToDB = function (model, data) {
var dbData = {};
if (!data) {
return dbData;
}
var props = this._models[model].properties;
for (var p in data) {
if(props[p]) {
var pType = props[p].type && props[p].type.name;
if (pType === 'GeoPoint' && data[p]) {
dbData[p] = '(' + data[p].lat + ',' + data[p].lng + ')';
} else {
dbData[p] = data[p];
}
}
}
return dbData;
}
/**
* Create the data model in Crate
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object} The newly created model instance
*/
Crate.prototype.create = function (model, data, callback) {
var self = this;
var insertModel = this.pg.Insert(model);
//Inserting a single row
insertModel.data(data)
.run(function(err, res) {
if (err) {
return callback(err);
}
var idValue = res.id || null;
callback(err, idValue);
});
/*
data = self.mapToDB(model, data);
var props = self._categorizeProperties(model, data);
var sql = [];
sql.push('INSERT INTO ', self.tableEscaped(model), ' ',
self.toFields(model, data, true));
sql.push(' RETURNING ');
sql.push(props.ids.map(function(key) {
return self.columnEscaped(model, key)}).join(',')
);
var idColName = self.idColumn(model);
this.query(sql.join(''), generateQueryParams(data, props), function (err, info) {
if (err) {
return callback(err);
}
var idValue = null;
if (info && info[0]) {
idValue = info[0][idColName];
}
callback(err, idValue);
});
*/
};
// FIXME: [rfeng] The native implementation of upsert only works with
// crate 9.1 or later as it requres writable CTE
// See https://github.com/strongloop/loopback-connector-crate/issues/27
/**
* Update if the model instance exists with the same id or create a new instance
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object} The updated model instance
*/
/*
Crate.prototype.updateOrCreate = function (model, data, callback) {
var self = this;
data = self.mapToDB(model, data);
var props = self._categorizeProperties(model, data);
var idColumns = props.ids.map(function(key) {
return self.columnEscaped(model, key); }
);
var nonIdsInData = props.nonIdsInData;
var query = [];
query.push('WITH update_outcome AS (UPDATE ', self.tableEscaped(model), ' SET ');
query.push(self.toFields(model, data, false));
query.push(' WHERE ');
query.push(idColumns.map(function (key, i) {
return ((i > 0) ? ' AND ' : ' ') + key + '=$' + (nonIdsInData.length + i + 1);
}).join(','));
query.push(' RETURNING ', idColumns.join(','), ')');
query.push(', insert_outcome AS (INSERT INTO ', self.tableEscaped(model), ' ');
query.push(self.toFields(model, data, true));
query.push(' WHERE NOT EXISTS (SELECT * FROM update_outcome) RETURNING ', idColumns.join(','), ')');
query.push(' SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome');
var queryParams = [];
nonIdsInData.forEach(function(key) {
queryParams.push(data[key]);
});
props.ids.forEach(function(key) {
queryParams.push(data[key] || null);
});
var idColName = self.idColumn(model);
self.query(query.join(''), queryParams, function(err, info) {
if (err) {
return callback(err);
}
var idValue = null;
if (info && info[0]) {
idValue = info[0][idColName];
}
callback(err, idValue);
});
};
*/
/**
* Save the model instance to Crate DB
* @param {String} model The model name
* @param {Object} data The model instance data
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
*/
Crate.prototype.save = function (model, data, callback) {
var self = this;
data = self.mapToDB(model, data);
var props = self._categorizeProperties(model, data);
var sql = [];
sql.push('UPDATE ', self.tableEscaped(model), ' SET ', self.toFields(model, data));
sql.push(' WHERE ');
props.ids.forEach(function (id, i) {
sql.push((i > 0) ? ' AND ' : ' ', self.idColumnEscaped(model), ' = $',
(props.nonIdsInData.length + i + 1));
});
self.query(sql.join(''), generateQueryParams(data, props), function (err) {
callback(err);
});
};
Crate.prototype.update =
Crate.prototype.updateAll = function (model, where, data, callback) {
//console.log(where, "update >> ",data);
var updateBars = this.pg.Update(model)
.where(where)
.set(data);
updateBars.run(function(err, res) {
if (callback) {
callback(err, res);
}
});
/*
var whereClause = this.buildWhere(model, where);
var sql = ['UPDATE ', this.tableEscaped(model), ' SET ',
this.toFields(model, data), ' ', whereClause].join('');
data = this.mapToDB(model, data);
var props = this._categorizeProperties(model, data);
this.query(sql, generateQueryParams(data, props), function (err, result) {
if (callback) {
callback(err, result);
}
});
*/
};
/*!
* Build a list of column name/value pairs
*
* @param {String} The model name
* @param {Object} The model instance data
* @param {Boolean} forCreate Indicate if it's for creation
*/
Crate.prototype.toFields = function (model, data, forCreate) {
var self = this;
var props = self._categorizeProperties(model, data);
var dataIdNames = props.idsInData;
var nonIdsInData = props.nonIdsInData;
var query = [];
if (forCreate) {
if(nonIdsInData.length == 0 && dataIdNames.length == 0) {
return 'default values';
}
query.push('(');
query.push(nonIdsInData.map(function (key) {
return self.columnEscaped(model, key);
}).join(','));
if (dataIdNames.length > 0) {
if (nonIdsInData.length > 0) {
query.push(',');
}
query.push(dataIdNames.map(function (key) {
return self.columnEscaped(model, key);
}).join(','));
}
query.push(') SELECT ');
for (var i = 1, len = nonIdsInData.length + dataIdNames.length; i <= len; i++) {
query.push('$', i);
if (i !== len) {
query.push(',');
}
}
query.push(' ');
} else {
query.push(nonIdsInData.map(function (key, i) {
return self.columnEscaped(model, key) + "=$" + (i + 1)
}).join(','));
}
return query.join('');
};
function dateToCrate(val, dateOnly) {
function fz(v) {
return v < 10 ? '0' + v : v;
}
function ms(v) {
if (v < 10) {
return '00' + v;
} else if (v < 100) {
return '0' + v;
} else {
return '' + v;
}
}
var dateStr = [
val.getUTCFullYear(),
fz(val.getUTCMonth() + 1),
fz(val.getUTCDate())
].join('-') + ' ' + [
fz(val.getUTCHours()),
fz(val.getUTCMinutes()),
fz(val.getUTCSeconds())
].join(':');
if (!dateOnly) {
dateStr += '.' + ms(val.getMilliseconds());
}
if (dateOnly) {
return "to_date('" + dateStr + "', 'yyyy-mm-dd hh24:mi:ss')";
} else {
return "to_timestamp('" + dateStr + "', 'yyyy-mm-dd hh24:mi:ss.ff3')";
}
}
/*!
* Convert name/value to database value
*
* @param {String} prop The property name
* @param {*} val The property value
*/
Crate.prototype.toDatabase = function (prop, val) {
if (val === null || val === undefined) {
// Crate complains with NULLs in not null columns
// If we have an autoincrement value, return DEFAULT instead
if (prop.autoIncrement) {
return 'DEFAULT';
}
else {
return 'NULL';
}
}
if (val.constructor.name === 'Object') {
if (prop.crate && prop.crate.dataType === 'json') {
return JSON.stringify(val);
}
var operator = Object.keys(val)[0]
val = val[operator];
if (operator === 'between') {
return this.toDatabase(prop, val[0]) + ' AND ' + this.toDatabase(prop, val[1]);
}
if (operator === 'inq' || operator === 'nin') {
for (var i = 0; i < val.length; i++) {
val[i] = escape(val[i]);
}
return val.join(',');
}
return this.toDatabase(prop, val);
}
if (prop.type.name === 'Number') {
if (!val && val !== 0) {
if (prop.autoIncrement) {
return 'DEFAULT';
}
else {
return 'NULL';
}
}
return escape(val);
}
if (prop.type.name === 'Date' || prop.type.name === 'Timestamp') {
if (!val) {
if (prop.autoIncrement) {
return 'DEFAULT';
}
else {
return 'NULL';
}
}
if (!val) {
if (prop.autoIncrement) {
return 'DEFAULT';
}
else {
return 'NULL';
}
}
if (!val.toISOString) {
val = new Date(val);
}
var iso = escape(val.toISOString());
return 'TIMESTAMP WITH TIME ZONE ' + iso;
/*
if (!val.toUTCString) {
val = new Date(val);
}
return dateToCrate(val, prop.type.name === 'Date');
*/
}
// Crate support char(1) Y/N
if (prop.type.name === 'Boolean') {
if (val) {
return "TRUE";
} else {
return "FALSE";
}
}
if (prop.type.name === 'GeoPoint') {
if (val) {
return '(' + escape(val.lat) + ',' + escape(val.lng) + ')';
} else {
return 'NULL';
}
}
return escape(val.toString());
};
/*!
* Convert the data from database to JSON
*
* @param {String} model The model name
* @param {Object} data The data from DB
*/
Crate.prototype.fromDatabase = function (model, data) {
if (!data) {
return null;
}
var props = this._models[model].properties;
var json = {};
for (var p in props) {
var key = this.column(model, p);
// console.log(data);
var val = data[key];
if (val === undefined) {
continue;
}
// console.log(key, val);
var prop = props[p];
var type = prop.type && prop.type.name;
if (prop && type === 'Boolean') {
if(typeof val === 'boolean') {
json[p] = val;
} else {
json[p] = (val === 'Y' || val === 'y' || val === 'T' || val === 't' || val === '1');
}
} else if (prop && type === 'GeoPoint' || type === 'Point') {
if (typeof val === 'string') {
// The point format is (x,y)
var point = val.split(/[\(\)\s,]+/).filter(Boolean);
json[p] = {
lat: +point[0],
lng: +point[1]
};
} else if (typeof val === 'object' && val !== null) {
// Now pg driver converts point to {x: lat, y: lng}
json[p] = {
lat: val.x,
lng: val.y
};
} else {
json[p] = val;
}
} else {
json[p] = val;
}
}
if (this.settings.debug) {
this.debug('JSON data: %j', json);
}
return json;
};
/*!
* Convert to the Database name
* @param {String} name The name
* @returns {String} The converted name
*/
Crate.prototype.dbName = function (name) {
if (!name) {
return name;
}
// Crate default to lowercase names
return name.toLowerCase();
};
/*!
* Escape the name for Crate DB
* @param {String} name The name
* @returns {String} The escaped name
*/
Crate.prototype.escapeName = function (name) {
if (!name) {
return name;
}
return '"' + name.replace(/\./g, '"."') + '"';
};
Crate.prototype.schemaName = function (model) {
// Check if there is a 'schema' property for crate
var dbMeta = this._models[model].settings && this._models[model].settings.crate;
var schemaName = (dbMeta && (dbMeta.schema || dbMeta.schemaName))
|| this.settings.schema || 'public';
return schemaName;
};
Crate.prototype.table = function (model) {
// Check if there is a 'table' property for crate
var dbMeta = this._models[model].settings && this._models[model].settings.crate;
var tableName = (dbMeta && (dbMeta.table || dbMeta.tableName)) || model.toLowerCase();
return tableName;
};
Crate.prototype.tableEscaped = function (model) {
return this.escapeName(this.schemaName(model)) + '.' + this.escapeName(this.table(model));
};
/*!
* Get a list of columns based on the fields pattern
*
* @param {String} model The model name
* @param {Object|String[]} props Fields pattern
* @returns {String}
*/
Crate.prototype.getColumns = function (model, props) {
var cols = this._models[model].properties;
var self = this;
var keys = Object.keys(cols);
if (Array.isArray(props) && props.length > 0) {
// No empty array, including all the fields
keys = props;
} else if ('object' === typeof props && Object.keys(props).length > 0) {
// { field1: boolean, field2: boolean ... }
var included = [];
var excluded = [];
keys.forEach(function (k) {
if (props[k]) {
included.push(k);
} else if ((k in props) && !props[k]) {
excluded.push(k);
}
});
if (included.length > 0) {
keys = included;
} else if (excluded.length > 0) {
excluded.forEach(function (e) {
var index = keys.indexOf(e);
keys.splice(index, 1);
});
}
}
var names = keys.map(function (c) {
return self.columnEscaped(model, c);
});
return names.join(', ');
};
/**
* Find matching model instances by the filter
*
* @param {String} model The model name
* @param {Object} filter The filter
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object[]} The matched model instances
*/
Crate.prototype.all = function all(model, filter, callback) {
// Order by id if no order is specified
filter = filter || {};
if (!filter.order) {
var idNames = this.idNames(model);
if (idNames && idNames.length) {
filter.order = idNames;
}
}
//console.log(model, "filters ", filter)
var findBars = this.pg.Select(model)
.columns(filter.columns || [])
.where(filter.where)
.limit(filter.limit || 99)
.order(filter.order, filter.sort || 'asc');
findBars.run(function(err, data) {
//console.log(err, "yyyyyyyyyy", data);
if (err) {
return callback(err, []);
}
if (data) {
// TODO: Optimize it using async map reduce
data.json = data.rows.map(function (e) {
var x = {}, i;
for (i = 0; i < data.cols.length; i += 1) {
x[data.cols[i]] = e[i];
}
return x;
});
callback(null, data.json);
}
});
/*
this.query('SELECT ' + this.getColumns(model, filter.fields) + ' FROM '
+ this.toFilter(model, filter), function (err, data) {
if (err) {
return callback(err, []);
}
if (data) {
for (var i = 0; i < data.length; i++) {
data[i] = this.fromDatabase(model, data[i]);
}
}
if (filter && filter.include) {
this._models[model].model.include(data, filter.include, callback);
} else {
callback(null, data);
}
}.bind(this));
*/
};
function getPagination(filter) {
var pagination = [];
if (filter && (filter.limit || filter.offset || filter.skip)) {
var offset = Number(filter.offset);
if (!offset) {
offset = Number(filter.skip);
}
if (offset) {
pagination.push('OFFSET ' + offset);
} else {
offset = 0;
}
var limit = Number(filter.limit);
if (limit) {
pagination.push('LIMIT ' + limit);
}
}
return pagination;
}
Crate.prototype.buildWhere = function (model, conds) {
var where = this._buildWhere(model, conds);
if (where) {
return ' WHERE ' + where;
} else {
return '';
}
};
Crate.prototype._buildWhere = function (model, conds) {
if (!conds) {
return '';
}
var self = this;
var props = self._models[model].properties;
var fields = [];
if (typeof conds === 'string') {
fields.push(conds);
} else if (util.isArray(conds)) {
var query = conds.shift().replace(/\?/g, function (s) {
return escape(conds.shift());
});
fields.push(query);
} else {
var sqlCond = null;
Object.keys(conds).forEach(function (key) {
if (key === 'and' || key === 'or') {
var clauses = conds[key];
if (Array.isArray(clauses)) {
clauses = clauses.map(function (c) {
return '(' + self._buildWhere(model, c) + ')';
});
return fields.push(clauses.join(' ' + key.toUpperCase() + ' '));
}
// The value is not an array, fall back to regular fields
}
if (conds[key] && conds[key].constructor.name === 'RegExp') {
var regex = conds[key];
sqlCond = self.columnEscaped(model, key);
if (regex.ignoreCase) {
sqlCond += ' ~* ';
} else {
sqlCond += ' ~ ';
}
sqlCond += "'" + regex.source + "'";
fields.push(sqlCond);
return;
}
if (props[key]) {
var filterValue = self.toDatabase(props[key], conds[key]);
if (filterValue === 'NULL') {
fields.push(self.columnEscaped(model, key) + ' IS ' + filterValue);
} else if (conds[key].constructor.name === 'Object') {
var condType = Object.keys(conds[key])[0];
sqlCond = self.columnEscaped(model, key);
if ((condType === 'inq' || condType === 'nin') && filterValue.length === 0) {
fields.push(condType === 'inq' ? '1 = 2' : '1 = 1');
return true;
}
switch (condType) {
case 'gt':
sqlCond += ' > ';
break;
case 'gte':
sqlCond += ' >= ';
break;
case 'lt':
sqlCond += ' < ';
break;
case 'lte':
sqlCond += ' <= ';
break;
case 'between':
sqlCond += ' BETWEEN ';
break;
case 'inq':
sqlCond += ' IN ';
break;
case 'nin':
sqlCond += ' NOT IN ';
break;
case 'neq':
sqlCond += ' != ';
break;
case 'like':
sqlCond += ' LIKE ';
filterValue += "ESCAPE '\\'";
break;
case 'nlike':
sqlCond += ' NOT LIKE ';
filterValue += "ESCAPE '\\'";
break;
default:
sqlCond += ' ' + condType + ' ';
break;
}
sqlCond += (condType === 'inq' || condType === 'nin')
? '(' + filterValue + ')' : filterValue;
fields.push(sqlCond);
} else {
fields.push(self.columnEscaped(model, key) + ' = ' + filterValue);
}
}
});
}
return fields.join(' AND ');
};
/*!
* Build the SQL clause
* @param {String} model The model name
* @param {Object} filter The filter
* @returns {*}
*/
Crate.prototype.toFilter = function (model, filter) {
var self = this;
if (filter && typeof filter.where === 'function') {
return self.tableEscaped(model) + ' ' + filter.where();
}
if (!filter) {
return self.tableEscaped(model);
}
var out = self.tableEscaped(model) + ' ';
var where = self.buildWhere(model, filter.where);
if (where) {
out += where;
}
// First check the pagination requirements
// http://docs.crate.com/cd/B19306_01/server.102/b14200/functions137.htm#i86310
var pagination = getPagination(filter);
if (filter.order) {
var order = filter.order;
if (typeof order === 'string') {
order = [order];
}
var orderBy = '';
filter.order = [];
for (var i = 0, n = order.length; i < n; i++) {
var t = order[i].split(/[\s]+/);
var field = t[0], dir = t[1];
filter.order.push(self.columnEscaped(model, field) + (dir ? ' ' + dir : ''));
}
orderBy = ' ORDER BY ' + filter.order.join(',');
if (pagination.length) {
out = out + ' ' + orderBy + ' ' + pagination.join(' ');
} else {
out = out + ' ' + orderBy;
}
} else {
if (pagination.length) {
out = out + ' '
+ pagination.join(' ');
}
}
return out;
};
/**
* Check if a model instance exists by id
* @param {String} model The model name
* @param {*} id The id value
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Boolean} true if the id exists
*
*/
Crate.prototype.exists = function (model, id, callback) {
var sql = 'SELECT 1 FROM ' +
this.tableEscaped(model);
if (id) {
sql += ' WHERE ' + this.idColumnEscaped(model) + ' = ' + id + ' LIMIT 1';
} else {
sql += ' WHERE ' + this.idColumnEscaped(model) + ' IS NULL LIMIT 1';
}
this.query(sql, function (err, data) {
if (err) return callback(err);
callback(null, data.length === 1);
});
};
/**
* Find a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
* @param {Object} The model instance
*/
Crate.prototype.find = function find(model, id, callback) {
var sql = 'SELECT * FROM ' +
this.tableEscaped(model);
if (id) {
var idVal = this.toDatabase(this._models[model].properties[this.idName(model)], id);
sql += ' WHERE ' + this.idColumnEscaped(model) + ' = ' + idVal + ' LIMIT 1';
}
else {
sql += ' WHERE ' + this.idColumnEscaped(model) + ' IS NULL LIMIT 1';
}
this.query(sql, function (err, data) {
if (data && data.length === 1) {
// data[0][this.idColumn(model)] = id;
} else {
data = [null];
}
callback(err, this.fromDatabase(model, data[0]));
}.bind(this));
};
/*!
* Discover the properties from a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
function getTableStatus(model, cb) {
function decoratedCallback(err, data) {
if (err) {
console.error(err);
}
if (!err) {
data.forEach(function (field) {
field.type = mapCrateDatatypes(field.type);
});
}
cb(err, data);
}
this.query('SELECT column_name AS "column", data_type AS "type", ' +
'is_nullable AS "nullable"' // , data_default AS "Default"'
+ ' FROM "information_schema"."columns" WHERE table_name=\'' +
this.table(model) + '\'', decoratedCallback);
}
/**
* Perform autoupdate for the given models
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
*/
Crate.prototype.autoupdate = 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(this._models);
async.each(models, function(model, done) {
if (!(model in self._models)) {
return process.nextTick(function() {
done(new Error('Model not found: ' + model));
});
}
getTableStatus.call(self, model, function(err, fields) {
if (!err && fields.length) {
self.alterTable(model, fields, done);
} else {
self.createTable(model, done);
}
});
}, cb);
};
/*!
* Check if the models exist
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
* @param {Function} [cb] The callback function
*/
Crate.prototype.isActual = 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(this._models);
var changes = [];
async.each(models, function(model, done) {
getTableStatus.call(self, model, function(err, fields) {
changes = changes.concat(getAddModifyColumns.call(self, model, fields));
changes = changes.concat(getDropColumns.call(self, model, fields));
done(err);
});
}, function done(err) {
if (err) {
return cb && cb(err);
}
var actual = (changes.length === 0);
cb && cb(null, actual);
});
};
/*!
* Alter the table for the given model
* @param {String} model The model name
* @param {Object[]} actualFields Actual columns in the table
* @param {Function} [cb] The callback function
*/
Crate.prototype.alterTable = function (model, actualFields, cb) {
var self = this;
var pendingChanges = getAddModifyColumns.call(self, model, actualFields);
if (pendingChanges.length > 0) {
applySqlChanges.call(self, model, pendingChanges, function (err, results) {
var dropColumns = getDropColumns.call(self, model, actualFields);
if (dropColumns.length > 0) {
applySqlChanges.call(self, model, dropColumns, cb);
} else {
cb && cb(err, results);
}
});
} else {
var dropColumns = getDropColumns.call(self, model, actualFields);
if (dropColumns.length > 0) {
applySqlChanges.call(self, model, dropColumns, cb);
} else {
cb && process.nextTick(cb.bind(null, null, []));
}
}
};
function getAddModifyColumns(model, actualFields) {
var sql = [];
var self = this;
sql = sql.concat(getColumnsToAdd.call(self, model, actualFields));
var drops = getPropertiesToModify.call(self, model, actualFields);
if (drops.length > 0) {
if (sql.length > 0) {
sql = sql.concat(', ');
}
sql = sql.concat(drops);
}
// sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
return sql;
}
function getDropColumns(model, actualFields) {
var sql = [];
var self = this;
sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
return sql;
}
function getColumnsToAdd(model, actualFields) {
var self = this;
var m = self._models[model];
var propNames = Object.keys(m.properties);
var sql = [];
propNames.forEach(function (propName) {
if (self.id(model, propName)) return;
var found = searchForPropertyInActual.call(self, model, self.column(model, propName), actualFields);
if (!found && propertyHasNotBeenDeleted.call(self, model, propName)) {
sql.push('ADD COLUMN ' + addPropertyToActual.call(self, model, propName));
}
});
if (sql.length > 0) {
sql = [sql.join(', ')];
}
return sql;
}
function addPropertyToActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName)
+ ' ' + self.columnDataType(model, propName) + (propertyCanBeNull.call(self, model, propName) ? "" : " NOT NULL");
return sqlCommand;
}
function searchForPropertyInActual(model, propName, actualFields) {
var self = this;
var found = false;
actualFields.forEach(function (f) {
if (f.column === self.column(model, propName)) {
found = f;
return;
}
});
return found;
}
function getPropertiesToModify(model, actualFields) {
var self = this;
var sql = [];
var m = self._models[model];
var propNames = Object.keys(m.properties);
var found;
propNames.forEach(function (propName) {
if (self.id(model, propName)) {
return;
}
found = searchForPropertyInActual.call(self, model, propName, actualFields);
if (found && propertyHasNotBeenDeleted.call(self, model, propName)) {
if (datatypeChanged(propName, found)) {
sql.push('ALTER COLUMN ' + modifyDatatypeInActual.call(self, model, propName));
}
if (nullabilityChanged(propName, found)) {
sql.push('ALTER COLUMN' + modifyNullabilityInActual.call(self, model, propName));
}
}
});
if (sql.length > 0) {
sql = [sql.join(', ')];
}
return sql;
function datatypeChanged(propName, oldSettings) {
var newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
return oldSettings.type.toUpperCase() !== self.columnDataType(model, propName);
}
function isNullable(p) {
return !(p.required ||
p.id ||
p.allowNull === false ||
p.null === false ||
p.nullable === false);
}
function nullabilityChanged(propName, oldSettings) {
var newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
var changed = false;
if (oldSettings.nullable === 'YES' && !isNullable(newSettings)) {
changed = true;
}
if (oldSettings.nullable === 'NO' && isNullable(newSettings)) {
changed = true;
}
return changed;
}
}
function modifyDatatypeInActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) + ' TYPE ' +
self.columnDataType(model, propName);
return sqlCommand;
}
function modifyNullabilityInActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) + ' ';
if (propertyCanBeNull.call(self, model, propName)) {
sqlCommand = sqlCommand + "DROP ";
} else {
sqlCommand = sqlCommand + "SET ";
}
sqlCommand = sqlCommand + "NOT NULL";
return sqlCommand;
}
function getColumnsToDrop(model, actualFields) {
var self = this;
var sql = [];
actualFields.forEach(function (actualField) {
if (self.idColumn(model) === actualField.column) {
return;
}
if (actualFieldNotPresentInModel(actualField, model)) {
sql.push('DROP COLUMN ' + self.escapeName(actualField.column));
}
});
if (sql.length > 0) {
sql = [sql.join(', ')];
}
return sql;
function actualFieldNotPresentInModel(actualField, model) {
return !(self.propertyName(model, actualField.column));
}
}
function applySqlChanges(model, pendingChanges, cb) {
var self = this;
if (pendingChanges.length) {
var thisQuery = 'ALTER TABLE ' + self.tableEscaped(model);
var ranOnce = false;
pendingChanges.forEach(function (change) {
if (ranOnce) {
thisQuery = thisQuery + ' ';
}
thisQuery = thisQuery + ' ' + change;
ranOnce = true;
});
// thisQuery = thisQuery + ';';
self.query(thisQuery, cb);
}
}
/*!
* Build a list of columns for the given model
* @param {String} model The model name
* @returns {String}
*/
Crate.prototype.propertiesSQL = function (model) {
var self = this;
var sql = [];
var pks = this.idNames(model).map(function (i) {
return self.columnEscaped(model, i);
});
Object.keys(this._models[model].properties).forEach(function (prop) {
var colName = self.columnEscaped(model, prop);
sql.push(colName + ' ' + self.propertySettingsSQL(model, prop));
});
if (pks.length > 0) {
sql.push('PRIMARY KEY(' + pks.join(',') + ')');
}
return sql.join(',\n ');
};
/*!
* Build settings for the model property
* @param {String} model The model name
* @param {String} propName The property name
* @returns {*|string}
*/
Crate.prototype.propertySettingsSQL = function (model, propName) {
var self = this;
if (this.id(model, propName) && this._models[model].properties[propName].generated) {
return 'SERIAL';
}
var result = self.columnDataType(model, propName);
if (!propertyCanBeNull.call(self, model, propName)) result = result + ' NOT NULL';
result += self.columnDbDefault(model, propName);
return result;
};
/*!
* Drop a table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
Crate.prototype.dropTable = function (model, cb) {
var self = this;
var name = self.tableEscaped(model);
var dropTableFun = function (callback) {
self.query('DROP TABLE IF EXISTS ' + name, function (err, data) {
callback(err, data);
});
};
dropTableFun(cb);
};
/*!
* Create a table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
Crate.prototype.createTable = function (model, cb) {
var self = this;
var name = self.tableEscaped(model);
// Please note IF NOT EXISTS is introduced in crate v9.3
self.query('CREATE SCHEMA ' +
self.escapeName(self.schemaName(model)),
function(err) {
if (err && err.code !== '42P06') {
return cb && cb(err);
}
self.query('CREATE TABLE ' + name + ' (\n ' +
self.propertiesSQL(model) + '\n)', cb);
});
};
/**
* Disconnect from Crate
* @param {Function} [cb] The callback function
*/
Crate.prototype.disconnect = function disconnect(cb) {
if (this.pg) {
if (this.settings.debug) {
this.debug('Disconnecting from ' + this.settings.hostname);
}
var pg = this.pg;
this.pg = null;
pg.end(); // This is sync
}
if (cb) {
process.nextTick(function () {
cb && cb();
});
}
};
Crate.prototype.ping = function(cb) {
this.query('SELECT 1 AS result', [], cb);
}
function propertyCanBeNull(model, propName) {
var p = this._models[model].properties[propName];
if (p.required || p.id) {
return false;
}
return !(p.allowNull === false ||
p['null'] === false || p.nullable === false);
}
function escape(val) {
if (val === undefined || val === null) {
return 'NULL';
}
switch (typeof val) {
case 'boolean':
return (val) ? "true" : "false";
case 'number':
return val + '';
}
if (typeof val === 'object') {
val = (typeof val.toISOString === 'function')
? val.toISOString()
: val.toString();
}
val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function (s) {
switch (s) {
case "\0":
return "\\0";
case "\n":
return "\\n";
case "\r":
return "\\r";
case "\b":
return "\\b";
case "\t":
return "\\t";
case "\x1a":
return "\\Z";
case "\'":
return "''"; // For crate
case "\"":
return s; // For crate
default:
return "\\" + s;
}
});
// return "q'#"+val+"#'";
return "'" + val + "'";
}
/*!
* Get the database-default value for column from given model property
*
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column default value
*/
Crate.prototype.columnDbDefault = function(model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colDefault = columnMetadata && columnMetadata.dbDefault;
return colDefault ? (' DEFAULT ' + columnMetadata.dbDefault): '';
}
/*!
* Find the column type for a given model property
*
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column type
*/
Crate.prototype.columnDataType = function (model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colType = columnMetadata && columnMetadata.dataType;
if (colType) {
colType = colType.toUpperCase();
}
var prop = this._models[model].properties[property];
if (!prop) {
return null;
}
var colLength = columnMetadata && columnMetadata.dataLength || prop.length;
if (colType) {
return colType + (colLength ? '(' + colLength + ')' : '');
}
switch (prop.type.name) {
default:
case 'String':
case 'JSON':
return 'VARCHAR' + (colLength ? '(' + colLength + ')' : '(1024)');
case 'Text':
return 'VARCHAR' + (colLength ? '(' + colLength + ')' : '(1024)');
case 'Number':
return 'INTEGER';
case 'Date':
return 'TIMESTAMP WITH TIME ZONE';
case 'Timestamp':
return 'TIMESTAMP WITH TIME ZONE';
case 'GeoPoint':
case 'Point':
return 'POINT';
case 'Boolean':
return 'BOOLEAN'; // Crate doesn't have built-in boolean
}
};
/*!
* Map crate data types to json types
* @param {String} crateType
* @param {Number} dataLength
* @returns {String}
*/
function crateDataTypeToJSONType(crateType, dataLength) {
var type = crateType.toUpperCase();
switch (type) {
case 'BOOLEAN':
return 'Boolean';
/*
- character varying(n), varchar(n) variable-length with limit
- character(n), char(n) fixed-length, blank padded
- text variable unlimited length
*/
case 'VARCHAR':
case 'CHARACTER VARYING':
case 'CHARACTER':
case 'CHAR':
case 'TEXT':
return 'String';
case 'BYTEA':
return 'Binary';
/*
- smallint 2 bytes small-range integer -32768 to +32767
- integer 4 bytes typical choice for integer -2147483648 to +2147483647
- bigint 8 bytes large-range integer -9223372036854775808 to 9223372036854775807
- decimal variable user-specified precision, exact no limit
- numeric variable user-specified precision, exact no limit
- real 4 bytes variable-precision, inexact 6 decimal digits precision
- double precision 8 bytes variable-precision, inexact 15 decimal digits precision
- serial 4 bytes autoincrementing integer 1 to 2147483647
- bigserial 8 bytes large autoincrementing integer 1 to 9223372036854775807
*/
case 'SMALLINT':
case 'INTEGER':
case 'BIGINT':
case 'DECIMAL':
case 'NUMERIC':
case 'REAL':
case 'DOUBLE':
case 'SERIAL':
case 'BIGSERIAL':
return 'Number';
/*
- timestamp [ (p) ] [ without time zone ] 8 bytes both date and time (no time zone) 4713 BC 294276 AD 1 microsecond / 14 digits
- timestamp [ (p) ] with time zone 8 bytes both date and time, with time zone 4713 BC 294276 AD 1 microsecond / 14 digits
- date 4 bytes date (no time of day) 4713 BC 5874897 AD 1 day
- time [ (p) ] [ without time zone ] 8 bytes time of day (no date) 00:00:00 24:00:00 1 microsecond / 14 digits
- time [ (p) ] with time zone 12 bytes times of day only, with time zone 00:00:00+1459 24:00:00-1459 1 microsecond / 14 digits
- interval [ fields ] [ (p) ] 12 bytes time interval -178000000 years 178000000 years 1 microsecond / 14 digits
*/
case 'DATE':
case 'TIMESTAMP':
case 'TIME':
case 'TIME WITH TIME ZONE':
case 'TIME WITHOUT TIME ZONE':
case 'TIMESTAMP WITH TIME ZONE':
case 'TIMESTAMP WITHOUT TIME ZONE':
return 'Date';
case 'POINT':
return 'GeoPoint';
default:
return 'String';
}
}
function mapCrateDatatypes(typeName) {
return typeName;
}
function propertyHasNotBeenDeleted(model, propName) {
return !!this._models[model].properties[propName];
}
function generateQueryParams(data, props) {
var queryParams = [];
function pushToQueryParams(key) {
queryParams.push(data[key] !== undefined ? data[key] : null);
}
props.nonIdsInData.forEach(pushToQueryParams);
props.idsInData.forEach(pushToQueryParams);
return queryParams;
}
require('./discovery')(Crate);