From a97d7540446a90ff9ab45a435805ae56564336ef Mon Sep 17 00:00:00 2001 From: Andrew Jo Date: Fri, 15 Nov 2013 21:15:43 +0900 Subject: [PATCH 01/81] Updated title and main description --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c6e466..6a2beab 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ ![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) -# BoilerplateAdapter +# SQLite3 Sails/Waterline Adapter -This template exists to make it easier for you to get started writing an official adapter for Sails.js. +A [Waterline](https://github.com/balderdashy/waterline) adapter for SQLite3. May be used in a [Sails](https://github.com/balderdashy/sails) app or anything using Waterline for the ORM. ## Getting started From 7a94377da3e703a9c3b867b30e9bf005cf9bec72 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 15 Nov 2013 22:36:16 +0900 Subject: [PATCH 02/81] Updated package name and authorship information --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a9f0670..802ebc1 100755 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sails-adapter-boilerplate", + "name": "sails-sqlite3", "version": "0.0.1", "description": "Boilerplate adapter for Sails.js", "main": "BoilerplateAdapter.js", @@ -17,7 +17,7 @@ "sailsjs", "sails.js" ], - "author": "Your name here", + "author": "Andrew Jo ", "license": "MIT", "readmeFilename": "README.md", "dependencies": { From 9a6497a18aa08316f2fa9607b24e83ca389b4578 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 15 Nov 2013 22:40:20 +0900 Subject: [PATCH 03/81] Added sqlite3 as dependency --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 802ebc1..aaff077 100755 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "license": "MIT", "readmeFilename": "README.md", "dependencies": { - "async": "0.1.22" + "async": "0.1.22", + "sqlite3": "2.1.19" } } From c23a09b7f150e7f0d8854cf58857da4e08d89797 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 19 Nov 2013 17:26:16 +0900 Subject: [PATCH 04/81] Added underscore.js --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index aaff077..baf2ed9 100755 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "readmeFilename": "README.md", "dependencies": { "async": "0.1.22", - "sqlite3": "2.1.19" + "sqlite3": "2.1.19", + "underscore": "1.5.2" } } From 2f3493bb29518e53f188e2a76fc4c878dfc5555c Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 19 Nov 2013 17:40:22 +0900 Subject: [PATCH 05/81] Added disclaimer --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6a2beab..5c7e771 100755 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ A [Waterline](https://github.com/balderdashy/waterline) adapter for SQLite3. May be used in a [Sails](https://github.com/balderdashy/sails) app or anything using Waterline for the ORM. +## Disclaimer +SQLite3 adapter is in a very early development stage and not ready for primetime. + ## Getting started It's usually pretty easy to add your own adapters for integrating with proprietary systems or existing open APIs. For most things, it's as easy as `require('some-module')` and mapping the appropriate methods to match waterline semantics. To get started: From 5bf928345ba0d8ec47ef0293e6e3f9f1371acab1 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 22 Nov 2013 16:43:29 +0900 Subject: [PATCH 06/81] Initial commit --- lib/adapter.js | 594 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 lib/adapter.js diff --git a/lib/adapter.js b/lib/adapter.js new file mode 100644 index 0000000..8351ab2 --- /dev/null +++ b/lib/adapter.js @@ -0,0 +1,594 @@ +/*--------------------------------------------------------------- + :: sails-sqlite3 + -> adapter + + The code here is loosely based on sails-postgres adapter. +---------------------------------------------------------------*/ + +// Dependencies +var sqlite3 = require('sqlite3'), + async = require('async'), + fs = require('fs'), + _ = require("underscore"), + utils = require("./utils"); + +module.exports = (function() { + + var dbs = {}; + + // Determines whether the database file already exists + var exists = false; + + var adapter = { + identity: 'sails-sqlite3', + + // Set to true if this adapter supports (or requires) things like data types, validations, keys, etc. + // If true, the schema for models using this adapter will be automatically synced when the server starts. + // Not terribly relevant if not using a non-SQL / non-schema-ed data store + syncable: false, + + // Default configuration for collections + // (same effect as if these properties were included at the top level of the model definitions) + defaults: { + + // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an + // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, + // their contents are lost. + filename: "", + + mode: sqlite3.OPEN_READWRITE | OPEN_CREATE, + verbose: false + }, + + + // This method runs when a model is initially registered at server start time + registerCollection: function(collection, cb) { + var def = _.clone(collection); + var key = def.identity; + + if (dbs[key]) return cb(); + dbs[key.toString()] = def; + + // Always call describe + this.describe(key, function(err, schema) { + if (err) return cb(err); + cb(null, schema); + }); + }, + + + // The following methods are optional + //////////////////////////////////////////////////////////// + + // Optional hook fired when a model is unregistered, typically at server halt + // useful for tearing down remaining open connections, etc. + teardown: function(cb) { + cb(); + }, + + + // Raw query interface + query: function(table, query, data, cb) { + if (_.isFunction(data)) { + cb = data; + data = null; + } + + spawnConnection(function __QUERY__(client, cb) { + if (data) client.all(query, data, cb); + client.all(query, cb); + }, dbs[table].config, cb); + }, + + // REQUIRED method if integrating with a schemaful database + define: function(table, definition, cb) { + + var describe = function(err, result) { + if (err) return cb(err); + + adapter.describe(table.replace(/["']/g, ""), cb); + }; + + spawnConnection(function __DEFINE__(client, cb) { + + // Escape table name + table = utils.escapeTable(table); + + // Iterate through each attribute, building a query string + var _schema = utils.buildSchema(definition); + + // Check for any index attributes + var indices = utils.buildIndexes(definition); + + // Build query + var query = 'CREATE TABLE ' + table + ' (' + _schema + ')'; + + // Run the query + client.run(query, function(err) { + if (err) return cb(err); + + // Build indices + function buildIndex(name, cb) { + + // Strip slashes from tablename, used to namespace index + var cleanTable = table.replace(/['"]/g, ''); + + // Build a query to create a namespaced index tableName_key + var query = 'CREATE INDEX ' + cleanTable + '_' + name + ' on ' + table + ' (' + name + ');'; + + // Run query + client.run(query, function(err) { + if (err) return cb(err); + cb(null, this); + }); + } + + async.eachSeries(indices, buildIndex, cb); + }); + }, dbs[table].config, cb); + }, + + // REQUIRED method if integrating with a schemaful database + describe: function(table, cb) { + var self = this; + + spawnConnection(function __DESCRIBE__(client, cb) { + + // Get a list of all the tables in this database (see http://www.sqlite.org/faq.html#q7) + var query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"; + + // Query to get information about each table (see http://www.sqlite.org/pragma.html#pragma_table_info) + var columnsQuery = "PRAGMA table_info(?)"; + + // Query to get information about indices + var indexListQuery = "PRAGMA index_list(?)"; + var indexInfoQuery = "PRAGMA index_info(?)"; + + client.each(query, function (err, schema) { + schema.indices = []; + schema.columns = []; + + // We want the following queries to run in series + client.serialize(function() { + + // Retrieve indices for this table first + client.each(indexListQuery, schema.name, function(err, index) { + index.columns = []; + // Retrieve detailed information for given index + client.each(indexInfoQuery, index.name, function(err, indexedCol) { + index.columns.push(indexedCol); + }); + + schema.indices.push(index); + }); + + // Then retrieve column information for each table + client.each(columnsQuery, schema.name, function(err, column) { + + // In SQLite3, AUTOINCREMENT only applies to PK columns of INTEGER type + column.autoIncrement = column.type.toLowerCase() == 'int' && column.pk == 1; + + // By default, assume column is not indexed until we find that it is + column.indexed = false; + + // Search for indexed columns + schema.indices.forEach(function(index) { + if (column.indexed) return; + else { + // Loop through each column in the index and check for a match + index.columns.forEach(function(indexedCol) { + if (indexedCol.name == column.name) { + column.indexed = true; + return; + } + }); + } + }); + + schema.columns.push(column); + }, function(err, resultCount) { + // This callback function is fired when all the columns have been iterated by the .each() function + if (err) { + console.error("Error while retrieving column information."); + console.error(err); + return cb(err); + } + + var normalizedSchema = utils.normalizeSchema(schema); + + // Set internal schema mapping + dbs[table] = normalizedSchema; + + // Fire the callback with the normalized schema + cb(null, normalizedSchema); + }); + }); + }); + }, dbs[table].config, cb); + }, + + // REQUIRED method if integrating with a schemaful database + drop: function(table, cb) { + spawnConnection(function __DROP__(client, cb) { + + // Escape table name + table = utils.escapeTable(table); + + // Build query + var query = 'DROP TABLE ' + table + ';'; + + // Run the query + client.run(query, function(err) { + if (err) cb(err); + cb(null, this); + }); + }, dbs[table].config, cb); + }, + + // Optional override of built-in alter logic + // Can be simulated with describe(), define(), and drop(), + // but will probably be made much more efficient by an override here + // alter: function (collectionName, attributes, cb) { + // Modify the schema of a table or collection in the data store + // cb(); + // }, + + + // Add a column to the table + addAttribute: function(table, attrName, attrDef, cb) { + spawnConnection(function __ADD_ATTRIBUTE__(client, cb) { + + // Escape table name + table = utils.escapeTable(table); + + // Set up a schema definition + var attrs = {}; + attrs[attrName] = attrDef; + + var _schema = utils.buildSchema(attrs); + + // Build query + var query = 'ALTER TABLE ' + table + ' ADD COLUMN ' + _schema; + + // Run query + client.run(query, function(err) { + if (err) return cb(err); + cb(null, this); + }); + }, dbs[table].config, cb); + }, + + + // Remove attribute from table + // In SQLite3, this is tricky since there's no support for DROP COLUMN + // in ALTER TABLE. We'll have to rename the old table, create a new table + // with the same name minus the column and copy all the data over. + removeAttribute: function(table, attrName, cb) { + spawnConnection(function __REMOVE_ATTRIBUTE__(client, cb) { + + // Escape table name + table = utils.escapeTable(table); + + // Build query to rename table + var renameQuery = 'ALTER TABLE ' + table + ' RENAME TO ' + table + '_old_'; + + }, dbs[table].config, cb); + }, + + + // REQUIRED method if users expect to call Model.create() or any methods + create: function(table, data, cb) { + spawnConnection(function __CREATE__(client, cb) { + + // Build a query object + var _query = new Query(dbs[table].definition); + + // Escape table name + var table = utils.escapeTable(table); + + // Transform the data object into arrays used in parametrized query + var attributes = util.mapAttributes(data), + columnNames = attributes.keys.join(', '), + paramValues = attributes.params.join(', '); + + // Build query + var insertQuery = 'INSERT INTO ' + table + ' (' + columnNames + ') values (' + paramValues + ')'; + var selectQuery = 'SELECT * FROM ' + table + ' ORDER BY rowid DESC LIMIT 1'; + + // First insert the values + client.run(insertQuery, function(err) { + if (err) return cb(err); + + // Get the last inserted row + client.get(selectQuery, function(err, row) { + if (err) return cb(err); + + var values = _query.cast(row); + + cb(null, values); + }); + }); + }, dbs[table].config, cb); + }, + + // REQUIRED method if users expect to call Model.find(), Model.findAll() or related methods + // You're actually supporting find(), findAll(), and other methods here + // but the core will take care of supporting all the different usages. + // (e.g. if this is a find(), not a findAll(), it will only send back a single model) + find: function(table, options, cb) { + spawnConnection(function __FIND__(client, cb) { + + // Check if this is an aggregate query and that there's something to return + if (options.groupBy || options.sum || options.average || options.min || options.max) { + if (!options.sum && !options.average && !options.min && !options.max) { + return cb(new Error('Cannot perform groupBy without a calculation')); + } + } + + // Build a query object + var _query = new Query(dbs[table].definition); + + // Escape table name + table = utils.escapeTable(table); + + // Build query + var _schema = dbs[table.replace(/["']/g, "")].schema; + var query = new Query(_schema).find(table, options); + + // Cast special values + var values = []; + + // Run query + client.each(query.query, query.values, function(err, row) { + if (err) return cb(err); + + values.push(_query.cast(row)); + }, function(err, resultCount) { + cb(null, values); + }); + }, dbs[table].config, cb); + }, + + // REQUIRED method if users expect to call Model.update() + update: function(table, options, data, cb) { + spawnConnection(function __UPDATE__(client, cb) { + + // Build a query object + var _query = new Query(dbs[table].definition); + + // Escape table name + table = utils.escapeTable(table); + + // Build query + var _schema = dbs[table.replace(/["']/g, "")].schema; + var updateQuery = new Query(_schema).update(table, options, data); + var selectQuery = new Query(_schema).find(table, options); + var rowIds = []; + + client.serialize(function() { + // Keep track of the row IDs of the rows that will be updated + client.each(selectQuery.query, selectQuery.values, function(err, row) { + if (err) return cb(err); + rowIds.push(row.rowid); + }); + + // Run query + client.run(updateQuery.query, updateQuery.values, function(err) { + if (err) return cb(err); + + // Build a query to return updated rows + if (this.changes > 0) { + + // Build criteria + var criteria = this.changes == 1 ? { where: {}, limit: 1 } : { where: {} }; + criteria.where.in = rowIds; + + // Return the updated items up the callback chain + adapter.find(table, criteria, function(err, models) { + if (err) return cb(err); + + var values = []; + + models.forEach(function(item) { + values.push(_query.cast(item)); + }); + + cb(null, values); + }); + } else { + console.error('WARNING: No rows updated.'); + cb(null); + } + }); + }); + }, dbs[table].config, cb); + }, + + // REQUIRED method if users expect to call Model.destroy() + destroy: function(table, options, cb) { + spawnConnection(function __DELETE__(client, cb) { + + // Build a query object + var _query = new Query(dbs[table].definition); + + // Escape table name + table = utils.escapeTable(table); + + // Build query + var _schema = dbs[table.replace(/["']/g, "")].schema; + var deleteQuery = new Query(_schema).destroy(table, options); + + // Run query + adapter.find(table, options, function(err, models) { + if (err) return cb(err); + + var values = []; + + models.forEach(function(model) { + values.push(_query.cast(model)); + }); + + client.run(deleteQuery.query, deleteQuery.values, function(err) { + if (err) return cb(err); + cb(null, values); + }); + }); + }, dbs[table].config, cb); + }, + + + + // REQUIRED method if users expect to call Model.stream() + stream: function(table, options, stream) { + // options is a standard criteria/options object (like in find) + + // stream.write() and stream.end() should be called. + // for an example, check out: + // https://github.com/balderdashy/sails-dirty/blob/master/DirtyAdapter.js#L247 + if (dbs[table].config.verbose) sqlite3 = sqlite3.verbose(); + + var client = new sqlite3.Database(dbs[table].config.filename, dbs[table].config.mode, function(err) { + if (err) return cb(err); + + // Escape table name + table = utils.escapeTable(table); + + // Build query + var query = new Query(dbs[table.replace(/["'/g, "")].schema).find(table, options); + + // Run the query + client.each(query.query, query.values, function(err, row) { + if (err) { + stream.end(); + client.close(); + } + + stream.write(row); + }, function(err, resultCount) { + stream.end(); + client.close(); + }); + }); + } + + + + /* + ********************************************** + * Optional overrides + ********************************************** + + // Optional override of built-in batch create logic for increased efficiency + // otherwise, uses create() + createEach: function (collectionName, cb) { cb(); }, + + // Optional override of built-in findOrCreate logic for increased efficiency + // otherwise, uses find() and create() + findOrCreate: function (collectionName, cb) { cb(); }, + + // Optional override of built-in batch findOrCreate logic for increased efficiency + // otherwise, uses findOrCreate() + findOrCreateEach: function (collectionName, cb) { cb(); } + */ + + + /* + ********************************************** + * Custom methods + ********************************************** + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // + // > NOTE: There are a few gotchas here you should be aware of. + // + // + The collectionName argument is always prepended as the first argument. + // This is so you can know which model is requesting the adapter. + // + // + All adapter functions are asynchronous, even the completely custom ones, + // and they must always include a callback as the final argument. + // The first argument of callbacks is always an error object. + // For some core methods, Sails.js will add support for .done()/promise usage. + // + // + + // + //////////////////////////////////////////////////////////////////////////////////////////////////// + + + // Any other methods you include will be available on your models + foo: function (collectionName, cb) { + cb(null,"ok"); + }, + bar: function (collectionName, baz, watson, cb) { + cb("Failure!"); + } + + + // Example success usage: + + Model.foo(function (err, result) { + if (err) console.error(err); + else console.log(result); + + // outputs: ok + }) + + // Example error usage: + + Model.bar(235, {test: 'yes'}, function (err, result){ + if (err) console.error(err); + else console.log(result); + + // outputs: Failure! + }) + + */ + }; + + ////////////// ////////////////////////////////////////// + ////////////// Private Methods ////////////////////////////////////////// + ////////////// ////////////////////////////////////////// + function spawnConnection(logic, config, cb) { + // Check if we want to run in verbose mode + // Note that once you go verbose, you can't go back (see https://github.com/mapbox/node-sqlite3/wiki/API) + if (config.verbose) sqlite3 = sqlite3.verbose(); + + // Make note whether the database already exists + exists = fs.existsSync(config.filename); + + // Create a new handle to our database + var client = new sqlite3.Database(config.filename, config.mode, function(err) { + after(err, client); + }); + + function after(err, client) { + if (err) { + console.error("Error creating/opening SQLite3 database."); + console.error(err); + + // Close the db instance on error + if (client) client.close(); + + return cb(err); + } + + // Run the logic + logic(client, function(err, result) { + if (err) { + console.error("Error while running SQLite3 logic."); + console.error(err); + + client.close(); + + return cb(err); + } + + // Close db instance after it's done running + client.close(); + + return cb(err, result); + }); + } + + return cb(); + } +})(); \ No newline at end of file From bc267448a0c6791f47c9250e0c8a8dcda71845b2 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 22 Nov 2013 17:08:56 +0900 Subject: [PATCH 07/81] Added reference to Query objects --- lib/adapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/adapter.js b/lib/adapter.js index 8351ab2..f105a53 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -10,7 +10,8 @@ var sqlite3 = require('sqlite3'), async = require('async'), fs = require('fs'), _ = require("underscore"), - utils = require("./utils"); + Query = require('./query'), + utils = require('./utils'); module.exports = (function() { From 74cee9106c23957eea6ff14319fd78b99ac89b13 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 22 Nov 2013 21:27:33 +0900 Subject: [PATCH 08/81] Initial commit --- lib/utils.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 lib/utils.js diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..413d673 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,86 @@ +var _ = require("underscore"); + +var utils = module.exports = {}; + +/** + * Build a schema from an attributes object + */ + +utils.buildSchema = function(obj) { + var schema = ""; + + // Iterate through the Object Keys and build a string + Object.keys(obj).forEach(function(key) { + var attr = {}; + + // Normalize Simple Key/Value attribute + // ex: name: 'string' + if(typeof obj[key] === 'string') { + attr.type = obj[key]; + } + + // Set the attribute values to the object key + else { + attr = obj[key]; + } + + // Override Type for autoIncrement + if(attr.autoIncrement) attr.type = 'serial'; + + var str = [ + '"' + key + '"', // attribute name + utils.sqlTypeCast(attr.type), // attribute type + attr.primaryKey ? 'PRIMARY KEY' : '', // primary key + attr.unique ? 'UNIQUE' : '' // unique constraint + ].join(' ').trim(); + + schema += str + ', '; + }); + + // Remove trailing seperator/trim + return schema.slice(0, -2); +}; + +/** + * Build an Index array from any attributes that + * have an index key set. + */ + +utils.buildIndexes = function(obj) { + var indexes = []; + + // Iterate through the Object keys and pull out any index attributes + Object.keys(obj).forEach(function(key) { + if (obj[key].hasOwnProperty('index')) indexes.push(key); + }); + + return indexes; +}; + + +/** + * Escape Table Name + * + * Wraps a table name in quotes to allow reserved + * words as table names such as user. + */ + +utils.escapeTable = function(table) { + return '"' + table + '"'; +}; + +utils.normalizeSchema = function(schema) { + var normalized = {}; + var clone = _.clone(schema); + + clone.forEach(function(column) { + + // Set type + normalized[column.Column] = { type: column.Type }; + + // Check for primary key + if (column.Constraint && column.C === 'p') { + normalized[column.Column].primaryKey = true; + } + }); +}; \ No newline at end of file From 964fb11e8920c4d966d7e617dc4efb34b5a49145 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Fri, 22 Nov 2013 22:05:45 +0900 Subject: [PATCH 09/81] Added buildSelectStatement function --- lib/utils.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index 413d673..a19d0f1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -41,6 +41,89 @@ utils.buildSchema = function(obj) { return schema.slice(0, -2); }; + +/** + * Builds a Select statement determining if Aggeragate options are needed. + */ + +utils.buildSelectStatement = function(criteria, table) { + var query = ''; + + if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || criteria.max) { + query = 'SELECT '; + + // Append groupBy columns to select statement + if(criteria.groupBy) { + if(criteria.groupBy instanceof Array) { + criteria.groupBy.forEach(function(opt){ + query += opt + ', '; + }); + + } else { + query += criteria.groupBy + ', '; + } + } + + // Handle SUM + if (criteria.sum) { + if(criteria.sum instanceof Array) { + criteria.sum.forEach(function(opt){ + query += 'CAST(SUM(' + opt + ') AS float) AS ' + opt + ', '; + }); + + } else { + query += 'CAST(SUM(' + criteria.sum + ') AS float) AS ' + criteria.sum + ', '; + } + } + + // Handle AVG (casting to float to fix percision with trailing zeros) + if (criteria.average) { + if(criteria.average instanceof Array) { + criteria.average.forEach(function(opt){ + query += 'CAST(AVG(' + opt + ') AS float) AS ' + opt + ', '; + }); + + } else { + query += 'CAST(AVG(' + criteria.average + ') AS float) AS ' + criteria.average + ', '; + } + } + + // Handle MAX + if (criteria.max) { + if(criteria.max instanceof Array) { + criteria.max.forEach(function(opt){ + query += 'MAX(' + opt + ') AS ' + opt + ', '; + }); + + } else { + query += 'MAX(' + criteria.max + ') AS ' + criteria.max + ', '; + } + } + + // Handle MIN + if (criteria.min) { + if(criteria.min instanceof Array) { + criteria.min.forEach(function(opt){ + query += 'MIN(' + opt + ') AS ' + opt + ', '; + }); + + } else { + query += 'MIN(' + criteria.min + ') AS ' + criteria.min + ', '; + } + } + + // trim trailing comma + query = query.slice(0, -2) + ' '; + + // Add FROM clause + return query += 'FROM ' + table + ' '; + } + + // Else select ALL + return 'SELECT rowid, * FROM ' + table + ' '; +}; + + /** * Build an Index array from any attributes that * have an index key set. From 8c04bdc5424e5e6a1998fa29014a5dfeb75ea1de Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 00:59:47 +0900 Subject: [PATCH 10/81] Initial commit --- lib/query.js | 534 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 534 insertions(+) create mode 100644 lib/query.js diff --git a/lib/query.js b/lib/query.js new file mode 100644 index 0000000..8976b5e --- /dev/null +++ b/lib/query.js @@ -0,0 +1,534 @@ +/** + * Dependencies + */ + +var _ = require('underscore'), + utils = require('./utils'); + +/** + * Query Builder for creating parameterized queries for use + * with the SQLite3 adapter + * + * Most of this code was adapted from the Query class of + * Postgres adapter + * + * If you have any questions, contact Andrew Jo + */ + +var Query = function(schema) { + this._values = []; + this._paramCount = 1; + this._query = ''; + + this._schema = _.clone(schema); + + return this; +}; + +/** + * SELECT Statement + */ + +Query.prototype.find = function(table, criteria) { + this._query = utils.buildSelectStatement(criteria, table); + if (criteria) this._build(criteria); + + return { + query: this._query, + values: this._values + }; +}; + +/** + * UPDATE Statement + */ + +Query.prototype.update = function(table, criteria, data) { + this._query = 'UPDATE ' + table + ' '; + + // Transform the Data object into arrays used in a parameterized query + var attributes = utils.mapAttributes(data); + + // Update the paramCount + this._paramCount = attributes.params.length + 1; + + // Build SET string + var str = ''; + for (var i = 0; i < attributes.keys.length; i++) { + str += attributes.keys[i] + ' = ' + attributes.params[i] + ', '; + } + + // Remove trailing comma + str = str.slice(0, -2); + + this._query += 'SET ' + str + ' '; + + // Add data values to this._values + this._values = this._values.concat(attributes.values); + + // Build criteria clause + if (criteria) this._build(criteria); + + return { + query: this._query, + values: this._values + }; +}; + + +/** + * DELETE Statement + */ + +Query.prototype.destroy = function(table, criteria) { + this._query = 'DELETE FROM ' + table + ' '; + if (criteria) this._build(criteria); + + return { + query: this._query, + values: this._values + }; +}; + + +/** + * String Builder + */ + +Query.prototype._build = function(criteria) { + var self = this; + + // Ensure criteria keys are in correct order + var orderedCriteria = {}; + if (criteria.where) orderedCriteria.where = criteria.where; + if (criteria.groupBy) orderedCriteria.groupBy = criteria.groupBy; + if (criteria.sort) orderedCriteria.sort = criteria.sort; + if (criteria.limit) orderedCriteria.limit = criteria.limit; + if (criteria.skip) orderedCriteria.skip = criteria.skip; + + // Loop through criteria parent keys + Object.keys(orderedCriteria).forEach(function(key) { + switch (key.toLowerCase()) { + case 'where': + self.where(criteria[key]); + return; + case 'groupby': + self.group(criteria[key]); + return; + case 'sort': + self.sort(criteria[key]); + return; + case 'limit': + self.limit(criteria[key]); + return; + case 'skip': + self.skip(criteria[key]); + return; + } + }); + + return { + query: this._query, + values: this._values + } +}; + +/** + * Specifiy a `where` condition + * + * `Where` conditions may use key/value model attributes for simple query + * look ups as well as more complex conditions. + * + * The following conditions are supported along with simple criteria: + * + * Conditions: + * [And, Or, Like, Not] + * + * Criteria Operators: + * [<, <=, >, >=, !] + * + * Criteria Helpers: + * [lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, not, like, contains, startsWith, endsWith] + * + * ####Example + * + * where: { + * name: 'foo', + * age: { + * '>': 25 + * }, + * like: { + * name: '%foo%' + * }, + * or: [ + * { like: { foo: '%foo%' } }, + * { like: { bar: '%bar%' } } + * ], + * name: [ 'foo', 'bar;, 'baz' ], + * age: { + * not: 40 + * } + * } + */ + +Query.prototype.where = function(options) { + var self = this, + operators = this.operators(); + + if (!options) return; + + // Begin WHERE query + this._query += 'WHERE '; + + // Process 'where' criteria + Object.keys(options).forEach(function(key) { + + switch (key.toLowerCase()) { + case 'or': + options[key].forEach(function(statement) { + Object.keys(statement).forEach(function(key) { + + switch (key) { + case 'and': + Object.keys(statement[key]).forEach(function(attribute) { + operators.and(attribute, statement[key][attribute], ' OR '); + }); + return; + + case 'like': + Object.keys(statement[key]).forEach(function(attribute) { + operators.like(attribute, key, statement, ' OR '); + }); + return; + + default: + if(typeof statement[key] === 'object') { + Object.keys(statement[key]).forEach(function(attribute) { + operators.and(attribute, statement[key][attribute], ' OR '); + }); + return; + } + + operators.and(key, statement[key], ' OR '); + return; + } + }); + }); + + return; + + case 'like': + Object.keys(options[key]).forEach(function(parent) { + operators.like(parent, key, options); + }); + + return; + + // Key/Value + default: + + // 'IN' + if (options[key] instanceof Array) { + operators.in(key, options[key]); + return; + } + + // 'AND' + operators.and(key, options[key]); + return; + } + }); + + // Remove trailing AND if it exists + if (this._query.slice(-4) === 'AND ') { + this._query = this._query.slice(0, -5); + } + + // Remove trailing OR if it exists + if (this._query.slice(-3) === 'OR ') { + this._query = this._query.slice(0, -4); + } +}; + +/** + * Operator Functions + */ + +Query.prototype.operators = function() { + var self = this; + + var sql = { + and: function(key, options, comparator) { + var caseSensitive = true; + + // Check if key is a string + if (self._schema[key].type === 'text') caseSensitive = false; + + processCriteria.call(self, key, options, '=', caseSensitive); + self._query += (comparator || ' AND '); + }, + + like: function(parent, key, options, comparator) { + var caseSensitive = true; + + // Check if parent is a string + if (self._schema[parent].type === 'text') caseSensitive = false; + + processCriteria.call(self, parent, options[key][parent], 'ILIKE', caseSensitive); + self._query += (comparator || ' AND '); + }, + + in: function(key, options) { + var caseSensitive = true; + + // Check if key is a string + if (self._schema[key].type === 'text') caseSensitive = false; + + // Check case sensitivity to decide if LOWER logic is used + if (!caseSensitive) key = 'LOWER("' + key + '")'; + else key = '"' + key + '"'; // for case sensitive camelCase columns + + // Build IN query + self._query += key + ' IN ('; + + // Append each value to query + options.forEach(function(value) { + self._query += '?, '; + self._paramCount++; + + // If case sensitivity is off, lowercase the value + if (!caseSensitive) value = value.toLowerCase(); + + self._values.push(value); + }); + + // Strip last comma and close criteria + self._query = self._query.slice(0, -2) + ')'; + self._query += ' AND '; + } + }; + + return sql; +}; + +/** + * Process Criteria + * + * Processes a query criteria object + */ + +function processCriteria(parent, value, combinator, caseSensitive) { + var self = this; + + // Complex object attributes + if (typeof value === 'object' && value !== null) { + var keys = Object.keys(value); + + // Escape parent + parent = '"' + parent + '"'; + + for (var i = 0; i < keys.length; i++) { + + // Check if value is a string and if so add LOWER logic + // to work with case insensitive queries + if (!caseSensitive && typeof value[[keys][i]] === 'string') { + parent = 'LOWER(' + parent + ')'; + value[keys][i] = value[keys][i].toLowerCase(); + } + + self._query += parent + ' '; + prepareCriterion.call(self, keys[i], value[keys[i]]); + + if (i+1 < keys.length) self._query += 'AND '; + } + + return; + } + + // Check if value is a string and if so add LOWER logic + // to work with case insensitive queries + if (!caseSensitive && typeof value === 'string') { + + // Escape parent + parent = '"' + parent + '"'; + + // ADD LOWER to parent + parent = 'LOWER(' + parent + ')'; + value = value.toLowerCase(); + } else { + // Escape parent + parent = '"' + parent + '"'; + } + + if (value !== null) { + // Simple Key/Value attributes + this._query += parent + ' ' + combinator + ' ?'; + + this._values.push(value); + this._paramCount++; + } else { + this._query += parent + ' IS NULL'; + } +} + +/** + * Prepare Criterion + * + * Processes comparators in a query. + */ + +function prepareCriterion(key, value) { + var str; + + switch (key) { + case '<': + case 'lessThan': + this._values.push(value); + str = '< ?'; + break; + + case '<=': + case 'lessThanOrEqual': + this._values.push(value); + str = '<= ?'; + break; + + case '>': + case 'greaterThan': + this._values.push(value); + str = '> ?'; + break; + + case '>=': + case 'greaterThanOrEqual': + this._values.push(value); + str = '>= ?'; + break; + + case '!': + case 'not': + if (value === null) { + str = 'IS NOT NULL'; + } else { + this._values.push(value); + str = '<> ?'; + } + break; + + case 'like': + this._values.push(value); + str = 'ILIKE ?'; + break; + + case 'contains': + this._values.push('%' + value + '%'); + str = 'ILIKE ?'; + break; + + case 'startsWith': + this._values.push(value + '%'); + str = 'ILIKE ?'; + break; + + case 'endsWith': + this._values.push('%' + value); + str = 'ILIKE ?'; + break; + + default: + throw new Error('Unknown comparator: ' + key); + } + + // Bump paramCount + this._paramCount++; + + // Add str to query + this._query += str; +} + +/** + * Specify a `limit` condition + */ + +Query.prototype.limit = function(options) { + this._query += ' LIMIT ' + options; +}; + +/** + * Specify a `skip` condition + */ + +Query.prototype.skip = function(options) { + this._query += ' OFFSET ' + options; +}; + +/** + * Specify a `sort` condition + */ + +Query.prototype.sort = function(options) { + var self = this; + + this._query += ' ORDER BY '; + + Object.keys(options).forEach(function(key) { + var direction = options[key] === 1 ? 'ASC' : 'DESC'; + self._query += '"' + key + '" ' + direction + ', '; + }); + + // Remove trailing comma + this._query = this._query.slice(0, -2); +}; + +/** + * Specify a `group by` condition + */ + +Query.prototype.group = function(options) { + var self = this; + + this._query += ' GROUP BY '; + + // Normalize to array + if(!Array.isArray(options)) options = [options]; + + options.forEach(function(key) { + self._query += key + ', '; + }); + + // Remove trailing comma + this._query = this._query.slice(0, -2); +}; + +/** + * Cast special values to proper types. + * + * Ex: Array is stored as "[0,1,2,3]" and should be cast to proper + * array for return values. + */ + +Query.prototype.cast = function(values) { + var self = this, + _values = _.clone(values); + + Object.keys(values).forEach(function(key) { + + // Lookup schema type + var type = self._schema[key].type; + if(!type) return; + + // Attempt to parse Array + if(type === 'array') { + try { + _values[key] = JSON.parse(values[key]); + } catch(e) { + return; + } + } + + }); + + return _values; +}; + +module.exports = Query; \ No newline at end of file From 3e3746a76a1ae6813fd58e6b308b085c0ae58105 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 14:03:36 +0900 Subject: [PATCH 11/81] Initial commit --- test/integration/runner.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/integration/runner.js diff --git a/test/integration/runner.js b/test/integration/runner.js new file mode 100644 index 0000000..ac4e135 --- /dev/null +++ b/test/integration/runner.js @@ -0,0 +1,19 @@ +var tests = require('waterline-adapter-tests'), + adapter = require('../../lib/adapter'), + mocha = require('mocha'); + +/** + * SQLite3 configuration + */ + +var config = { + filename: ":memory:", + mode: sqlite3.OPEN_READWRITE | OPEN_CREATE, + verbose: true +}; + +/** + * Run Tests + */ + +var suite = new tests({ adapter: adapter, config: config }); \ No newline at end of file From 1da61e75a79d297d1223c23edfd25bd6890a7d78 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 14:04:18 +0900 Subject: [PATCH 12/81] Added unit test packages --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index baf2ed9..59d00c5 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sails-sqlite3", "version": "0.0.1", "description": "Boilerplate adapter for Sails.js", - "main": "BoilerplateAdapter.js", + "main": "lib/adapter", "scripts": { "test": "echo \"Adapter should be tested using Sails.js core.\" && exit 1" }, @@ -24,5 +24,13 @@ "async": "0.1.22", "sqlite3": "2.1.19", "underscore": "1.5.2" + }, + "devDependencies": { + "mocha": "*", + "should": "*", + "waterline-adapter-tests": "~0.9.4" + }, + "scripts": { + "test": "make test" } } From dc228943211d3b43429261a8448f1c702f0889b9 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 16:45:42 +0900 Subject: [PATCH 13/81] Fixed namespace typo --- lib/adapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/adapter.js b/lib/adapter.js index f105a53..437da3d 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -37,7 +37,7 @@ module.exports = (function() { // their contents are lost. filename: "", - mode: sqlite3.OPEN_READWRITE | OPEN_CREATE, + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, verbose: false }, From 2bb54efa8ff8dd7820ac0ff393e0f744d7094e41 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 16:46:00 +0900 Subject: [PATCH 14/81] Initial commit --- test/unit/support/bootstrap.js | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/unit/support/bootstrap.js diff --git a/test/unit/support/bootstrap.js b/test/unit/support/bootstrap.js new file mode 100644 index 0000000..078371c --- /dev/null +++ b/test/unit/support/bootstrap.js @@ -0,0 +1,59 @@ +var sqlite3 = require('sqlite3'), + adapter = require('../../lib/adapter'); + +var Support = module.exports = {}; + +Support.Config = { + filename: 'sailssqlite.db', + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, + verbose: true +}; + +Support.Definition = { + field_1: { type: 'string' }, + field_2: { type: 'string' }, + id: { + type: 'integer', + autoIncrement: true, + defaultsTo: 'AUTO_INCREMENT', + primaryKey: true + } +}; + +Support.Collection = function(name) { + return { + identity: name, + config: Support.Config, + definition: Support.Definition + }; +}; + +// Register and define a collection +Support.Setup = function(tableName, cb) { + adapter.registerCollection(Support.Collection(tableName), function(err) { + if (err) return cb(err); + adapter.define(tableName, Support.Definition, cb); + }); +}; + +// Remove a table +Support.Teardown = function(tableName, cb) { + var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { + dropTable(tableName, client, function(err) { + if (err) { + done(); + return cb(err); + } + + done(); + return cb(); + }); + }); +}; + +function dropTable(table, client, cb) { + table = '"' + table + '"'; + + var query = "DROP TABLE " + table; + client.run(query, cb); +} \ No newline at end of file From 2ad9477c71c691da696f10ddb9d4872db54c5137 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sat, 23 Nov 2013 16:46:13 +0900 Subject: [PATCH 15/81] Removed index.js --- index.js | 223 ------------------------------------------------------- 1 file changed, 223 deletions(-) delete mode 100644 index.js diff --git a/index.js b/index.js deleted file mode 100644 index 32ed3d4..0000000 --- a/index.js +++ /dev/null @@ -1,223 +0,0 @@ -/*--------------------------------------------------------------- - :: sails-boilerplate - -> adapter ----------------------------------------------------------------*/ - -var async = require('async'); - -var adapter = module.exports = { - - // Set to true if this adapter supports (or requires) things like data types, validations, keys, etc. - // If true, the schema for models using this adapter will be automatically synced when the server starts. - // Not terribly relevant if not using a non-SQL / non-schema-ed data store - syncable: false, - - // Including a commitLog config enables transactions in this adapter - // Please note that these are not ACID-compliant transactions: - // They guarantee *ISOLATION*, and use a configurable persistent store, so they are *DURABLE* in the face of server crashes. - // However there is no scheduled task that rebuild state from a mid-step commit log at server start, so they're not CONSISTENT yet. - // and there is still lots of work to do as far as making them ATOMIC (they're not undoable right now) - // - // However, for the immediate future, they do a great job of preventing race conditions, and are - // better than a naive solution. They add the most value in findOrCreate() and createEach(). - // - // commitLog: { - // identity: '__default_mongo_transaction', - // adapter: 'sails-mongo' - // }, - - // Default configuration for collections - // (same effect as if these properties were included at the top level of the model definitions) - defaults: { - - // For example: - // port: 3306, - // host: 'localhost' - - // If setting syncable, you should consider the migrate option, - // which allows you to set how the sync will be performed. - // It can be overridden globally in an app (config/adapters.js) and on a per-model basis. - // - // drop => Drop schema and data, then recreate it - // alter => Drop/add columns as necessary, but try - // safe => Don't change anything (good for production DBs) - migrate: 'alter' - }, - - // This method runs when a model is initially registered at server start time - registerCollection: function(collection, cb) { - - cb(); - }, - - - // The following methods are optional - //////////////////////////////////////////////////////////// - - // Optional hook fired when a model is unregistered, typically at server halt - // useful for tearing down remaining open connections, etc. - teardown: function(cb) { - cb(); - }, - - - // REQUIRED method if integrating with a schemaful database - define: function(collectionName, definition, cb) { - - // Define a new "table" or "collection" schema in the data store - cb(); - }, - // REQUIRED method if integrating with a schemaful database - describe: function(collectionName, cb) { - - // Respond with the schema (attributes) for a collection or table in the data store - var attributes = {}; - cb(null, attributes); - }, - // REQUIRED method if integrating with a schemaful database - drop: function(collectionName, cb) { - // Drop a "table" or "collection" schema from the data store - cb(); - }, - - // Optional override of built-in alter logic - // Can be simulated with describe(), define(), and drop(), - // but will probably be made much more efficient by an override here - // alter: function (collectionName, attributes, cb) { - // Modify the schema of a table or collection in the data store - // cb(); - // }, - - - // REQUIRED method if users expect to call Model.create() or any methods - create: function(collectionName, values, cb) { - // Create a single new model specified by values - - // Respond with error or newly created model instance - cb(null, values); - }, - - // REQUIRED method if users expect to call Model.find(), Model.findAll() or related methods - // You're actually supporting find(), findAll(), and other methods here - // but the core will take care of supporting all the different usages. - // (e.g. if this is a find(), not a findAll(), it will only send back a single model) - find: function(collectionName, options, cb) { - - // ** Filter by criteria in options to generate result set - - // Respond with an error or a *list* of models in result set - cb(null, []); - }, - - // REQUIRED method if users expect to call Model.update() - update: function(collectionName, options, values, cb) { - - // ** Filter by criteria in options to generate result set - - // Then update all model(s) in the result set - - // Respond with error or a *list* of models that were updated - cb(); - }, - - // REQUIRED method if users expect to call Model.destroy() - destroy: function(collectionName, options, cb) { - - // ** Filter by criteria in options to generate result set - - // Destroy all model(s) in the result set - - // Return an error or nothing at all - cb(); - }, - - - - // REQUIRED method if users expect to call Model.stream() - stream: function(collectionName, options, stream) { - // options is a standard criteria/options object (like in find) - - // stream.write() and stream.end() should be called. - // for an example, check out: - // https://github.com/balderdashy/sails-dirty/blob/master/DirtyAdapter.js#L247 - - } - - - - /* - ********************************************** - * Optional overrides - ********************************************** - - // Optional override of built-in batch create logic for increased efficiency - // otherwise, uses create() - createEach: function (collectionName, cb) { cb(); }, - - // Optional override of built-in findOrCreate logic for increased efficiency - // otherwise, uses find() and create() - findOrCreate: function (collectionName, cb) { cb(); }, - - // Optional override of built-in batch findOrCreate logic for increased efficiency - // otherwise, uses findOrCreate() - findOrCreateEach: function (collectionName, cb) { cb(); } - */ - - - /* - ********************************************** - * Custom methods - ********************************************** - - //////////////////////////////////////////////////////////////////////////////////////////////////// - // - // > NOTE: There are a few gotchas here you should be aware of. - // - // + The collectionName argument is always prepended as the first argument. - // This is so you can know which model is requesting the adapter. - // - // + All adapter functions are asynchronous, even the completely custom ones, - // and they must always include a callback as the final argument. - // The first argument of callbacks is always an error object. - // For some core methods, Sails.js will add support for .done()/promise usage. - // - // + - // - //////////////////////////////////////////////////////////////////////////////////////////////////// - - - // Any other methods you include will be available on your models - foo: function (collectionName, cb) { - cb(null,"ok"); - }, - bar: function (collectionName, baz, watson, cb) { - cb("Failure!"); - } - - - // Example success usage: - - Model.foo(function (err, result) { - if (err) console.error(err); - else console.log(result); - - // outputs: ok - }) - - // Example error usage: - - Model.bar(235, {test: 'yes'}, function (err, result){ - if (err) console.error(err); - else console.log(result); - - // outputs: Failure! - }) - - */ - - -}; - -////////////// ////////////////////////////////////////// -////////////// Private Methods ////////////////////////////////////////// -////////////// ////////////////////////////////////////// \ No newline at end of file From 72e0b0c28ff6b428a576eadf212a1de9c722eceb Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sun, 24 Nov 2013 20:21:12 +0900 Subject: [PATCH 16/81] Fixed a typo in the regular expression and added a return statement --- lib/adapter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/adapter.js b/lib/adapter.js index 437da3d..3c9734e 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -455,7 +455,7 @@ module.exports = (function() { table = utils.escapeTable(table); // Build query - var query = new Query(dbs[table.replace(/["'/g, "")].schema).find(table, options); + var query = new Query(dbs[table.replace(/["']/g, "")].schema).find(table, options); // Run the query client.each(query.query, query.values, function(err, row) { @@ -592,4 +592,6 @@ module.exports = (function() { return cb(); } + + return adapter; })(); \ No newline at end of file From 61b48780c2a9bde513405fea52985337d5f2a733 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Sun, 24 Nov 2013 21:28:21 +0900 Subject: [PATCH 17/81] Prevent raising errors for dropping tables that don't exist --- lib/adapter.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index 3c9734e..f09bd8d 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -216,11 +216,10 @@ module.exports = (function() { table = utils.escapeTable(table); // Build query - var query = 'DROP TABLE ' + table + ';'; + var query = 'DROP TABLE ' + table; // Run the query client.run(query, function(err) { - if (err) cb(err); cb(null, this); }); }, dbs[table].config, cb); @@ -574,23 +573,13 @@ module.exports = (function() { // Run the logic logic(client, function(err, result) { - if (err) { - console.error("Error while running SQLite3 logic."); - console.error(err); - - client.close(); - - return cb(err); - } - + // Close db instance after it's done running client.close(); return cb(err, result); }); } - - return cb(); } return adapter; From 4fff0e74d1d5bbcc8f6abf39ae07020ae92d94c0 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Mon, 25 Nov 2013 00:11:10 +0900 Subject: [PATCH 18/81] Switched to named parameters for index list query and fixed callback issue --- lib/adapter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index f09bd8d..578a816 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -132,7 +132,7 @@ module.exports = (function() { // REQUIRED method if integrating with a schemaful database describe: function(table, cb) { var self = this; - + spawnConnection(function __DESCRIBE__(client, cb) { // Get a list of all the tables in this database (see http://www.sqlite.org/faq.html#q7) @@ -142,7 +142,7 @@ module.exports = (function() { var columnsQuery = "PRAGMA table_info(?)"; // Query to get information about indices - var indexListQuery = "PRAGMA index_list(?)"; + var indexListQuery = "PRAGMA index_list($table)"; var indexInfoQuery = "PRAGMA index_info(?)"; client.each(query, function (err, schema) { @@ -153,7 +153,7 @@ module.exports = (function() { client.serialize(function() { // Retrieve indices for this table first - client.each(indexListQuery, schema.name, function(err, index) { + client.each(indexListQuery, { $table: schema.name }, function(err, index) { index.columns = []; // Retrieve detailed information for given index client.each(indexInfoQuery, index.name, function(err, indexedCol) { @@ -204,6 +204,9 @@ module.exports = (function() { cb(null, normalizedSchema); }); }); + }, function(err, resultCount) { + if (err) return cb(err); + if (resultCount === 0) return cb(); }); }, dbs[table].config, cb); }, @@ -220,7 +223,7 @@ module.exports = (function() { // Run the query client.run(query, function(err) { - cb(null, this); + cb(null, null); }); }, dbs[table].config, cb); }, @@ -559,7 +562,7 @@ module.exports = (function() { var client = new sqlite3.Database(config.filename, config.mode, function(err) { after(err, client); }); - + function after(err, client) { if (err) { console.error("Error creating/opening SQLite3 database."); @@ -573,7 +576,6 @@ module.exports = (function() { // Run the logic logic(client, function(err, result) { - // Close db instance after it's done running client.close(); From 375ec9d912a7cde44a0145274403c16655db7fe4 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Mon, 25 Nov 2013 00:11:34 +0900 Subject: [PATCH 19/81] Switched to named parameters for index list query and fixed callback issue --- lib/adapter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/adapter.js b/lib/adapter.js index 578a816..3e22a75 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -154,6 +154,7 @@ module.exports = (function() { // Retrieve indices for this table first client.each(indexListQuery, { $table: schema.name }, function(err, index) { + console.log(index); index.columns = []; // Retrieve detailed information for given index client.each(indexInfoQuery, index.name, function(err, indexedCol) { From 296a80af9c8b0f31ed689bd1a58c27f2f4818927 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:40:31 +0900 Subject: [PATCH 20/81] Initial commit --- Makefile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..60d2792 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +MOCHA_OPTS= --check-leaks +REPORTER = spec + +test: test-unit test-integration + +test-unit: + @NODE_ENV=test ./node_modules/.bin/mocha \ + --reporter $(REPORTER) \ + $(MOCHA_OPTS) \ + test/unit/** + +test-integration: + @NODE_ENV=test node test/integration/runner.js + +test-load: + @NODE_ENV=test ./node_modules/.bin/mocha \ + --reporter $(REPORTER) \ + $(MOCHA_OPTS) \ + test/load/** \ No newline at end of file From 32a7a23c4c69ccaa1020fca06dfae701bab42574 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:41:53 +0900 Subject: [PATCH 21/81] Initial unit test commit --- test/unit/adapter.create.js | 80 +++++++++++++++++++++++++ test/unit/adapter.define.js | 106 ++++++++++++++++++++++++++++++++++ test/unit/adapter.describe.js | 42 ++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 test/unit/adapter.create.js create mode 100644 test/unit/adapter.define.js create mode 100644 test/unit/adapter.describe.js diff --git a/test/unit/adapter.create.js b/test/unit/adapter.create.js new file mode 100644 index 0000000..f4239aa --- /dev/null +++ b/test/unit/adapter.create.js @@ -0,0 +1,80 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_create', done); + }); + + after(function(done) { + support.Teardown('test_create', done); + }); + + // Attributes for the test table + var attributes = { + field_1: 'foo', + field_2: 'bar' + }; + + /** + * CREATE + * + * Insert a row into a table + */ + + describe('.create()', function() { + + // Insert a record + it('should insert a single record', function(done) { + adapter.create('test_create', attributes, function(err, result) { + + // Check record was actually inserted + support.Client(function(err, client, close) { + client.all('SELECT * FROM "test_create"', function(err, rows) { + + // Test 1 row is returned + rows.length.should.eql(1); + + // close client + client.close(); + + done(); + }); + }); + }); + }); + + // Create Auto-Incremented ID + it('should create an auto-incremented ID field', function(done) { + adapter.create('test_create', attributes, function(err, result) { + + // Should have an ID of 2 + result.id.should.eql(2); + + done(); + }); + }); + + it('should keep case', function(done) { + var attributes = { + field_1: 'Foo', + field_2: 'bAr' + }; + + adapter.create('test_create', attributes, function(err, result) { + + result.field_1.should.eql('Foo'); + result.field_2.should.eql('bAr'); + + done(); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.define.js b/test/unit/adapter.define.js new file mode 100644 index 0000000..92678f9 --- /dev/null +++ b/test/unit/adapter.define.js @@ -0,0 +1,106 @@ +var adapter = require('../../lib/adapter'), + _ = require('underscore'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + after(function(done) { + support.Teardown('test_define', done); + }); + + // Attributes for the test table + var definition = { + id : { + type: 'serial', + autoIncrement: true + }, + name : 'string', + email : 'string', + title : 'string', + phone : 'string', + type : 'string', + favoriteFruit : { + defaultsTo: 'blueberry', + type: 'string' + }, + age : 'integer' + }; + + /** + * DEFINE + * + * Create a new table with a defined set of attributes + */ + + describe('.define()', function() { + + describe('basic usage', function() { + + // Register the collection + before(function(done) { + var collection = _.extend({ config: support.Config }, { + identity: 'test_define' + }); + + adapter.registerCollection(collection, done); + }); + + // Build Table from attributes + it('should build the table', function(done) { + + adapter.define('test_define', definition, function(err) { + adapter.describe('test_define', function(err, result) { + Object.keys(result).length.should.eql(8); + done(); + }); + }); + + }); + + }); + + describe('reserved words', function() { + + // Register the collection + before(function(done) { + var collection = _.extend({ config: support.Config }, { + identity: 'user' + }); + + adapter.registerCollection(collection, done); + }); + + after(function(done) { + support.Client(function(err, client) { + var query = 'DROP TABLE "user";'; + client.run(query, function(err) { + + // close client + client.close(); + + done(); + }); + }); + }); + + // Build Table from attributes + it('should escape reserved words', function(done) { + + adapter.define('user', definition, function(err) { + adapter.describe('user', function(err, result) { + Object.keys(result).length.should.eql(8); + done(); + }); + }); + + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.describe.js b/test/unit/adapter.describe.js new file mode 100644 index 0000000..f603674 --- /dev/null +++ b/test/unit/adapter.describe.js @@ -0,0 +1,42 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_describe', done); + }); + + after(function(done) { + support.Teardown('test_describe', done); + }); + + /** + * DESCRIBE + * + * Similar to MySQL's Describe method this should list the + * properties of a table. + */ + + describe('.describe()', function() { + + // Output Column Names + it('should output the column names', function(done) { + adapter.describe('test_describe', function(err, results) { + Object.keys(results).length.should.eql(3); + + should.exist(results.id); + should.exist(results.field_1); + should.exist(results.field_2); + + done(); + }); + }); + + }); +}); \ No newline at end of file From f53c2082e13f99fc6aa431c566839da8886d36e4 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:42:49 +0900 Subject: [PATCH 22/81] Fixed bugs found from unit tests --- lib/utils.js | 130 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 8 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index a19d0f1..b08f976 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -8,7 +8,7 @@ var utils = module.exports = {}; utils.buildSchema = function(obj) { var schema = ""; - + // Iterate through the Object Keys and build a string Object.keys(obj).forEach(function(key) { var attr = {}; @@ -25,13 +25,17 @@ utils.buildSchema = function(obj) { } // Override Type for autoIncrement - if(attr.autoIncrement) attr.type = 'serial'; + if(attr.autoIncrement) { + attr.type = 'serial'; + attr.primaryKey = true; + } var str = [ '"' + key + '"', // attribute name utils.sqlTypeCast(attr.type), // attribute type attr.primaryKey ? 'PRIMARY KEY' : '', // primary key - attr.unique ? 'UNIQUE' : '' // unique constraint + attr.unique ? 'UNIQUE' : '', // unique constraint + attr.defaultsTo ? 'DEFAULT "' + attr.defaultsTo + '"': '' ].join(' ').trim(); schema += str + ', '; @@ -152,18 +156,128 @@ utils.escapeTable = function(table) { return '"' + table + '"'; }; +utils.mapAttributes = function(data) { + var keys = [], // Column Names + values = [], // Column Values + params = [], // Param Index, ex: $1, $2 + i = 1; + + Object.keys(data).forEach(function(key) { + keys.push('"' + key + '"'); + values.push(utils.prepareValue(data[key])); + params.push('$' + i); + i++; + }); + + return({ keys: keys, values: values, params: params }); +}; + utils.normalizeSchema = function(schema) { var normalized = {}; var clone = _.clone(schema); - clone.forEach(function(column) { + clone.columns.forEach(function(column) { // Set type - normalized[column.Column] = { type: column.Type }; + normalized[column.name] = { type: column.type }; // Check for primary key - if (column.Constraint && column.C === 'p') { - normalized[column.Column].primaryKey = true; - } + normalized[column.name].primaryKey = column.pk ? true : false; + + // Indicate whether the column is indexed + normalized[column.name].indexed = column.indexed; + + // Show unique constraint + if (column.unique) normalized[column.name].unique = true; + + if (column.autoIncrement) normalized[column.name].autoIncrement = true; }); + + return normalized; +}; + +/** + * Prepare values + * + * Transform a JS date to SQL date and functions + * to strings. + */ + +utils.prepareValue = function(value) { + + // Cast dates to SQL + if (_.isDate(value)) { + value = utils.toSqlDate(value); + } + + // Cast functions to strings + if (_.isFunction(value)) { + value = value.toString(); + } + + // Store Arrays as strings + if (Array.isArray(value)) { + value = JSON.stringify(value); + } + + // Store Buffers as hex strings (for BYTEA) + if (Buffer.isBuffer(value)) { + value = '\\x' + value.toString('hex'); + } + + return value; +}; + +/** + * Cast waterline types to SQLite3 data types + */ + +utils.sqlTypeCast = function(type) { + switch (type.toLowerCase()) { + case 'serial': + return 'INTEGER'; + + case 'string': + case 'text': + return 'TEXT'; + + case 'boolean': + case 'int': + case 'integer': + return 'INTEGER'; + + case 'float': + case 'double': + return 'REAL'; + + case 'date': + case 'datestamp': + case 'datetime': + return 'TEXT'; + + case 'array': + return 'TEXT'; + + case 'json': + return 'TEXT'; + + case 'binary': + case 'bytea': + return 'BLOB'; + + default: + console.error("Warning: Unregistered type given: " + type); + return 'TEXT'; + } +}; + +/** + * JS Date to UTC Timestamp + * + * Dates should be stored in Postgres with UTC timestamps + * and then converted to local time on the client. + */ + +utils.toSqlDate = function(date) { + return date.toUTCString(); }; \ No newline at end of file From e5f7b1b022722d68fcd095ec93381bdd2e65a723 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:43:08 +0900 Subject: [PATCH 23/81] Updated async version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 59d00c5..a60f14e 100755 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "license": "MIT", "readmeFilename": "README.md", "dependencies": { - "async": "0.1.22", - "sqlite3": "2.1.19", + "async": "~0.2.9", + "sqlite3": "~2.1.x", "underscore": "1.5.2" }, "devDependencies": { From 852fcd53bdbcfcc58bf56ca3f2ba08a1c9855417 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:44:19 +0900 Subject: [PATCH 24/81] Added sqlite3 module and typo fix to db config --- test/integration/runner.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/integration/runner.js b/test/integration/runner.js index ac4e135..dc65e73 100644 --- a/test/integration/runner.js +++ b/test/integration/runner.js @@ -1,4 +1,5 @@ var tests = require('waterline-adapter-tests'), + sqlite3 = require('sqlite3'), adapter = require('../../lib/adapter'), mocha = require('mocha'); @@ -7,8 +8,8 @@ var tests = require('waterline-adapter-tests'), */ var config = { - filename: ":memory:", - mode: sqlite3.OPEN_READWRITE | OPEN_CREATE, + filename: "sailssqlite.db", + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, verbose: true }; From 44c4f569e31cf1b914a60c5db8be9a9dc82c15db Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:45:24 +0900 Subject: [PATCH 25/81] Added new method and typo fix for closing db --- test/unit/support/bootstrap.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/unit/support/bootstrap.js b/test/unit/support/bootstrap.js index 078371c..611a4b2 100644 --- a/test/unit/support/bootstrap.js +++ b/test/unit/support/bootstrap.js @@ -1,5 +1,5 @@ var sqlite3 = require('sqlite3'), - adapter = require('../../lib/adapter'); + adapter = require('../../../lib/adapter'); var Support = module.exports = {}; @@ -20,6 +20,12 @@ Support.Definition = { } }; +Support.Client = function(cb) { + var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { + cb(err, client); + }); +}; + Support.Collection = function(name) { return { identity: name, @@ -41,11 +47,11 @@ Support.Teardown = function(tableName, cb) { var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { dropTable(tableName, client, function(err) { if (err) { - done(); + client.close(); return cb(err); } - done(); + client.close(); return cb(); }); }); From a76dcd28fea6f3c6df13ac3c2e34cd372d0528d1 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 04:45:54 +0900 Subject: [PATCH 26/81] Major bug fixes found from unit tests --- lib/adapter.js | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index 3e22a75..47179ae 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -136,16 +136,17 @@ module.exports = (function() { spawnConnection(function __DESCRIBE__(client, cb) { // Get a list of all the tables in this database (see http://www.sqlite.org/faq.html#q7) - var query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"; + var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; - // Query to get information about each table (see http://www.sqlite.org/pragma.html#pragma_table_info) - var columnsQuery = "PRAGMA table_info(?)"; + client.get(query, function (err, schema) { + if (err || !schema) return cb(); - // Query to get information about indices - var indexListQuery = "PRAGMA index_list($table)"; - var indexInfoQuery = "PRAGMA index_info(?)"; + // Query to get information about each table (see http://www.sqlite.org/pragma.html#pragma_table_info) + var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; + + // Query to get a list of indices for a given table + var indexListQuery = "PRAGMA index_list(" + schema.name + ")"; - client.each(query, function (err, schema) { schema.indices = []; schema.columns = []; @@ -153,11 +154,14 @@ module.exports = (function() { client.serialize(function() { // Retrieve indices for this table first - client.each(indexListQuery, { $table: schema.name }, function(err, index) { - console.log(index); + client.each(indexListQuery, function(err, index) { index.columns = []; + + // Query to get information about indices + var indexInfoQuery = "PRAGMA index_info(" + index.name + ")"; + // Retrieve detailed information for given index - client.each(indexInfoQuery, index.name, function(err, indexedCol) { + client.each(indexInfoQuery, function(err, indexedCol) { index.columns.push(indexedCol); }); @@ -165,16 +169,16 @@ module.exports = (function() { }); // Then retrieve column information for each table - client.each(columnsQuery, schema.name, function(err, column) { + client.each(columnsQuery, function(err, column) { // In SQLite3, AUTOINCREMENT only applies to PK columns of INTEGER type - column.autoIncrement = column.type.toLowerCase() == 'int' && column.pk == 1; + column.autoIncrement = column.type.toLowerCase() == 'integer' && column.pk == 1; // By default, assume column is not indexed until we find that it is column.indexed = false; // Search for indexed columns - schema.indices.forEach(function(index) { + schema.indices.forEach(function(idx) { if (column.indexed) return; else { // Loop through each column in the index and check for a match @@ -187,6 +191,14 @@ module.exports = (function() { } }); + schema.indices.forEach(function(idx) { + index.columns.forEach(function(indexedCol) { + if (indexedCol.name == column.name) { + if (index.unique) column.unique = true; + } + }); + }); + schema.columns.push(column); }, function(err, resultCount) { // This callback function is fired when all the columns have been iterated by the .each() function @@ -199,15 +211,12 @@ module.exports = (function() { var normalizedSchema = utils.normalizeSchema(schema); // Set internal schema mapping - dbs[table] = normalizedSchema; + dbs[table].schema = normalizedSchema; // Fire the callback with the normalized schema cb(null, normalizedSchema); }); }); - }, function(err, resultCount) { - if (err) return cb(err); - if (resultCount === 0) return cb(); }); }, dbs[table].config, cb); }, @@ -288,10 +297,10 @@ module.exports = (function() { var _query = new Query(dbs[table].definition); // Escape table name - var table = utils.escapeTable(table); + table = utils.escapeTable(table); // Transform the data object into arrays used in parametrized query - var attributes = util.mapAttributes(data), + var attributes = utils.mapAttributes(data), columnNames = attributes.keys.join(', '), paramValues = attributes.params.join(', '); @@ -300,7 +309,7 @@ module.exports = (function() { var selectQuery = 'SELECT * FROM ' + table + ' ORDER BY rowid DESC LIMIT 1'; // First insert the values - client.run(insertQuery, function(err) { + client.run(insertQuery, attributes.values, function(err) { if (err) return cb(err); // Get the last inserted row From befb698e6b830bbd26c61324e426a5a572483b99 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 20:04:52 +0900 Subject: [PATCH 27/81] Initial commit --- test/unit/adapter.drop.js | 35 +++++++++++++++ test/unit/adapter.find.js | 87 +++++++++++++++++++++++++++++++++++++ test/unit/adapter.update.js | 53 ++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 test/unit/adapter.drop.js create mode 100644 test/unit/adapter.find.js create mode 100644 test/unit/adapter.update.js diff --git a/test/unit/adapter.drop.js b/test/unit/adapter.drop.js new file mode 100644 index 0000000..46ecbe5 --- /dev/null +++ b/test/unit/adapter.drop.js @@ -0,0 +1,35 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_drop', done); + }); + + /** + * DROP + * + * Drop a table and all it's records. + */ + + describe('.drop()', function() { + + // Drop the Test table + it('should drop the table', function(done) { + + adapter.drop('test_drop', function(err, result) { + adapter.describe('test_drop', function(err, result) { + should.not.exist(result); + done(); + }); + }); + + }); + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.find.js b/test/unit/adapter.find.js new file mode 100644 index 0000000..4d91ee1 --- /dev/null +++ b/test/unit/adapter.find.js @@ -0,0 +1,87 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_find', done); + }); + + after(function(done) { + support.Teardown('test_find', done); + }); + + /** + * FIND + * + * Returns an array of records from a SELECT query + */ + + describe('.find()', function() { + + describe('WHERE clause', function() { + + before(function(done) { + support.Seed('test_find', done); + }); + + describe('key/value attributes', function() { + + it('should return the record set', function(done) { + adapter.find('test_find', { where: { field_1: 'foo' } }, function(err, results) { + results.length.should.eql(1); + results[0].id.should.eql(1); + done(); + }); + }); + + }); + + describe('comparators', function() { + + // Insert a unique record to test with + before(function(done) { + var query = [ + 'INSERT INTO "test_find" (field_1, field_2)', + "values ('foobar', 'AR)H$daxx');" + ].join(''); + + support.Client(function(err, client, close) { + client.run(query, function(err) { + + // close client + client.close(); + + done(); + }); + }); + }); + + it('should support endsWith', function(done) { + + var criteria = { + where: { + field_2: { + endsWith: 'AR)H$daxx' + } + } + }; + + adapter.find('test_find', criteria, function(err, results) { + results.length.should.eql(1); + results[0].field_2.should.eql('AR)H$daxx'); + done(); + }); + }); + + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.update.js b/test/unit/adapter.update.js new file mode 100644 index 0000000..9809b12 --- /dev/null +++ b/test/unit/adapter.update.js @@ -0,0 +1,53 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_update', done); + }); + + after(function(done) { + support.Teardown('test_update', done); + }); + + /** + * UPDATE + * + * Update a row in a table + */ + + describe('.update()', function() { + + describe('with options', function() { + + before(function(done) { + support.Seed('test_update', done); + }); + + it('should update the record', function(done) { + + adapter.update('test_update', { where: { id: 1 }}, { field_1: 'foobar' }, function(err, result) { + result[0].field_1.should.eql('foobar'); + done(); + }); + + }); + + it('should keep case', function(done) { + + adapter.update('test_update', { where: { id: 1 }}, { field_1: 'FooBar' }, function(err, result) { + result[0].field_1.should.eql('FooBar'); + done(); + }); + + }); + + }); + }); +}); \ No newline at end of file From 12ada0b616d3d010a4fe085391d11e4fec813fe6 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 20:05:20 +0900 Subject: [PATCH 28/81] Fixed find(), update() bugs --- lib/adapter.js | 53 ++++++++++++++++++++++++++++++++++++++------------ lib/query.js | 23 +++++++++++----------- lib/utils.js | 2 +- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index 47179ae..5bcc842 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -46,10 +46,28 @@ module.exports = (function() { registerCollection: function(collection, cb) { var def = _.clone(collection); var key = def.identity; + var definition = def.definition || {}; if (dbs[key]) return cb(); dbs[key.toString()] = def; + // Set default primary key as 'id' + var pkName = "id"; + + // Set primary key field + for (var attribute in definition) { + if (!definition[attribute].hasOwnProperty('primaryKey')) continue; + + // Check if custom primaryKey value is false + if (!definition[attribute].primaryKey) continue; + + // Set the pkName to the custom primaryKey value + pkName = attribute; + } + + // Set the primary key to the definition object + def.primaryKey = pkName; + // Always call describe this.describe(key, function(err, schema) { if (err) return cb(err); @@ -123,10 +141,9 @@ module.exports = (function() { cb(null, this); }); } - async.eachSeries(indices, buildIndex, cb); }); - }, dbs[table].config, cb); + }, dbs[table].config, describe); }, // REQUIRED method if integrating with a schemaful database @@ -350,7 +367,7 @@ module.exports = (function() { // Cast special values var values = []; - + // Run query client.each(query.query, query.values, function(err, row) { if (err) return cb(err); @@ -373,32 +390,44 @@ module.exports = (function() { table = utils.escapeTable(table); // Build query + var primaryKey = dbs[table.replace(/["']/g, "")].primaryKey; var _schema = dbs[table.replace(/["']/g, "")].schema; + var selectQuery = 'SELECT "' + primaryKey + '" FROM ' + table; //new Query(_schema).find(table, options); var updateQuery = new Query(_schema).update(table, options, data); - var selectQuery = new Query(_schema).find(table, options); - var rowIds = []; + var primaryKeys = []; + + var criteria = _query._build(options); + + selectQuery += ' ' + criteria.query; client.serialize(function() { + // Keep track of the row IDs of the rows that will be updated - client.each(selectQuery.query, selectQuery.values, function(err, row) { + client.each(selectQuery, criteria.values, function(err, row) { if (err) return cb(err); - rowIds.push(row.rowid); + primaryKeys.push(row[primaryKey]); }); // Run query client.run(updateQuery.query, updateQuery.values, function(err) { - if (err) return cb(err); + if (err) { console.error(err); return cb(err); } // Build a query to return updated rows if (this.changes > 0) { // Build criteria - var criteria = this.changes == 1 ? { where: {}, limit: 1 } : { where: {} }; - criteria.where.in = rowIds; + var criteria; + if (this.changes == 1) { + criteria = { where: {}, limit: 1 }; + criteria.where[primaryKey] = primaryKeys[0]; + } else { + criteria = { where: {} }; + criteria.where[primaryKey] = primaryKeys; + } // Return the updated items up the callback chain - adapter.find(table, criteria, function(err, models) { - if (err) return cb(err); + adapter.find(table.replace(/["']/g, ""), criteria, function(err, models) { + if (err) { console.log(models); return cb(err); } var values = []; diff --git a/lib/query.js b/lib/query.js index 8976b5e..d83eea4 100644 --- a/lib/query.js +++ b/lib/query.js @@ -31,6 +31,7 @@ var Query = function(schema) { Query.prototype.find = function(table, criteria) { this._query = utils.buildSelectStatement(criteria, table); + if (criteria) this._build(criteria); return { @@ -293,7 +294,7 @@ Query.prototype.operators = function() { // Append each value to query options.forEach(function(value) { - self._query += '?, '; + self._query += '$' + self._paramCount + ', '; self._paramCount++; // If case sensitivity is off, lowercase the value @@ -362,7 +363,7 @@ function processCriteria(parent, value, combinator, caseSensitive) { if (value !== null) { // Simple Key/Value attributes - this._query += parent + ' ' + combinator + ' ?'; + this._query += parent + ' ' + combinator + ' $' + this._paramCount; this._values.push(value); this._paramCount++; @@ -384,25 +385,25 @@ function prepareCriterion(key, value) { case '<': case 'lessThan': this._values.push(value); - str = '< ?'; + str = '< $' + this._paramCount; break; case '<=': case 'lessThanOrEqual': this._values.push(value); - str = '<= ?'; + str = '<= $' + this._paramCount; break; case '>': case 'greaterThan': this._values.push(value); - str = '> ?'; + str = '> $' + this._paramCount; break; case '>=': case 'greaterThanOrEqual': this._values.push(value); - str = '>= ?'; + str = '>= $' + this._paramCount; break; case '!': @@ -411,28 +412,28 @@ function prepareCriterion(key, value) { str = 'IS NOT NULL'; } else { this._values.push(value); - str = '<> ?'; + str = '<> $' + this._paramCount; } break; case 'like': this._values.push(value); - str = 'ILIKE ?'; + str = 'LIKE $' + this._paramCount; break; case 'contains': this._values.push('%' + value + '%'); - str = 'ILIKE ?'; + str = 'LIKE $' + this._paramCount; break; case 'startsWith': this._values.push(value + '%'); - str = 'ILIKE ?'; + str = 'LIKE $' + this._paramCount; break; case 'endsWith': this._values.push('%' + value); - str = 'ILIKE ?'; + str = 'LIKE $' + this._paramCount; break; default: diff --git a/lib/utils.js b/lib/utils.js index b08f976..fe348d0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -54,7 +54,7 @@ utils.buildSelectStatement = function(criteria, table) { var query = ''; if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || criteria.max) { - query = 'SELECT '; + query = 'SELECT rowid, '; // Append groupBy columns to select statement if(criteria.groupBy) { From f18f935e66ae3378f5ea7f1bf5e3ceed7260fee5 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 20:05:57 +0900 Subject: [PATCH 29/81] Added Seed() method --- test/unit/support/bootstrap.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/test/unit/support/bootstrap.js b/test/unit/support/bootstrap.js index 611a4b2..63fb7c8 100644 --- a/test/unit/support/bootstrap.js +++ b/test/unit/support/bootstrap.js @@ -1,4 +1,4 @@ -var sqlite3 = require('sqlite3'), +var sqlite3 = require('sqlite3').verbose(), adapter = require('../../../lib/adapter'); var Support = module.exports = {}; @@ -34,6 +34,21 @@ Support.Collection = function(name) { }; }; +// Seed a record to use for testing +Support.Seed = function(tableName, cb) { + var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { + createRecord(tableName, client, function(err) { + if(err) { + client.close(); + return cb(err); + } + + client.close(); + cb(); + }); + }); +}; + // Register and define a collection Support.Setup = function(tableName, cb) { adapter.registerCollection(Support.Collection(tableName), function(err) { @@ -61,5 +76,16 @@ function dropTable(table, client, cb) { table = '"' + table + '"'; var query = "DROP TABLE " + table; + client.run(query, cb); +} + +function createRecord(table, client, cb) { + table = '"' + table + '"'; + + var query = [ + "INSERT INTO " + table + ' (field_1, field_2)', + " values ('foo', 'bar');" + ].join(''); + client.run(query, cb); } \ No newline at end of file From 245daca21a6a29f1121dcd499a8e206e6e66009e Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 20:24:09 +0900 Subject: [PATCH 30/81] Fixed a bug in destroy() method --- lib/adapter.js | 4 +-- test/unit/adapter.destroy.js | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 test/unit/adapter.destroy.js diff --git a/lib/adapter.js b/lib/adapter.js index 5bcc842..09bd64e 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -461,8 +461,8 @@ module.exports = (function() { var deleteQuery = new Query(_schema).destroy(table, options); // Run query - adapter.find(table, options, function(err, models) { - if (err) return cb(err); + adapter.find(table.replace(/["']/g, ""), options, function(err, models) { + if (err) { console.log(err); return cb(err); } var values = []; diff --git a/test/unit/adapter.destroy.js b/test/unit/adapter.destroy.js new file mode 100644 index 0000000..0b47d8c --- /dev/null +++ b/test/unit/adapter.destroy.js @@ -0,0 +1,55 @@ +var adapter = require('../../lib/adapter'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Setup and Teardown + */ + + before(function(done) { + support.Setup('test_destroy', done); + }); + + after(function(done) { + support.Teardown('test_destroy', done); + }); + + /** + * DESTROY + * + * Remove a row from a table + */ + + describe('.destroy()', function() { + + describe('with options', function() { + + before(function(done) { + support.Seed('test_destroy', done); + }); + + it('should destroy the record', function(done) { + adapter.destroy('test_destroy', { where: { id: 1 }}, function(err, result) { + + // Check record was actually removed + support.Client(function(err, client, close) { + client.all('SELECT * FROM "test_destroy"', function(err, rows) { + + // Test no rows are returned + rows.length.should.eql(0); + + // close client + client.close(); + + done(); + }); + }); + + }); + }); + + }); + }); +}); \ No newline at end of file From 6f3447fc7f5336b45730f5b94627329667318e94 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 22:38:52 +0900 Subject: [PATCH 31/81] Fixed a bug with creating index --- lib/adapter.js | 44 ++++++++++++++++---------------------------- lib/utils.js | 2 +- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index 09bd64e..d61ad5c 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -138,7 +138,7 @@ module.exports = (function() { // Run query client.run(query, function(err) { if (err) return cb(err); - cb(null, this); + cb(); }); } async.eachSeries(indices, buildIndex, cb); @@ -162,30 +162,27 @@ module.exports = (function() { var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; // Query to get a list of indices for a given table - var indexListQuery = "PRAGMA index_list(" + schema.name + ")"; + var indexListQuery = 'PRAGMA index_list("' + schema.name + '")'; schema.indices = []; schema.columns = []; - // We want the following queries to run in series - client.serialize(function() { + var index = { columns: [] }; - // Retrieve indices for this table first - client.each(indexListQuery, function(err, index) { - index.columns = []; - - // Query to get information about indices - var indexInfoQuery = "PRAGMA index_info(" + index.name + ")"; + client.each(indexListQuery, function(err, currentIndex) { - // Retrieve detailed information for given index - client.each(indexInfoQuery, function(err, indexedCol) { - index.columns.push(indexedCol); - }); + // Query to get information about indices + var indexInfoQuery = 'PRAGMA index_info("' + currentIndex.name + '")'; - schema.indices.push(index); + // Retrieve detailed information for given index + client.each(indexInfoQuery, function(err, indexedCol) { + index.columns.push(indexedCol); }); - // Then retrieve column information for each table + schema.indices.push(currentIndex); + }, function(err, resultCount) { + if (err) return cb(err); + client.each(columnsQuery, function(err, column) { // In SQLite3, AUTOINCREMENT only applies to PK columns of INTEGER type @@ -196,28 +193,19 @@ module.exports = (function() { // Search for indexed columns schema.indices.forEach(function(idx) { - if (column.indexed) return; - else { - // Loop through each column in the index and check for a match + if (!column.indexed) { index.columns.forEach(function(indexedCol) { if (indexedCol.name == column.name) { column.indexed = true; - return; + if (idx.unique) column.unique = true; } }); } }); - schema.indices.forEach(function(idx) { - index.columns.forEach(function(indexedCol) { - if (indexedCol.name == column.name) { - if (index.unique) column.unique = true; - } - }); - }); - schema.columns.push(column); }, function(err, resultCount) { + // This callback function is fired when all the columns have been iterated by the .each() function if (err) { console.error("Error while retrieving column information."); diff --git a/lib/utils.js b/lib/utils.js index fe348d0..01caf7d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -140,7 +140,7 @@ utils.buildIndexes = function(obj) { Object.keys(obj).forEach(function(key) { if (obj[key].hasOwnProperty('index')) indexes.push(key); }); - + return indexes; }; From 3e3036ba2056e9bbf260a606b023656b1114dca7 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 22:40:07 +0900 Subject: [PATCH 32/81] Indexing unit test --- test/unit/adapter.index.js | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/unit/adapter.index.js diff --git a/test/unit/adapter.index.js b/test/unit/adapter.index.js new file mode 100644 index 0000000..0d92f73 --- /dev/null +++ b/test/unit/adapter.index.js @@ -0,0 +1,60 @@ +var adapter = require('../../lib/adapter'), + _ = require('underscore'), + should = require('should'), + support = require('./support/bootstrap'); + +describe('adapter', function() { + + /** + * Teardown + */ + + after(function(done) { + support.Teardown('test_index', done); + }); + + // Attributes for the test table + var definition = { + id: { + type: 'serial', + autoIncrement: true + }, + name: { + type: 'string', + index: true + } + }; + + /** + * Indexes + * + * Ensure Indexes get created correctly + */ + + describe('Index Attributes', function() { + + before(function(done) { + var collection = _.extend({ config: support.Config }, { + identity: 'test_index' + }); + + adapter.registerCollection(collection, function(err) { + if(err) return cb(err); + adapter.define('test_index', definition, done); + }); + }); + + // Build Indicies from definition + it('should add indicies', function(done) { + + adapter.define('test_index', definition, function(err) { + adapter.describe('test_index', function(err, result) { + result.name.indexed.should.eql(true); + done(); + }); + }); + + }); + + }); +}); \ No newline at end of file From 5d2ebf8e0e709c8a2a3d1e7e63d36f957db2c68a Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 22:40:45 +0900 Subject: [PATCH 33/81] Aggregate unit tests --- test/unit/adapter.avg.js | 47 ++++++++++++++++++++++++++++++++++++ test/unit/adapter.groupBy.js | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 test/unit/adapter.avg.js create mode 100644 test/unit/adapter.groupBy.js diff --git a/test/unit/adapter.avg.js b/test/unit/adapter.avg.js new file mode 100644 index 0000000..e3fcff0 --- /dev/null +++ b/test/unit/adapter.avg.js @@ -0,0 +1,47 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * AVG + * + * Adds a AVG select parameter to a sql statement + */ + + describe('.avg()', function() { + + describe('with array', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + average: ['age'] + }; + + it('should use the AVG aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, CAST(AVG(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + describe('with string', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + average: 'age' + }; + + it('should use the AVG aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, CAST(AVG(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.groupBy.js b/test/unit/adapter.groupBy.js new file mode 100644 index 0000000..91b906f --- /dev/null +++ b/test/unit/adapter.groupBy.js @@ -0,0 +1,47 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * groupBy + * + * Adds a Group By statement to a sql statement + */ + + describe('.groupBy()', function() { + + describe('with array', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + groupBy: ['name'] + }; + + it('should append a Group By clause to the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, name FROM test WHERE LOWER("name") = $1 GROUP BY name'); + }); + }); + + describe('with string', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + groupBy: 'name' + }; + + it('should use the MAX aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, name FROM test WHERE LOWER("name") = $1 GROUP BY name'); + }); + }); + + }); +}); \ No newline at end of file From 23b6c11f223d7016eaac5cd6fbffa928bf55f4a1 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 23:17:24 +0900 Subject: [PATCH 34/81] Added query unit tests --- test/unit/query.cast.js | 25 ++++++ test/unit/query.limit.js | 29 +++++++ test/unit/query.skip.js | 28 ++++++ test/unit/query.sort.js | 69 +++++++++++++++ test/unit/query.where.js | 179 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 330 insertions(+) create mode 100644 test/unit/query.cast.js create mode 100644 test/unit/query.limit.js create mode 100644 test/unit/query.skip.js create mode 100644 test/unit/query.sort.js create mode 100644 test/unit/query.where.js diff --git a/test/unit/query.cast.js b/test/unit/query.cast.js new file mode 100644 index 0000000..3ce827d --- /dev/null +++ b/test/unit/query.cast.js @@ -0,0 +1,25 @@ +var Query = require('../../lib/query'), + assert = require('assert'); + +describe('query', function() { + + /** + * CAST + * + * Cast values to proper types + */ + + describe('.cast()', function() { + + describe('Array', function() { + + it('should cast to values to array', function() { + var values = new Query({ list: { type: 'array' }}).cast({ list: "[0,1,2,3]" }); + assert(Array.isArray(values.list)); + assert(values.list.length === 4); + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/query.limit.js b/test/unit/query.limit.js new file mode 100644 index 0000000..3b0d56f --- /dev/null +++ b/test/unit/query.limit.js @@ -0,0 +1,29 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * LIMIT + * + * Adds a LIMIT parameter to a sql statement + */ + + describe('.limit()', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + limit: 1 + }; + + it('should append the LIMIT clause to the query', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 LIMIT 1'); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/query.skip.js b/test/unit/query.skip.js new file mode 100644 index 0000000..ae44b47 --- /dev/null +++ b/test/unit/query.skip.js @@ -0,0 +1,28 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * SKIP + * + * Adds an OFFSET parameter to a sql statement + */ + + describe('.skip()', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + skip: 1 + }; + + it('should append the SKIP clause to the query', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 OFFSET 1'); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/query.sort.js b/test/unit/query.sort.js new file mode 100644 index 0000000..59dc63a --- /dev/null +++ b/test/unit/query.sort.js @@ -0,0 +1,69 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * SORT + * + * Adds an ORDER BY parameter to a sql statement + */ + + describe('.sort()', function() { + + it('should append the ORDER BY clause to the query', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + sort: { + name: 1 + } + }; + + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC'); + + }); + + it('should sort by multiple columns', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + sort: { + name: 1, + age: 1 + } + }; + + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC, "age" ASC'); + + }); + + it('should allow desc and asc ordering', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + sort: { + name: 1, + age: -1 + } + }; + + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC, "age" DESC'); + + }); + + }); + +}); \ No newline at end of file diff --git a/test/unit/query.where.js b/test/unit/query.where.js new file mode 100644 index 0000000..b67f66c --- /dev/null +++ b/test/unit/query.where.js @@ -0,0 +1,179 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * WHERE + * + * Build the WHERE part of an sql statement from a js object + */ + + describe('.where()', function() { + + describe('`AND` criteria', function() { + + describe('case insensitivity', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'Foo', + age: 1 + } + }; + + it('should build a SELECT statement using LOWER() on strings', function() { + var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" = $2'); + query.values[0].should.eql('foo'); + }); + }); + + describe('criteria is simple key value lookups', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo', + age: 27 + } + }; + + it('should build a simple SELECT statement', function() { + var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" = $2'); + query.values.length.should.eql(2); + }); + + }); + + describe('has multiple comparators', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo', + age: { + '>' : 27, + '<' : 30 + } + } + }; + + it('should build a SELECT statement with comparators', function() { + var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" > $2 AND "age" < $3'); + query.values.length.should.eql(3); + }); + + }); + + }); + + describe('`LIKE` criteria', function() { + + // Lookup criteria + var criteria = { + where: { + like: { + type: '%foo%', + name: 'bar%' + } + } + }; + + it('should build a SELECT statement with ILIKE', function() { + var query = new Query({ type: { type: 'text' }, name: { type: 'text'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("type") ILIKE $1 AND LOWER("name") ILIKE $2'); + query.values.length.should.eql(2); + }); + + }); + + describe('`OR` criteria', function() { + + // Lookup criteria + var criteria = { + where: { + or: [ + { like: { foo: '%foo%' } }, + { like: { bar: '%bar%' } } + ] + } + }; + + it('should build a SELECT statement with multiple like statements', function() { + var query = new Query({ foo: { type: 'text' }, bar: { type: 'text'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("foo") ILIKE $1 OR LOWER("bar") ILIKE $2'); + query.values.length.should.eql(2); + }); + + }); + + describe('`IN` criteria', function() { + + // Lookup criteria + var criteria = { + where: { + name: [ + 'foo', + 'bar', + 'baz' + ] + } + }; + + var camelCaseCriteria = { + where: { + myId: [ + 1, + 2, + 3 + ] + } + }; + + it('should build a SELECT statement with an IN array', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") IN ($1, $2, $3)'); + query.values.length.should.eql(3); + }); + + it('should build a SELECT statememnt with an IN array and camel case column', function() { + var query = new Query({ myId: { type: 'integer' }}).find('test', camelCaseCriteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE "myId" IN ($1, $2, $3)'); + query.values.length.should.eql(3); + }); + + }); + + describe('`NOT` criteria', function() { + + // Lookup criteria + var criteria = { + where: { + age: { + not: 40 + } + } + }; + + it('should build a SELECT statement with an NOT clause', function() { + var query = new Query({age: { type: 'integer'}}).find('test', criteria); + + query.query.should.eql('SELECT rowid, * FROM test WHERE "age" <> $1'); + query.values.length.should.eql(1); + }); + + }); + + }); +}); \ No newline at end of file From 64c822690b6517de4386b1ee13ac2487241407d0 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 23:18:35 +0900 Subject: [PATCH 35/81] Added aggregate query unit tests --- test/unit/adapter.max.js | 47 ++++++++++++++++++++++++++++++++++++++++ test/unit/adapter.min.js | 47 ++++++++++++++++++++++++++++++++++++++++ test/unit/adapter.sum.js | 47 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/unit/adapter.max.js create mode 100644 test/unit/adapter.min.js create mode 100644 test/unit/adapter.sum.js diff --git a/test/unit/adapter.max.js b/test/unit/adapter.max.js new file mode 100644 index 0000000..d8eb5a4 --- /dev/null +++ b/test/unit/adapter.max.js @@ -0,0 +1,47 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * MAX + * + * Adds a MAX select parameter to a sql statement + */ + + describe('.max()', function() { + + describe('with array', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + max: ['age'] + }; + + it('should use the max aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, MAX(age) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + describe('with string', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + max: 'age' + }; + + it('should use the MAX aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, MAX(age) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.min.js b/test/unit/adapter.min.js new file mode 100644 index 0000000..503c623 --- /dev/null +++ b/test/unit/adapter.min.js @@ -0,0 +1,47 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * MIN + * + * Adds a MIN select parameter to a sql statement + */ + + describe('.min()', function() { + + describe('with array', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + min: ['age'] + }; + + it('should use the min aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, MIN(age) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + describe('with string', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + min: 'age' + }; + + it('should use the MIN aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, MIN(age) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/adapter.sum.js b/test/unit/adapter.sum.js new file mode 100644 index 0000000..b6076a7 --- /dev/null +++ b/test/unit/adapter.sum.js @@ -0,0 +1,47 @@ +var Query = require('../../lib/query'), + should = require('should'); + +describe('query', function() { + + /** + * SUM + * + * Adds a SUM select parameter to a sql statement + */ + + describe('.sum()', function() { + + describe('with array', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + sum: ['age'] + }; + + it('should use the SUM aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, CAST(SUM(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + describe('with string', function() { + + // Lookup criteria + var criteria = { + where: { + name: 'foo' + }, + sum: 'age' + }; + + it('should use the SUM aggregate option in the select statement', function() { + var query = new Query({ name: { type: 'text' }}).find('test', criteria); + query.query.should.eql('SELECT rowid, CAST(SUM(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); + }); + }); + + }); +}); \ No newline at end of file From 19c163644378974de04087b3417d9576b8bae1e8 Mon Sep 17 00:00:00 2001 From: AndrewJo Date: Tue, 26 Nov 2013 23:18:55 +0900 Subject: [PATCH 36/81] Fixed a spacing typo --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index d83eea4..babc0b0 100644 --- a/lib/query.js +++ b/lib/query.js @@ -340,7 +340,7 @@ function processCriteria(parent, value, combinator, caseSensitive) { self._query += parent + ' '; prepareCriterion.call(self, keys[i], value[keys[i]]); - if (i+1 < keys.length) self._query += 'AND '; + if (i+1 < keys.length) self._query += ' AND '; } return; From d3a19a274350f9b0533895536f7115e2c2ed0888 Mon Sep 17 00:00:00 2001 From: Richard Pinedo Date: Mon, 12 Jun 2017 15:44:18 -0400 Subject: [PATCH 37/81] tested and working copy, has been running stable for a few years now --- lib/adapter.js | 713 ++++++++++++++++++++++++++++++------------------- lib/query.js | 46 ++-- lib/utils.js | 238 +++++++++++++++-- 3 files changed, 684 insertions(+), 313 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index d61ad5c..c9810aa 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -9,12 +9,14 @@ var sqlite3 = require('sqlite3'), async = require('async'), fs = require('fs'), - _ = require("underscore"), + _ = require('lodash'), Query = require('./query'), utils = require('./utils'); + Errors = require('waterline-errors').adapter module.exports = (function() { + var connections = {}; var dbs = {}; // Determines whether the database file already exists @@ -23,142 +25,100 @@ module.exports = (function() { var adapter = { identity: 'sails-sqlite3', - // Set to true if this adapter supports (or requires) things like data types, validations, keys, etc. - // If true, the schema for models using this adapter will be automatically synced when the server starts. + // Set to true if this adapter supports (or requires) things like data + // types, validations, keys, etc. If true, the schema for models using this + // adapter will be automatically synced when the server starts. // Not terribly relevant if not using a non-SQL / non-schema-ed data store - syncable: false, + syncable: true, // Default configuration for collections - // (same effect as if these properties were included at the top level of the model definitions) + // (same effect as if these properties were included at the top level of the + // model definitions) defaults: { // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, // their contents are lost. filename: "", - mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, verbose: false }, + /*************************************************************************/ + /* Public Methods for Sails/Waterline Adapter Compatibility */ + /*************************************************************************/ + + /** + * This method runs when a model is initially registered + * at server-start-time. This is the only required method. + * + * @param {[type]} connection [description] + * @param {[type]} collection [description] + * @param {Function} cb [description] + * @return {[type]} [description] + */ + registerConnection: function(connection, collections, cb) { + //console.log("registering connection for " + connection.identity); + //console.log(cb.toString()) + var self = this; - // This method runs when a model is initially registered at server start time - registerCollection: function(collection, cb) { - var def = _.clone(collection); - var key = def.identity; - var definition = def.definition || {}; - - if (dbs[key]) return cb(); - dbs[key.toString()] = def; - - // Set default primary key as 'id' - var pkName = "id"; - - // Set primary key field - for (var attribute in definition) { - if (!definition[attribute].hasOwnProperty('primaryKey')) continue; - - // Check if custom primaryKey value is false - if (!definition[attribute].primaryKey) continue; - - // Set the pkName to the custom primaryKey value - pkName = attribute; - } + if (!connection.identity) return cb(Errors.IdentityMissing); + if (connections[connection.identity]) return cb(Errors.IdentityDuplicate); - // Set the primary key to the definition object - def.primaryKey = pkName; + connections[connection.identity] = { + config: connection, + collections: collections + }; - // Always call describe - this.describe(key, function(err, schema) { - if (err) return cb(err); - cb(null, schema); - }); + async.map(Object.keys(collections), function(columnName, cb) { + self.describe(connection.identity, columnName, cb); + }, cb); }, - // The following methods are optional - //////////////////////////////////////////////////////////// - - // Optional hook fired when a model is unregistered, typically at server halt - // useful for tearing down remaining open connections, etc. - teardown: function(cb) { + /** + * Fired when a model is unregistered, typically when the server + * is killed. Useful for tearing-down remaining open connections, + * etc. + * + * @param {[type]} connectionId [description] + * @param {Function} cb [description] + * @return {[type]} [description] + */ + teardown: function(connectionId, cb) { + if (!connections[connectionId]) return cb(); + //console.log("Tearing down connection " + connectionId) + delete connections[connectionId]; cb(); }, - - // Raw query interface - query: function(table, query, data, cb) { - if (_.isFunction(data)) { - cb = data; - data = null; - } - - spawnConnection(function __QUERY__(client, cb) { - if (data) client.all(query, data, cb); - client.all(query, cb); - }, dbs[table].config, cb); - }, - - // REQUIRED method if integrating with a schemaful database - define: function(table, definition, cb) { - - var describe = function(err, result) { - if (err) return cb(err); - - adapter.describe(table.replace(/["']/g, ""), cb); - }; - - spawnConnection(function __DEFINE__(client, cb) { - - // Escape table name - table = utils.escapeTable(table); - - // Iterate through each attribute, building a query string - var _schema = utils.buildSchema(definition); - - // Check for any index attributes - var indices = utils.buildIndexes(definition); - - // Build query - var query = 'CREATE TABLE ' + table + ' (' + _schema + ')'; - - // Run the query - client.run(query, function(err) { - if (err) return cb(err); - - // Build indices - function buildIndex(name, cb) { - - // Strip slashes from tablename, used to namespace index - var cleanTable = table.replace(/['"]/g, ''); - - // Build a query to create a namespaced index tableName_key - var query = 'CREATE INDEX ' + cleanTable + '_' + name + ' on ' + table + ' (' + name + ');'; - - // Run query - client.run(query, function(err) { - if (err) return cb(err); - cb(); - }); - } - async.eachSeries(indices, buildIndex, cb); - }); - }, dbs[table].config, describe); - }, - - // REQUIRED method if integrating with a schemaful database - describe: function(table, cb) { + /** + * This method returns attributes and is required when integrating with a + * schemaful database. + * + * @param {[type]} connectionId [description] + * @param {[type]} collection [description] + * @param {[Function]} cb [description] + * @return {[type]} [description] + */ + describe: function(connectionId, table, cb) { var self = this; - spawnConnection(function __DESCRIBE__(client, cb) { + spawnConnection(connectionId, function __DESCRIBE__(client, cb) { - // Get a list of all the tables in this database (see http://www.sqlite.org/faq.html#q7) + var connection = connections[connectionId]; + var collection = connection.collections[table]; + + // Get a list of all the tables in this database + // See: http://www.sqlite.org/faq.html#q7) var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; - + client.get(query, function (err, schema) { if (err || !schema) return cb(); - - // Query to get information about each table (see http://www.sqlite.org/pragma.html#pragma_table_info) + //console.log("client.get") + //console.log(schema); + // Query to get information about each table + // See: http://www.sqlite.org/pragma.html#pragma_table_info var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; // Query to get a list of indices for a given table @@ -170,9 +130,10 @@ module.exports = (function() { var index = { columns: [] }; client.each(indexListQuery, function(err, currentIndex) { - + //console.log(currentIndex) // Query to get information about indices - var indexInfoQuery = 'PRAGMA index_info("' + currentIndex.name + '")'; + var indexInfoQuery = + 'PRAGMA index_info("' + currentIndex.name + '")'; // Retrieve detailed information for given index client.each(indexInfoQuery, function(err, indexedCol) { @@ -185,10 +146,13 @@ module.exports = (function() { client.each(columnsQuery, function(err, column) { - // In SQLite3, AUTOINCREMENT only applies to PK columns of INTEGER type - column.autoIncrement = column.type.toLowerCase() == 'integer' && column.pk == 1; + // In SQLite3, AUTOINCREMENT only applies to PK columns of + // INTEGER type + column.autoIncrement = (column.type.toLowerCase() == 'integer' + && column.pk == 1); - // By default, assume column is not indexed until we find that it is + // By default, assume column is not indexed until we find that it + // is column.indexed = false; // Search for indexed columns @@ -205,57 +169,147 @@ module.exports = (function() { schema.columns.push(column); }, function(err, resultCount) { - - // This callback function is fired when all the columns have been iterated by the .each() function + + // This callback function is fired when all the columns have been + // iterated by the .each() function if (err) { console.error("Error while retrieving column information."); console.error(err); return cb(err); } - + //console.log("schema") + //console.log(schema) var normalizedSchema = utils.normalizeSchema(schema); - - // Set internal schema mapping - dbs[table].schema = normalizedSchema; + //console.log(normalizedSchema); + try { + // Set internal schema mapping + collection.schema = normalizedSchema; + + } catch(e){ + console.log(e); + //console.log(connection.collections[table]); + //console.log(table) + //console.log(connection) + } + // Fire the callback with the normalized schema cb(null, normalizedSchema); }); }); }); - }, dbs[table].config, cb); + }, cb); }, - // REQUIRED method if integrating with a schemaful database - drop: function(table, cb) { - spawnConnection(function __DROP__(client, cb) { + + /** + * Creates a new table in the database when defining a model. + * + * @param {[type]} connectionId [description] + * @param {[type]} table [description] + * @param {[type]} definition [description] + * @param {[Function]} cb [description] + * @return {[type]} [description] + */ + define: function(connectionId, table, definition, cb) { + + var describe = function(err, result) { + if (err) return cb(err); + + adapter.describe(connectionId, table.replace(/["']/g, ""), cb); + }; + + spawnConnection(connectionId, function __DEFINE__(client, cb) { // Escape table name table = utils.escapeTable(table); - // Build query - var query = 'DROP TABLE ' + table; + // Iterate through each attribute, building a query string + var _schema = utils.buildSchema(definition); - // Run the query + // Check for any index attributes + var indices = utils.buildIndexes(definition); + + // Build query + var query = 'CREATE TABLE ' + table + ' (' + _schema + ')'; + //client.on("trace", console.log) + //client.on("profile", console.log) + //console.log(client.run); client.run(query, function(err) { - cb(null, null); + if (err) return cb(err); + + // Build indices + function buildIndex(name, cb) { + + // Strip slashes from tablename, used to namespace index + var cleanTable = table.replace(/['"]/g, ''); + + // Build a query to create a namespaced index tableName_key + var query = 'CREATE INDEX ' + cleanTable + '_' + name + ' on ' + + table + ' (' + name + ');'; + + // Run query + client.run(query, function(err) { + if (err) return cb(err); + cb(); + }); + } + async.eachSeries(indices, buildIndex, cb); }); - }, dbs[table].config, cb); + }, describe); }, - // Optional override of built-in alter logic - // Can be simulated with describe(), define(), and drop(), - // but will probably be made much more efficient by an override here - // alter: function (collectionName, attributes, cb) { - // Modify the schema of a table or collection in the data store - // cb(); - // }, + /** + * Drops a table corresponding to the model. + * + * @param {[type]} connectionId [description] + * @param {[type]} table [description] + * @param {[type]} relations [description] + * @param {[Function]} cb [description] + * @return {[type]} [description] + */ + drop: function(connectionId, table, relations, cb) { + + if (typeof relations == 'function') { + cb = relations; + relations = []; + } + + spawnConnection(connectionId, function __DROP__(client, cb) { + + function dropTable(item, next) { + + // Build query + var query = 'DROP TABLE ' + utils.escapeTable(table); + + // Run the query + client.run(query, function(err) { + cb(null, null); + }); + } + + async.eachSeries(relations, dropTable, function(err) { + if (err) return cb(err); + dropTable(table, cb); + }); + }, cb); + }, + + + /** + * Add a column to the table. + * + * @param {[type]} connectionId [description] + * @param {[type]} table [description] + * @param {[type]} attrName [description] + * @param {[type]} attrDef [description] + * @param {[Function]} cb [description] + * @return {[type]} [description] + */ + addAttribute: function(connectionId, table, attrName, attrDef, cb) { + spawnConnection(connectionId, function __ADD_ATTRIBUTE__(client, cb) { - // Add a column to the table - addAttribute: function(table, attrName, attrDef, cb) { - spawnConnection(function __ADD_ATTRIBUTE__(client, cb) { - // Escape table name table = utils.escapeTable(table); @@ -273,85 +327,119 @@ module.exports = (function() { if (err) return cb(err); cb(null, this); }); - }, dbs[table].config, cb); + }, cb); }, - // Remove attribute from table - // In SQLite3, this is tricky since there's no support for DROP COLUMN - // in ALTER TABLE. We'll have to rename the old table, create a new table - // with the same name minus the column and copy all the data over. - removeAttribute: function(table, attrName, cb) { - spawnConnection(function __REMOVE_ATTRIBUTE__(client, cb) { - - // Escape table name - table = utils.escapeTable(table); + /** + * Remove attribute from table. + * In SQLite3, this is tricky since there's no support for DROP COLUMN + * in ALTER TABLE. We'll have to rename the old table, create a new table + * with the same name minus the column and copy all the data over. + */ + removeAttribute: function(connectionId, table, attrName, cb) { + spawnConnection(connectionId, function __REMOVE_ATTRIBUTE__(client, cb) { + + // NOTE on this method just so I don't forget: Below is a pretty hackish + // way to remove attributes. Proper SQLite way would be to write all of + // the logic below into a single SQL statement wrapped in BEGIN TRANSAC- + // TION and COMMIT block like this: + // + // BEGIN TRANSACTION; + // ALTER TABLE table RENAME TO table_old_; + // CREATE TABLE table(attrName1, ...); + // INSERT INTO table SELECT attrName1, ... FROM table_old_; + // DROP TABLE table_old_; + // COMMIT; + // + // This will ensure that removing attribute would be atomic. For now, + // hacking it cause I'm actually feeling lazy. + + var oldTable = table + '_old_'; // Build query to rename table - var renameQuery = 'ALTER TABLE ' + table + ' RENAME TO ' + table + '_old_'; + var renameQuery = 'ALTER TABLE ' + utils.escapeTable(table) + + ' RENAME TO ' + utils.escapeTable(oldTable); - }, dbs[table].config, cb); - }, + // Run query + client.run(query, function(err) { + if (err) return cb(err); + // Get the attributes + adapter.describe(connectionId, oldTable, function(err, schema) { + if (err) return cb(err); - // REQUIRED method if users expect to call Model.create() or any methods - create: function(table, data, cb) { - spawnConnection(function __CREATE__(client, cb) { + // Deep copy the schema and remove the attribute + var newAttributes = _.clone(schema); + delete newAttributes[attrName]; - // Build a query object - var _query = new Query(dbs[table].definition); + // Recreate the table + adapter.define(connectionId, table, newAttributes, + function (err, schema) { + if (err) return cb(err); - // Escape table name - table = utils.escapeTable(table); + // Copy data back from old table to new table + var copyQuery = 'INSERT INTO ' + utils.escapeTable(table) + + ' SELECT rowid, '; - // Transform the data object into arrays used in parametrized query - var attributes = utils.mapAttributes(data), - columnNames = attributes.keys.join(', '), - paramValues = attributes.params.join(', '); + Object.keys(newAttributes).forEach( + function(colName, idx, columns) { + copyQuery += colName; + if (idx < keys.length) + copyQuery += ', ' + } + ); - // Build query - var insertQuery = 'INSERT INTO ' + table + ' (' + columnNames + ') values (' + paramValues + ')'; - var selectQuery = 'SELECT * FROM ' + table + ' ORDER BY rowid DESC LIMIT 1'; + copyQuery += ' FROM ' + utils.escapeTable(oldTable); - // First insert the values - client.run(insertQuery, attributes.values, function(err) { - if (err) return cb(err); + client.run(copyQuery, function(err) { + if (err) return cb(err); - // Get the last inserted row - client.get(selectQuery, function(err, row) { - if (err) return cb(err); + var dropQuery = 'DROP TABLE ' + utils.escapeTable(oldTable); - var values = _query.cast(row); + client.run(dropQuery, function(err) { + if (err) return cb(err); - cb(null, values); + // End of operation! + cb(); + }); + }); + } + ); }); }); - }, dbs[table].config, cb); + }, cb); }, + + /** + * Finds and returns an instance of a model that matches search criteria. + */ // REQUIRED method if users expect to call Model.find(), Model.findAll() or related methods // You're actually supporting find(), findAll(), and other methods here // but the core will take care of supporting all the different usages. // (e.g. if this is a find(), not a findAll(), it will only send back a single model) - find: function(table, options, cb) { - spawnConnection(function __FIND__(client, cb) { + find: function(connectionId, table, options, cb) { + spawnConnection(connectionId, function __FIND__(client, cb) { // Check if this is an aggregate query and that there's something to return - if (options.groupBy || options.sum || options.average || options.min || options.max) { - if (!options.sum && !options.average && !options.min && !options.max) { - return cb(new Error('Cannot perform groupBy without a calculation')); + if (options.groupBy || options.sum || options.average || options.min || + options.max) { + if (!options.sum && !options.average && !options.min && + !options.max) { + return cb(Errors.InvalidGroupBy); } } - // Build a query object - var _query = new Query(dbs[table].definition); - - // Escape table name - table = utils.escapeTable(table); + var connection = connections[connectionId]; + var collection = connection.collections[table]; + // Grab connection schema + var schema = getSchema(connection.collections); + // Build query - var _schema = dbs[table.replace(/["']/g, "")].schema; - var query = new Query(_schema).find(table, options); + var queryObj = new Query(collection.definition, schema); + var query = queryObj.find(table, options); // Cast special values var values = []; @@ -360,42 +448,96 @@ module.exports = (function() { client.each(query.query, query.values, function(err, row) { if (err) return cb(err); - values.push(_query.cast(row)); + values.push(queryObj.cast(row)); }, function(err, resultCount) { + var _values = options.joins ? utils.group(values) : values; + cb(null, values); }); - }, dbs[table].config, cb); + }, cb); }, - // REQUIRED method if users expect to call Model.update() - update: function(table, options, data, cb) { - spawnConnection(function __UPDATE__(client, cb) { - // Build a query object - var _query = new Query(dbs[table].definition); + /** + * Add a new row to the table + */ + // REQUIRED method if users expect to call Model.create() or any methods + create: function(connectionId, table, data, cb) { + spawnConnection(connectionId, function __CREATE__(client, cb) { + + // Grab Connection Schema + var connection = connections[connectionId]; + var collection = connection.collections[table]; + // Grab connection schema + var schema = getSchema(connection.collections); + + // Build query + var _query = new Query(collection.schema, schema); + + // Escape table name + table = utils.escapeTable(table); + + // Transform the data object into arrays used in parametrized query + var attributes = utils.mapAttributes(data), + columnNames = attributes.keys.join(', '), + paramValues = attributes.params.join(', '); + + + // Build query + var insertQuery = 'INSERT INTO ' + table + ' (' + columnNames + ') values (' + paramValues + ')'; + var selectQuery = 'SELECT * FROM ' + table + ' ORDER BY rowid DESC LIMIT 1'; + + // First insert the values + client.run(insertQuery, attributes.values, function(err) { + if (err) return cb(err); - // Escape table name - table = utils.escapeTable(table); + // Get the last inserted row + client.get(selectQuery, function(err, row) { + if (err) return cb(err); - // Build query - var primaryKey = dbs[table.replace(/["']/g, "")].primaryKey; - var _schema = dbs[table.replace(/["']/g, "")].schema; - var selectQuery = 'SELECT "' + primaryKey + '" FROM ' + table; //new Query(_schema).find(table, options); - var updateQuery = new Query(_schema).update(table, options, data); - var primaryKeys = []; + var values = _query.cast(row); + + cb(null, values); + }); + }); + }, cb); + }, - var criteria = _query._build(options); - selectQuery += ' ' + criteria.query; + // Raw query interface + query: function(table, query, data, cb) { + if (_.isFunction(data)) { + cb = data; + data = null; + } - client.serialize(function() { + spawnConnection(function __QUERY__(client, cb) { + if (data) client.all(query, data, cb); + client.all(query, cb); + }, dbs[table].config, cb); + }, - // Keep track of the row IDs of the rows that will be updated - client.each(selectQuery, criteria.values, function(err, row) { - if (err) return cb(err); - primaryKeys.push(row[primaryKey]); - }); + // REQUIRED method if users expect to call Model.update() + update: function(connectionId, table, options, data, cb) { + spawnConnection(connectionId, function __UPDATE__(client, cb) { + + // Grab Connection Schema + var connection = connections[connectionId]; + var collection = connection.collections[table]; + // Grab connection schema + var schema = getSchema(connection.collections); + + // Build query + var _query = new Query(collection.schema, schema); + + // Build a query for the specific query strategy + var selectQuery = _query.find(table, options); + var updateQuery = _query.update(table, options, data); + var primaryKeys = []; + + client.serialize(function() { + // Run query client.run(updateQuery.query, updateQuery.values, function(err) { if (err) { console.error(err); return cb(err); } @@ -403,27 +545,16 @@ module.exports = (function() { // Build a query to return updated rows if (this.changes > 0) { - // Build criteria - var criteria; - if (this.changes == 1) { - criteria = { where: {}, limit: 1 }; - criteria.where[primaryKey] = primaryKeys[0]; - } else { - criteria = { where: {} }; - criteria.where[primaryKey] = primaryKeys; - } - - // Return the updated items up the callback chain - adapter.find(table.replace(/["']/g, ""), criteria, function(err, models) { - if (err) { console.log(models); return cb(err); } - - var values = []; + adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { + if (err) { console.error(err); return cb(err); } + //console.log(arguments) + var values = []; - models.forEach(function(item) { - values.push(_query.cast(item)); - }); + models.forEach(function(item) { + values.push(_query.cast(item)); + }); - cb(null, values); + cb(null, values); }); } else { console.error('WARNING: No rows updated.'); @@ -431,39 +562,42 @@ module.exports = (function() { } }); }); - }, dbs[table].config, cb); + }, cb); }, // REQUIRED method if users expect to call Model.destroy() - destroy: function(table, options, cb) { - spawnConnection(function __DELETE__(client, cb) { - - // Build a query object - var _query = new Query(dbs[table].definition); - - // Escape table name - table = utils.escapeTable(table); - - // Build query - var _schema = dbs[table.replace(/["']/g, "")].schema; - var deleteQuery = new Query(_schema).destroy(table, options); - - // Run query - adapter.find(table.replace(/["']/g, ""), options, function(err, models) { - if (err) { console.log(err); return cb(err); } - - var values = []; - - models.forEach(function(model) { - values.push(_query.cast(model)); - }); - - client.run(deleteQuery.query, deleteQuery.values, function(err) { - if (err) return cb(err); - cb(null, values); - }); - }); - }, dbs[table].config, cb); + destroy: function(connectionId, table, options, cb) { + + spawnConnection(connectionId, function __DELETE__(client, cb) { + + var connection = connections[connectionId]; + var collection = connection.collections[table]; + //console.log("definition: " + JSON.stringify(collection.definition)) + var _schema = utils.buildSchema(collection.definition); + + // Build a query for the specific query strategy + var _query = new Query(_schema); + var query = _query.destroy(table, options); + + // Run Query + adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { + //if (err) { console.log(err); return cb(err); } + + //console.log("adapter.find") + //console.log(arguments) + var values = []; + models.forEach(function(model) { + values.push(_query.cast(model)); + }); + + client.run(query.query, query.values, function __DELETE__(err, result) { + //console.log(arguments) + if(err) return cb(handleQueryError(err)); + cb(null, values); + }); + }); + + }, cb); }, @@ -508,10 +642,6 @@ module.exports = (function() { * Optional overrides ********************************************** - // Optional override of built-in batch create logic for increased efficiency - // otherwise, uses create() - createEach: function (collectionName, cb) { cb(); }, - // Optional override of built-in findOrCreate logic for increased efficiency // otherwise, uses find() and create() findOrCreate: function (collectionName, cb) { cb(); }, @@ -539,7 +669,7 @@ module.exports = (function() { // The first argument of callbacks is always an error object. // For some core methods, Sails.js will add support for .done()/promise usage. // - // + + // + // //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -574,26 +704,47 @@ module.exports = (function() { */ }; - ////////////// ////////////////////////////////////////// - ////////////// Private Methods ////////////////////////////////////////// - ////////////// ////////////////////////////////////////// - function spawnConnection(logic, config, cb) { - // Check if we want to run in verbose mode - // Note that once you go verbose, you can't go back (see https://github.com/mapbox/node-sqlite3/wiki/API) - if (config.verbose) sqlite3 = sqlite3.verbose(); + /***************************************************************************/ + /* Private Methods + /***************************************************************************/ + function getSchema(collections){ + var schema = {}; + Object.keys(collections).forEach(function(collectionId) { + schema[collectionId] = collections[collectionId].schema; + }); + return schema; + } + + function spawnConnection(connectionName, logic, cb) { + //console.log("spawnConnection") + //console.log("connectionName " + connectionName) + //console.log(connections) + var connectionObject = connections[connectionName]; + if (!connectionObject) return cb(Errors.InvalidConnection); + + var connectionConfig = connectionObject.config; + + // Check if we want to run in verbose mode + // Note that once you go verbose, you can't go back. + // See: https://github.com/mapbox/node-sqlite3/wiki/API + if (connectionConfig.verbose) sqlite3 = sqlite3.verbose(); + // Make note whether the database already exists - exists = fs.existsSync(config.filename); + exists = fs.existsSync(connectionConfig.filename); // Create a new handle to our database - var client = new sqlite3.Database(config.filename, config.mode, function(err) { - after(err, client); - }); - + var client = new sqlite3.Database( + connectionConfig.filename, + connectionConfig.mode, + function(err) { + after(err, client); + } + ); + function after(err, client) { if (err) { - console.error("Error creating/opening SQLite3 database."); - console.error(err); + console.error("Error creating/opening SQLite3 database: " + err); // Close the db instance on error if (client) client.close(); @@ -603,13 +754,13 @@ module.exports = (function() { // Run the logic logic(client, function(err, result) { + // Close db instance after it's done running client.close(); - return cb(err, result); }); } } return adapter; -})(); \ No newline at end of file +})(); diff --git a/lib/query.js b/lib/query.js index babc0b0..d010b14 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2,8 +2,9 @@ * Dependencies */ -var _ = require('underscore'), - utils = require('./utils'); +var _ = require('lodash'), + utils = require('./utils'), + hop = utils.object.hasOwnProperty; /** * Query Builder for creating parameterized queries for use @@ -15,13 +16,17 @@ var _ = require('underscore'), * If you have any questions, contact Andrew Jo */ -var Query = function(schema) { +var Query = function(schema, tableDefs) { + //console.log("new query"); + //console.log(arguments); + this._values = []; this._paramCount = 1; this._query = ''; + this._tableDefs = tableDefs || {}; this._schema = _.clone(schema); - + return this; }; @@ -30,7 +35,17 @@ var Query = function(schema) { */ Query.prototype.find = function(table, criteria) { - this._query = utils.buildSelectStatement(criteria, table); + + // Normalize joins key, allows use of both join and joins + if (criteria.join) { + criteria.joins = _.cloneDeep(criteria.join); + delete criteria.join; + } + //console.log("query.find") + //console.log(arguments); + //console.log(this); +// console.log(abort0) + this._query = utils.buildSelectStatement(criteria, table, this._schema, this._tableDefs); if (criteria) this._build(criteria); @@ -63,10 +78,9 @@ Query.prototype.update = function(table, criteria, data) { str = str.slice(0, -2); this._query += 'SET ' + str + ' '; - + // Add data values to this._values - this._values = this._values.concat(attributes.values); - + this._values = attributes.values; // Build criteria clause if (criteria) this._build(criteria); @@ -98,7 +112,7 @@ Query.prototype.destroy = function(table, criteria) { Query.prototype._build = function(criteria) { var self = this; - + // Ensure criteria keys are in correct order var orderedCriteria = {}; if (criteria.where) orderedCriteria.where = criteria.where; @@ -175,7 +189,9 @@ Query.prototype._build = function(criteria) { Query.prototype.where = function(options) { var self = this, operators = this.operators(); - + + //console.log("options " + JSON.stringify(options)); + if (!options) return; // Begin WHERE query @@ -263,7 +279,7 @@ Query.prototype.operators = function() { var caseSensitive = true; // Check if key is a string - if (self._schema[key].type === 'text') caseSensitive = false; + if (self._schema[key] && self._schema[key].type === 'text') caseSensitive = false; processCriteria.call(self, key, options, '=', caseSensitive); self._query += (comparator || ' AND '); @@ -513,10 +529,10 @@ Query.prototype.cast = function(values) { _values = _.clone(values); Object.keys(values).forEach(function(key) { - + + if(!self._schema[key]) return; // Lookup schema type - var type = self._schema[key].type; - if(!type) return; + var type = self._schema[key].type; // Attempt to parse Array if(type === 'array') { @@ -532,4 +548,4 @@ Query.prototype.cast = function(values) { return _values; }; -module.exports = Query; \ No newline at end of file +module.exports = Query; diff --git a/lib/utils.js b/lib/utils.js index 01caf7d..d1eee55 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -8,7 +8,7 @@ var utils = module.exports = {}; utils.buildSchema = function(obj) { var schema = ""; - + // Iterate through the Object Keys and build a string Object.keys(obj).forEach(function(key) { var attr = {}; @@ -45,26 +45,62 @@ utils.buildSchema = function(obj) { return schema.slice(0, -2); }; +/** +* Safe hasOwnProperty +*/ +utils.object = {}; +/** +* Safer helper for hasOwnProperty checks +* +* @param {Object} obj +* @param {String} prop +* @return {Boolean} +* @api public +*/ +var hop = Object.prototype.hasOwnProperty; + utils.object.hasOwnProperty = function(obj, prop) { + return hop.call(obj, prop); +}; + +/** +* Escape Name +* +* Wraps a name in quotes to allow reserved +* words as table or column names such as user. +*/ +function escapeName(name) { + return '"' + name + '"'; +} +utils.escapeName = escapeName; /** * Builds a Select statement determining if Aggeragate options are needed. */ -utils.buildSelectStatement = function(criteria, table) { +utils.buildSelectStatement = function(criteria, table, attributes, schema) { + //console.log("buildSelectStatement") + //console.log(arguments); + //abort0; var query = ''; - if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || criteria.max) { + // Escape table name + var schemaName = criteria._schemaName ? utils.escapeName(criteria._schemaName) + '.' : ''; + var tableName = schemaName + utils.escapeName(table); + + if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || + criteria.max) { + query = 'SELECT rowid, '; // Append groupBy columns to select statement if(criteria.groupBy) { if(criteria.groupBy instanceof Array) { criteria.groupBy.forEach(function(opt){ - query += opt + ', '; + query += tableName + '.' + utils.escapeName(opt) + ', '; }); } else { - query += criteria.groupBy + ', '; + query += tableName + '.' + utils.escapeName(criteria.groupBy) + ', '; } } @@ -72,11 +108,14 @@ utils.buildSelectStatement = function(criteria, table) { if (criteria.sum) { if(criteria.sum instanceof Array) { criteria.sum.forEach(function(opt){ - query += 'CAST(SUM(' + opt + ') AS float) AS ' + opt + ', '; + query += 'CAST(SUM(' + tableName + '.' + utils.escapeName(opt) + + ') AS float) AS ' + opt + ', '; }); } else { - query += 'CAST(SUM(' + criteria.sum + ') AS float) AS ' + criteria.sum + ', '; + query += 'CAST(SUM(' + tableName + '.' + + utils.escapeName(criteria.sum) + ') AS float) AS ' + criteria.sum + + ', '; } } @@ -84,11 +123,14 @@ utils.buildSelectStatement = function(criteria, table) { if (criteria.average) { if(criteria.average instanceof Array) { criteria.average.forEach(function(opt){ - query += 'CAST(AVG(' + opt + ') AS float) AS ' + opt + ', '; + query += 'CAST(AVG(' + tableName + '.' + utils.escapeName(opt) + + ') AS float) AS ' + opt + ', '; }); } else { - query += 'CAST(AVG(' + criteria.average + ') AS float) AS ' + criteria.average + ', '; + query += 'CAST(AVG(' + tableName + '.' + + utils.escapeName(criteria.average) + ') AS float) AS ' + + criteria.average + ', '; } } @@ -96,11 +138,13 @@ utils.buildSelectStatement = function(criteria, table) { if (criteria.max) { if(criteria.max instanceof Array) { criteria.max.forEach(function(opt){ - query += 'MAX(' + opt + ') AS ' + opt + ', '; + query += 'MAX(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + + opt + ', '; }); } else { - query += 'MAX(' + criteria.max + ') AS ' + criteria.max + ', '; + query += 'MAX(' + tableName + '.' + utils.escapeName(criteria.max) + + ') AS ' + criteria.max + ', '; } } @@ -108,11 +152,13 @@ utils.buildSelectStatement = function(criteria, table) { if (criteria.min) { if(criteria.min instanceof Array) { criteria.min.forEach(function(opt){ - query += 'MIN(' + opt + ') AS ' + opt + ', '; + query += 'MIN(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + + opt + ', '; }); } else { - query += 'MIN(' + criteria.min + ') AS ' + criteria.min + ', '; + query += 'MIN(' + tableName + '.' + utils.escapeName(criteria.min) + ') AS ' + + criteria.min + ', '; } } @@ -123,8 +169,82 @@ utils.buildSelectStatement = function(criteria, table) { return query += 'FROM ' + table + ' '; } - // Else select ALL - return 'SELECT rowid, * FROM ' + table + ' '; + + query += 'SELECT rowid, '; + + var selectKeys = [], joinSelectKeys = []; + //console.log("table: " + table); + //console.log("schema " + JSON.stringify(schema)); + //console.log("table", table, "schema", schema); + Object.keys(schema[table]).forEach(function(key) { + selectKeys.push({ table: table, key: key }); + }); + + // Check for joins + if (criteria.joins) { + + var joins = criteria.joins; + + joins.forEach(function(join) { + if (!join.select) return; + + Object.keys( + schema[join.child.toLowerCase()].schema + ).forEach(function(key) { + var _join = _.cloneDeep(join); + _join.key = key; + joinSelectKeys.push(_join); + }); + + // Remove the foreign key for this join from the selectKeys array + selectKeys = selectKeys.filter(function(select) { + var keep = true; + if (select.key === join.parentKey && join.removeParentKey) keep = false; + return keep; + }); + }); + } + + // Add all the columns to be selected that are not joins + selectKeys.forEach(function(select) { + query += utils.escapeName(select.table) + '.' + utils.escapeName(select.key) + ', '; + }); + + // Add all the columns from the joined tables + joinSelectKeys.forEach(function(select) { + + // Create an alias by prepending the child table with the alias of the join + var alias = select.alias.toLowerCase() + '_' + select.child.toLowerCase(); + + // If this is a belongs_to relationship, keep the foreign key name from the + // AS part of the query. This will result in a selected column like: + // "user"."id" AS "user_id__id" + if (select.model) { + return query += utils.escapeName(alias) + '.' + + utils.escapeName(select.key) + ' AS ' + + utils.escapeName(select.parentKey + '__' + select.key) + ', '; + } + + // If a junctionTable is used, the child value should be used in the AS part + // of the select query. + if (select.junctionTable) { + return query += utils.escapeName(alias) + '.' + + utils.escapeName(select.key) + ' AS ' + + utils.escapeName(select.alias + '_' + select.child + '__' + select.key) + + ', '; + } + + // Else if a hasMany attribute is being selected, use the alias plus the + // child. + return query += utils.escapeName(alias) + '.' + utils.escapeName(select.key) + + ' AS ' + utils.escapeName(select.alias + '_' + select.child + '__' + + select.key) + ', '; + }); + + // Remove the last comma + query = query.slice(0, -2) + ' FROM ' + tableName + ' '; + + return query; }; @@ -140,11 +260,95 @@ utils.buildIndexes = function(obj) { Object.keys(obj).forEach(function(key) { if (obj[key].hasOwnProperty('index')) indexes.push(key); }); - + return indexes; }; +/** + * Group Results into an Array + * + * Groups values returned from an association query into a single result. + * For each collection association the object returned should have an array + * under the user defined key with the joined results. + * + * @param {Array} results returned from a query + * @return {Object} a single values object + */ +utils.group = function(values) { + + var self = this; + var joinKeys = []; + var _value; + + if (!Array.isArray(values)) return values; + + // Grab all the keys needed to be grouped + var associationKeys = []; + + values.forEach(function(value) { + Object.keys(value).forEach(function(key) { + key = key.split('__'); + if (key.length === 2) associationKeys.push(key[0].toLowerCase()); + }); + }); + + associationKeys = _.unique(associationKeys); + + // Store the values to be grouped by id + var groupings = {}; + + values.forEach(function(value) { + + // Add to groupings + if (!groupings[value.id]) groupings[value.id] = {}; + + associationKeys.forEach(function(key) { + if(!Array.isArray(groupings[value.id][key])) + groupings[value.id][key] = []; + var props = {}; + + Object.keys(value).forEach(function(prop) { + var attr = prop.split('__'); + if (attr.length === 2 && attr[0] === key) { + props[attr[1]] = value[prop]; + delete value[prop]; + } + }); + + // Don't add empty records that come from a left join + var empty = true; + + Object.keys(props).forEach(function(prop) { + if (props[prop] !== null) empty = false; + }); + + if (!empty) groupings[value.id][key].push(props); + }); + }); + + var _values = []; + + values.forEach(function(value) { + var unique = true; + + _values.forEach(function(_value) { + if (_value.id === value.id) unique = false; + }); + + if (!unique) return; + + Object.keys(groupings[value.id]).forEach(function(key) { + value[key] = _.uniq(groupings[value.id][key], 'id'); + }); + + _values.push(value); + }); + + return _values; +}; + + /** * Escape Table Name * @@ -280,4 +484,4 @@ utils.sqlTypeCast = function(type) { utils.toSqlDate = function(date) { return date.toUTCString(); -}; \ No newline at end of file +}; From bbf1e1df2e5e5abc89d2a48534f63881fdb076e4 Mon Sep 17 00:00:00 2001 From: Richard Pinedo Date: Mon, 29 Jan 2018 10:03:13 -0500 Subject: [PATCH 38/81] removed all comments and unused code --- lib/adapter.js | 96 ++------------------------------------------------ lib/query.js | 10 +----- lib/utils.js | 6 ---- 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index c9810aa..327916e 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -58,8 +58,6 @@ module.exports = (function() { * @return {[type]} [description] */ registerConnection: function(connection, collections, cb) { - //console.log("registering connection for " + connection.identity); - //console.log(cb.toString()) var self = this; if (!connection.identity) return cb(Errors.IdentityMissing); @@ -87,7 +85,6 @@ module.exports = (function() { */ teardown: function(connectionId, cb) { if (!connections[connectionId]) return cb(); - //console.log("Tearing down connection " + connectionId) delete connections[connectionId]; cb(); }, @@ -115,8 +112,6 @@ module.exports = (function() { client.get(query, function (err, schema) { if (err || !schema) return cb(); - //console.log("client.get") - //console.log(schema); // Query to get information about each table // See: http://www.sqlite.org/pragma.html#pragma_table_info var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; @@ -130,7 +125,6 @@ module.exports = (function() { var index = { columns: [] }; client.each(indexListQuery, function(err, currentIndex) { - //console.log(currentIndex) // Query to get information about indices var indexInfoQuery = 'PRAGMA index_info("' + currentIndex.name + '")'; @@ -177,19 +171,13 @@ module.exports = (function() { console.error(err); return cb(err); } - //console.log("schema") - //console.log(schema) var normalizedSchema = utils.normalizeSchema(schema); - //console.log(normalizedSchema); try { // Set internal schema mapping collection.schema = normalizedSchema; } catch(e){ console.log(e); - //console.log(connection.collections[table]); - //console.log(table) - //console.log(connection) } @@ -232,9 +220,7 @@ module.exports = (function() { // Build query var query = 'CREATE TABLE ' + table + ' (' + _schema + ')'; - //client.on("trace", console.log) - //client.on("profile", console.log) - //console.log(client.run); + client.run(query, function(err) { if (err) return cb(err); @@ -547,7 +533,6 @@ module.exports = (function() { adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { if (err) { console.error(err); return cb(err); } - //console.log(arguments) var values = []; models.forEach(function(item) { @@ -572,7 +557,6 @@ module.exports = (function() { var connection = connections[connectionId]; var collection = connection.collections[table]; - //console.log("definition: " + JSON.stringify(collection.definition)) var _schema = utils.buildSchema(collection.definition); // Build a query for the specific query strategy @@ -581,17 +565,14 @@ module.exports = (function() { // Run Query adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { - //if (err) { console.log(err); return cb(err); } - - //console.log("adapter.find") - //console.log(arguments) + if (err) { return cb(err); } + var values = []; models.forEach(function(model) { values.push(_query.cast(model)); }); client.run(query.query, query.values, function __DELETE__(err, result) { - //console.log(arguments) if(err) return cb(handleQueryError(err)); cb(null, values); }); @@ -634,74 +615,6 @@ module.exports = (function() { }); }); } - - - - /* - ********************************************** - * Optional overrides - ********************************************** - - // Optional override of built-in findOrCreate logic for increased efficiency - // otherwise, uses find() and create() - findOrCreate: function (collectionName, cb) { cb(); }, - - // Optional override of built-in batch findOrCreate logic for increased efficiency - // otherwise, uses findOrCreate() - findOrCreateEach: function (collectionName, cb) { cb(); } - */ - - - /* - ********************************************** - * Custom methods - ********************************************** - - //////////////////////////////////////////////////////////////////////////////////////////////////// - // - // > NOTE: There are a few gotchas here you should be aware of. - // - // + The collectionName argument is always prepended as the first argument. - // This is so you can know which model is requesting the adapter. - // - // + All adapter functions are asynchronous, even the completely custom ones, - // and they must always include a callback as the final argument. - // The first argument of callbacks is always an error object. - // For some core methods, Sails.js will add support for .done()/promise usage. - // - // + - // - //////////////////////////////////////////////////////////////////////////////////////////////////// - - - // Any other methods you include will be available on your models - foo: function (collectionName, cb) { - cb(null,"ok"); - }, - bar: function (collectionName, baz, watson, cb) { - cb("Failure!"); - } - - - // Example success usage: - - Model.foo(function (err, result) { - if (err) console.error(err); - else console.log(result); - - // outputs: ok - }) - - // Example error usage: - - Model.bar(235, {test: 'yes'}, function (err, result){ - if (err) console.error(err); - else console.log(result); - - // outputs: Failure! - }) - - */ }; /***************************************************************************/ @@ -717,9 +630,6 @@ module.exports = (function() { } function spawnConnection(connectionName, logic, cb) { - //console.log("spawnConnection") - //console.log("connectionName " + connectionName) - //console.log(connections) var connectionObject = connections[connectionName]; if (!connectionObject) return cb(Errors.InvalidConnection); diff --git a/lib/query.js b/lib/query.js index d010b14..4efae4f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -17,9 +17,6 @@ var _ = require('lodash'), */ var Query = function(schema, tableDefs) { - //console.log("new query"); - //console.log(arguments); - this._values = []; this._paramCount = 1; this._query = ''; @@ -41,10 +38,7 @@ Query.prototype.find = function(table, criteria) { criteria.joins = _.cloneDeep(criteria.join); delete criteria.join; } - //console.log("query.find") - //console.log(arguments); - //console.log(this); -// console.log(abort0) + this._query = utils.buildSelectStatement(criteria, table, this._schema, this._tableDefs); if (criteria) this._build(criteria); @@ -190,8 +184,6 @@ Query.prototype.where = function(options) { var self = this, operators = this.operators(); - //console.log("options " + JSON.stringify(options)); - if (!options) return; // Begin WHERE query diff --git a/lib/utils.js b/lib/utils.js index d1eee55..4d97bbc 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -78,9 +78,6 @@ utils.escapeName = escapeName; */ utils.buildSelectStatement = function(criteria, table, attributes, schema) { - //console.log("buildSelectStatement") - //console.log(arguments); - //abort0; var query = ''; // Escape table name @@ -173,9 +170,6 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { query += 'SELECT rowid, '; var selectKeys = [], joinSelectKeys = []; - //console.log("table: " + table); - //console.log("schema " + JSON.stringify(schema)); - //console.log("table", table, "schema", schema); Object.keys(schema[table]).forEach(function(key) { selectKeys.push({ table: table, key: key }); }); From b9230bcac998ec5a81247779f418c8e5e055bf86 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 1 Dec 2018 10:25:51 -0500 Subject: [PATCH 39/81] Upgrading sqlite3 to avoid windows install error Old version of sqlite3 required dll on Windows --- package-lock.json | 1130 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 7 +- 2 files changed, 1132 insertions(+), 5 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..08365f3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1130 @@ +{ + "name": "sails-sqlite3", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "anchor": { + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/anchor/-/anchor-0.9.13.tgz", + "integrity": "sha1-K+SteA3SCo78vnnMFUWksMaxBEE=", + "dev": true, + "requires": { + "async": "0.2.10", + "lodash": "~2.4.1", + "validator": "~3.3.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "0.2.10", + "resolved": "http://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chownr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "2.0.3", + "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", + "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "mkdirp": "0.3.0" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "~1.37.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", + "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.10.0", + "resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, + "needle": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", + "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + }, + "npm-packlist": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", + "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", + "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U=", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "requires": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "requires": { + "should-type": "^1.4.0" + } + }, + "should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", + "dev": true + }, + "should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "requires": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "should-util": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", + "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "sqlite3": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", + "integrity": "sha512-CO8vZMyUXBPC+E3iXOCc7Tz2pAdq5BWfLcQmOokCOZW5S5sZ/paijiPOCdvzpdP83RroWHYa5xYlVqCxSqpnQg==", + "requires": { + "nan": "~2.10.0", + "node-pre-gyp": "^0.10.3", + "request": "^2.87.0" + } + }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "underscore": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", + "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "validator": { + "version": "3.3.0", + "resolved": "http://registry.npmjs.org/validator/-/validator-3.3.0.tgz", + "integrity": "sha1-GCIZTgpGsR+MI7GLwTDvxWCtTYc=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "waterline": { + "version": "0.9.16", + "resolved": "http://registry.npmjs.org/waterline/-/waterline-0.9.16.tgz", + "integrity": "sha1-khsI7pXRV5C9aQsYGuilJNAQSHQ=", + "dev": true, + "requires": { + "anchor": "~0.9.12", + "async": "~0.2.9", + "q": "~0.9.7", + "underscore": "~1.5.2" + } + }, + "waterline-adapter-tests": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/waterline-adapter-tests/-/waterline-adapter-tests-0.9.4.tgz", + "integrity": "sha1-wXBBIoCLK2PMMfLvLa6GWbsHHuU=", + "dev": true, + "requires": { + "mocha": "~1.13.0", + "underscore": "~1.5.2", + "waterline": "~0.9.8" + }, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "dev": true + }, + "glob": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", + "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "dev": true, + "requires": { + "graceful-fs": "~2.0.0", + "inherits": "2", + "minimatch": "~0.2.11" + } + }, + "growl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", + "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "mkdirp": { + "version": "0.3.5", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "dev": true + }, + "mocha": { + "version": "1.13.0", + "resolved": "http://registry.npmjs.org/mocha/-/mocha-1.13.0.tgz", + "integrity": "sha1-jY+k4xC5TMbv6z7SauypbeqTMHw=", + "dev": true, + "requires": { + "commander": "0.6.1", + "debug": "*", + "diff": "1.0.7", + "glob": "3.2.3", + "growl": "1.7.x", + "jade": "0.26.3", + "mkdirp": "0.3.5" + } + } + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } +} diff --git a/package.json b/package.json index a60f14e..d1c496d 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Boilerplate adapter for Sails.js", "main": "lib/adapter", "scripts": { - "test": "echo \"Adapter should be tested using Sails.js core.\" && exit 1" + "test": "make test" }, "repository": { "type": "git", @@ -22,15 +22,12 @@ "readmeFilename": "README.md", "dependencies": { "async": "~0.2.9", - "sqlite3": "~2.1.x", + "sqlite3": "~4.0.x", "underscore": "1.5.2" }, "devDependencies": { "mocha": "*", "should": "*", "waterline-adapter-tests": "~0.9.4" - }, - "scripts": { - "test": "make test" } } From 73920235d2ce150fdb48e99291c7022c31cd39f9 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 1 Dec 2018 11:54:58 -0500 Subject: [PATCH 40/81] Add waterline-errors to dependencies --- package-lock.json | 5 +++++ package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 08365f3..1d92c00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1108,6 +1108,11 @@ } } }, + "waterline-errors": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/waterline-errors/-/waterline-errors-0.10.1.tgz", + "integrity": "sha1-7mNjKq3emTJxt1FLfKmNn9W4ai4=" + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index d1c496d..342b4ca 100755 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "dependencies": { "async": "~0.2.9", "sqlite3": "~4.0.x", - "underscore": "1.5.2" + "underscore": "1.5.2", + "waterline-errors": "^0.10.1" }, "devDependencies": { "mocha": "*", From 5f7977fd320afe2845df7f1e920fd036847a8be9 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 1 Dec 2018 12:40:30 -0500 Subject: [PATCH 41/81] updating package.json for sails 1.x --- package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/package.json b/package.json index 342b4ca..43db172 100755 --- a/package.json +++ b/package.json @@ -30,5 +30,15 @@ "mocha": "*", "should": "*", "waterline-adapter-tests": "~0.9.4" + }, + "sails": { + "adapter": { + "sailsVersion": "^1.0.0", + "implements": [ + "semantic", + "queryable", + "migratable" + ] + } } } From 6e03d90fe2b025e6bebeaf81d157fecf259ccd6c Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 1 Dec 2018 15:06:25 -0500 Subject: [PATCH 42/81] start refactor for waterline adapter api version 1 --- index.js | 857 ++++++++++++++++++++++++++++++++++++++++++++++ lib/query.js | 16 +- package-lock.json | 5 + package.json | 4 +- 4 files changed, 873 insertions(+), 9 deletions(-) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..4b21dbe --- /dev/null +++ b/index.js @@ -0,0 +1,857 @@ +/* eslint-disable prefer-arrow-callback */ +/*--------------------------------------------------------------- + :: sails-sqlite3 + -> adapter + + Code refactored for Sails 1.0.0 release +---------------------------------------------------------------*/ +/** + * Module dependencies + */ + +const fs = require('fs'); + +const _ = require('@sailshq/lodash'); +const sqlite3 = require('sqlite3'); + +const Query = require('./query'); +const utils = require('./utils'); + + +/** + * Module state + */ + +// Private var to track of all the datastores that use this adapter. In order for your adapter +// to be able to connect to the database, you'll want to expose this var publicly as well. +// (See the `registerDatastore()` method for info on the format of each datastore entry herein.) +// +// > Note that this approach of process global state will be changing in an upcoming version of +// > the Waterline adapter spec (a breaking change). But if you follow the conventions laid out +// > below in this adapter template, future upgrades should be a breeze. +var registeredDatastores = {}; + + +/** + * sails-sqlite3 + * + * Expose the adapater definition. + * + * > Most of the methods below are optional. + * > + * > If you don't need / can't get to every method, just implement + * > what you have time for. The other methods will only fail if + * > you try to call them! + * > + * > For many adapters, this file is all you need. For very complex adapters, you may need more flexiblity. + * > In any case, it's probably a good idea to start with one file and refactor only if necessary. + * > If you do go that route, it's conventional in Node to create a `./lib` directory for your private submodules + * > and `require` them at the top of this file with other dependencies. e.g.: + * > ``` + * > var updateMethod = require('./lib/update'); + * > ``` + * + * @type {Dictionary} + */ +module.exports = { + + + // The identity of this adapter, to be referenced by datastore configurations in a Sails app. + identity: 'sqlite-3', + + + // Waterline Adapter API Version + // + // > Note that this is not necessarily tied to the major version release cycle of Sails/Waterline! + // > For example, Sails v1.5.0 might generate apps which use sails-hook-orm@2.3.0, which might + // > include Waterline v0.13.4. And all those things might rely on version 1 of the adapter API. + // > But Waterline v0.13.5 might support version 2 of the adapter API!! And while you can generally + // > trust semantic versioning to predict/understand userland API changes, be aware that the maximum + // > and/or minimum _adapter API version_ supported by Waterline could be incremented between major + // > version releases. When possible, compatibility for past versions of the adapter spec will be + // > maintained; just bear in mind that this is a _separate_ number, different from the NPM package + // > version. sails-hook-orm verifies this adapter API version when loading adapters to ensure + // > compatibility, so you should be able to rely on it to provide a good error message to the Sails + // > applications which use this adapter. + adapterApiVersion: 1, + + + // Default datastore configuration. + defaults: { + // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an + // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, + // their contents are lost. + filename: "", + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, + verbose: false + }, + + + // ╔═╗═╗ ╦╔═╗╔═╗╔═╗╔═╗ ┌─┐┬─┐┬┬ ┬┌─┐┌┬┐┌─┐ + // ║╣ ╔╩╦╝╠═╝║ ║╚═╗║╣ ├─┘├┬┘│└┐┌┘├─┤ │ ├┤ + // ╚═╝╩ ╚═╩ ╚═╝╚═╝╚═╝ ┴ ┴└─┴ └┘ ┴ ┴ ┴ └─┘ + // ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐┌─┐ + // ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ └─┐ + // ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘└─┘ + // This allows outside access to this adapter's internal registry of datastore entries, + // for use in datastore methods like `.leaseConnection()`. + datastores: registeredDatastores, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██╗ ██╗███████╗███████╗ ██████╗██╗ ██╗ ██████╗██╗ ███████╗ // + // ██║ ██║██╔════╝██╔════╝██╔════╝╚██╗ ██╔╝██╔════╝██║ ██╔════╝ // + // ██║ ██║█████╗ █████╗ ██║ ╚████╔╝ ██║ ██║ █████╗ // + // ██║ ██║██╔══╝ ██╔══╝ ██║ ╚██╔╝ ██║ ██║ ██╔══╝ // + // ███████╗██║██║ ███████╗╚██████╗ ██║ ╚██████╗███████╗███████╗ // + // ╚══════╝╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚══════╝ // + // // + // Lifecycle adapter methods: // + // Methods related to setting up and tearing down; registering/un-registering datastores. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * ╦═╗╔═╗╔═╗╦╔═╗╔╦╗╔═╗╦═╗ ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐ + * ╠╦╝║╣ ║ ╦║╚═╗ ║ ║╣ ╠╦╝ ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ + * ╩╚═╚═╝╚═╝╩╚═╝ ╩ ╚═╝╩╚═ ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘ + * Register a new datastore with this adapter. This usually involves creating a new + * connection manager (e.g. MySQL pool or MongoDB client) for the underlying database layer. + * + * > Waterline calls this method once for every datastore that is configured to use this adapter. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Dictionary} datastoreConfig Dictionary (plain JavaScript object) of configuration options for this datastore (e.g. host, port, etc.) + * @param {Dictionary} physicalModelsReport Experimental: The physical models using this datastore (keyed by "tableName"-- NOT by `identity`!). This may change in a future release of the adapter spec. + * @property {Dictionary} * [Info about a physical model using this datastore. WARNING: This is in a bit of an unusual format.] + * @property {String} primaryKey [the name of the primary key attribute (NOT the column name-- the attribute name!)] + * @property {Dictionary} definition [the physical-layer report from waterline-schema. NOTE THAT THIS IS NOT A NORMAL MODEL DEF!] + * @property {String} tableName [the model's `tableName` (same as the key this is under, just here for convenience)] + * @property {String} identity [the model's `identity`] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done A callback to trigger after successfully registering this datastore, or if an error is encountered. + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + registerDatastore: function (datastoreConfig, physicalModelsReport, done) { + + // Grab the unique name for this datastore for easy access below. + var datastoreName = datastoreConfig.identity; + + // Some sanity checks: + if (!datastoreName) { + return done(new Error('Consistency violation: A datastore should contain an "identity" property: a special identifier that uniquely identifies it across this app. This should have been provided by Waterline core! If you are seeing this message, there could be a bug in Waterline, or the datastore could have become corrupted by userland code, or other code in this adapter. If you determine that this is a Waterline bug, please report this at https://sailsjs.com/bugs.')); + } + if (registeredDatastores[datastoreName]) { + return done(new Error('Consistency violation: Cannot register datastore: `' + datastoreName + '`, because it is already registered with this adapter! This could be due to an unexpected race condition in userland code (e.g. attempting to initialize Waterline more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // To maintain the spirit of this repository, this implementation will + // continue to spin up and tear down a connection to the Sqlite db on every + // request. + // TODO: Consider creating the connection and maintaining through the life + // of the sails app. (This would lock it from changes outside sails) + registeredDatastores[datastoreName] = { + config: datastoreConfig, + manager: { + physicalModelsReport, + schemas: {} + }, //temporarily store this here until I know what to do with it... + driver: undefined // << TODO: include driver here (if relevant) + }; + + return done(); + + }, + + + /** + * ╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╗╔ + * ║ ║╣ ╠═╣╠╦╝ ║║║ ║║║║║║║ + * ╩ ╚═╝╩ ╩╩╚══╩╝╚═╝╚╩╝╝╚╝ + * Tear down (un-register) a datastore. + * + * Fired when a datastore is unregistered. Typically called once for + * each relevant datastore when the server is killed, or when Waterline + * is shut down after a series of tests. Useful for destroying the manager + * (i.e. terminating any remaining open connections, etc.). + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The unique name (identity) of the datastore to un-register. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + teardown: function (datastoreName, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Attempting to tear down a datastore (`'+datastoreName+'`) which is not currently registered with this adapter. This is usually due to a race condition in userland code (e.g. attempting to tear down the same ORM instance more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // No manager to speak of + delete registeredDatastores[datastoreName]; + + // Inform Waterline that we're done, and that everything went as expected. + return done(); + + }, + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ███╗ ███╗██╗ // + // ██╔══██╗████╗ ████║██║ // + // ██║ ██║██╔████╔██║██║ // + // ██║ ██║██║╚██╔╝██║██║ // + // ██████╔╝██║ ╚═╝ ██║███████╗ // + // ╚═════╝ ╚═╝ ╚═╝╚══════╝ // + // (D)ata (M)anipulation (L)anguage // + // // + // DML adapter methods: // + // Methods related to manipulating records stored in the database. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ + * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ + * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ + * Create a new record. + * + * (e.g. add a new row to a SQL table, or a new document to a MongoDB collection.) + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the physical record that was + * > created (a dictionary) as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Dictionary?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + create: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query (and if relevant, send back a result.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`create`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔═╗╦ ╦ + * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ ║╣ ╠═╣║ ╠═╣ + * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚═╝╩ ╩ + * Create multiple new records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were created as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + createEach: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query (and if relevant, send back a result.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`createEach`) not implemented yet.')); + }, 16); + + }, + + + + /** + * ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗ + * ║ ║╠═╝ ║║╠═╣ ║ ║╣ + * ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝ + * Update matching records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were updated as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + update: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query (and if relevant, send back a result.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`update`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔╦╗╔═╗╔═╗╔╦╗╦═╗╔═╗╦ ╦ + * ║║║╣ ╚═╗ ║ ╠╦╝║ ║╚╦╝ + * ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝ ╩ + * Destroy one or more records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were destroyed as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + destroy: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query (and if relevant, send back a result.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`destroy`) not implemented yet.')); + }, 16); + + }, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ██████╗ ██╗ // + // ██╔══██╗██╔═══██╗██║ // + // ██║ ██║██║ ██║██║ // + // ██║ ██║██║▄▄ ██║██║ // + // ██████╔╝╚██████╔╝███████╗ // + // ╚═════╝ ╚══▀▀═╝ ╚══════╝ // + // (D)ata (Q)uery (L)anguage // + // // + // DQL adapter methods: // + // Methods related to fetching information from the database (e.g. finding stored records). // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * ╔═╗╦╔╗╔╔╦╗ + * ╠╣ ║║║║ ║║ + * ╚ ╩╝╚╝═╩╝ + * Find matching records. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array} [matching physical records] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + find: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query and send back a result. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`find`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╦╔═╗╦╔╗╔ + * ║║ ║║║║║ + * ╚╝╚═╝╩╝╚╝ + * ┌─ ┌─┐┌─┐┬─┐ ┌┐┌┌─┐┌┬┐┬┬ ┬┌─┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐ ─┐ + * │─── ├┤ │ │├┬┘ │││├─┤ │ │└┐┌┘├┤ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ ───│ + * └─ └ └─┘┴└─ ┘└┘┴ ┴ ┴ ┴ └┘ └─┘ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘ ─┘ + * Perform a "find" query with one or more native joins. + * + * > NOTE: If you don't want to support native joins (or if your database does not + * > support native joins, e.g. Mongo) remove this method completely! Without this method, + * > Waterline will handle `.populate()` using its built-in join polyfill (aka "polypopulate"), + * > which sends multiple queries to the adapter and joins the results in-memory. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array} [matching physical records, populated according to the join instructions] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + join: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query and send back a result. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`join`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔═╗╔═╗╦ ╦╔╗╔╔╦╗ + * ║ ║ ║║ ║║║║ ║ + * ╚═╝╚═╝╚═╝╝╚╝ ╩ + * Get the number of matching records. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the number of matching records] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + count: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query and send back a result. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`count`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔═╗╦ ╦╔╦╗ + * ╚═╗║ ║║║║ + * ╚═╝╚═╝╩ ╩ + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the sum] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + sum: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query and send back a result. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`sum`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔═╗╦ ╦╔═╗ + * ╠═╣╚╗╔╝║ ╦ + * ╩ ╩ ╚╝ ╚═╝ + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the average ("mean")] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + avg: function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Perform the query and send back a result. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`avg`) not implemented yet.')); + }, 16); + + }, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ██████╗ ██╗ // + // ██╔══██╗██╔══██╗██║ // + // ██║ ██║██║ ██║██║ // + // ██║ ██║██║ ██║██║ // + // ██████╔╝██████╔╝███████╗ // + // ╚═════╝ ╚═════╝ ╚══════╝ // + // (D)ata (D)efinition (L)anguage // + // // + // DDL adapter methods: // + // Methods related to modifying the underlying structure of physical models in the database. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + /* ╔╦╗╔═╗╔═╗╔═╗╦═╗╦╔╗ ╔═╗ ┌┬┐┌─┐┌┐ ┬ ┌─┐ + * ║║║╣ ╚═╗║ ╠╦╝║╠╩╗║╣ │ ├─┤├┴┐│ ├┤ + * ═╩╝╚═╝╚═╝╚═╝╩╚═╩╚═╝╚═╝ ┴ ┴ ┴└─┘┴─┘└─┘ + * Describe a table and get back a normalized model schema format. + * (This is used to allow Sails to do auto-migrations) + */ + describe: async function describe(datastoreName, tableName, cb, meta) { + var datastore = datastores[datastoreName]; + spawnConnection(datastore, function __DESCRIBE__(client) { + // Get a list of all the tables in this database + // See: http://www.sqlite.org/faq.html#q7) + var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; + + try { + const schema = await wrapAsyncStatements(client.get.bind(client, query)); + + // Query to get information about each table + // See: http://www.sqlite.org/pragma.html#pragma_table_info + var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; + + // Query to get a list of indices for a given table + var indexListQuery = 'PRAGMA index_list("' + schema.name + '")'; + + schema.indices = []; + schema.columns = []; + + var index = { columns: [] }; + + // Binding to the each method which takes a function that runs for every + // row returned, then a complete callback function + await wrapAsyncStatements(client.each.bind(client, indexListQuery, (err, currentIndex) => { + if (err) throw err; + // Query to get information about indices + var indexInfoQuery = + 'PRAGMA index_info("' + currentIndex.name + '")'; + + // Retrieve detailed information for given index + client.each(indexInfoQuery, function (err, indexedCol) { + index.columns.push(indexedCol); + }); + + schema.indices.push(currentIndex); + })); + + await wrapAsyncStatements(client.each.bind(client, columnsQuery, (err, column) => { + if (err) throw err; + + // In SQLite3, AUTOINCREMENT only applies to PK columns of + // INTEGER type + column.autoIncrement = (column.type.toLowerCase() == 'integer' + && column.pk == 1); + + // By default, assume column is not indexed until we find that it + // is + column.indexed = false; + + // Search for indexed columns + schema.indices.forEach(function (idx) { + if (!column.indexed) { + index.columns.forEach(function (indexedCol) { + if (indexedCol.name == column.name) { + column.indexed = true; + if (idx.unique) column.unique = true; + } + }); + } + }); + + schema.columns.push(column); + })); + + var normalizedSchema = utils.normalizeSchema(schema); + // Set internal schema mapping + dataStore.schemas[tableName] = normalizedSchema; + + return Promise.resolve(normalizedSchema); + } catch (err) { + return Promise.reject(err); + } + }) + .then(schema => cb(undefined, schema)) + .catch(err => cb(err)); + }, + + /** + * ╔╦╗╔═╗╔═╗╦╔╗╔╔═╗ + * ║║║╣ ╠╣ ║║║║║╣ + * ═╩╝╚═╝╚ ╩╝╚╝╚═╝ + * Build a new physical model (e.g. table/etc) to use for storing records in the database. + * + * (This is used for schema migrations.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table to define. + * @param {String} tableName The name of the table to define. + * @param {Dictionary} definition The physical model definition (not a normal Sails/Waterline model-- log this for details.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + define: function (datastoreName, tableName, definition, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Define the physical model (e.g. table/etc.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`define`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔╦╗╦═╗╔═╗╔═╗ + * ║║╠╦╝║ ║╠═╝ + * ═╩╝╩╚═╚═╝╩ + * Drop a physical model (table/etc.) from the database, including all of its records. + * + * (This is used for schema migrations.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table to drop. + * @param {String} tableName The name of the table to drop. + * @param {Ref} unused Currently unused (do not use this argument.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + drop: function (datastoreName, tableName, unused, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Drop the physical model (e.g. table/etc.) + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`drop`) not implemented yet.')); + }, 16); + + }, + + + /** + * ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┌─┐ ┬ ┬┌─┐┌┐┌┌─┐┌─┐ + * ╚═╗║╣ ║ └─┐├┤ │─┼┐│ │├┤ ││││ ├┤ + * ╚═╝╚═╝ ╩ └─┘└─┘└─┘└└─┘└─┘┘└┘└─┘└─┘ + * Set a sequence in a physical model (specifically, the auto-incrementing + * counter for the primary key) to the specified value. + * + * (This is used for schema migrations.) + * + * > NOTE - If your adapter doesn't support sequence entities (like PostgreSQL), + * > you should remove this method. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table/etc. + * @param {String} sequenceName The name of the sequence to update. + * @param {Number} sequenceValue The new value for the sequence (e.g. 1) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + setSequence: function (datastoreName, sequenceName, sequenceValue, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Update the sequence. + // + // > TODO: Replace this setTimeout with real logic that calls + // > `done()` when finished. (Or remove this method from the + // > adapter altogether + setTimeout(function(){ + return done(new Error('Adapter method (`setSequence`) not implemented yet.')); + }, 16); + + }, +}; + +/** + * Spawns temporary connection and executes given logic. Returns promise for + * use with async/await + * @param {*} datastore + * @param {Function} logic Takes the client as its only argument. Can return a + * value or a Promise + * @param {*} cb + * @return Promise + */ +function spawnConnection(datastore, logic) { + return new Promise((resolve, reject) => { + if (!datastore) reject(Errors.InvalidConnection); + + var datastoreConfig = datastore.config; + + // Check if we want to run in verbose mode + // Note that once you go verbose, you can't go back. + // See: https://github.com/mapbox/node-sqlite3/wiki/API + if (datastoreConfig.verbose) sqlite3 = sqlite3.verbose(); + + // Make note whether the database already exists + exists = fs.existsSync(datastoreConfig.filename); + + // Create a new handle to our database + var client = new sqlite3.Database( + datastoreConfig.filename, + datastoreConfig.mode, + err => { + if (err) reject(err); + else resolve(client); + } + ); + }) + .then(logic) + .catch(err => { + console.error(err) + return Promise.reject(err); //we want the user process to get this error as well + }) + .finally(() => { + if (client) client.close(); + }); +} + +/** + * Simple utility function that wraps an async function in a promise + * @param {Function} func Async function which takes 1 argument: a callback + * function that takes err, value as args (in that order) + * @return Promise + */ +function wrapAsyncStatements(func) { + return new Promise((resolve, reject) => { + func((err, value) => { + if (err) reject(err); + else resolve(value); + }); + }); +} \ No newline at end of file diff --git a/lib/query.js b/lib/query.js index 4efae4f..709dd31 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2,7 +2,7 @@ * Dependencies */ -var _ = require('lodash'), +var _ = require('@sailshq/lodash'), utils = require('./utils'), hop = utils.object.hasOwnProperty; @@ -23,7 +23,7 @@ var Query = function(schema, tableDefs) { this._tableDefs = tableDefs || {}; this._schema = _.clone(schema); - + return this; }; @@ -72,7 +72,7 @@ Query.prototype.update = function(table, criteria, data) { str = str.slice(0, -2); this._query += 'SET ' + str + ' '; - + // Add data values to this._values this._values = attributes.values; // Build criteria clause @@ -106,7 +106,7 @@ Query.prototype.destroy = function(table, criteria) { Query.prototype._build = function(criteria) { var self = this; - + // Ensure criteria keys are in correct order var orderedCriteria = {}; if (criteria.where) orderedCriteria.where = criteria.where; @@ -183,7 +183,7 @@ Query.prototype._build = function(criteria) { Query.prototype.where = function(options) { var self = this, operators = this.operators(); - + if (!options) return; // Begin WHERE query @@ -521,10 +521,10 @@ Query.prototype.cast = function(values) { _values = _.clone(values); Object.keys(values).forEach(function(key) { - - if(!self._schema[key]) return; + + if(!self._schema[key]) return; // Lookup schema type - var type = self._schema[key].type; + var type = self._schema[key].type; // Attempt to parse Array if(type === 'array') { diff --git a/package-lock.json b/package-lock.json index 1d92c00..fdcbdb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sailshq/lodash": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@sailshq/lodash/-/lodash-3.10.3.tgz", + "integrity": "sha512-XTF5BtsTSiSpTnfqrCGS5Q8FvSHWCywA0oRxFAZo8E1a8k1MMFUvk3VlRk3q/SusEYwy7gvVdyt9vvNlTa2VuA==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", diff --git a/package.json b/package.json index 43db172..5364211 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sails-sqlite3", "version": "0.0.1", "description": "Boilerplate adapter for Sails.js", - "main": "lib/adapter", + "main": "index.js", "scripts": { "test": "make test" }, @@ -21,6 +21,7 @@ "license": "MIT", "readmeFilename": "README.md", "dependencies": { + "@sailshq/lodash": "^3.10.3", "async": "~0.2.9", "sqlite3": "~4.0.x", "underscore": "1.5.2", @@ -33,6 +34,7 @@ }, "sails": { "adapter": { + "type": "sqlite3", "sailsVersion": "^1.0.0", "implements": [ "semantic", From 25d5d516e8ede3ce768fbae77ec6dcddcdf83628 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 1 Dec 2018 15:09:33 -0500 Subject: [PATCH 43/81] add missing require --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 4b21dbe..a6fbe27 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,7 @@ const fs = require('fs'); const _ = require('@sailshq/lodash'); const sqlite3 = require('sqlite3'); +const Errors = require('waterline-errors').adapter; const Query = require('./query'); const utils = require('./utils'); From a66ecb48e110c753bcc911c2cd5901c11c240e57 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 1 Dec 2018 20:52:49 -0500 Subject: [PATCH 44/81] attempt define method I don't think it will work. Probably need to update utils for Waterline Adapter API --- index.js | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index a6fbe27..12a5c2d 100644 --- a/index.js +++ b/index.js @@ -58,7 +58,7 @@ module.exports = { // The identity of this adapter, to be referenced by datastore configurations in a Sails app. - identity: 'sqlite-3', + identity: 'sails-sqlite3', // Waterline Adapter API Version @@ -605,7 +605,7 @@ module.exports = { */ describe: async function describe(datastoreName, tableName, cb, meta) { var datastore = datastores[datastoreName]; - spawnConnection(datastore, function __DESCRIBE__(client) { + spawnConnection(datastore, async function __DESCRIBE__(client) { // Get a list of all the tables in this database // See: http://www.sqlite.org/faq.html#q7) var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; @@ -697,24 +697,44 @@ module.exports = { * @param {Error?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - define: function (datastoreName, tableName, definition, done) { + define: async function (datastoreName, tableName, definition, done) { // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; + var datastore = registeredDatastores[datastoreName]; // Sanity check: - if (_.isUndefined(dsEntry)) { + if (_.isUndefined(datastore)) { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Define the physical model (e.g. table/etc.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`define`) not implemented yet.')); - }, 16); + try { + await spawnConnection(datastore, async function __DEFINE__(client){ + const escapedTable = utils.escapeTable(tableName); + + // Iterate through each attribute, building a query string + const _schema = utils.buildSchema(definition); + + // Check for any index attributes + const indices = utils.buildIndexes(definition); + + // Build query + const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema + ')'; + + await wrapAsyncStatements(client.run.bind(client, query)); + + await Promise.all(indices.forEach(async index => { + // Build a query to create a namespaced index tableName_key + var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + + tableName + ' (' + index + ');'; + + await wrapAsyncStatements(client.run.bind(client, query)); + })); + }); + + done(); + } catch (err) { + done(err); + } }, From b0d9fe910a92db2bc48bf1bd76195ae8043ba301 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 1 Dec 2018 21:09:28 -0500 Subject: [PATCH 45/81] add drop function --- index.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 12a5c2d..42d60dc 100644 --- a/index.js +++ b/index.js @@ -15,8 +15,8 @@ const _ = require('@sailshq/lodash'); const sqlite3 = require('sqlite3'); const Errors = require('waterline-errors').adapter; -const Query = require('./query'); -const utils = require('./utils'); +const Query = require('./lib/query'); +const utils = require('./lib/utils'); /** @@ -765,14 +765,16 @@ module.exports = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Drop the physical model (e.g. table/etc.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`drop`) not implemented yet.')); - }, 16); + // Build query + var query = 'DROP TABLE ' + utils.escapeTable(table); + + try { + await wrapAsyncStatements(client.run.bind(client, query)); + + done(); + } catch (err) { + done(err); + } }, From bee3faf244afbe79f51cf8b3c3d51b538f66caf7 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 1 Dec 2018 21:11:07 -0500 Subject: [PATCH 46/81] async bug --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 42d60dc..096e5e5 100644 --- a/index.js +++ b/index.js @@ -755,7 +755,7 @@ module.exports = { * @param {Error?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - drop: function (datastoreName, tableName, unused, done) { + drop: async function (datastoreName, tableName, unused, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; From ad4a5375a2112029b219dbe78e8254678f803b9f Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 1 Dec 2018 21:14:38 -0500 Subject: [PATCH 47/81] wrap drop call in spawn --- index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 096e5e5..afd27fc 100644 --- a/index.js +++ b/index.js @@ -766,10 +766,13 @@ module.exports = { } // Build query - var query = 'DROP TABLE ' + utils.escapeTable(table); + const query = 'DROP TABLE ' + utils.escapeTable(tableName); + try { - await wrapAsyncStatements(client.run.bind(client, query)); + await spawnConnection(dsEntry, async function __DROP__(client) { + await wrapAsyncStatements(client.run.bind(client, query)); + }); done(); } catch (err) { From f418c05cdefa2194ac0671080fde1bc4e054a221 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 1 Dec 2018 22:05:06 -0500 Subject: [PATCH 48/81] minor bug fixes, notes on other bugs --- index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index afd27fc..d76073c 100644 --- a/index.js +++ b/index.js @@ -698,6 +698,12 @@ module.exports = { * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ define: async function (datastoreName, tableName, definition, done) { + /**************************************************************** + * NOTICE: + * This function is broken. util.buildSchema does not read + * defintion objects correctly for Waterline API Version 1 + ****************************************************************/ + // Look up the datastore entry (manager/driver/config). var datastore = registeredDatastores[datastoreName]; @@ -766,7 +772,7 @@ module.exports = { } // Build query - const query = 'DROP TABLE ' + utils.escapeTable(tableName); + const query = 'DROP TABLE IF EXISTS ' + utils.escapeTable(tableName); try { @@ -834,6 +840,7 @@ module.exports = { * @return Promise */ function spawnConnection(datastore, logic) { + let client; return new Promise((resolve, reject) => { if (!datastore) reject(Errors.InvalidConnection); @@ -848,7 +855,7 @@ function spawnConnection(datastore, logic) { exists = fs.existsSync(datastoreConfig.filename); // Create a new handle to our database - var client = new sqlite3.Database( + client = new sqlite3.Database( datastoreConfig.filename, datastoreConfig.mode, err => { From a8d036060e023e8adb227a29c27b534a2e13fd41 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sun, 2 Dec 2018 13:18:33 -0500 Subject: [PATCH 49/81] updates to util for creating tables automigrations *might* be ready --- index.js | 61 +++++++++++++++++------------- lib/utils.js | 105 ++++++++++++++++++++++++++------------------------- 2 files changed, 89 insertions(+), 77 deletions(-) diff --git a/index.js b/index.js index d76073c..d53c9d5 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,12 @@ -> adapter Code refactored for Sails 1.0.0 release + + Supports Migratable interface, but (as docs on this interface + stipulate) this should only be used for dev. This adapter + does not implement the majority of possibly desired + constraints since the waterline auto-migration is intended + to be light and quick ---------------------------------------------------------------*/ /** * Module dependencies @@ -154,8 +160,9 @@ module.exports = { registeredDatastores[datastoreName] = { config: datastoreConfig, manager: { - physicalModelsReport, - schemas: {} + physicalModelsReport, //for reference + schemas: {}, + foreignKeys: utils.buildForeignKeyMap(physicalModelsReport) }, //temporarily store this here until I know what to do with it... driver: undefined // << TODO: include driver here (if relevant) }; @@ -718,7 +725,7 @@ module.exports = { const escapedTable = utils.escapeTable(tableName); // Iterate through each attribute, building a query string - const _schema = utils.buildSchema(definition); + const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); // Check for any index attributes const indices = utils.buildIndexes(definition); @@ -732,7 +739,7 @@ module.exports = { // Build a query to create a namespaced index tableName_key var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + tableName + ' (' + index + ');'; - + await wrapAsyncStatements(client.run.bind(client, query)); })); }); @@ -797,8 +804,10 @@ module.exports = { * * (This is used for schema migrations.) * - * > NOTE - If your adapter doesn't support sequence entities (like PostgreSQL), - * > you should remove this method. + * > NOTE - removing method. SQLite can support setting a sequence on + * > primary key fields (or other autoincrement fields), however the + * > need is slim and I don't have time. + * > Leaving shell here for future developers if necessary * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore containing the table/etc. * @param {String} sequenceName The name of the sequence to update. @@ -808,26 +817,26 @@ module.exports = { * @param {Error?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - setSequence: function (datastoreName, sequenceName, sequenceValue, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - // Update the sequence. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`setSequence`) not implemented yet.')); - }, 16); - - }, + // setSequence: function (datastoreName, sequenceName, sequenceValue, done) { + + // // Look up the datastore entry (manager/driver/config). + // var dsEntry = registeredDatastores[datastoreName]; + + // // Sanity check: + // if (_.isUndefined(dsEntry)) { + // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + // } + + // // Update the sequence. + // // + // // > TODO: Replace this setTimeout with real logic that calls + // // > `done()` when finished. (Or remove this method from the + // // > adapter altogether + // setTimeout(function(){ + // return done(new Error('Adapter method (`setSequence`) not implemented yet.')); + // }, 16); + + // }, }; /** diff --git a/lib/utils.js b/lib/utils.js index 4d97bbc..178bdfd 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -6,45 +6,60 @@ var utils = module.exports = {}; * Build a schema from an attributes object */ -utils.buildSchema = function(obj) { - var schema = ""; +utils.buildSchema = function(obj, foreignKeys) { + let columnDefs = []; + let constraintDefs = []; // Iterate through the Object Keys and build a string Object.keys(obj).forEach(function(key) { - var attr = {}; + const attr = obj[key]; - // Normalize Simple Key/Value attribute - // ex: name: 'string' - if(typeof obj[key] === 'string') { - attr.type = obj[key]; - } - - // Set the attribute values to the object key - else { - attr = obj[key]; - } - - // Override Type for autoIncrement - if(attr.autoIncrement) { - attr.type = 'serial'; - attr.primaryKey = true; - } + //remove autoincrement from pk defs + attr.autoIncrement = attr.primaryKey ? false : attr.autoIncrement; - var str = [ + const def = [ '"' + key + '"', // attribute name - utils.sqlTypeCast(attr.type), // attribute type + utils.sqlTypeCast(attr.columnType, key), // attribute type attr.primaryKey ? 'PRIMARY KEY' : '', // primary key attr.unique ? 'UNIQUE' : '', // unique constraint - attr.defaultsTo ? 'DEFAULT "' + attr.defaultsTo + '"': '' + attr.autoIncrement ? 'AUTOINCREMENT' : '' //autoincrement ].join(' ').trim(); - schema += str + ', '; + columnDefs.push(def); }); - // Remove trailing seperator/trim - return schema.slice(0, -2); + for (let columnName in foreignKeys) { + const keyDef = foreignKeys[columnName]; + constraintDefs.push(`FOREIGN KEY(${columnName}) REFERENCES ${keyDef.table}(${keyDef.column})`); + } + + return columnDefs.concat(constraintDefs).join(', '); }; +/** + * @return Map by unescaped tablename of the foreign key fields and the tables + * they look up to + */ +utils.buildForeignKeyMap = physicalModelsReport => { + const foreignKeyMap = {}; + + for (let tableName in physicalModelsReport) { + tableKeys = {}; + foreignKeyMap[tableName] = tableKeys; + + for (let columnName in physicalModelsReport[tableName].definition) { + const column = physicalModelsReport[tableName].definition[columnName]; + + if (column.foreignKey) { + tableKeys[columnName] = { + table: column.references, + column: column.on + } + } + } + } +} + /** * Safe hasOwnProperty */ @@ -170,7 +185,7 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { query += 'SELECT rowid, '; var selectKeys = [], joinSelectKeys = []; - Object.keys(schema[table]).forEach(function(key) { + Object.keys(schema[table]).forEach(function(key) { selectKeys.push({ table: table, key: key }); }); @@ -246,7 +261,6 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { * Build an Index array from any attributes that * have an index key set. */ - utils.buildIndexes = function(obj) { var indexes = []; @@ -429,40 +443,30 @@ utils.prepareValue = function(value) { /** * Cast waterline types to SQLite3 data types */ +utils.sqlTypeCast = function(type, columnName) { + // type has been explicitly specified by the user + if (!type.startsWith('_')) return type; -utils.sqlTypeCast = function(type) { switch (type.toLowerCase()) { - case 'serial': - return 'INTEGER'; - - case 'string': - case 'text': + case '_string': return 'TEXT'; - case 'boolean': - case 'int': - case 'integer': + case '_boolean': + return `INTEGER CHECK(${columnName} IN (0, 1))`; + + case '_numberkey': + case '_numbertimestamp': //dates return 'INTEGER'; - case 'float': - case 'double': + case '_number': return 'REAL'; - case 'date': - case 'datestamp': - case 'datetime': - return 'TEXT'; - - case 'array': - return 'TEXT'; + case '_ref': + return 'BLOB'; - case 'json': + case '_json': return 'TEXT'; - case 'binary': - case 'bytea': - return 'BLOB'; - default: console.error("Warning: Unregistered type given: " + type); return 'TEXT'; @@ -475,7 +479,6 @@ utils.sqlTypeCast = function(type) { * Dates should be stored in Postgres with UTC timestamps * and then converted to local time on the client. */ - utils.toSqlDate = function(date) { return date.toUTCString(); }; From c42892a9ddb4b4c29bc03bc8367a889e5ac3519c Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sun, 2 Dec 2018 13:31:04 -0500 Subject: [PATCH 50/81] bug fixes --- index.js | 2 +- lib/utils.js | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index d53c9d5..af15ba0 100644 --- a/index.js +++ b/index.js @@ -735,7 +735,7 @@ module.exports = { await wrapAsyncStatements(client.run.bind(client, query)); - await Promise.all(indices.forEach(async index => { + await Promise.all(indices.map(async index => { // Build a query to create a namespaced index tableName_key var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + tableName + ' (' + index + ');'; diff --git a/lib/utils.js b/lib/utils.js index 178bdfd..4b28e73 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,15 +14,14 @@ utils.buildSchema = function(obj, foreignKeys) { Object.keys(obj).forEach(function(key) { const attr = obj[key]; - //remove autoincrement from pk defs - attr.autoIncrement = attr.primaryKey ? false : attr.autoIncrement; - + // Note: we are ignoring autoincrement b/c it is only supported on + // primary key defs, but probably shouldn't be used there anyway + // https://sqlite.org/autoinc.html const def = [ '"' + key + '"', // attribute name utils.sqlTypeCast(attr.columnType, key), // attribute type attr.primaryKey ? 'PRIMARY KEY' : '', // primary key - attr.unique ? 'UNIQUE' : '', // unique constraint - attr.autoIncrement ? 'AUTOINCREMENT' : '' //autoincrement + attr.unique ? 'UNIQUE' : '' // unique constraint ].join(' ').trim(); columnDefs.push(def); @@ -58,6 +57,8 @@ utils.buildForeignKeyMap = physicalModelsReport => { } } } + + return foreignKeyMap; } /** From d78ee1efe951ffdea72f54c605c44fb7c33e6a56 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sun, 2 Dec 2018 17:26:49 -0500 Subject: [PATCH 51/81] get describes in prep for create --- index.js | 20 +++++++++++++++----- lib/utils.js | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index af15ba0..4521236 100644 --- a/index.js +++ b/index.js @@ -161,14 +161,21 @@ module.exports = { config: datastoreConfig, manager: { physicalModelsReport, //for reference - schemas: {}, + schema: {}, foreignKeys: utils.buildForeignKeyMap(physicalModelsReport) - }, //temporarily store this here until I know what to do with it... - driver: undefined // << TODO: include driver here (if relevant) + }, + // driver: undefined // << TODO: include driver here (if relevant) }; - return done(); + try { + for (let tableName in physicalModelsReport) { + await wrapAsyncStatements(this.describe.bind(this, datastoreName, tableName)); + } + } catch (err) { + done(err); + } + return done(); }, @@ -619,6 +626,7 @@ module.exports = { try { const schema = await wrapAsyncStatements(client.get.bind(client, query)); + if (!schema) return Promise.resolve(); // Query to get information about each table // See: http://www.sqlite.org/pragma.html#pragma_table_info @@ -677,7 +685,7 @@ module.exports = { var normalizedSchema = utils.normalizeSchema(schema); // Set internal schema mapping - dataStore.schemas[tableName] = normalizedSchema; + dataStore.schema[tableName] = normalizedSchema; return Promise.resolve(normalizedSchema); } catch (err) { @@ -744,6 +752,7 @@ module.exports = { })); }); + datastore.schema[tableName] = definition; done(); } catch (err) { done(err); @@ -787,6 +796,7 @@ module.exports = { await wrapAsyncStatements(client.run.bind(client, query)); }); + delete dsEntry.schema[tableName]; done(); } catch (err) { done(err); diff --git a/lib/utils.js b/lib/utils.js index 4b28e73..49317fe 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -392,7 +392,7 @@ utils.normalizeSchema = function(schema) { clone.columns.forEach(function(column) { // Set type - normalized[column.name] = { type: column.type }; + normalized[column.name] = { columnType: column.type }; // Check for primary key normalized[column.name].primaryKey = column.pk ? true : false; From d2c6eda0b0e3dc244f978fb369e659fe0b292c14 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 13:24:52 -0500 Subject: [PATCH 52/81] modify describe call --- index.js | 11 ++++++++--- lib/utils.js | 19 ++++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index af15ba0..6a986f6 100644 --- a/index.js +++ b/index.js @@ -160,10 +160,10 @@ module.exports = { registeredDatastores[datastoreName] = { config: datastoreConfig, manager: { - physicalModelsReport, //for reference + models: physicalModelsReport, //for reference schemas: {}, foreignKeys: utils.buildForeignKeyMap(physicalModelsReport) - }, //temporarily store this here until I know what to do with it... + }, driver: undefined // << TODO: include driver here (if relevant) }; @@ -731,7 +731,7 @@ module.exports = { const indices = utils.buildIndexes(definition); // Build query - const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema + ')'; + const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; await wrapAsyncStatements(client.run.bind(client, query)); @@ -742,6 +742,9 @@ module.exports = { await wrapAsyncStatements(client.run.bind(client, query)); })); + + // Replacing if it already existed + datastore.schemas[tableName] = _schema.schema; }); done(); @@ -787,6 +790,8 @@ module.exports = { await wrapAsyncStatements(client.run.bind(client, query)); }); + delete dsEntry.schemas[tableName]; + done(); } catch (err) { done(err); diff --git a/lib/utils.js b/lib/utils.js index 4b28e73..10a9288 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,9 +4,10 @@ var utils = module.exports = {}; /** * Build a schema from an attributes object + * @return Object {declaration: string, schema: object} */ - utils.buildSchema = function(obj, foreignKeys) { + let schema = {}; let columnDefs = []; let constraintDefs = []; @@ -14,12 +15,21 @@ utils.buildSchema = function(obj, foreignKeys) { Object.keys(obj).forEach(function(key) { const attr = obj[key]; + const sqliteType = utils.sqlTypeCast(attr.columnType, key); + + schema[key] = { + primaryKey: attr.primaryKey, + unique: attr.unique, + indexed: attr.unique || attr.primaryKey, // indexing rules in sqlite + type: sqliteType.replace(/CHECK.*$/, '') + } + // Note: we are ignoring autoincrement b/c it is only supported on // primary key defs, but probably shouldn't be used there anyway // https://sqlite.org/autoinc.html const def = [ '"' + key + '"', // attribute name - utils.sqlTypeCast(attr.columnType, key), // attribute type + sqliteType, // attribute type attr.primaryKey ? 'PRIMARY KEY' : '', // primary key attr.unique ? 'UNIQUE' : '' // unique constraint ].join(' ').trim(); @@ -32,7 +42,10 @@ utils.buildSchema = function(obj, foreignKeys) { constraintDefs.push(`FOREIGN KEY(${columnName}) REFERENCES ${keyDef.table}(${keyDef.column})`); } - return columnDefs.concat(constraintDefs).join(', '); + return { + declaration: columnDefs.concat(constraintDefs).join(', '), + schema + }; }; /** From 4720a7f64e352cea3e964d7051fae56f770e7fbd Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 13:52:41 -0500 Subject: [PATCH 53/81] implement most of create --- index.js | 36 +++++++++++++++++++++++-------- lib/utils.js | 61 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index 0e1fbd9..7a90419 100644 --- a/index.js +++ b/index.js @@ -250,7 +250,7 @@ module.exports = { * @param {Dictionary?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - create: function (datastoreName, query, done) { + create: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -260,14 +260,32 @@ module.exports = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query (and if relevant, send back a result.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`create`) not implemented yet.')); - }, 16); + try { + await spawnConnection(dsEntry, client => { + const tableName = query.using; + const escapedTable = utils.escapeTable(tableName); + + const attributes = utils.mapAttributes(query.newRecord, dsEntry.schema[tableName]); + + const columnNames = attributes.keys.join(', '); + const paramValues = attributes.params.join(', '); + + // Build query + var insertQuery = 'INSERT INTO ' + escapedTable + ' (' + columnNames + ') values (' + paramValues + ')'; + var selectQuery = 'SELECT * FROM ' + escapedTable + ' ORDER BY rowid DESC LIMIT 1'; + + // first insert values + await wrapAsyncStatements(client.run.bind(client, insertQuery)); + + // get the last inserted row + const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); + + throw new Error('Did not finish casting from SQLite to Waterline type'); + done(undefined, newRow); + }); + } catch (err) { + done(err); + } }, diff --git a/lib/utils.js b/lib/utils.js index 10a9288..e55e465 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -382,18 +382,19 @@ utils.escapeTable = function(table) { return '"' + table + '"'; }; -utils.mapAttributes = function(data) { - var keys = [], // Column Names - values = [], // Column Values - params = [], // Param Index, ex: $1, $2 - i = 1; - - Object.keys(data).forEach(function(key) { - keys.push('"' + key + '"'); - values.push(utils.prepareValue(data[key])); +utils.mapAttributes = function(record, schema) { + const keys = []; // Column Names + const values = []; // Column Values + const params = []; // Param Index, ex: $1, $2 + let i = 1; + + for (let columnName in record) { + keys.push(`"${columnName}"`); + values.push(utils.prepareValue(record[columnName], schema[columnName])); params.push('$' + i); + i++; - }); + } return({ keys: keys, values: values, params: params }); }; @@ -429,29 +430,51 @@ utils.normalizeSchema = function(schema) { * to strings. */ -utils.prepareValue = function(value) { +utils.prepareValue = function(value, columnType) { // Cast dates to SQL if (_.isDate(value)) { - value = utils.toSqlDate(value); + switch(columnType) { + case 'TEXT': + value = value.toUTCString; + break; + case 'INTEGER': + case 'REAL': + value = value.valueOf(); + default: + throw new Error(`Cannot cast date to ${columnType}`); + } + + return value; } // Cast functions to strings if (_.isFunction(value)) { - value = value.toString(); + if (columnType !== 'TEXT') throw new Error('Function can only cast to TEXT'); + return value.toString(); + } + + // Store Buffers as hex strings (for BYTEA) + if (Buffer.isBuffer(value)) { + if (columnType !== 'BLOB') throw new Error('Buffers may only represent BLOB types'); } // Store Arrays as strings - if (Array.isArray(value)) { - value = JSON.stringify(value); + if (typeof value === 'object') { + return JSON.stringify(value); } - // Store Buffers as hex strings (for BYTEA) - if (Buffer.isBuffer(value)) { - value = '\\x' + value.toString('hex'); + switch(columnType) { + case 'TEXT': + return value.toString(); + case 'INTEGER': + if (typeof value === 'boolean') return value ? 1 : 0; + return parseInt(value, 0); + case 'REAL': + return parseFloat(value); } - return value; + return value; //BLOB }; /** From 67ce012c715a798ecef010f605a4b1ac66b55b73 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 13:53:30 -0500 Subject: [PATCH 54/81] bugfix async --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7a90419..4377e19 100644 --- a/index.js +++ b/index.js @@ -261,7 +261,7 @@ module.exports = { } try { - await spawnConnection(dsEntry, client => { + await spawnConnection(dsEntry, async client => { const tableName = query.using; const escapedTable = utils.escapeTable(tableName); From 1b02e979dc200becebc28d30b3aba899e919a5a9 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 21:06:13 -0500 Subject: [PATCH 55/81] bug fixes and create method --- index.js | 31 ++++++++++++++++++------------- lib/query.js | 51 +++++++++++++++++++++++++++++---------------------- lib/utils.js | 16 ++++++++-------- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/index.js b/index.js index 4377e19..3f7aa1c 100644 --- a/index.js +++ b/index.js @@ -139,7 +139,7 @@ module.exports = { * @param {Error?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - registerDatastore: function (datastoreConfig, physicalModelsReport, done) { + registerDatastore: async function (datastoreConfig, physicalModelsReport, done) { // Grab the unique name for this datastore for easy access below. var datastoreName = datastoreConfig.identity; @@ -253,7 +253,8 @@ module.exports = { create: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; + const dsEntry = registeredDatastores[datastoreName]; + const {manager} = dsEntry; // Sanity check: if (_.isUndefined(dsEntry)) { @@ -265,7 +266,7 @@ module.exports = { const tableName = query.using; const escapedTable = utils.escapeTable(tableName); - const attributes = utils.mapAttributes(query.newRecord, dsEntry.schema[tableName]); + const attributes = utils.mapAttributes(query.newRecord, manager.schema[tableName]); const columnNames = attributes.keys.join(', '); const paramValues = attributes.params.join(', '); @@ -275,13 +276,17 @@ module.exports = { var selectQuery = 'SELECT * FROM ' + escapedTable + ' ORDER BY rowid DESC LIMIT 1'; // first insert values - await wrapAsyncStatements(client.run.bind(client, insertQuery)); + await wrapAsyncStatements( + client.run.bind(client, insertQuery, attributes.values)); - // get the last inserted row - const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); + // get the last inserted row if requested + let newRecord; + if (query.meta.fetch) { + const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); + newRecord = new Query(manager.schema, manager.models).castRow(newRow); + } - throw new Error('Did not finish casting from SQLite to Waterline type'); - done(undefined, newRow); + done(undefined, newRecord); }); } catch (err) { done(err); @@ -636,11 +641,11 @@ module.exports = { * (This is used to allow Sails to do auto-migrations) */ describe: async function describe(datastoreName, tableName, cb, meta) { - var datastore = datastores[datastoreName]; + var datastore = registeredDatastores[datastoreName]; spawnConnection(datastore, async function __DESCRIBE__(client) { // Get a list of all the tables in this database // See: http://www.sqlite.org/faq.html#q7) - var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; + var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + tableName + '" ORDER BY name'; try { const schema = await wrapAsyncStatements(client.get.bind(client, query)); @@ -703,7 +708,7 @@ module.exports = { var normalizedSchema = utils.normalizeSchema(schema); // Set internal schema mapping - dataStore.schema[tableName] = normalizedSchema; + dataStore.manager.schema[tableName] = normalizedSchema; return Promise.resolve(normalizedSchema); } catch (err) { @@ -770,7 +775,7 @@ module.exports = { })); // Replacing if it already existed - datastore.schema[tableName] = _schema.schema; + datastore.manager.schema[tableName] = _schema.schema; }); done(); @@ -816,7 +821,7 @@ module.exports = { await wrapAsyncStatements(client.run.bind(client, query)); }); - delete dsEntry.schema[tableName]; + delete dsEntry.manager.schema[tableName]; done(); } catch (err) { done(err); diff --git a/lib/query.js b/lib/query.js index 709dd31..b8a7d9f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -16,11 +16,12 @@ var _ = require('@sailshq/lodash'), * If you have any questions, contact Andrew Jo */ -var Query = function(schema, tableDefs) { +const Query = function(schema, models) { this._values = []; this._paramCount = 1; this._query = ''; - this._tableDefs = tableDefs || {}; + /** Waterline models - provides info on type */ + this._models = models || {}; this._schema = _.clone(schema); @@ -516,28 +517,34 @@ Query.prototype.group = function(options) { * array for return values. */ -Query.prototype.cast = function(values) { - var self = this, - _values = _.clone(values); - - Object.keys(values).forEach(function(key) { - - if(!self._schema[key]) return; - // Lookup schema type - var type = self._schema[key].type; - - // Attempt to parse Array - if(type === 'array') { - try { - _values[key] = JSON.parse(values[key]); - } catch(e) { - return; - } +Query.prototype.castRow = function(tableName, values) { + const waterlineModel = this._models[tableName]; + const newModel = {}; + + for (let columnName of values) { + const attrDef = waterlineModel.definition[columnName]; + + switch(attrDef.type) { + case 'json': + newModel[columnName] = JSON.parse(values[columnName]); + break; + case 'boolean': + newModel[columnName] = !!values[columnName]; + break; + case 'number': + newModel[columnName] = parseFloat(values[columnName]); + break; + case 'numberkey': + newModel[columnName] = parseInt(values[columnName], 10); + break; + case 'string': + newModel[columnName] = values[columnName].toString(); + default: + newModel[columnName] = values[columnName]; } + } - }); - - return _values; + return newModel; }; module.exports = Query; diff --git a/lib/utils.js b/lib/utils.js index e55e465..258ec66 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -21,7 +21,7 @@ utils.buildSchema = function(obj, foreignKeys) { primaryKey: attr.primaryKey, unique: attr.unique, indexed: attr.unique || attr.primaryKey, // indexing rules in sqlite - type: sqliteType.replace(/CHECK.*$/, '') + type: sqliteType.replace(/CHECK.*$/, '').trim() } // Note: we are ignoring autoincrement b/c it is only supported on @@ -390,7 +390,7 @@ utils.mapAttributes = function(record, schema) { for (let columnName in record) { keys.push(`"${columnName}"`); - values.push(utils.prepareValue(record[columnName], schema[columnName])); + values.push(utils.prepareValue(record[columnName], schema[columnName].type)); params.push('$' + i); i++; @@ -412,12 +412,12 @@ utils.normalizeSchema = function(schema) { normalized[column.name].primaryKey = column.pk ? true : false; // Indicate whether the column is indexed - normalized[column.name].indexed = column.indexed; + normalized[column.name].indexed = !!column.indexed; // Show unique constraint - if (column.unique) normalized[column.name].unique = true; + normalized[column.name].unique = !!column.unique; - if (column.autoIncrement) normalized[column.name].autoIncrement = true; + normalized[column.name].autoIncrement = !!column.autoIncrement; }); return normalized; @@ -436,7 +436,7 @@ utils.prepareValue = function(value, columnType) { if (_.isDate(value)) { switch(columnType) { case 'TEXT': - value = value.toUTCString; + value = value.toUTCString(); break; case 'INTEGER': case 'REAL': @@ -459,8 +459,8 @@ utils.prepareValue = function(value, columnType) { if (columnType !== 'BLOB') throw new Error('Buffers may only represent BLOB types'); } - // Store Arrays as strings - if (typeof value === 'object') { + // Store Arrays / Objects as JSON strings + if (typeof value === 'object' && columnType !== 'BLOB') { return JSON.stringify(value); } From 5015ced50612e65d596754b0f2fc4a55ffedb3f2 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 22:35:55 -0500 Subject: [PATCH 56/81] start update implementation --- index.js | 62 +++++++++++++++++++++++++++++++++++++++++----------- lib/query.js | 31 +++++++++++++++----------- lib/utils.js | 4 +++- 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/index.js b/index.js index 3f7aa1c..a9ef75e 100644 --- a/index.js +++ b/index.js @@ -60,7 +60,7 @@ var registeredDatastores = {}; * * @type {Dictionary} */ -module.exports = { +const adapter = { // The identity of this adapter, to be referenced by datastore configurations in a Sails app. @@ -283,7 +283,8 @@ module.exports = { let newRecord; if (query.meta.fetch) { const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); - newRecord = new Query(manager.schema, manager.models).castRow(newRow); + newRecord = new Query(tableName, manager.schema[tableName], manager.models[tableName]) + .castRow(newRow); } done(undefined, newRecord); @@ -356,7 +357,7 @@ module.exports = { * @param {Array?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - update: function (datastoreName, query, done) { + update: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -366,14 +367,31 @@ module.exports = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query (and if relevant, send back a result.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`update`) not implemented yet.')); - }, 16); + try { + await spawnConnection(dsEntry, (client) => { + const tableName = query.using; + const escapedTable = utils.escapeTable(tableName); + + const tableSchema = dsEntry.manager.schemas[tableName]; + const model = dsEntry.manager.models[tableName]; + + const _query = new Query(tableName, tableSchema, model); + const updateQuery = _query.update(query.criteria, query.valuesToSet); + + const statement = await wrapAsyncForThis( + client.run.bind(client, updateQuery.query, updateQuery.values)); + + let results; + if (statement.changes > 0 && query.meta.fetch) { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, query)); + } + + done (undefined, results); + }); + } catch (err) { + done(err); + } }, @@ -708,7 +726,7 @@ module.exports = { var normalizedSchema = utils.normalizeSchema(schema); // Set internal schema mapping - dataStore.manager.schema[tableName] = normalizedSchema; + datastore.manager.schema[tableName] = normalizedSchema; return Promise.resolve(normalizedSchema); } catch (err) { @@ -931,4 +949,22 @@ function wrapAsyncStatements(func) { else resolve(value); }); }); -} \ No newline at end of file +} + +/** + * Utility function that wraps an async function in a promise. In contrast + * to the above, this method specifically resolves with the `this` value + * passed to the callback function + * @param {Function} func Async function which takes 1 argument: a callback + * function that takes an err and invokes its callback with a `this` property + */ +function wrapAsyncForThis(func) { + return new Promise((resolve, reject) => { + func(function(err) { + if (err) reject(err); + else resolve(this); + }); + }) +} + +module.exports = adapter; \ No newline at end of file diff --git a/lib/query.js b/lib/query.js index b8a7d9f..643b66f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -16,12 +16,13 @@ var _ = require('@sailshq/lodash'), * If you have any questions, contact Andrew Jo */ -const Query = function(schema, models) { +const Query = function (tableName, schema, model) { this._values = []; this._paramCount = 1; this._query = ''; - /** Waterline models - provides info on type */ - this._models = models || {}; + this._tableName = tableName; + /** Waterline model - provides info on type */ + this._model = model || {}; this._schema = _.clone(schema); @@ -54,11 +55,11 @@ Query.prototype.find = function(table, criteria) { * UPDATE Statement */ -Query.prototype.update = function(table, criteria, data) { - this._query = 'UPDATE ' + table + ' '; +Query.prototype.update = function(criteria, data) { + this._query = 'UPDATE ' + this._tableName + ' '; // Transform the Data object into arrays used in a parameterized query - var attributes = utils.mapAttributes(data); + var attributes = utils.mapAttributes(data, this._schema); // Update the paramCount this._paramCount = attributes.params.length + 1; @@ -272,7 +273,7 @@ Query.prototype.operators = function() { var caseSensitive = true; // Check if key is a string - if (self._schema[key] && self._schema[key].type === 'text') caseSensitive = false; + if (self._schema[key] && self._schema[key].type === 'TEXT') caseSensitive = false; processCriteria.call(self, key, options, '=', caseSensitive); self._query += (comparator || ' AND '); @@ -282,7 +283,7 @@ Query.prototype.operators = function() { var caseSensitive = true; // Check if parent is a string - if (self._schema[parent].type === 'text') caseSensitive = false; + if (self._schema[parent].type === 'TEXT') caseSensitive = false; processCriteria.call(self, parent, options[key][parent], 'ILIKE', caseSensitive); self._query += (comparator || ' AND '); @@ -292,7 +293,7 @@ Query.prototype.operators = function() { var caseSensitive = true; // Check if key is a string - if (self._schema[key].type === 'text') caseSensitive = false; + if (self._schema[key].type === 'TEXT') caseSensitive = false; // Check case sensitivity to decide if LOWER logic is used if (!caseSensitive) key = 'LOWER("' + key + '")'; @@ -517,12 +518,16 @@ Query.prototype.group = function(options) { * array for return values. */ -Query.prototype.castRow = function(tableName, values) { - const waterlineModel = this._models[tableName]; +Query.prototype.castRow = function(values) { const newModel = {}; - for (let columnName of values) { - const attrDef = waterlineModel.definition[columnName]; + for (let columnName in values) { + const attrDef = this._model.definition[columnName]; + + if (values[columnName] === null) { + newModel[columnName] = null; + continue; + } switch(attrDef.type) { case 'json': diff --git a/lib/utils.js b/lib/utils.js index 258ec66..5cdd70d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -16,12 +16,13 @@ utils.buildSchema = function(obj, foreignKeys) { const attr = obj[key]; const sqliteType = utils.sqlTypeCast(attr.columnType, key); + const realType = /TEXT|INTEGER|REAL|BLOB/i.exec(sqliteType)[0]; schema[key] = { primaryKey: attr.primaryKey, unique: attr.unique, indexed: attr.unique || attr.primaryKey, // indexing rules in sqlite - type: sqliteType.replace(/CHECK.*$/, '').trim() + type: realType } // Note: we are ignoring autoincrement b/c it is only supported on @@ -431,6 +432,7 @@ utils.normalizeSchema = function(schema) { */ utils.prepareValue = function(value, columnType) { + if (value === null) return null; // Cast dates to SQL if (_.isDate(value)) { From 569b3f88689b1fa6954251aa09ba4b163d1356d2 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 3 Dec 2018 22:53:00 -0500 Subject: [PATCH 57/81] bug fix update --- index.js | 10 +++++----- lib/query.js | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index a9ef75e..4df2722 100644 --- a/index.js +++ b/index.js @@ -281,7 +281,7 @@ const adapter = { // get the last inserted row if requested let newRecord; - if (query.meta.fetch) { + if (query.meta && query.meta.fetch) { const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); newRecord = new Query(tableName, manager.schema[tableName], manager.models[tableName]) .castRow(newRow); @@ -368,11 +368,11 @@ const adapter = { } try { - await spawnConnection(dsEntry, (client) => { + await spawnConnection(dsEntry, async (client) => { const tableName = query.using; const escapedTable = utils.escapeTable(tableName); - const tableSchema = dsEntry.manager.schemas[tableName]; + const tableSchema = dsEntry.manager.schema[tableName]; const model = dsEntry.manager.models[tableName]; const _query = new Query(tableName, tableSchema, model); @@ -382,12 +382,12 @@ const adapter = { client.run.bind(client, updateQuery.query, updateQuery.values)); let results; - if (statement.changes > 0 && query.meta.fetch) { + if (statement.changes > 0 && query.meta && query.meta.fetch) { results = await wrapAsyncStatements( adapter.find.bind(adapter, datastoreName, query)); } - done (undefined, results); + done(undefined, results); }); } catch (err) { done(err); diff --git a/lib/query.js b/lib/query.js index 643b66f..08999b2 100644 --- a/lib/query.js +++ b/lib/query.js @@ -78,7 +78,7 @@ Query.prototype.update = function(criteria, data) { // Add data values to this._values this._values = attributes.values; // Build criteria clause - if (criteria) this._build(criteria); + if (criteria) this._build({where: criteria.where}); return { query: this._query, @@ -478,6 +478,7 @@ Query.prototype.skip = function(options) { */ Query.prototype.sort = function(options) { + if (options.length >>> 0 === 0) return; var self = this; this._query += ' ORDER BY '; From 51d4853f07fc3959ce1029709eac3a60777cb581 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 6 Dec 2018 21:33:20 -0500 Subject: [PATCH 58/81] an attempt at find --- index.js | 27 +++++--- lib/query.js | 44 ++---------- lib/utils.js | 185 +++++++++++++++++++++++++++------------------------ 3 files changed, 125 insertions(+), 131 deletions(-) diff --git a/index.js b/index.js index 4df2722..5205c02 100644 --- a/index.js +++ b/index.js @@ -476,15 +476,26 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query and send back a result. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`find`) not implemented yet.')); - }, 16); + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + const queryObj = new Query(tableName, schema, model); + const queryStatement = queryObj.find(query.criteria); + try { + const values = []; + let resultCount = await wrapAsyncStatements( + client.each.bind(client, queryStatement.query, queryStatement.values,(err, row) => { + if (err) throw err; + + values.push(queryObj.castRow(row)); + })); + + console.log(`${resultCount} results returned`); + done(undefined, values); + } catch (err) { + done(err); + } }, diff --git a/lib/query.js b/lib/query.js index 08999b2..0648d4b 100644 --- a/lib/query.js +++ b/lib/query.js @@ -33,15 +33,9 @@ const Query = function (tableName, schema, model) { * SELECT Statement */ -Query.prototype.find = function(table, criteria) { +Query.prototype.find = function(criteria) { - // Normalize joins key, allows use of both join and joins - if (criteria.join) { - criteria.joins = _.cloneDeep(criteria.join); - delete criteria.join; - } - - this._query = utils.buildSelectStatement(criteria, table, this._schema, this._tableDefs); + this._query = utils.buildSelectStatement(criteria, this._tableName, this._schema, this._model); if (criteria) this._build(criteria); @@ -107,36 +101,12 @@ Query.prototype.destroy = function(table, criteria) { */ Query.prototype._build = function(criteria) { - var self = this; - // Ensure criteria keys are in correct order - var orderedCriteria = {}; - if (criteria.where) orderedCriteria.where = criteria.where; - if (criteria.groupBy) orderedCriteria.groupBy = criteria.groupBy; - if (criteria.sort) orderedCriteria.sort = criteria.sort; - if (criteria.limit) orderedCriteria.limit = criteria.limit; - if (criteria.skip) orderedCriteria.skip = criteria.skip; - - // Loop through criteria parent keys - Object.keys(orderedCriteria).forEach(function(key) { - switch (key.toLowerCase()) { - case 'where': - self.where(criteria[key]); - return; - case 'groupby': - self.group(criteria[key]); - return; - case 'sort': - self.sort(criteria[key]); - return; - case 'limit': - self.limit(criteria[key]); - return; - case 'skip': - self.skip(criteria[key]); - return; - } - }); + // Evaluate criteria in correct order + if (criteria.where) this.where(criteria.where); + if (criteria.sort) this.sort(criteria.sort); + if (criteria.limit) this.limit(criteria.limit); + if (criteria.skip) this.skip(criteria.skip); return { query: this._query, diff --git a/lib/utils.js b/lib/utils.js index 5cdd70d..b5082be 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -114,97 +114,108 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { var schemaName = criteria._schemaName ? utils.escapeName(criteria._schemaName) + '.' : ''; var tableName = schemaName + utils.escapeName(table); - if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || - criteria.max) { - - query = 'SELECT rowid, '; - - // Append groupBy columns to select statement - if(criteria.groupBy) { - if(criteria.groupBy instanceof Array) { - criteria.groupBy.forEach(function(opt){ - query += tableName + '.' + utils.escapeName(opt) + ', '; - }); - - } else { - query += tableName + '.' + utils.escapeName(criteria.groupBy) + ', '; - } - } - - // Handle SUM - if (criteria.sum) { - if(criteria.sum instanceof Array) { - criteria.sum.forEach(function(opt){ - query += 'CAST(SUM(' + tableName + '.' + utils.escapeName(opt) + - ') AS float) AS ' + opt + ', '; - }); - - } else { - query += 'CAST(SUM(' + tableName + '.' + - utils.escapeName(criteria.sum) + ') AS float) AS ' + criteria.sum + - ', '; - } - } - - // Handle AVG (casting to float to fix percision with trailing zeros) - if (criteria.average) { - if(criteria.average instanceof Array) { - criteria.average.forEach(function(opt){ - query += 'CAST(AVG(' + tableName + '.' + utils.escapeName(opt) + - ') AS float) AS ' + opt + ', '; - }); - - } else { - query += 'CAST(AVG(' + tableName + '.' + - utils.escapeName(criteria.average) + ') AS float) AS ' + - criteria.average + ', '; - } - } - - // Handle MAX - if (criteria.max) { - if(criteria.max instanceof Array) { - criteria.max.forEach(function(opt){ - query += 'MAX(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + - opt + ', '; - }); - - } else { - query += 'MAX(' + tableName + '.' + utils.escapeName(criteria.max) + - ') AS ' + criteria.max + ', '; - } - } - - // Handle MIN - if (criteria.min) { - if(criteria.min instanceof Array) { - criteria.min.forEach(function(opt){ - query += 'MIN(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + - opt + ', '; - }); - - } else { - query += 'MIN(' + tableName + '.' + utils.escapeName(criteria.min) + ') AS ' + - criteria.min + ', '; - } - } - - // trim trailing comma - query = query.slice(0, -2) + ' '; - - // Add FROM clause - return query += 'FROM ' + table + ' '; - } - + /************************************************* + * Commenting out below because new waterline + * deals with aggregates in separate functions. + * However, I am keeping the code for reference + * as I migrate it into those other functions + *************************************************/ + // if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || + // criteria.max) { + + // query = 'SELECT rowid, '; + + // // Append groupBy columns to select statement + // if(criteria.groupBy) { + // if(criteria.groupBy instanceof Array) { + // criteria.groupBy.forEach(function(opt){ + // query += tableName + '.' + utils.escapeName(opt) + ', '; + // }); + + // } else { + // query += tableName + '.' + utils.escapeName(criteria.groupBy) + ', '; + // } + // } + + // // Handle SUM + // if (criteria.sum) { + // if(criteria.sum instanceof Array) { + // criteria.sum.forEach(function(opt){ + // query += 'CAST(SUM(' + tableName + '.' + utils.escapeName(opt) + + // ') AS float) AS ' + opt + ', '; + // }); + + // } else { + // query += 'CAST(SUM(' + tableName + '.' + + // utils.escapeName(criteria.sum) + ') AS float) AS ' + criteria.sum + + // ', '; + // } + // } + + // // Handle AVG (casting to float to fix percision with trailing zeros) + // if (criteria.average) { + // if(criteria.average instanceof Array) { + // criteria.average.forEach(function(opt){ + // query += 'CAST(AVG(' + tableName + '.' + utils.escapeName(opt) + + // ') AS float) AS ' + opt + ', '; + // }); + + // } else { + // query += 'CAST(AVG(' + tableName + '.' + + // utils.escapeName(criteria.average) + ') AS float) AS ' + + // criteria.average + ', '; + // } + // } + + // // Handle MAX + // if (criteria.max) { + // if(criteria.max instanceof Array) { + // criteria.max.forEach(function(opt){ + // query += 'MAX(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + + // opt + ', '; + // }); + + // } else { + // query += 'MAX(' + tableName + '.' + utils.escapeName(criteria.max) + + // ') AS ' + criteria.max + ', '; + // } + // } + + // // Handle MIN + // if (criteria.min) { + // if(criteria.min instanceof Array) { + // criteria.min.forEach(function(opt){ + // query += 'MIN(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + + // opt + ', '; + // }); + + // } else { + // query += 'MIN(' + tableName + '.' + utils.escapeName(criteria.min) + ') AS ' + + // criteria.min + ', '; + // } + // } + + // // trim trailing comma + // query = query.slice(0, -2) + ' '; + + // // Add FROM clause + // return query += 'FROM ' + table + ' '; + // } query += 'SELECT rowid, '; var selectKeys = [], joinSelectKeys = []; - Object.keys(schema[table]).forEach(function(key) { - selectKeys.push({ table: table, key: key }); - }); + if (criteria.select && criteria.select.length >>> 0 > 0) { + for (let key of criteria.select) { + selectKeys.push({ table: tableName, key }); + } + } else { + selectKeys.push({ table: '', key: '*' }); + } // Check for joins + // NOTE: keeping joins here until I figure out how the + // new Waterline join method works if (criteria.joins) { var joins = criteria.joins; @@ -231,10 +242,12 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { // Add all the columns to be selected that are not joins selectKeys.forEach(function(select) { - query += utils.escapeName(select.table) + '.' + utils.escapeName(select.key) + ', '; + query += (!!select.table ? utils.escapeName(select.table) + '.' : '') + + utils.escapeName(select.key) + ', '; }); // Add all the columns from the joined tables + // NOTE keeping joins here until I know how the new waterline works joinSelectKeys.forEach(function(select) { // Create an alias by prepending the child table with the alias of the join From 800b58292c74b8db924cc6fbebe50ed6d73d963e Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 6 Dec 2018 22:15:26 -0500 Subject: [PATCH 59/81] Basic find works Don't try too much, including where conditions which are still untried --- index.js | 18 ++++++++++-------- lib/query.js | 17 +++++++++-------- lib/utils.js | 8 +++++--- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index 5205c02..c1dfae5 100644 --- a/index.js +++ b/index.js @@ -466,7 +466,7 @@ const adapter = { * @param {Array} [matching physical records] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - find: function (datastoreName, query, done) { + find: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -484,15 +484,17 @@ const adapter = { try { const values = []; - let resultCount = await wrapAsyncStatements( - client.each.bind(client, queryStatement.query, queryStatement.values,(err, row) => { - if (err) throw err; + await spawnConnection(dsEntry, async function __FIND__(client) { + let resultCount = await wrapAsyncStatements( + client.each.bind(client, queryStatement.query, queryStatement.values, (err, row) => { + if (err) throw err; - values.push(queryObj.castRow(row)); - })); + values.push(queryObj.castRow(row)); + })); - console.log(`${resultCount} results returned`); - done(undefined, values); + console.log(`${resultCount} results returned`); + done(undefined, values); + }) } catch (err) { done(err); } diff --git a/lib/query.js b/lib/query.js index 0648d4b..11748ab 100644 --- a/lib/query.js +++ b/lib/query.js @@ -35,7 +35,7 @@ const Query = function (tableName, schema, model) { Query.prototype.find = function(criteria) { - this._query = utils.buildSelectStatement(criteria, this._tableName, this._schema, this._model); + this._query = utils.buildSelectStatement(criteria, this._tableName, this._schema); if (criteria) this._build(criteria); @@ -156,7 +156,7 @@ Query.prototype.where = function(options) { var self = this, operators = this.operators(); - if (!options) return; + if (!options || Object.keys(options).length === 0) return; // Begin WHERE query this._query += 'WHERE '; @@ -452,14 +452,15 @@ Query.prototype.sort = function(options) { var self = this; this._query += ' ORDER BY '; + const sortItems = []; - Object.keys(options).forEach(function(key) { - var direction = options[key] === 1 ? 'ASC' : 'DESC'; - self._query += '"' + key + '" ' + direction + ', '; - }); + for (let sortItem of options) { + for (let column of sortItem) { + sortItems.push(`"${column} ${sortItem[column]}`); + } + } - // Remove trailing comma - this._query = this._query.slice(0, -2); + this._query += sortItems.join(', '); }; /** diff --git a/lib/utils.js b/lib/utils.js index b5082be..bd5f823 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -107,7 +107,7 @@ utils.escapeName = escapeName; * Builds a Select statement determining if Aggeragate options are needed. */ -utils.buildSelectStatement = function(criteria, table, attributes, schema) { +utils.buildSelectStatement = function(criteria, table, schema) { var query = ''; // Escape table name @@ -210,7 +210,9 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { selectKeys.push({ table: tableName, key }); } } else { - selectKeys.push({ table: '', key: '*' }); + for (let columnName in schema) { + selectKeys.push({ table: tableName, key: columnName }); + } } // Check for joins @@ -242,7 +244,7 @@ utils.buildSelectStatement = function(criteria, table, attributes, schema) { // Add all the columns to be selected that are not joins selectKeys.forEach(function(select) { - query += (!!select.table ? utils.escapeName(select.table) + '.' : '') + query += select.table + '.' + utils.escapeName(select.key) + ', '; }); From cbf871ad334176bf5741efa73f7bd74bdda2982b Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Fri, 7 Dec 2018 17:14:52 -0500 Subject: [PATCH 60/81] refactor create into createEach --- index.js | 92 ++++++++++++++++++++++++++++------------------------ lib/utils.js | 55 ++++++++++++++++++++++++------- 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/index.js b/index.js index c1dfae5..0fafccf 100644 --- a/index.js +++ b/index.js @@ -252,47 +252,20 @@ const adapter = { */ create: async function (datastoreName, query, done) { - // Look up the datastore entry (manager/driver/config). - const dsEntry = registeredDatastores[datastoreName]; - const {manager} = dsEntry; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } + // normalize newRecords property + query.newRecords = [query.newRecord]; + delete query.newRecord; try { - await spawnConnection(dsEntry, async client => { - const tableName = query.using; - const escapedTable = utils.escapeTable(tableName); - - const attributes = utils.mapAttributes(query.newRecord, manager.schema[tableName]); - - const columnNames = attributes.keys.join(', '); - const paramValues = attributes.params.join(', '); + const record = await wrapAsyncStatements( + adapter.createEach.bind(adapter, datastoreName, query)); - // Build query - var insertQuery = 'INSERT INTO ' + escapedTable + ' (' + columnNames + ') values (' + paramValues + ')'; - var selectQuery = 'SELECT * FROM ' + escapedTable + ' ORDER BY rowid DESC LIMIT 1'; - - // first insert values - await wrapAsyncStatements( - client.run.bind(client, insertQuery, attributes.values)); - - // get the last inserted row if requested - let newRecord; - if (query.meta && query.meta.fetch) { - const newRow = await wrapAsyncStatements(client.get.bind(client, selectQuery)); - newRecord = new Query(tableName, manager.schema[tableName], manager.models[tableName]) - .castRow(newRow); - } - - done(undefined, newRecord); - }); + if (record && record.length >>> 0 > 0) { + done(record[0]); + } } catch (err) { done(err); } - }, @@ -325,14 +298,47 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query (and if relevant, send back a result.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`createEach`) not implemented yet.')); - }, 16); + try { + await spawnConnection(dsEntry, async client => { + const tableName = query.using; + const escapedTable = utils.escapeTable(tableName); + + const attributeSets = utils.mapAllAttributes(query.newRecords, manager.schema[tableName]); + + const columnNames = attributeSets.keys.join(', '); + + const paramValues = attribute.paramLists.map((paramList) => { + return `( ${paramList.join(', ')} )`; + }).join(' '); + + const paramValues = attributes.params.join(', '); + + // Build query + var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues})`; + var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; + + // first insert values + await wrapAsyncStatements( + client.run.bind(client, insertQuery, attributes.values)); + + // get the last inserted rows if requested + let newRows; + if (query.meta && query.meta.fetch) { + newRows = []; + const queryObj = new Query(tableName, manager.schema[tableName], manager.models[tableName]); + + await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { + if (err) throw err; + + newRows.push(queryObj.castRow(row)); + })); + } + + done(undefined, newRows); + }); + } catch (err) { + done(err); + } }, diff --git a/lib/utils.js b/lib/utils.js index bd5f823..683a527 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -398,21 +398,54 @@ utils.escapeTable = function(table) { return '"' + table + '"'; }; -utils.mapAttributes = function(record, schema) { - const keys = []; // Column Names - const values = []; // Column Values - const params = []; // Param Index, ex: $1, $2 - let i = 1; +utils.mapAllAttributes = function(recordList, schema) { + const keys = new Set(); + const valueMaps = []; + + for (let record of recordList) { + const recordValueMap = {} + valueMaps.push(recordValueMap); + + for (let columnName in record) { + keys.add(`"${columnName}"`); + recordValueMap[columnName] = + utils.prepareValue(record[columnName], schema[columnName].type); + } + } + + const keyList = []; + + // create set order of columns (keys) + const keyIterator = keys.values(); + let next = keyIterator.next(); + while (!next.done) { + keyList.push(next.value); - for (let columnName in record) { - keys.push(`"${columnName}"`); - values.push(utils.prepareValue(record[columnName], schema[columnName].type)); - params.push('$' + i); + next = keyIterator.next(); + } - i++; + const paramLists = []; + let i = 1; + const valueList = []; + for (let values of valueMaps) { + const paramList = []; + paramLists.push(paramList); + + for (let key of keyList) { + let nextValue = values[key]; + + if (nextValue === undefined || nextValue === null) { + valueList.push(null); + } else { + valueList.push(nextValue); + } + + paramList.push('$' + i); + i++; + } } - return({ keys: keys, values: values, params: params }); + return ({ keys: keyList, values: valueList, paramLists: paramLists }); }; utils.normalizeSchema = function(schema) { From 0dad649f41ae98e6f74d01597e4a1fc83c38b269 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Fri, 7 Dec 2018 21:18:31 -0500 Subject: [PATCH 61/81] bug fix find, update, createEach find where criteria do not match current waterline api. must be migrated --- index.js | 17 ++++++++--------- lib/query.js | 23 +++++++++++------------ lib/utils.js | 29 ++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index 0fafccf..60604ad 100644 --- a/index.js +++ b/index.js @@ -261,7 +261,7 @@ const adapter = { adapter.createEach.bind(adapter, datastoreName, query)); if (record && record.length >>> 0 > 0) { - done(record[0]); + done(undefined, record[0]); } } catch (err) { done(err); @@ -288,10 +288,11 @@ const adapter = { * @param {Array?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - createEach: function (datastoreName, query, done) { + createEach: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; + const dsEntry = registeredDatastores[datastoreName]; + const manager = dsEntry.manager; // Sanity check: if (_.isUndefined(dsEntry)) { @@ -307,19 +308,17 @@ const adapter = { const columnNames = attributeSets.keys.join(', '); - const paramValues = attribute.paramLists.map((paramList) => { + const paramValues = attributeSets.paramLists.map((paramList) => { return `( ${paramList.join(', ')} )`; - }).join(' '); - - const paramValues = attributes.params.join(', '); + }).join(', '); // Build query - var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues})`; + var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues}`; var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; // first insert values await wrapAsyncStatements( - client.run.bind(client, insertQuery, attributes.values)); + client.run.bind(client, insertQuery, attributeSets.values)); // get the last inserted rows if requested let newRows; diff --git a/lib/query.js b/lib/query.js index 11748ab..314cec3 100644 --- a/lib/query.js +++ b/lib/query.js @@ -50,29 +50,28 @@ Query.prototype.find = function(criteria) { */ Query.prototype.update = function(criteria, data) { - this._query = 'UPDATE ' + this._tableName + ' '; + this._query = 'UPDATE ' + utils.escapeTable(this._tableName) + ' '; // Transform the Data object into arrays used in a parameterized query - var attributes = utils.mapAttributes(data, this._schema); + var attributes = utils.mapAllAttributes([data], this._schema); + + const params = attributes.paramLists[0]; // Update the paramCount - this._paramCount = attributes.params.length + 1; + this._paramCount = params.length + 1; // Build SET string - var str = ''; + const assignments = []; for (var i = 0; i < attributes.keys.length; i++) { - str += attributes.keys[i] + ' = ' + attributes.params[i] + ', '; + assignments.push(`${attributes.keys[i]} = ${params[i]}`); } - // Remove trailing comma - str = str.slice(0, -2); - - this._query += 'SET ' + str + ' '; + this._query += `SET ${assignments.join(', ')} `; // Add data values to this._values this._values = attributes.values; // Build criteria clause - if (criteria) this._build({where: criteria.where}); + if (criteria) this._build({ where: criteria.where }); return { query: this._query, @@ -455,8 +454,8 @@ Query.prototype.sort = function(options) { const sortItems = []; for (let sortItem of options) { - for (let column of sortItem) { - sortItems.push(`"${column} ${sortItem[column]}`); + for (let column in sortItem) { + sortItems.push(`"${column}" ${sortItem[column]}`); } } diff --git a/lib/utils.js b/lib/utils.js index 683a527..62dcff1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -398,8 +398,21 @@ utils.escapeTable = function(table) { return '"' + table + '"'; }; +/** + * Map data from a stage-3 query to its representation + * in Sqlite. Collects a list of columns and processes + * an group of records. + * @param recordList An array of records to be processed + * @param schema The schema for the table + * @return Object with below properties: + * - keys: array of keys (column names) in the order the data will be represented + * - values: array of values. Values are ordered such that they line up + * exactly with a flattened version of the paramLists property + * - paramLists: array of arrays. Each element of the outer array is an array of + * values in the same order as keys. + */ utils.mapAllAttributes = function(recordList, schema) { - const keys = new Set(); + const keys = new Map(); const valueMaps = []; for (let record of recordList) { @@ -407,21 +420,19 @@ utils.mapAllAttributes = function(recordList, schema) { valueMaps.push(recordValueMap); for (let columnName in record) { - keys.add(`"${columnName}"`); + keys.set(columnName, `"${columnName}"`); recordValueMap[columnName] = utils.prepareValue(record[columnName], schema[columnName].type); } } const keyList = []; + const objKeys = []; // create set order of columns (keys) - const keyIterator = keys.values(); - let next = keyIterator.next(); - while (!next.done) { - keyList.push(next.value); - - next = keyIterator.next(); + for (let entry of keys) { + objKeys.push(entry[0]); + keyList.push(entry[1]); } const paramLists = []; @@ -431,7 +442,7 @@ utils.mapAllAttributes = function(recordList, schema) { const paramList = []; paramLists.push(paramList); - for (let key of keyList) { + for (let key of objKeys) { let nextValue = values[key]; if (nextValue === undefined || nextValue === null) { From 98e673002d84cf504f6972cd34a1845cc2c6837a Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 8 Dec 2018 13:03:17 -0500 Subject: [PATCH 62/81] Begin refactor query.where refactoring whole method for better logical structuring and new waterline where format --- lib/query.js | 137 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 22 deletions(-) diff --git a/lib/query.js b/lib/query.js index 314cec3..b1fef1f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -117,41 +117,134 @@ Query.prototype._build = function(criteria) { * Specifiy a `where` condition * * `Where` conditions may use key/value model attributes for simple query - * look ups as well as more complex conditions. + * look ups. Complex conditions are grouped in 'AND' and 'OR' arrays * * The following conditions are supported along with simple criteria: * * Conditions: - * [And, Or, Like, Not] + * [And, Or] * * Criteria Operators: - * [<, <=, >, >=, !] - * - * Criteria Helpers: - * [lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual, not, like, contains, startsWith, endsWith] + * [<, <=, >, >=, !=, nin, in, like] * * ####Example * * where: { - * name: 'foo', - * age: { - * '>': 25 - * }, - * like: { - * name: '%foo%' - * }, - * or: [ - * { like: { foo: '%foo%' } }, - * { like: { bar: '%bar%' } } - * ], - * name: [ 'foo', 'bar;, 'baz' ], - * age: { - * not: 40 - * } + * and: [ + * {name: 'foo'}, + * {age: { '>': 25 }}, + * {desc: {like: '%hello%'}} + * ] * } */ +Query.prototype.where = function(criteria) { + const criteriaTree = new Criterion(this._schema); + criteriaTree.addCriterion(criteria); + + const parsedCriteria = criteriaTree.generateCriteria(); + this._query += parsedCriteria.whereClause; + this._values = this._values.concat(parsedCriteria.values); +} + +/** + * Utility class for building criteria. Constructs a tree and recurses down + * it to build the final string + */ +class Criterion { + constructor(schema, joinString = '', rawCriterion){ + this._schema = schema; + this._joinString = joinString; + this._isLeaf = !!rawCriterion; + this._criteriaList = []; + this._rawCriterion = rawCriterion + this._paramIndex = 1; + } + + addCriterion(rawCriterion) { + const newCriterion = this._constructCriterionTree(rawCriterion); + + this._criteriaList.push(newCriterion); + } + + /** + * Private method + * @param {Object} rawCriterion + */ + _constructCriterionTree(rawCriterion) { + let subCriterion; + if (!!rawCriterion.and) { + subCriterion = new Criterion(this._schema, ' AND '); + + for (let andElement of rawCriterion.and) { + subCriterion.addCriterion(andElement); + } + } else if (!!rawCriterion.or) { + subCriterion = new Criterion(this._schema, ' OR '); + + for (let orElement of rawCriterion.or) { + subCriterion.addCriterion(orElement); + } + } else { + subCriterion = new Criterion(this._schema, '', rawCriterion); + } + + return subCriterion; + } + + /** + * Performs the logic of generating the criteria. If not a leaf, recurses + * down and wraps sub-criteria in parentheses + * @return {Object} + * @property {String} whereClause The constructed string + * @property {Array} values The values matching params + * @property {Number} paramIndex The number of parameters generated by this function + */ + generateCriteria(paramIndex = -1) { + //differentiate from root call + if (paramIndex > 0) { + this._paramIndex = paramIndex; + } + + if (!this._isLeaf) { + const subCriteriaResults = this._criteriaList + .map(criterion => { + const result = criterion.generateCriteria(this._paramIndex); + this._paramIndex = result.paramIndex; + + return result; + }); + + const values = subCriteriaResults.reduce((previous, next) => { + return previous.concat(next.values); + }, []); + + const whereClause = (paramIndex === -1 ? 'WHERE ' : '') + + subCriteriaResults + .map(sub => `(${sub.whereClause})`) + .join(this._joinString); + + return {whereClause, values, paramIndex: this._paramIndex}; + } + + /**** Leaf Node Code *****/ + if (Object.keys(this._rawCriterion).length > 1) { + throw new Error('Unexpected query object: more than one column key'); + } + for (let columnName in this._rawCriterion) { + // get schema type so we know how to process + const columnType = this._schema[columnName]; + + /************************************ + * START HERE + * Parse operators + * Do checks for operators with valid + * data types + ***********************************/ + } + } +} -Query.prototype.where = function(options) { +Query.prototype.whereBak = function(options) { var self = this, operators = this.operators(); From 6847f1df5d37d482a43ef95234997443c3270af4 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Wed, 12 Dec 2018 11:00:42 -0500 Subject: [PATCH 63/81] Finish an implementation of where Criteria tree evaluated recursively, includes all v1 operators, unsure about JSON column type --- lib/query.js | 90 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/lib/query.js b/lib/query.js index b1fef1f..80926a7 100644 --- a/lib/query.js +++ b/lib/query.js @@ -191,6 +191,30 @@ class Criterion { return subCriterion; } + /** + * Private Method. Converts list to string. + * Increments _paramIndex + * @param {Array} list + * @return {Object} + * @param {String} setString + * @param {Array} dedupedValues + */ + _arrayToSqlSet(list) { + const valueSet = new Set(list); + const dedupedValues = []; + const paramList = []; + + for (let val of valueSet) { + dedupedValues.push(val); + paramList.push(`$${this._paramIndex++}`); + } + + return { + setString: `(${paramList.map(obj => obj.toString()).join(', ')})`, + dedupedValues + }; + } + /** * Performs the logic of generating the criteria. If not a leaf, recurses * down and wraps sub-criteria in parentheses @@ -227,20 +251,62 @@ class Criterion { } /**** Leaf Node Code *****/ - if (Object.keys(this._rawCriterion).length > 1) { - throw new Error('Unexpected query object: more than one column key'); + if (Object.keys(this._rawCriterion).length !== 1) { + throw new Error('Unexpected query object: exactly one column key expected'); } - for (let columnName in this._rawCriterion) { - // get schema type so we know how to process - const columnType = this._schema[columnName]; - - /************************************ - * START HERE - * Parse operators - * Do checks for operators with valid - * data types - ***********************************/ + let columnName = Object.keys(this._rawCriterion)[0]; + let condition = this._rawCriterion[columnName]; + let values = []; + let whereClause; + // get schema / model type so we know how to process + const columnType = this._schema[columnName].type; + const waterlineType = this._model[columnName].columnType; + + /*************************************************** + * NOTE: What about JSON waterline typed columns? + * Not really accounted for yet + ***************************************************/ + + if (condition == null) { + whereClause = `${columnName} IS NULL`; + } else if (typeof condition != 'object' || columnType === 'BLOB') { + whereClause = `${columnName} = $${this._paramIndex++}`; + values.push(condition); + } else { + const conditionKeys = Object.keys(condition); + if (conditionKeys.length !== 1) { + throw new Error('Multiple conditions detected for one column condition. The adapter is confused'); + } + + const operator = conditionKeys[0]; + let dedupedValues, setString; + switch (operator) { + case '>': + case '>=': + case '<': + case '<=': + case 'like': + whereClause = `${columnName} ${operator.toUpperCase()} $${this._paramIndex++}`; + values.push(condition[operator]); + break; + case '!=': + whereClause = `${columnName} <> $${this._paramIndex++}`; + values.push(condition[operator]); + break; + case 'in': + ({dedupedValues, setString}) = this._arrayToSqlSet(condition[operator]); + values = dedupedValues; + whereClause = `${columnName} IN ${setString}`; + break; + case 'nin': + ({ dedupedValues, setString }) = this._arrayToSqlSet(condition[operator]); + values = dedupedValues; + whereClause = `${columnName} IN ${setString}`; + break; + } } + + return {whereClause, values, paramIndex: this._paramIndex}; } } From e45bc6517f789d87f30259130f2cd2b4d1397fe6 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Wed, 12 Dec 2018 11:25:54 -0500 Subject: [PATCH 64/81] Query.where bug fixes All criteria pass smoke tests (No real testing though) --- index.js | 1 - lib/query.js | 22 +++++++++------------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 60604ad..aeb4a58 100644 --- a/index.js +++ b/index.js @@ -497,7 +497,6 @@ const adapter = { values.push(queryObj.castRow(row)); })); - console.log(`${resultCount} results returned`); done(undefined, values); }) } catch (err) { diff --git a/lib/query.js b/lib/query.js index 80926a7..a78dd9c 100644 --- a/lib/query.js +++ b/lib/query.js @@ -138,12 +138,14 @@ Query.prototype._build = function(criteria) { * } */ Query.prototype.where = function(criteria) { - const criteriaTree = new Criterion(this._schema); - criteriaTree.addCriterion(criteria); + if (Object.keys(criteria).length > 0) { + const criteriaTree = new Criterion(this._schema); + criteriaTree.addCriterion(criteria); - const parsedCriteria = criteriaTree.generateCriteria(); - this._query += parsedCriteria.whereClause; - this._values = this._values.concat(parsedCriteria.values); + const parsedCriteria = criteriaTree.generateCriteria(); + this._query += parsedCriteria.whereClause; + this._values = this._values.concat(parsedCriteria.values); + } } /** @@ -260,12 +262,6 @@ class Criterion { let whereClause; // get schema / model type so we know how to process const columnType = this._schema[columnName].type; - const waterlineType = this._model[columnName].columnType; - - /*************************************************** - * NOTE: What about JSON waterline typed columns? - * Not really accounted for yet - ***************************************************/ if (condition == null) { whereClause = `${columnName} IS NULL`; @@ -294,12 +290,12 @@ class Criterion { values.push(condition[operator]); break; case 'in': - ({dedupedValues, setString}) = this._arrayToSqlSet(condition[operator]); + ({dedupedValues, setString} = this._arrayToSqlSet(condition[operator])) values = dedupedValues; whereClause = `${columnName} IN ${setString}`; break; case 'nin': - ({ dedupedValues, setString }) = this._arrayToSqlSet(condition[operator]); + ({ dedupedValues, setString } = this._arrayToSqlSet(condition[operator])) values = dedupedValues; whereClause = `${columnName} IN ${setString}`; break; From 88e4b321c0c4fd707c4db0a26a3468cb573c7b9f Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 15 Dec 2018 16:20:59 -0500 Subject: [PATCH 65/81] destroy method --- index.js | 40 +++++++++++++++++++++++++++++----------- lib/query.js | 4 ++-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index aeb4a58..1152b7e 100644 --- a/index.js +++ b/index.js @@ -375,8 +375,6 @@ const adapter = { try { await spawnConnection(dsEntry, async (client) => { const tableName = query.using; - const escapedTable = utils.escapeTable(tableName); - const tableSchema = dsEntry.manager.schema[tableName]; const model = dsEntry.manager.models[tableName]; @@ -420,7 +418,7 @@ const adapter = { * @param {Array?} * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - destroy: function (datastoreName, query, done) { + destroy: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -430,14 +428,34 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query (and if relevant, send back a result.) - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`destroy`) not implemented yet.')); - }, 16); + try { + await spawnConnection(dsEntry, async function __DELETE__(client) { + const tableName = query.using; + const tableSchema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const _query = new Query(tableName, tableSchema, model); + const queryObj = _query.destroy(query.criteria); + + let results; + if (query.meta.fetch) { + results = []; + const findRows = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, query)); + + for (let row of findRows) { + results.push(_query.castRow(row)); + } + } + + await wrapAsyncStatements( + client.run.bind(client, queryObj.query, queryObj.values)); + + done(undefined, results); + }); + } catch (err) { + done(err); + } }, diff --git a/lib/query.js b/lib/query.js index a78dd9c..51074fa 100644 --- a/lib/query.js +++ b/lib/query.js @@ -84,8 +84,8 @@ Query.prototype.update = function(criteria, data) { * DELETE Statement */ -Query.prototype.destroy = function(table, criteria) { - this._query = 'DELETE FROM ' + table + ' '; +Query.prototype.destroy = function(criteria) { + this._query = `DELETE FROM ${utils.escapeTable(this._tableName)} `; if (criteria) this._build(criteria); return { From 4ddafc01923bd5267261deb6e4b272580a300e8c Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 15 Dec 2018 17:02:51 -0500 Subject: [PATCH 66/81] bug fixes --- index.js | 44 ++++++++++++++++++++++---------------------- lib/query.js | 6 +++++- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/index.js b/index.js index 1152b7e..48d978a 100644 --- a/index.js +++ b/index.js @@ -439,13 +439,8 @@ const adapter = { let results; if (query.meta.fetch) { - results = []; - const findRows = await wrapAsyncStatements( + results = await wrapAsyncStatements( adapter.find.bind(adapter, datastoreName, query)); - - for (let row of findRows) { - results.push(_query.castRow(row)); - } } await wrapAsyncStatements( @@ -545,26 +540,31 @@ const adapter = { * @param {Array} [matching physical records, populated according to the join instructions] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - join: function (datastoreName, query, done) { + /**************************************** + * NOTE: Intention is to support joins. + * Ignoring for the time being since + * waterline polyfills a join in memory + ***************************************/ + // join: function (datastoreName, query, done) { - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; + // // Look up the datastore entry (manager/driver/config). + // var dsEntry = registeredDatastores[datastoreName]; - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } + // // Sanity check: + // if (_.isUndefined(dsEntry)) { + // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + // } - // Perform the query and send back a result. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`join`) not implemented yet.')); - }, 16); + // // Perform the query and send back a result. + // // + // // > TODO: Replace this setTimeout with real logic that calls + // // > `done()` when finished. (Or remove this method from the + // // > adapter altogether + // setTimeout(function(){ + // return done(new Error('Adapter method (`join`) not implemented yet.')); + // }, 16); - }, + // }, /** diff --git a/lib/query.js b/lib/query.js index 51074fa..65966b0 100644 --- a/lib/query.js +++ b/lib/query.js @@ -86,7 +86,11 @@ Query.prototype.update = function(criteria, data) { Query.prototype.destroy = function(criteria) { this._query = `DELETE FROM ${utils.escapeTable(this._tableName)} `; - if (criteria) this._build(criteria); + if (criteria) { + criteria = Object.assign({}, criteria); + delete criteria.limit; //we don't want a limit in a delete query + this._build(criteria); + } return { query: this._query, From 9d4730116da271c858aa713ea18cc08a9eaa7147 Mon Sep 17 00:00:00 2001 From: Kevin C Gall Date: Sat, 15 Dec 2018 19:21:24 -0500 Subject: [PATCH 67/81] refactor for sequential writes Sqlite is not made for concurrent writes - refactor to only spawn one write client --- index.js | 188 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 85 deletions(-) diff --git a/index.js b/index.js index 48d978a..eec9f02 100644 --- a/index.js +++ b/index.js @@ -152,6 +152,27 @@ const adapter = { return done(new Error('Consistency violation: Cannot register datastore: `' + datastoreName + '`, because it is already registered with this adapter! This could be due to an unexpected race condition in userland code (e.g. attempting to initialize Waterline more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } + let writeClient; + try { + writeClient = await wrapAsyncStatements((cb) => { + if (datastoreConfig.verbose) sqlite3 = sqlite3.verbose(); + const writeClient = new sqlite3.Database( + datastoreConfig.filename, + datastoreConfig.mode, + err => { + if (!err) { + //set write client to serialize mode + writeClient.serialize(); + } + + cb(err, writeClient); + } + ); + }); + } catch (err) { + done(err); + } + // To maintain the spirit of this repository, this implementation will // continue to spin up and tear down a connection to the Sqlite db on every // request. @@ -162,7 +183,8 @@ const adapter = { manager: { models: physicalModelsReport, //for reference schema: {}, - foreignKeys: utils.buildForeignKeyMap(physicalModelsReport) + foreignKeys: utils.buildForeignKeyMap(physicalModelsReport), + writeClient, }, // driver: undefined // << TODO: include driver here (if relevant) }; @@ -206,7 +228,8 @@ const adapter = { return done(new Error('Consistency violation: Attempting to tear down a datastore (`'+datastoreName+'`) which is not currently registered with this adapter. This is usually due to a race condition in userland code (e.g. attempting to tear down the same ORM instance more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // No manager to speak of + // Close write client + dsEntry.manager.writeClient.close(); delete registeredDatastores[datastoreName]; // Inform Waterline that we're done, and that everything went as expected. @@ -300,41 +323,40 @@ const adapter = { } try { - await spawnConnection(dsEntry, async client => { - const tableName = query.using; - const escapedTable = utils.escapeTable(tableName); + const client = manager.writeClient; + const tableName = query.using; + const escapedTable = utils.escapeTable(tableName); - const attributeSets = utils.mapAllAttributes(query.newRecords, manager.schema[tableName]); + const attributeSets = utils.mapAllAttributes(query.newRecords, manager.schema[tableName]); - const columnNames = attributeSets.keys.join(', '); + const columnNames = attributeSets.keys.join(', '); - const paramValues = attributeSets.paramLists.map((paramList) => { - return `( ${paramList.join(', ')} )`; - }).join(', '); + const paramValues = attributeSets.paramLists.map((paramList) => { + return `( ${paramList.join(', ')} )`; + }).join(', '); - // Build query - var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues}`; - var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; + // Build query + var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues}`; + var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; - // first insert values - await wrapAsyncStatements( - client.run.bind(client, insertQuery, attributeSets.values)); + // first insert values + await wrapAsyncStatements( + client.run.bind(client, insertQuery, attributeSets.values)); - // get the last inserted rows if requested - let newRows; - if (query.meta && query.meta.fetch) { - newRows = []; - const queryObj = new Query(tableName, manager.schema[tableName], manager.models[tableName]); + // get the last inserted rows if requested + let newRows; + if (query.meta && query.meta.fetch) { + newRows = []; + const queryObj = new Query(tableName, manager.schema[tableName], manager.models[tableName]); - await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { - if (err) throw err; + await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { + if (err) throw err; - newRows.push(queryObj.castRow(row)); - })); - } + newRows.push(queryObj.castRow(row)); + })); + } - done(undefined, newRows); - }); + done(undefined, newRows); } catch (err) { done(err); } @@ -373,25 +395,24 @@ const adapter = { } try { - await spawnConnection(dsEntry, async (client) => { - const tableName = query.using; - const tableSchema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; + const client = dsEntry.manager.writeClient; + const tableName = query.using; + const tableSchema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; - const _query = new Query(tableName, tableSchema, model); - const updateQuery = _query.update(query.criteria, query.valuesToSet); + const _query = new Query(tableName, tableSchema, model); + const updateQuery = _query.update(query.criteria, query.valuesToSet); - const statement = await wrapAsyncForThis( - client.run.bind(client, updateQuery.query, updateQuery.values)); + const statement = await wrapAsyncForThis( + client.run.bind(client, updateQuery.query, updateQuery.values)); - let results; - if (statement.changes > 0 && query.meta && query.meta.fetch) { - results = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, query)); - } + let results; + if (statement.changes > 0 && query.meta && query.meta.fetch) { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, query)); + } - done(undefined, results); - }); + done(undefined, results); } catch (err) { done(err); } @@ -429,25 +450,24 @@ const adapter = { } try { - await spawnConnection(dsEntry, async function __DELETE__(client) { - const tableName = query.using; - const tableSchema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const _query = new Query(tableName, tableSchema, model); - const queryObj = _query.destroy(query.criteria); - - let results; - if (query.meta.fetch) { - results = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, query)); - } + const client = dsEntry.manager.writeClient; + const tableName = query.using; + const tableSchema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const _query = new Query(tableName, tableSchema, model); + const queryObj = _query.destroy(query.criteria); + + let results; + if (query.meta.fetch) { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, query)); + } - await wrapAsyncStatements( - client.run.bind(client, queryObj.query, queryObj.values)); + await wrapAsyncStatements( + client.run.bind(client, queryObj.query, queryObj.values)); - done(undefined, results); - }); + done(undefined, results); } catch (err) { done(err); } @@ -502,7 +522,7 @@ const adapter = { try { const values = []; - await spawnConnection(dsEntry, async function __FIND__(client) { + await spawnReadonlyConnection(dsEntry, async function __FIND__(client) { let resultCount = await wrapAsyncStatements( client.each.bind(client, queryStatement.query, queryStatement.values, (err, row) => { if (err) throw err; @@ -695,7 +715,7 @@ const adapter = { */ describe: async function describe(datastoreName, tableName, cb, meta) { var datastore = registeredDatastores[datastoreName]; - spawnConnection(datastore, async function __DESCRIBE__(client) { + spawnReadonlyConnection(datastore, async function __DESCRIBE__(client) { // Get a list of all the tables in this database // See: http://www.sqlite.org/faq.html#q7) var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + tableName + '" ORDER BY name'; @@ -805,31 +825,30 @@ const adapter = { } try { - await spawnConnection(datastore, async function __DEFINE__(client){ - const escapedTable = utils.escapeTable(tableName); + const client = datastore.manager.writeClient; + const escapedTable = utils.escapeTable(tableName); - // Iterate through each attribute, building a query string - const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); + // Iterate through each attribute, building a query string + const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); - // Check for any index attributes - const indices = utils.buildIndexes(definition); + // Check for any index attributes + const indices = utils.buildIndexes(definition); - // Build query - const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; + // Build query + const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; - await wrapAsyncStatements(client.run.bind(client, query)); + await wrapAsyncStatements(client.run.bind(client, query)); - await Promise.all(indices.map(async index => { - // Build a query to create a namespaced index tableName_key - var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + - tableName + ' (' + index + ');'; + await Promise.all(indices.map(async index => { + // Build a query to create a namespaced index tableName_key + var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + + tableName + ' (' + index + ');'; - await wrapAsyncStatements(client.run.bind(client, query)); - })); + await wrapAsyncStatements(client.run.bind(client, query)); + })); - // Replacing if it already existed - datastore.manager.schema[tableName] = _schema.schema; - }); + // Replacing if it already existed + datastore.manager.schema[tableName] = _schema.schema; done(); } catch (err) { @@ -870,9 +889,8 @@ const adapter = { try { - await spawnConnection(dsEntry, async function __DROP__(client) { - await wrapAsyncStatements(client.run.bind(client, query)); - }); + const client = dsEntry.manager.writeClient; + await wrapAsyncStatements(client.run.bind(client, query)); delete dsEntry.manager.schema[tableName]; done(); @@ -936,7 +954,7 @@ const adapter = { * @param {*} cb * @return Promise */ -function spawnConnection(datastore, logic) { +function spawnReadonlyConnection(datastore, logic) { let client; return new Promise((resolve, reject) => { if (!datastore) reject(Errors.InvalidConnection); @@ -954,7 +972,7 @@ function spawnConnection(datastore, logic) { // Create a new handle to our database client = new sqlite3.Database( datastoreConfig.filename, - datastoreConfig.mode, + sqlite3.OPEN_READONLY, err => { if (err) reject(err); else resolve(client); From 3f85426c9141a7db6b81aff4d6260c970cb1c97f Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sun, 16 Dec 2018 14:20:37 -0500 Subject: [PATCH 68/81] Prepping for new tests --- package-lock.json | 436 +++++++++++++++++++++++++++---------- package.json | 5 +- test/integration/runner.js | 8 +- 3 files changed, 335 insertions(+), 114 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdcbdb4..4f6ed65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,14 +26,13 @@ } }, "anchor": { - "version": "0.9.13", - "resolved": "https://registry.npmjs.org/anchor/-/anchor-0.9.13.tgz", - "integrity": "sha1-K+SteA3SCo78vnnMFUWksMaxBEE=", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/anchor/-/anchor-1.3.0.tgz", + "integrity": "sha512-mA+EfMr/WVT69u1HisKqQED7+LmTxpb0Lm9Lo/qTT/uf7AOFA3qYYb/ZPiMi3aQqWn2ji4fC6UQuRIP0XBV9ZA==", "dev": true, "requires": { - "async": "0.2.10", - "lodash": "~2.4.1", - "validator": "~3.3.0" + "@sailshq/lodash": "^3.10.2", + "validator": "5.7.0" } }, "ansi-regex": { @@ -68,11 +67,6 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, - "async": { - "version": "0.2.10", - "resolved": "http://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -101,6 +95,12 @@ "tweetnacl": "^0.14.3" } }, + "bluebird": { + "version": "3.2.1", + "resolved": "http://registry.npmjs.org/bluebird/-/bluebird-3.2.1.tgz", + "integrity": "sha1-POzzUEkEwwzj55wXCHfok6EZEP0=", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -211,6 +211,15 @@ "safer-buffer": "^2.1.0" } }, + "encrypted-attr": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/encrypted-attr/-/encrypted-attr-1.0.6.tgz", + "integrity": "sha512-12WE8GDkbhKcGmVp6+TyJXCcFj9NF7db33nutjOSBLlMuYY4oCGricgTEUAuRSI1xLeE1nhoDD6jSx20WgFVYg==", + "dev": true, + "requires": { + "lodash": "^4.17.4" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -237,6 +246,15 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "flaverr": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/flaverr/-/flaverr-1.9.2.tgz", + "integrity": "sha512-14CoGOGUhFkhzDCgGdpFFJE9PrdMPhGmhuS39WxxgTpTKVzfWW3DAVfolUiHwYpaROz7UFrJuaSJtsxhem+i9g==", + "dev": true, + "requires": { + "@sailshq/lodash": "^3.10.2" + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -252,6 +270,19 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "0.30.0", + "resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", + "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" + } + }, "fs-minipass": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", @@ -302,9 +333,15 @@ } }, "graceful-fs": { - "version": "2.0.3", - "resolved": "http://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz", - "integrity": "sha1-fNLNsiiko/Nule+mzBQt59GhNtA=", + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", "dev": true }, "growl": { @@ -412,30 +449,6 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jade": { - "version": "0.26.3", - "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", - "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", - "dev": true, - "requires": { - "commander": "0.6.1", - "mkdirp": "0.3.0" - }, - "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", - "dev": true - }, - "mkdirp": { - "version": "0.3.0", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", - "dev": true - } - } - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -456,6 +469,21 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -467,18 +495,95 @@ "verror": "1.10.0" } }, + "klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, "lodash": { - "version": "2.4.2", - "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", "dev": true }, - "lru-cache": { - "version": "2.7.3", - "resolved": "http://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "lodash.issafeinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.issafeinteger/-/lodash.issafeinteger-4.0.4.tgz", + "integrity": "sha1-sXbVmQ7GSdBr7cvOLwKOeJBJT5A=", "dev": true }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, "mime-db": { "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", @@ -687,6 +792,17 @@ "os-tmpdir": "^1.0.0" } }, + "parley": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/parley/-/parley-3.8.0.tgz", + "integrity": "sha512-WhNhNMPoxTydFg7U/MCAraE4JJLVSYeCJ3Wg5xHb1Z3EKC5N+SXldq6NYAtrgBSLpo4jHKQI2SWOhWEHJ82CGw==", + "dev": true, + "requires": { + "@sailshq/lodash": "^3.10.2", + "bluebird": "3.2.1", + "flaverr": "^1.5.1" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -712,12 +828,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "q": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz", - "integrity": "sha1-TeLmyzspCIyeTLwDv51C+5bOL3U=", - "dev": true - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -790,6 +900,15 @@ "glob": "^7.0.5" } }, + "rttc": { + "version": "10.0.0-4", + "resolved": "https://registry.npmjs.org/rttc/-/rttc-10.0.0-4.tgz", + "integrity": "sha512-HroJ9z+RVipbPCeFdglopiVM18w9BM5PFqXivM6ZceNQEphjpDGZ154srk1JhviNacrmFqhdzDbGQZsB13g6JA==", + "dev": true, + "requires": { + "@sailshq/lodash": "^3.10.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -869,12 +988,6 @@ "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", "dev": true }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -1013,9 +1126,9 @@ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "validator": { - "version": "3.3.0", - "resolved": "http://registry.npmjs.org/validator/-/validator-3.3.0.tgz", - "integrity": "sha1-GCIZTgpGsR+MI7GLwTDvxWCtTYc=", + "version": "5.7.0", + "resolved": "http://registry.npmjs.org/validator/-/validator-5.7.0.tgz", + "integrity": "sha1-eoelgUa2laxIYHEUHAxJ1n2gXlw=", "dev": true }, "verror": { @@ -1029,86 +1142,150 @@ } }, "waterline": { - "version": "0.9.16", - "resolved": "http://registry.npmjs.org/waterline/-/waterline-0.9.16.tgz", - "integrity": "sha1-khsI7pXRV5C9aQsYGuilJNAQSHQ=", + "version": "github:balderdashy/waterline#90b8a0a9132862faaa3b6851a8c4db1f8c41b23c", + "from": "github:balderdashy/waterline", "dev": true, "requires": { - "anchor": "~0.9.12", - "async": "~0.2.9", - "q": "~0.9.7", - "underscore": "~1.5.2" + "@sailshq/lodash": "^3.10.2", + "anchor": "^1.2.0", + "async": "2.0.1", + "encrypted-attr": "1.0.6", + "flaverr": "^1.8.3", + "lodash.issafeinteger": "4.0.4", + "parley": "^3.3.2", + "rttc": "^10.0.0-1", + "waterline-schema": "^1.0.0-20", + "waterline-utils": "^1.3.7" + }, + "dependencies": { + "async": { + "version": "2.0.1", + "resolved": "http://registry.npmjs.org/async/-/async-2.0.1.tgz", + "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", + "dev": true, + "requires": { + "lodash": "^4.8.0" + } + } } }, "waterline-adapter-tests": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/waterline-adapter-tests/-/waterline-adapter-tests-0.9.4.tgz", - "integrity": "sha1-wXBBIoCLK2PMMfLvLa6GWbsHHuU=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/waterline-adapter-tests/-/waterline-adapter-tests-1.0.1.tgz", + "integrity": "sha512-dBD2410D1bYWXVTcBqRSVvOF3MNEmUDMKry7L5p4PlT4tIkmaR5JfKvSMWvXr5m3edSfL4VaEVhWAEJ27+VoqQ==", "dev": true, "requires": { - "mocha": "~1.13.0", - "underscore": "~1.5.2", - "waterline": "~0.9.8" + "@sailshq/lodash": "3.10.2", + "async": "2.0.1", + "bluebird": "3.2.1", + "mocha": "3.0.2", + "waterline": "github:balderdashy/waterline#90b8a0a9132862faaa3b6851a8c4db1f8c41b23c", + "waterline-utils": "^1.3.2" }, "dependencies": { - "commander": { - "version": "0.6.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "@sailshq/lodash": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@sailshq/lodash/-/lodash-3.10.2.tgz", + "integrity": "sha1-FWfUc0U2TCwuIHe8ETSHsd/mIVQ=", "dev": true }, + "async": { + "version": "2.0.1", + "resolved": "http://registry.npmjs.org/async/-/async-2.0.1.tgz", + "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", + "dev": true, + "requires": { + "lodash": "^4.8.0" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.9.0", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, "diff": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", - "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", "dev": true }, "glob": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", - "integrity": "sha1-4xPusknHr/qlxHUoaw4RW1mDlGc=", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", "dev": true, "requires": { - "graceful-fs": "~2.0.0", + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", "inherits": "2", - "minimatch": "~0.2.11" + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "growl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", - "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "mocha": { + "version": "3.0.2", + "resolved": "http://registry.npmjs.org/mocha/-/mocha-3.0.2.tgz", + "integrity": "sha1-Y6l/Phj00+ZZ1HphdnfQiYdFV/A=", "dev": true, "requires": { - "lru-cache": "2", - "sigmund": "~1.0.0" + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.0.5", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" } }, - "mkdirp": { - "version": "0.3.5", - "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", + "ms": { + "version": "0.7.1", + "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", "dev": true }, - "mocha": { - "version": "1.13.0", - "resolved": "http://registry.npmjs.org/mocha/-/mocha-1.13.0.tgz", - "integrity": "sha1-jY+k4xC5TMbv6z7SauypbeqTMHw=", + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", "dev": true, "requires": { - "commander": "0.6.1", - "debug": "*", - "diff": "1.0.7", - "glob": "3.2.3", - "growl": "1.7.x", - "jade": "0.26.3", - "mkdirp": "0.3.5" + "has-flag": "^1.0.0" } } } @@ -1118,6 +1295,47 @@ "resolved": "https://registry.npmjs.org/waterline-errors/-/waterline-errors-0.10.1.tgz", "integrity": "sha1-7mNjKq3emTJxt1FLfKmNn9W4ai4=" }, + "waterline-schema": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/waterline-schema/-/waterline-schema-1.0.0.tgz", + "integrity": "sha512-dSz/CvOLYMULKieB91+ZSv415+AVgrLhlSWbhpVHfpczIbKyj+zorsB5AG+ukGw1z0CPs6F1ib8MicBNjtwv6g==", + "dev": true, + "requires": { + "@sailshq/lodash": "^3.10.2", + "flaverr": "^1.8.1", + "rttc": "^10.0.0-1" + } + }, + "waterline-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/waterline-utils/-/waterline-utils-1.4.2.tgz", + "integrity": "sha512-WsS1yRw8xT6O7iIK8oAcYr3/vhMYiTHdA/vQnU/JsGbcad2Xi4p8bie8/F+BoEyN7SEpBZ8nFT9OdszDAt7Ugg==", + "dev": true, + "requires": { + "@sailshq/lodash": "^3.10.2", + "async": "2.0.1", + "flaverr": "^1.1.1", + "fs-extra": "0.30.0", + "qs": "6.4.0" + }, + "dependencies": { + "async": { + "version": "2.0.1", + "resolved": "http://registry.npmjs.org/async/-/async-2.0.1.tgz", + "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", + "dev": true, + "requires": { + "lodash": "^4.8.0" + } + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + } + } + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/package.json b/package.json index 5364211..1c2d56f 100755 --- a/package.json +++ b/package.json @@ -22,15 +22,14 @@ "readmeFilename": "README.md", "dependencies": { "@sailshq/lodash": "^3.10.3", - "async": "~0.2.9", "sqlite3": "~4.0.x", "underscore": "1.5.2", "waterline-errors": "^0.10.1" }, "devDependencies": { - "mocha": "*", + "mocha": "^5.2.0", "should": "*", - "waterline-adapter-tests": "~0.9.4" + "waterline-adapter-tests": "^1.0.1" }, "sails": { "adapter": { diff --git a/test/integration/runner.js b/test/integration/runner.js index dc65e73..0e1e9d9 100644 --- a/test/integration/runner.js +++ b/test/integration/runner.js @@ -1,6 +1,6 @@ var tests = require('waterline-adapter-tests'), sqlite3 = require('sqlite3'), - adapter = require('../../lib/adapter'), + adapter = require('../../index'), mocha = require('mocha'); /** @@ -17,4 +17,8 @@ var config = { * Run Tests */ -var suite = new tests({ adapter: adapter, config: config }); \ No newline at end of file +var suite = new tests({ + adapter: adapter, + config: config, + interfaces: ['semantic','queryable','migratable'], +}); \ No newline at end of file From b4c1fae71e6a4d094660d46e803f65408a959222 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Wed, 19 Dec 2018 15:39:16 -0500 Subject: [PATCH 69/81] Start testing w/ waterline tests fix some bugs, discover others --- index.js | 6 +- lib/query.js | 294 +++------------------------------------------------ lib/utils.js | 35 ++++-- package.json | 3 + 4 files changed, 52 insertions(+), 286 deletions(-) diff --git a/index.js b/index.js index eec9f02..f0a0d6b 100644 --- a/index.js +++ b/index.js @@ -155,7 +155,7 @@ const adapter = { let writeClient; try { writeClient = await wrapAsyncStatements((cb) => { - if (datastoreConfig.verbose) sqlite3 = sqlite3.verbose(); + if (datastoreConfig.verbose) sqlite3.verbose(); const writeClient = new sqlite3.Database( datastoreConfig.filename, datastoreConfig.mode, @@ -459,7 +459,7 @@ const adapter = { const queryObj = _query.destroy(query.criteria); let results; - if (query.meta.fetch) { + if (query.meta && query.meta.fetch) { results = await wrapAsyncStatements( adapter.find.bind(adapter, datastoreName, query)); } @@ -964,7 +964,7 @@ function spawnReadonlyConnection(datastore, logic) { // Check if we want to run in verbose mode // Note that once you go verbose, you can't go back. // See: https://github.com/mapbox/node-sqlite3/wiki/API - if (datastoreConfig.verbose) sqlite3 = sqlite3.verbose(); + if (datastoreConfig.verbose) sqlite3.verbose(); // Make note whether the database already exists exists = fs.existsSync(datastoreConfig.filename); diff --git a/lib/query.js b/lib/query.js index 65966b0..2849982 100644 --- a/lib/query.js +++ b/lib/query.js @@ -22,7 +22,22 @@ const Query = function (tableName, schema, model) { this._query = ''; this._tableName = tableName; /** Waterline model - provides info on type */ - this._model = model || {}; + this._modelByColumnName = {}; + if (!!model) { + for (let prop in model) { + if (prop !== 'definition') { + this._modelByColumnName[prop] = model[prop]; + } else { + const definitions = this._modelByColumnName[prop] = {}; + const attrs = model[prop]; + + for (let attrName in attrs) { + const attrDef = attrs[attrName]; + definitions[attrDef.columnName] = attrDef; + } + } + } + } this._schema = _.clone(schema); @@ -310,281 +325,6 @@ class Criterion { } } -Query.prototype.whereBak = function(options) { - var self = this, - operators = this.operators(); - - if (!options || Object.keys(options).length === 0) return; - - // Begin WHERE query - this._query += 'WHERE '; - - // Process 'where' criteria - Object.keys(options).forEach(function(key) { - - switch (key.toLowerCase()) { - case 'or': - options[key].forEach(function(statement) { - Object.keys(statement).forEach(function(key) { - - switch (key) { - case 'and': - Object.keys(statement[key]).forEach(function(attribute) { - operators.and(attribute, statement[key][attribute], ' OR '); - }); - return; - - case 'like': - Object.keys(statement[key]).forEach(function(attribute) { - operators.like(attribute, key, statement, ' OR '); - }); - return; - - default: - if(typeof statement[key] === 'object') { - Object.keys(statement[key]).forEach(function(attribute) { - operators.and(attribute, statement[key][attribute], ' OR '); - }); - return; - } - - operators.and(key, statement[key], ' OR '); - return; - } - }); - }); - - return; - - case 'like': - Object.keys(options[key]).forEach(function(parent) { - operators.like(parent, key, options); - }); - - return; - - // Key/Value - default: - - // 'IN' - if (options[key] instanceof Array) { - operators.in(key, options[key]); - return; - } - - // 'AND' - operators.and(key, options[key]); - return; - } - }); - - // Remove trailing AND if it exists - if (this._query.slice(-4) === 'AND ') { - this._query = this._query.slice(0, -5); - } - - // Remove trailing OR if it exists - if (this._query.slice(-3) === 'OR ') { - this._query = this._query.slice(0, -4); - } -}; - -/** - * Operator Functions - */ - -Query.prototype.operators = function() { - var self = this; - - var sql = { - and: function(key, options, comparator) { - var caseSensitive = true; - - // Check if key is a string - if (self._schema[key] && self._schema[key].type === 'TEXT') caseSensitive = false; - - processCriteria.call(self, key, options, '=', caseSensitive); - self._query += (comparator || ' AND '); - }, - - like: function(parent, key, options, comparator) { - var caseSensitive = true; - - // Check if parent is a string - if (self._schema[parent].type === 'TEXT') caseSensitive = false; - - processCriteria.call(self, parent, options[key][parent], 'ILIKE', caseSensitive); - self._query += (comparator || ' AND '); - }, - - in: function(key, options) { - var caseSensitive = true; - - // Check if key is a string - if (self._schema[key].type === 'TEXT') caseSensitive = false; - - // Check case sensitivity to decide if LOWER logic is used - if (!caseSensitive) key = 'LOWER("' + key + '")'; - else key = '"' + key + '"'; // for case sensitive camelCase columns - - // Build IN query - self._query += key + ' IN ('; - - // Append each value to query - options.forEach(function(value) { - self._query += '$' + self._paramCount + ', '; - self._paramCount++; - - // If case sensitivity is off, lowercase the value - if (!caseSensitive) value = value.toLowerCase(); - - self._values.push(value); - }); - - // Strip last comma and close criteria - self._query = self._query.slice(0, -2) + ')'; - self._query += ' AND '; - } - }; - - return sql; -}; - -/** - * Process Criteria - * - * Processes a query criteria object - */ - -function processCriteria(parent, value, combinator, caseSensitive) { - var self = this; - - // Complex object attributes - if (typeof value === 'object' && value !== null) { - var keys = Object.keys(value); - - // Escape parent - parent = '"' + parent + '"'; - - for (var i = 0; i < keys.length; i++) { - - // Check if value is a string and if so add LOWER logic - // to work with case insensitive queries - if (!caseSensitive && typeof value[[keys][i]] === 'string') { - parent = 'LOWER(' + parent + ')'; - value[keys][i] = value[keys][i].toLowerCase(); - } - - self._query += parent + ' '; - prepareCriterion.call(self, keys[i], value[keys[i]]); - - if (i+1 < keys.length) self._query += ' AND '; - } - - return; - } - - // Check if value is a string and if so add LOWER logic - // to work with case insensitive queries - if (!caseSensitive && typeof value === 'string') { - - // Escape parent - parent = '"' + parent + '"'; - - // ADD LOWER to parent - parent = 'LOWER(' + parent + ')'; - value = value.toLowerCase(); - } else { - // Escape parent - parent = '"' + parent + '"'; - } - - if (value !== null) { - // Simple Key/Value attributes - this._query += parent + ' ' + combinator + ' $' + this._paramCount; - - this._values.push(value); - this._paramCount++; - } else { - this._query += parent + ' IS NULL'; - } -} - -/** - * Prepare Criterion - * - * Processes comparators in a query. - */ - -function prepareCriterion(key, value) { - var str; - - switch (key) { - case '<': - case 'lessThan': - this._values.push(value); - str = '< $' + this._paramCount; - break; - - case '<=': - case 'lessThanOrEqual': - this._values.push(value); - str = '<= $' + this._paramCount; - break; - - case '>': - case 'greaterThan': - this._values.push(value); - str = '> $' + this._paramCount; - break; - - case '>=': - case 'greaterThanOrEqual': - this._values.push(value); - str = '>= $' + this._paramCount; - break; - - case '!': - case 'not': - if (value === null) { - str = 'IS NOT NULL'; - } else { - this._values.push(value); - str = '<> $' + this._paramCount; - } - break; - - case 'like': - this._values.push(value); - str = 'LIKE $' + this._paramCount; - break; - - case 'contains': - this._values.push('%' + value + '%'); - str = 'LIKE $' + this._paramCount; - break; - - case 'startsWith': - this._values.push(value + '%'); - str = 'LIKE $' + this._paramCount; - break; - - case 'endsWith': - this._values.push('%' + value); - str = 'LIKE $' + this._paramCount; - break; - - default: - throw new Error('Unknown comparator: ' + key); - } - - // Bump paramCount - this._paramCount++; - - // Add str to query - this._query += str; -} - /** * Specify a `limit` condition */ @@ -652,7 +392,7 @@ Query.prototype.castRow = function(values) { const newModel = {}; for (let columnName in values) { - const attrDef = this._model.definition[columnName]; + const attrDef = this._modelByColumnName.definition[columnName]; if (values[columnName] === null) { newModel[columnName] = null; diff --git a/lib/utils.js b/lib/utils.js index 62dcff1..b58bb43 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,12 @@ -var _ = require("underscore"); +const _ = require("underscore"); -var utils = module.exports = {}; +const utils = module.exports = {}; + +// CONSTANT Regular Expressions for data types +const coreAffinities = /TEXT|INTEGER|REAL|BLOB|NUMERIC/i; +const intTypes = /INT/i; +const textTypes = /CHAR|CLOB/i; +const realTypes = /DOUBLE|FLOAT/i; /** * Build a schema from an attributes object @@ -16,13 +22,13 @@ utils.buildSchema = function(obj, foreignKeys) { const attr = obj[key]; const sqliteType = utils.sqlTypeCast(attr.columnType, key); - const realType = /TEXT|INTEGER|REAL|BLOB/i.exec(sqliteType)[0]; + const affinity = utils.getAffinity(sqliteType); schema[key] = { primaryKey: attr.primaryKey, unique: attr.unique, indexed: attr.unique || attr.primaryKey, // indexing rules in sqlite - type: realType + type: affinity } // Note: we are ignoring autoincrement b/c it is only supported on @@ -49,6 +55,23 @@ utils.buildSchema = function(obj, foreignKeys) { }; }; +/** + * Sqlite3 defines types as "affinities" since all columns can contain + * all types + */ +utils.getAffinity = sqliteType => { + if (!sqliteType) return 'BLOB'; // essentially no type + + const matches = coreAffinities.exec(sqliteType); + if (matches !== null) return matches[0].toUpperCase(); + + if (intTypes.exec(sqliteType) !== null) return 'INTEGER'; + if (textTypes.exec(sqliteType) !== null) return 'TEXT'; + if (realTypes.exec(sqliteType) !== null) return 'REAL'; + + return 'NUMERIC'; +}; + /** * @return Map by unescaped tablename of the foreign key fields and the tables * they look up to @@ -515,7 +538,7 @@ utils.prepareValue = function(value, columnType) { return value.toString(); } - // Store Buffers as hex strings (for BYTEA) + // Check buffers for BLOB typs if (Buffer.isBuffer(value)) { if (columnType !== 'BLOB') throw new Error('Buffers may only represent BLOB types'); } @@ -535,7 +558,7 @@ utils.prepareValue = function(value, columnType) { return parseFloat(value); } - return value; //BLOB + return value; //BLOB or NUMERIC }; /** diff --git a/package.json b/package.json index 1c2d56f..dacaf93 100755 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "should": "*", "waterline-adapter-tests": "^1.0.1" }, + "scripts": { + "test": "node test/integration/runner.js" + }, "sails": { "adapter": { "type": "sqlite3", From 6f5d3e7117e023189550236266ce8931a1f92c97 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Wed, 19 Dec 2018 17:24:19 -0500 Subject: [PATCH 70/81] Bug fix createEach fetch --- index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index f0a0d6b..7758ba8 100644 --- a/index.js +++ b/index.js @@ -312,6 +312,7 @@ const adapter = { * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ createEach: async function (datastoreName, query, done) { + let verbose = false; // Look up the datastore entry (manager/driver/config). const dsEntry = registeredDatastores[datastoreName]; @@ -343,11 +344,12 @@ const adapter = { await wrapAsyncStatements( client.run.bind(client, insertQuery, attributeSets.values)); - // get the last inserted rows if requested + // get the last inserted rows if requested + const model = manager.models[tableName]; let newRows; if (query.meta && query.meta.fetch) { newRows = []; - const queryObj = new Query(tableName, manager.schema[tableName], manager.models[tableName]); + const queryObj = new Query(tableName, manager.schema[tableName], model); await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { if (err) throw err; @@ -356,6 +358,17 @@ const adapter = { })); } + // resort for the order we were given the records. + // we can guarantee that the first records will be given the + // first available row IDs (even if some were deleted creating gaps), + // so it's as easy as a sort using the primary key as the comparator + let pkName = model.definition[model.primaryKey].columnName; + newRows.sort((lhs, rhs) => { + if (lhs[pkName] < rhs[pkName]) return -1; + if (lhs[pkName] > rhs[pkName]) return 1; + return 0; + }); + done(undefined, newRows); } catch (err) { done(err); From 7f4d77128dc3e3cf735d9b86f9603db13a05f4c2 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 20 Dec 2018 09:32:03 -0500 Subject: [PATCH 71/81] Fix NOT IN error Was coded as 'IN', added 'NOT' --- lib/query.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index 2849982..a727eb5 100644 --- a/lib/query.js +++ b/lib/query.js @@ -316,7 +316,7 @@ class Criterion { case 'nin': ({ dedupedValues, setString } = this._arrayToSqlSet(condition[operator])) values = dedupedValues; - whereClause = `${columnName} IN ${setString}`; + whereClause = `${columnName} NOT IN ${setString}`; break; } } From 8d0c251680c3040c49f5f8c1bc6841f6ae2e6b86 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 20 Dec 2018 19:15:52 -0500 Subject: [PATCH 72/81] Passing all waterline tests --- .gitignore | 5 +- index.js | 158 ++++++++++++++++++++++++++++++---------- lib/query.js | 103 +++++++++++++++++++++++--- lib/utils.js | 201 +-------------------------------------------------- 4 files changed, 216 insertions(+), 251 deletions(-) diff --git a/.gitignore b/.gitignore index bd3d81c..825fe29 100755 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ ssl *~ .idea nbproject -npm-debug.log \ No newline at end of file +npm-debug.log + +# Waterline +sailssqlite.db \ No newline at end of file diff --git a/index.js b/index.js index 7758ba8..d0cae15 100644 --- a/index.js +++ b/index.js @@ -170,7 +170,7 @@ const adapter = { ); }); } catch (err) { - done(err); + return done(err); } // To maintain the spirit of this repository, this implementation will @@ -194,7 +194,7 @@ const adapter = { await wrapAsyncStatements(this.describe.bind(this, datastoreName, tableName)); } } catch (err) { - done(err); + return done(err); } return done(); @@ -416,13 +416,62 @@ const adapter = { const _query = new Query(tableName, tableSchema, model); const updateQuery = _query.update(query.criteria, query.valuesToSet); + /* TODO: See below note and fix so that we do not query the db twice where unnecessary + * Note: The sqlite driver we're using does not return changed values + * on an update. If we are expected to fetch, we need to deterministically + * be able to fetch the exact records that we updated. + * We cannot simply query off the same criteria because it is possible + * (nay likely) that one of the criteria is based on a field that is + * changed in the update call. In most cases, acquiring the primary key + * value before the update and then re-querying that key after the update + * will be sufficient. However, it is possible to update the primary key + * itself. So we will construct 2 cases: + * 1: Query the primary key for all records that will be updated. Then + * craft a new where object based on only those primary keys to + * query again after the update executes + * 2: craft a new where object based on what the primary key is changing + * to. + * + * Note that option 1 sucks. However, an analysis of the where criteria to + * determine the optimal *post-update* where criteria is more work than + * I have time to do, so option 1 it is. + */ + + let newQuery; + if (query.meta && query.meta.fetch) { + const pkCol = model.definition[model.primaryKey].columnName; + let newWhere = {}; + newQuery = _.cloneDeep(query); + newQuery.criteria = newQuery.criteria || {}; + + if (query.valuesToSet[pkCol]) { + newWhere[pkCol] = query.valuesToSet[pkCol]; + } else { + newQuery.criteria.select = [pkCol]; + + const rows = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, newQuery)); + + delete newQuery.criteria.select; + + const inSet = {in: rows.map(row => row[pkCol])}; + newWhere[pkCol] = inSet; + } + + newQuery.criteria.where = newWhere; + } + const statement = await wrapAsyncForThis( client.run.bind(client, updateQuery.query, updateQuery.values)); let results; - if (statement.changes > 0 && query.meta && query.meta.fetch) { - results = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, query)); + if (query.meta && query.meta.fetch) { + if (statement.changes === 0) { + results = []; + } else { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, newQuery)); + } } done(undefined, results); @@ -534,8 +583,8 @@ const adapter = { const queryStatement = queryObj.find(query.criteria); try { - const values = []; await spawnReadonlyConnection(dsEntry, async function __FIND__(client) { + const values = []; let resultCount = await wrapAsyncStatements( client.each.bind(client, queryStatement.query, queryStatement.values, (err, row) => { if (err) throw err; @@ -544,7 +593,7 @@ const adapter = { })); done(undefined, values); - }) + }); } catch (err) { done(err); } @@ -614,7 +663,7 @@ const adapter = { * @param {Number} [the number of matching records] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - count: function (datastoreName, query, done) { + count: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -624,15 +673,25 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query and send back a result. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`count`) not implemented yet.')); - }, 16); + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const countQuery = new Query(tableName, schema, model); + const statement = countQuery.count(query.criteria, 'count_alias'); + await spawnReadonlyConnection(dsEntry, async function __COUNT__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); + + if (!row) throw new Error('No rows returned by count query?'); + + done(undefined, row.count_alias); + }); + } catch(err) { + done(err); + } }, @@ -649,7 +708,7 @@ const adapter = { * @param {Number} [the sum] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - sum: function (datastoreName, query, done) { + sum: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -659,14 +718,25 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query and send back a result. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`sum`) not implemented yet.')); - }, 16); + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const sumQuery = new Query(tableName, schema, model); + const statement = sumQuery.sum(query.criteria, query.numericAttrName, 'sum_alias'); + + await spawnReadonlyConnection(dsEntry, async function __SUM__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); + + if (!row) throw new Error('No rows returned by sum query?'); + + done(undefined, row.sum_alias); + }); + } catch (err) { + done(err); + } }, @@ -684,7 +754,7 @@ const adapter = { * @param {Number} [the average ("mean")] * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - avg: function (datastoreName, query, done) { + avg: async function (datastoreName, query, done) { // Look up the datastore entry (manager/driver/config). var dsEntry = registeredDatastores[datastoreName]; @@ -694,14 +764,25 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - // Perform the query and send back a result. - // - // > TODO: Replace this setTimeout with real logic that calls - // > `done()` when finished. (Or remove this method from the - // > adapter altogether - setTimeout(function(){ - return done(new Error('Adapter method (`avg`) not implemented yet.')); - }, 16); + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const avgQuery = new Query(tableName, schema, model); + const statement = avgQuery.avg(query.criteria, query.numericAttrName, 'avg_alias'); + + await spawnReadonlyConnection(dsEntry, async function __AVG__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); + + if (!row) throw new Error('No rows returned by avg query?'); + + done(undefined, row.avg_alias); + }); + } catch (err) { + done(err); + } }, @@ -731,7 +812,7 @@ const adapter = { spawnReadonlyConnection(datastore, async function __DESCRIBE__(client) { // Get a list of all the tables in this database // See: http://www.sqlite.org/faq.html#q7) - var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + tableName + '" ORDER BY name'; + var query = `SELECT * FROM sqlite_master WHERE type="table" AND name="${tableName}" ORDER BY name`; try { const schema = await wrapAsyncStatements(client.get.bind(client, query)); @@ -739,10 +820,10 @@ const adapter = { // Query to get information about each table // See: http://www.sqlite.org/pragma.html#pragma_table_info - var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; + var columnsQuery = `PRAGMA table_info("${schema.name}")`; // Query to get a list of indices for a given table - var indexListQuery = 'PRAGMA index_list("' + schema.name + '")'; + var indexListQuery = `PRAGMA index_list("${schema.name}")`; schema.indices = []; schema.columns = []; @@ -755,7 +836,7 @@ const adapter = { if (err) throw err; // Query to get information about indices var indexInfoQuery = - 'PRAGMA index_info("' + currentIndex.name + '")'; + `PRAGMA index_info("${currentIndex.name}")`; // Retrieve detailed information for given index client.each(indexInfoQuery, function (err, indexedCol) { @@ -994,7 +1075,6 @@ function spawnReadonlyConnection(datastore, logic) { }) .then(logic) .catch(err => { - console.error(err) return Promise.reject(err); //we want the user process to get this error as well }) .finally(() => { diff --git a/lib/query.js b/lib/query.js index a727eb5..bd05b0c 100644 --- a/lib/query.js +++ b/lib/query.js @@ -21,6 +21,7 @@ const Query = function (tableName, schema, model) { this._paramCount = 1; this._query = ''; this._tableName = tableName; + this._escapedTable = utils.escapeTable(tableName); /** Waterline model - provides info on type */ this._modelByColumnName = {}; if (!!model) { @@ -48,11 +49,26 @@ const Query = function (tableName, schema, model) { * SELECT Statement */ -Query.prototype.find = function(criteria) { +Query.prototype.find = function(criteria = {}) { - this._query = utils.buildSelectStatement(criteria, this._tableName, this._schema); + const selectKeys = [{table: this._escapedTable, key: 'rowid'}]; + if (criteria.select && criteria.select.length >>> 0 > 0) { + for (let key of criteria.select) { + selectKeys.push({ table: this._escapedTable, key }); + } + } else { + for (let columnName in this._schema) { + selectKeys.push({ table: this._escapedTable, key: columnName }); + } + } + const selects = + selectKeys + .map(keyObj => `${keyObj.table}.${utils.escapeName(keyObj.key)}`) + .join(', '); - if (criteria) this._build(criteria); + this._query = `SELECT ${selects} FROM ${this._escapedTable} `; + + this._build(criteria); return { query: this._query, @@ -60,6 +76,49 @@ Query.prototype.find = function(criteria) { }; }; +/** + * COUNT Statement + * Waterline only supports counting based on criteria, so we only need + * count(*) syntax + */ +Query.prototype.count = function(criteria = {}, alias = 'count_alias') { + this._query = `SELECT COUNT(*) as ${alias} FROM ${this._escapedTable} `; + this._build(criteria); + + return { + query: this._query, + values: this._values + }; +}; + +/** + * SUM Statement + * No Group By in Waterline api v1. Odd... + */ +Query.prototype.sum = function(criteria = {}, columnName, alias = 'sum_alias') { + this._query = `SELECT TOTAL(${utils.escapeName(columnName)}) as ${alias} FROM ${this._escapedTable} `; + this._build(criteria); + + return { + query: this._query, + values: this._values + }; +} + +/** + * AVG Statement + * No Group By in Waterline api v1. Odd... + */ +Query.prototype.avg = function (criteria = {}, columnName, alias = 'avg_alias') { + this._query = `SELECT AVG(${utils.escapeName(columnName)}) as ${alias} FROM ${this._escapedTable} `; + this._build(criteria); + + return { + query: this._query, + values: this._values + }; +} + /** * UPDATE Statement */ @@ -161,6 +220,8 @@ Query.prototype.where = function(criteria) { const criteriaTree = new Criterion(this._schema); criteriaTree.addCriterion(criteria); + criteriaTree.setParamIndex(this._paramCount); + const parsedCriteria = criteriaTree.generateCriteria(); this._query += parsedCriteria.whereClause; this._values = this._values.concat(parsedCriteria.values); @@ -181,6 +242,10 @@ class Criterion { this._paramIndex = 1; } + setParamIndex(index) { + this._paramIndex = index; + } + addCriterion(rawCriterion) { const newCriterion = this._constructCriterionTree(rawCriterion); @@ -246,8 +311,11 @@ class Criterion { */ generateCriteria(paramIndex = -1) { //differentiate from root call + let isRoot = false; if (paramIndex > 0) { this._paramIndex = paramIndex; + } else { + isRoot = true; } if (!this._isLeaf) { @@ -263,7 +331,7 @@ class Criterion { return previous.concat(next.values); }, []); - const whereClause = (paramIndex === -1 ? 'WHERE ' : '') + const whereClause = (isRoot ? 'WHERE ' : '') + subCriteriaResults .map(sub => `(${sub.whereClause})`) .join(this._joinString); @@ -393,29 +461,42 @@ Query.prototype.castRow = function(values) { for (let columnName in values) { const attrDef = this._modelByColumnName.definition[columnName]; + const value = values[columnName]; - if (values[columnName] === null) { + if (value === null) { newModel[columnName] = null; continue; } switch(attrDef.type) { case 'json': - newModel[columnName] = JSON.parse(values[columnName]); + let parsedVal; + try { + parsedVal = JSON.parse(value); + } catch (err) { + // edge case of just string + if (!value.startsWith('{') && !value.startsWith('[')) { + parsedVal = JSON.parse(`"${value}"`); + } else { + throw err; + } + } + + newModel[columnName] = parsedVal; break; case 'boolean': - newModel[columnName] = !!values[columnName]; + newModel[columnName] = !!value; break; case 'number': - newModel[columnName] = parseFloat(values[columnName]); + newModel[columnName] = parseFloat(value); break; case 'numberkey': - newModel[columnName] = parseInt(values[columnName], 10); + newModel[columnName] = parseInt(value, 10); break; case 'string': - newModel[columnName] = values[columnName].toString(); + newModel[columnName] = value.toString(); default: - newModel[columnName] = values[columnName]; + newModel[columnName] = value; } } diff --git a/lib/utils.js b/lib/utils.js index b58bb43..2160bd1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -121,194 +121,7 @@ var hop = Object.prototype.hasOwnProperty; * Wraps a name in quotes to allow reserved * words as table or column names such as user. */ -function escapeName(name) { - return '"' + name + '"'; -} -utils.escapeName = escapeName; - -/** - * Builds a Select statement determining if Aggeragate options are needed. - */ - -utils.buildSelectStatement = function(criteria, table, schema) { - var query = ''; - - // Escape table name - var schemaName = criteria._schemaName ? utils.escapeName(criteria._schemaName) + '.' : ''; - var tableName = schemaName + utils.escapeName(table); - - /************************************************* - * Commenting out below because new waterline - * deals with aggregates in separate functions. - * However, I am keeping the code for reference - * as I migrate it into those other functions - *************************************************/ - // if (criteria.groupBy || criteria.sum || criteria.average || criteria.min || - // criteria.max) { - - // query = 'SELECT rowid, '; - - // // Append groupBy columns to select statement - // if(criteria.groupBy) { - // if(criteria.groupBy instanceof Array) { - // criteria.groupBy.forEach(function(opt){ - // query += tableName + '.' + utils.escapeName(opt) + ', '; - // }); - - // } else { - // query += tableName + '.' + utils.escapeName(criteria.groupBy) + ', '; - // } - // } - - // // Handle SUM - // if (criteria.sum) { - // if(criteria.sum instanceof Array) { - // criteria.sum.forEach(function(opt){ - // query += 'CAST(SUM(' + tableName + '.' + utils.escapeName(opt) + - // ') AS float) AS ' + opt + ', '; - // }); - - // } else { - // query += 'CAST(SUM(' + tableName + '.' + - // utils.escapeName(criteria.sum) + ') AS float) AS ' + criteria.sum + - // ', '; - // } - // } - - // // Handle AVG (casting to float to fix percision with trailing zeros) - // if (criteria.average) { - // if(criteria.average instanceof Array) { - // criteria.average.forEach(function(opt){ - // query += 'CAST(AVG(' + tableName + '.' + utils.escapeName(opt) + - // ') AS float) AS ' + opt + ', '; - // }); - - // } else { - // query += 'CAST(AVG(' + tableName + '.' + - // utils.escapeName(criteria.average) + ') AS float) AS ' + - // criteria.average + ', '; - // } - // } - - // // Handle MAX - // if (criteria.max) { - // if(criteria.max instanceof Array) { - // criteria.max.forEach(function(opt){ - // query += 'MAX(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + - // opt + ', '; - // }); - - // } else { - // query += 'MAX(' + tableName + '.' + utils.escapeName(criteria.max) + - // ') AS ' + criteria.max + ', '; - // } - // } - - // // Handle MIN - // if (criteria.min) { - // if(criteria.min instanceof Array) { - // criteria.min.forEach(function(opt){ - // query += 'MIN(' + tableName + '.' + utils.escapeName(opt) + ') AS ' + - // opt + ', '; - // }); - - // } else { - // query += 'MIN(' + tableName + '.' + utils.escapeName(criteria.min) + ') AS ' + - // criteria.min + ', '; - // } - // } - - // // trim trailing comma - // query = query.slice(0, -2) + ' '; - - // // Add FROM clause - // return query += 'FROM ' + table + ' '; - // } - - query += 'SELECT rowid, '; - - var selectKeys = [], joinSelectKeys = []; - if (criteria.select && criteria.select.length >>> 0 > 0) { - for (let key of criteria.select) { - selectKeys.push({ table: tableName, key }); - } - } else { - for (let columnName in schema) { - selectKeys.push({ table: tableName, key: columnName }); - } - } - - // Check for joins - // NOTE: keeping joins here until I figure out how the - // new Waterline join method works - if (criteria.joins) { - - var joins = criteria.joins; - - joins.forEach(function(join) { - if (!join.select) return; - - Object.keys( - schema[join.child.toLowerCase()].schema - ).forEach(function(key) { - var _join = _.cloneDeep(join); - _join.key = key; - joinSelectKeys.push(_join); - }); - - // Remove the foreign key for this join from the selectKeys array - selectKeys = selectKeys.filter(function(select) { - var keep = true; - if (select.key === join.parentKey && join.removeParentKey) keep = false; - return keep; - }); - }); - } - - // Add all the columns to be selected that are not joins - selectKeys.forEach(function(select) { - query += select.table + '.' - + utils.escapeName(select.key) + ', '; - }); - - // Add all the columns from the joined tables - // NOTE keeping joins here until I know how the new waterline works - joinSelectKeys.forEach(function(select) { - - // Create an alias by prepending the child table with the alias of the join - var alias = select.alias.toLowerCase() + '_' + select.child.toLowerCase(); - - // If this is a belongs_to relationship, keep the foreign key name from the - // AS part of the query. This will result in a selected column like: - // "user"."id" AS "user_id__id" - if (select.model) { - return query += utils.escapeName(alias) + '.' + - utils.escapeName(select.key) + ' AS ' + - utils.escapeName(select.parentKey + '__' + select.key) + ', '; - } - - // If a junctionTable is used, the child value should be used in the AS part - // of the select query. - if (select.junctionTable) { - return query += utils.escapeName(alias) + '.' + - utils.escapeName(select.key) + ' AS ' + - utils.escapeName(select.alias + '_' + select.child + '__' + select.key) - + ', '; - } - - // Else if a hasMany attribute is being selected, use the alias plus the - // child. - return query += utils.escapeName(alias) + '.' + utils.escapeName(select.key) - + ' AS ' + utils.escapeName(select.alias + '_' + select.child + '__' + - select.key) + ', '; - }); - - // Remove the last comma - query = query.slice(0, -2) + ' FROM ' + tableName + ' '; - - return query; -}; - +utils.escapeName = utils.escapeTable = name => `"${name}"`; /** * Build an Index array from any attributes that @@ -409,18 +222,6 @@ utils.group = function(values) { return _values; }; - -/** - * Escape Table Name - * - * Wraps a table name in quotes to allow reserved - * words as table names such as user. - */ - -utils.escapeTable = function(table) { - return '"' + table + '"'; -}; - /** * Map data from a stage-3 query to its representation * in Sqlite. Collects a list of columns and processes From fe06efeb184c576d401cb4ce97014491e0b0fc1d Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 20 Dec 2018 20:49:44 -0500 Subject: [PATCH 73/81] Bug fixes for Associations Adding associations interface tests --- index.js | 41 +++++++++++++++++++------------------- lib/query.js | 3 +-- lib/utils.js | 2 +- package.json | 1 + test/integration/runner.js | 2 +- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index d0cae15..95d4e10 100644 --- a/index.js +++ b/index.js @@ -356,18 +356,18 @@ const adapter = { newRows.push(queryObj.castRow(row)); })); - } - // resort for the order we were given the records. - // we can guarantee that the first records will be given the - // first available row IDs (even if some were deleted creating gaps), - // so it's as easy as a sort using the primary key as the comparator - let pkName = model.definition[model.primaryKey].columnName; - newRows.sort((lhs, rhs) => { - if (lhs[pkName] < rhs[pkName]) return -1; - if (lhs[pkName] > rhs[pkName]) return 1; - return 0; - }); + // resort for the order we were given the records. + // we can guarantee that the first records will be given the + // first available row IDs (even if some were deleted creating gaps), + // so it's as easy as a sort using the primary key as the comparator + let pkName = model.definition[model.primaryKey].columnName; + newRows.sort((lhs, rhs) => { + if (lhs[pkName] < rhs[pkName]) return -1; + if (lhs[pkName] > rhs[pkName]) return 1; + return 0; + }); + } done(undefined, newRows); } catch (err) { @@ -903,12 +903,6 @@ const adapter = { * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ define: async function (datastoreName, tableName, definition, done) { - /**************************************************************** - * NOTICE: - * This function is broken. util.buildSchema does not read - * defintion objects correctly for Waterline API Version 1 - ****************************************************************/ - // Look up the datastore entry (manager/driver/config). var datastore = registeredDatastores[datastoreName]; @@ -918,27 +912,32 @@ const adapter = { return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } + let tableQuery; + let outerSchema try { const client = datastore.manager.writeClient; const escapedTable = utils.escapeTable(tableName); // Iterate through each attribute, building a query string const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); + outerSchema = _schema.schema; // Check for any index attributes const indices = utils.buildIndexes(definition); // Build query - const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; + // const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; + tableQuery = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; - await wrapAsyncStatements(client.run.bind(client, query)); + // await wrapAsyncStatements(client.run.bind(client, query)); + await wrapAsyncStatements(client.run.bind(client, tableQuery)); await Promise.all(indices.map(async index => { // Build a query to create a namespaced index tableName_key - var query = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + + const indexQuery = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + tableName + ' (' + index + ');'; - await wrapAsyncStatements(client.run.bind(client, query)); + await wrapAsyncStatements(client.run.bind(client, indexQuery)); })); // Replacing if it already existed diff --git a/lib/query.js b/lib/query.js index bd05b0c..01c841e 100644 --- a/lib/query.js +++ b/lib/query.js @@ -51,7 +51,7 @@ const Query = function (tableName, schema, model) { Query.prototype.find = function(criteria = {}) { - const selectKeys = [{table: this._escapedTable, key: 'rowid'}]; + const selectKeys = []; if (criteria.select && criteria.select.length >>> 0 > 0) { for (let key of criteria.select) { selectKeys.push({ table: this._escapedTable, key }); @@ -455,7 +455,6 @@ Query.prototype.group = function(options) { * Ex: Array is stored as "[0,1,2,3]" and should be cast to proper * array for return values. */ - Query.prototype.castRow = function(values) { const newModel = {}; diff --git a/lib/utils.js b/lib/utils.js index 2160bd1..31e5806 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -87,7 +87,7 @@ utils.buildForeignKeyMap = physicalModelsReport => { const column = physicalModelsReport[tableName].definition[columnName]; if (column.foreignKey) { - tableKeys[columnName] = { + tableKeys[column.columnName] = { table: column.references, column: column.on } diff --git a/package.json b/package.json index dacaf93..64b432b 100755 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "implements": [ "semantic", "queryable", + "associations", "migratable" ] } diff --git a/test/integration/runner.js b/test/integration/runner.js index 0e1e9d9..b9cdf2d 100644 --- a/test/integration/runner.js +++ b/test/integration/runner.js @@ -20,5 +20,5 @@ var config = { var suite = new tests({ adapter: adapter, config: config, - interfaces: ['semantic','queryable','migratable'], + interfaces: ['semantic','queryable','migratable','associations'], }); \ No newline at end of file From cdba3b0ffe3c128830140705faa9397fb991a81a Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Thu, 20 Dec 2018 21:36:56 -0500 Subject: [PATCH 74/81] Clean unused code, update readme --- README.md | 53 +- index.js | 1115 -------------------- lib/adapter.js | 1687 +++++++++++++++++++----------- lib/query.js | 23 +- lib/utils.js | 113 +- package-lock.json | 61 +- package.json | 29 +- test/register.js | 14 - test/{integration => }/runner.js | 2 +- test/unit/adapter.avg.js | 47 - test/unit/adapter.create.js | 80 -- test/unit/adapter.define.js | 106 -- test/unit/adapter.describe.js | 42 - test/unit/adapter.destroy.js | 55 - test/unit/adapter.drop.js | 35 - test/unit/adapter.find.js | 87 -- test/unit/adapter.groupBy.js | 47 - test/unit/adapter.index.js | 60 -- test/unit/adapter.max.js | 47 - test/unit/adapter.min.js | 47 - test/unit/adapter.sum.js | 47 - test/unit/adapter.update.js | 53 - test/unit/query.cast.js | 25 - test/unit/query.limit.js | 29 - test/unit/query.skip.js | 28 - test/unit/query.sort.js | 69 -- test/unit/query.where.js | 179 ---- test/unit/support/bootstrap.js | 91 -- 28 files changed, 1119 insertions(+), 3152 deletions(-) delete mode 100644 index.js delete mode 100644 test/register.js rename test/{integration => }/runner.js (91%) delete mode 100644 test/unit/adapter.avg.js delete mode 100644 test/unit/adapter.create.js delete mode 100644 test/unit/adapter.define.js delete mode 100644 test/unit/adapter.describe.js delete mode 100644 test/unit/adapter.destroy.js delete mode 100644 test/unit/adapter.drop.js delete mode 100644 test/unit/adapter.find.js delete mode 100644 test/unit/adapter.groupBy.js delete mode 100644 test/unit/adapter.index.js delete mode 100644 test/unit/adapter.max.js delete mode 100644 test/unit/adapter.min.js delete mode 100644 test/unit/adapter.sum.js delete mode 100644 test/unit/adapter.update.js delete mode 100644 test/unit/query.cast.js delete mode 100644 test/unit/query.limit.js delete mode 100644 test/unit/query.skip.js delete mode 100644 test/unit/query.sort.js delete mode 100644 test/unit/query.where.js delete mode 100644 test/unit/support/bootstrap.js diff --git a/README.md b/README.md index 5c7e771..87f61ea 100755 --- a/README.md +++ b/README.md @@ -1,33 +1,50 @@ -![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) +![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) # SQLite3 Sails/Waterline Adapter A [Waterline](https://github.com/balderdashy/waterline) adapter for SQLite3. May be used in a [Sails](https://github.com/balderdashy/sails) app or anything using Waterline for the ORM. -## Disclaimer -SQLite3 adapter is in a very early development stage and not ready for primetime. +## Disclaimers +- SQLite3 adapter is not optimized for performance. Native joins are not implemented (among other issues). +- This codebase contains no unit tests, though all integration tests with the waterline api (v1) pass. + +#### People who should use this package right now: +Those prototyping apps with sailsjs and looking to use sqlite for a test database. + +For anyone looking to use this adapter in production, contributions welcome! ## Getting started -It's usually pretty easy to add your own adapters for integrating with proprietary systems or existing open APIs. For most things, it's as easy as `require('some-module')` and mapping the appropriate methods to match waterline semantics. To get started: +To use this in your sails app, install using: + +> npm install --save sails-sqlite3 + +In your `config\datastores.js` file, add a property with your datastore name. Supported configuration: + +```js +default: { + adapter: 'sails-sqlite3', + filename: '[YOUR DATABASE].db', + mode: AS PER sqlite3 MODE OPTIONS, + verbose: false +} +``` + +For more information on the `mode` configuration property, see the [driver documentation](https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback) + +## Testing -1. Fork this repository -2. Set up your README and package.json file. Sails.js adapter module names are of the form sails-*, where * is the name of the datastore or service you're integrating with. -3. Build your adapter. +> npm test -## How to test your adapter -1. Run `npm link` in this adapter's directory -2. Clone the sails.js core and modify the tests to use your new adapter. -3. Run `npm link sails-boilerplate` -4. From the sails.js core directory, run `npm test`. +Currently only `waterline-adapter-tests` are hooked up. Passing interfaces: -## Submitting your adapter -1. Do a pull request to this repository (make sure you attribute yourself as the author set the license in the package.json to "MIT") Please let us know about any special instructions for usage/testing. -2. We'll run the tests one last time. If there are any issues, we'll let you know. -3. When it's ready, we'll update the documentation with information about your new adapter -4. Then we'll tweet and post about it on our blog, adoring you with lavish praises. -5. Mike will send you jelly beans. +- semantic +- queryable +- associations +- migratable +## Acknowledgements +This is a rewrite from a fork of the sails-sqlite3 adapter written for sailsjs < 1.0.0 originally by [Andrew Jo](https://github.com/AndrewJo). I borrowed most of the structure of the code and a lot of the sql querying from the original codebase. ## About Sails.js and Waterline http://SailsJs.com diff --git a/index.js b/index.js deleted file mode 100644 index 95d4e10..0000000 --- a/index.js +++ /dev/null @@ -1,1115 +0,0 @@ -/* eslint-disable prefer-arrow-callback */ -/*--------------------------------------------------------------- - :: sails-sqlite3 - -> adapter - - Code refactored for Sails 1.0.0 release - - Supports Migratable interface, but (as docs on this interface - stipulate) this should only be used for dev. This adapter - does not implement the majority of possibly desired - constraints since the waterline auto-migration is intended - to be light and quick ----------------------------------------------------------------*/ -/** - * Module dependencies - */ - -const fs = require('fs'); - -const _ = require('@sailshq/lodash'); -const sqlite3 = require('sqlite3'); -const Errors = require('waterline-errors').adapter; - -const Query = require('./lib/query'); -const utils = require('./lib/utils'); - - -/** - * Module state - */ - -// Private var to track of all the datastores that use this adapter. In order for your adapter -// to be able to connect to the database, you'll want to expose this var publicly as well. -// (See the `registerDatastore()` method for info on the format of each datastore entry herein.) -// -// > Note that this approach of process global state will be changing in an upcoming version of -// > the Waterline adapter spec (a breaking change). But if you follow the conventions laid out -// > below in this adapter template, future upgrades should be a breeze. -var registeredDatastores = {}; - - -/** - * sails-sqlite3 - * - * Expose the adapater definition. - * - * > Most of the methods below are optional. - * > - * > If you don't need / can't get to every method, just implement - * > what you have time for. The other methods will only fail if - * > you try to call them! - * > - * > For many adapters, this file is all you need. For very complex adapters, you may need more flexiblity. - * > In any case, it's probably a good idea to start with one file and refactor only if necessary. - * > If you do go that route, it's conventional in Node to create a `./lib` directory for your private submodules - * > and `require` them at the top of this file with other dependencies. e.g.: - * > ``` - * > var updateMethod = require('./lib/update'); - * > ``` - * - * @type {Dictionary} - */ -const adapter = { - - - // The identity of this adapter, to be referenced by datastore configurations in a Sails app. - identity: 'sails-sqlite3', - - - // Waterline Adapter API Version - // - // > Note that this is not necessarily tied to the major version release cycle of Sails/Waterline! - // > For example, Sails v1.5.0 might generate apps which use sails-hook-orm@2.3.0, which might - // > include Waterline v0.13.4. And all those things might rely on version 1 of the adapter API. - // > But Waterline v0.13.5 might support version 2 of the adapter API!! And while you can generally - // > trust semantic versioning to predict/understand userland API changes, be aware that the maximum - // > and/or minimum _adapter API version_ supported by Waterline could be incremented between major - // > version releases. When possible, compatibility for past versions of the adapter spec will be - // > maintained; just bear in mind that this is a _separate_ number, different from the NPM package - // > version. sails-hook-orm verifies this adapter API version when loading adapters to ensure - // > compatibility, so you should be able to rely on it to provide a good error message to the Sails - // > applications which use this adapter. - adapterApiVersion: 1, - - - // Default datastore configuration. - defaults: { - // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an - // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, - // their contents are lost. - filename: "", - mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, - verbose: false - }, - - - // ╔═╗═╗ ╦╔═╗╔═╗╔═╗╔═╗ ┌─┐┬─┐┬┬ ┬┌─┐┌┬┐┌─┐ - // ║╣ ╔╩╦╝╠═╝║ ║╚═╗║╣ ├─┘├┬┘│└┐┌┘├─┤ │ ├┤ - // ╚═╝╩ ╚═╩ ╚═╝╚═╝╚═╝ ┴ ┴└─┴ └┘ ┴ ┴ ┴ └─┘ - // ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐┌─┐ - // ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ └─┐ - // ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘└─┘ - // This allows outside access to this adapter's internal registry of datastore entries, - // for use in datastore methods like `.leaseConnection()`. - datastores: registeredDatastores, - - - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // ██╗ ██╗███████╗███████╗ ██████╗██╗ ██╗ ██████╗██╗ ███████╗ // - // ██║ ██║██╔════╝██╔════╝██╔════╝╚██╗ ██╔╝██╔════╝██║ ██╔════╝ // - // ██║ ██║█████╗ █████╗ ██║ ╚████╔╝ ██║ ██║ █████╗ // - // ██║ ██║██╔══╝ ██╔══╝ ██║ ╚██╔╝ ██║ ██║ ██╔══╝ // - // ███████╗██║██║ ███████╗╚██████╗ ██║ ╚██████╗███████╗███████╗ // - // ╚══════╝╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚══════╝ // - // // - // Lifecycle adapter methods: // - // Methods related to setting up and tearing down; registering/un-registering datastores. // - ////////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * ╦═╗╔═╗╔═╗╦╔═╗╔╦╗╔═╗╦═╗ ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐ - * ╠╦╝║╣ ║ ╦║╚═╗ ║ ║╣ ╠╦╝ ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ - * ╩╚═╚═╝╚═╝╩╚═╝ ╩ ╚═╝╩╚═ ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘ - * Register a new datastore with this adapter. This usually involves creating a new - * connection manager (e.g. MySQL pool or MongoDB client) for the underlying database layer. - * - * > Waterline calls this method once for every datastore that is configured to use this adapter. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Dictionary} datastoreConfig Dictionary (plain JavaScript object) of configuration options for this datastore (e.g. host, port, etc.) - * @param {Dictionary} physicalModelsReport Experimental: The physical models using this datastore (keyed by "tableName"-- NOT by `identity`!). This may change in a future release of the adapter spec. - * @property {Dictionary} * [Info about a physical model using this datastore. WARNING: This is in a bit of an unusual format.] - * @property {String} primaryKey [the name of the primary key attribute (NOT the column name-- the attribute name!)] - * @property {Dictionary} definition [the physical-layer report from waterline-schema. NOTE THAT THIS IS NOT A NORMAL MODEL DEF!] - * @property {String} tableName [the model's `tableName` (same as the key this is under, just here for convenience)] - * @property {String} identity [the model's `identity`] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done A callback to trigger after successfully registering this datastore, or if an error is encountered. - * @param {Error?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - registerDatastore: async function (datastoreConfig, physicalModelsReport, done) { - - // Grab the unique name for this datastore for easy access below. - var datastoreName = datastoreConfig.identity; - - // Some sanity checks: - if (!datastoreName) { - return done(new Error('Consistency violation: A datastore should contain an "identity" property: a special identifier that uniquely identifies it across this app. This should have been provided by Waterline core! If you are seeing this message, there could be a bug in Waterline, or the datastore could have become corrupted by userland code, or other code in this adapter. If you determine that this is a Waterline bug, please report this at https://sailsjs.com/bugs.')); - } - if (registeredDatastores[datastoreName]) { - return done(new Error('Consistency violation: Cannot register datastore: `' + datastoreName + '`, because it is already registered with this adapter! This could be due to an unexpected race condition in userland code (e.g. attempting to initialize Waterline more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - let writeClient; - try { - writeClient = await wrapAsyncStatements((cb) => { - if (datastoreConfig.verbose) sqlite3.verbose(); - const writeClient = new sqlite3.Database( - datastoreConfig.filename, - datastoreConfig.mode, - err => { - if (!err) { - //set write client to serialize mode - writeClient.serialize(); - } - - cb(err, writeClient); - } - ); - }); - } catch (err) { - return done(err); - } - - // To maintain the spirit of this repository, this implementation will - // continue to spin up and tear down a connection to the Sqlite db on every - // request. - // TODO: Consider creating the connection and maintaining through the life - // of the sails app. (This would lock it from changes outside sails) - registeredDatastores[datastoreName] = { - config: datastoreConfig, - manager: { - models: physicalModelsReport, //for reference - schema: {}, - foreignKeys: utils.buildForeignKeyMap(physicalModelsReport), - writeClient, - }, - // driver: undefined // << TODO: include driver here (if relevant) - }; - - try { - for (let tableName in physicalModelsReport) { - await wrapAsyncStatements(this.describe.bind(this, datastoreName, tableName)); - } - } catch (err) { - return done(err); - } - - return done(); - }, - - - /** - * ╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╗╔ - * ║ ║╣ ╠═╣╠╦╝ ║║║ ║║║║║║║ - * ╩ ╚═╝╩ ╩╩╚══╩╝╚═╝╚╩╝╝╚╝ - * Tear down (un-register) a datastore. - * - * Fired when a datastore is unregistered. Typically called once for - * each relevant datastore when the server is killed, or when Waterline - * is shut down after a series of tests. Useful for destroying the manager - * (i.e. terminating any remaining open connections, etc.). - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The unique name (identity) of the datastore to un-register. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - teardown: function (datastoreName, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Attempting to tear down a datastore (`'+datastoreName+'`) which is not currently registered with this adapter. This is usually due to a race condition in userland code (e.g. attempting to tear down the same ORM instance more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - // Close write client - dsEntry.manager.writeClient.close(); - delete registeredDatastores[datastoreName]; - - // Inform Waterline that we're done, and that everything went as expected. - return done(); - - }, - - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // ██████╗ ███╗ ███╗██╗ // - // ██╔══██╗████╗ ████║██║ // - // ██║ ██║██╔████╔██║██║ // - // ██║ ██║██║╚██╔╝██║██║ // - // ██████╔╝██║ ╚═╝ ██║███████╗ // - // ╚═════╝ ╚═╝ ╚═╝╚══════╝ // - // (D)ata (M)anipulation (L)anguage // - // // - // DML adapter methods: // - // Methods related to manipulating records stored in the database. // - ////////////////////////////////////////////////////////////////////////////////////////////////// - - - /** - * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ - * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ - * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ - * Create a new record. - * - * (e.g. add a new row to a SQL table, or a new document to a MongoDB collection.) - * - * > Note that depending on the value of `query.meta.fetch`, - * > you may be expected to return the physical record that was - * > created (a dictionary) as the second argument to the callback. - * > (Otherwise, exclude the 2nd argument or send back `undefined`.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Dictionary?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - create: async function (datastoreName, query, done) { - - // normalize newRecords property - query.newRecords = [query.newRecord]; - delete query.newRecord; - - try { - const record = await wrapAsyncStatements( - adapter.createEach.bind(adapter, datastoreName, query)); - - if (record && record.length >>> 0 > 0) { - done(undefined, record[0]); - } - } catch (err) { - done(err); - } - }, - - - /** - * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔═╗╦ ╦ - * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ ║╣ ╠═╣║ ╠═╣ - * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚═╝╩ ╩ - * Create multiple new records. - * - * > Note that depending on the value of `query.meta.fetch`, - * > you may be expected to return the array of physical records - * > that were created as the second argument to the callback. - * > (Otherwise, exclude the 2nd argument or send back `undefined`.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Array?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - createEach: async function (datastoreName, query, done) { - let verbose = false; - - // Look up the datastore entry (manager/driver/config). - const dsEntry = registeredDatastores[datastoreName]; - const manager = dsEntry.manager; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const client = manager.writeClient; - const tableName = query.using; - const escapedTable = utils.escapeTable(tableName); - - const attributeSets = utils.mapAllAttributes(query.newRecords, manager.schema[tableName]); - - const columnNames = attributeSets.keys.join(', '); - - const paramValues = attributeSets.paramLists.map((paramList) => { - return `( ${paramList.join(', ')} )`; - }).join(', '); - - // Build query - var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues}`; - var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; - - // first insert values - await wrapAsyncStatements( - client.run.bind(client, insertQuery, attributeSets.values)); - - // get the last inserted rows if requested - const model = manager.models[tableName]; - let newRows; - if (query.meta && query.meta.fetch) { - newRows = []; - const queryObj = new Query(tableName, manager.schema[tableName], model); - - await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { - if (err) throw err; - - newRows.push(queryObj.castRow(row)); - })); - - // resort for the order we were given the records. - // we can guarantee that the first records will be given the - // first available row IDs (even if some were deleted creating gaps), - // so it's as easy as a sort using the primary key as the comparator - let pkName = model.definition[model.primaryKey].columnName; - newRows.sort((lhs, rhs) => { - if (lhs[pkName] < rhs[pkName]) return -1; - if (lhs[pkName] > rhs[pkName]) return 1; - return 0; - }); - } - - done(undefined, newRows); - } catch (err) { - done(err); - } - - }, - - - - /** - * ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗ - * ║ ║╠═╝ ║║╠═╣ ║ ║╣ - * ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝ - * Update matching records. - * - * > Note that depending on the value of `query.meta.fetch`, - * > you may be expected to return the array of physical records - * > that were updated as the second argument to the callback. - * > (Otherwise, exclude the 2nd argument or send back `undefined`.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Array?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - update: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const client = dsEntry.manager.writeClient; - const tableName = query.using; - const tableSchema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const _query = new Query(tableName, tableSchema, model); - const updateQuery = _query.update(query.criteria, query.valuesToSet); - - /* TODO: See below note and fix so that we do not query the db twice where unnecessary - * Note: The sqlite driver we're using does not return changed values - * on an update. If we are expected to fetch, we need to deterministically - * be able to fetch the exact records that we updated. - * We cannot simply query off the same criteria because it is possible - * (nay likely) that one of the criteria is based on a field that is - * changed in the update call. In most cases, acquiring the primary key - * value before the update and then re-querying that key after the update - * will be sufficient. However, it is possible to update the primary key - * itself. So we will construct 2 cases: - * 1: Query the primary key for all records that will be updated. Then - * craft a new where object based on only those primary keys to - * query again after the update executes - * 2: craft a new where object based on what the primary key is changing - * to. - * - * Note that option 1 sucks. However, an analysis of the where criteria to - * determine the optimal *post-update* where criteria is more work than - * I have time to do, so option 1 it is. - */ - - let newQuery; - if (query.meta && query.meta.fetch) { - const pkCol = model.definition[model.primaryKey].columnName; - let newWhere = {}; - newQuery = _.cloneDeep(query); - newQuery.criteria = newQuery.criteria || {}; - - if (query.valuesToSet[pkCol]) { - newWhere[pkCol] = query.valuesToSet[pkCol]; - } else { - newQuery.criteria.select = [pkCol]; - - const rows = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, newQuery)); - - delete newQuery.criteria.select; - - const inSet = {in: rows.map(row => row[pkCol])}; - newWhere[pkCol] = inSet; - } - - newQuery.criteria.where = newWhere; - } - - const statement = await wrapAsyncForThis( - client.run.bind(client, updateQuery.query, updateQuery.values)); - - let results; - if (query.meta && query.meta.fetch) { - if (statement.changes === 0) { - results = []; - } else { - results = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, newQuery)); - } - } - - done(undefined, results); - } catch (err) { - done(err); - } - - }, - - - /** - * ╔╦╗╔═╗╔═╗╔╦╗╦═╗╔═╗╦ ╦ - * ║║║╣ ╚═╗ ║ ╠╦╝║ ║╚╦╝ - * ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝ ╩ - * Destroy one or more records. - * - * > Note that depending on the value of `query.meta.fetch`, - * > you may be expected to return the array of physical records - * > that were destroyed as the second argument to the callback. - * > (Otherwise, exclude the 2nd argument or send back `undefined`.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Array?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - destroy: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const client = dsEntry.manager.writeClient; - const tableName = query.using; - const tableSchema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const _query = new Query(tableName, tableSchema, model); - const queryObj = _query.destroy(query.criteria); - - let results; - if (query.meta && query.meta.fetch) { - results = await wrapAsyncStatements( - adapter.find.bind(adapter, datastoreName, query)); - } - - await wrapAsyncStatements( - client.run.bind(client, queryObj.query, queryObj.values)); - - done(undefined, results); - } catch (err) { - done(err); - } - - }, - - - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // ██████╗ ██████╗ ██╗ // - // ██╔══██╗██╔═══██╗██║ // - // ██║ ██║██║ ██║██║ // - // ██║ ██║██║▄▄ ██║██║ // - // ██████╔╝╚██████╔╝███████╗ // - // ╚═════╝ ╚══▀▀═╝ ╚══════╝ // - // (D)ata (Q)uery (L)anguage // - // // - // DQL adapter methods: // - // Methods related to fetching information from the database (e.g. finding stored records). // - ////////////////////////////////////////////////////////////////////////////////////////////////// - - - /** - * ╔═╗╦╔╗╔╔╦╗ - * ╠╣ ║║║║ ║║ - * ╚ ╩╝╚╝═╩╝ - * Find matching records. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Array} [matching physical records] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - find: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - const tableName = query.using; - const schema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - const queryObj = new Query(tableName, schema, model); - const queryStatement = queryObj.find(query.criteria); - - try { - await spawnReadonlyConnection(dsEntry, async function __FIND__(client) { - const values = []; - let resultCount = await wrapAsyncStatements( - client.each.bind(client, queryStatement.query, queryStatement.values, (err, row) => { - if (err) throw err; - - values.push(queryObj.castRow(row)); - })); - - done(undefined, values); - }); - } catch (err) { - done(err); - } - }, - - - /** - * ╦╔═╗╦╔╗╔ - * ║║ ║║║║║ - * ╚╝╚═╝╩╝╚╝ - * ┌─ ┌─┐┌─┐┬─┐ ┌┐┌┌─┐┌┬┐┬┬ ┬┌─┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐ ─┐ - * │─── ├┤ │ │├┬┘ │││├─┤ │ │└┐┌┘├┤ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ ───│ - * └─ └ └─┘┴└─ ┘└┘┴ ┴ ┴ ┴ └┘ └─┘ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘ ─┘ - * Perform a "find" query with one or more native joins. - * - * > NOTE: If you don't want to support native joins (or if your database does not - * > support native joins, e.g. Mongo) remove this method completely! Without this method, - * > Waterline will handle `.populate()` using its built-in join polyfill (aka "polypopulate"), - * > which sends multiple queries to the adapter and joins the results in-memory. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Array} [matching physical records, populated according to the join instructions] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - /**************************************** - * NOTE: Intention is to support joins. - * Ignoring for the time being since - * waterline polyfills a join in memory - ***************************************/ - // join: function (datastoreName, query, done) { - - // // Look up the datastore entry (manager/driver/config). - // var dsEntry = registeredDatastores[datastoreName]; - - // // Sanity check: - // if (_.isUndefined(dsEntry)) { - // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - // } - - // // Perform the query and send back a result. - // // - // // > TODO: Replace this setTimeout with real logic that calls - // // > `done()` when finished. (Or remove this method from the - // // > adapter altogether - // setTimeout(function(){ - // return done(new Error('Adapter method (`join`) not implemented yet.')); - // }, 16); - - // }, - - - /** - * ╔═╗╔═╗╦ ╦╔╗╔╔╦╗ - * ║ ║ ║║ ║║║║ ║ - * ╚═╝╚═╝╚═╝╝╚╝ ╩ - * Get the number of matching records. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Number} [the number of matching records] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - count: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const tableName = query.using; - const schema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const countQuery = new Query(tableName, schema, model); - const statement = countQuery.count(query.criteria, 'count_alias'); - - await spawnReadonlyConnection(dsEntry, async function __COUNT__(client) { - const row = await wrapAsyncStatements( - client.get.bind(client, statement.query, statement.values)); - - if (!row) throw new Error('No rows returned by count query?'); - - done(undefined, row.count_alias); - }); - } catch(err) { - done(err); - } - }, - - - /** - * ╔═╗╦ ╦╔╦╗ - * ╚═╗║ ║║║║ - * ╚═╝╚═╝╩ ╩ - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Number} [the sum] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - sum: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const tableName = query.using; - const schema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const sumQuery = new Query(tableName, schema, model); - const statement = sumQuery.sum(query.criteria, query.numericAttrName, 'sum_alias'); - - await spawnReadonlyConnection(dsEntry, async function __SUM__(client) { - const row = await wrapAsyncStatements( - client.get.bind(client, statement.query, statement.values)); - - if (!row) throw new Error('No rows returned by sum query?'); - - done(undefined, row.sum_alias); - }); - } catch (err) { - done(err); - } - - }, - - - /** - * ╔═╗╦ ╦╔═╗ - * ╠═╣╚╗╔╝║ ╦ - * ╩ ╩ ╚╝ ╚═╝ - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore to perform the query on. - * @param {Dictionary} query The stage-3 query to perform. - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * @param {Number} [the average ("mean")] - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - avg: async function (datastoreName, query, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - try { - const tableName = query.using; - const schema = dsEntry.manager.schema[tableName]; - const model = dsEntry.manager.models[tableName]; - - const avgQuery = new Query(tableName, schema, model); - const statement = avgQuery.avg(query.criteria, query.numericAttrName, 'avg_alias'); - - await spawnReadonlyConnection(dsEntry, async function __AVG__(client) { - const row = await wrapAsyncStatements( - client.get.bind(client, statement.query, statement.values)); - - if (!row) throw new Error('No rows returned by avg query?'); - - done(undefined, row.avg_alias); - }); - } catch (err) { - done(err); - } - - }, - - - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // ██████╗ ██████╗ ██╗ // - // ██╔══██╗██╔══██╗██║ // - // ██║ ██║██║ ██║██║ // - // ██║ ██║██║ ██║██║ // - // ██████╔╝██████╔╝███████╗ // - // ╚═════╝ ╚═════╝ ╚══════╝ // - // (D)ata (D)efinition (L)anguage // - // // - // DDL adapter methods: // - // Methods related to modifying the underlying structure of physical models in the database. // - ////////////////////////////////////////////////////////////////////////////////////////////////// - - /* ╔╦╗╔═╗╔═╗╔═╗╦═╗╦╔╗ ╔═╗ ┌┬┐┌─┐┌┐ ┬ ┌─┐ - * ║║║╣ ╚═╗║ ╠╦╝║╠╩╗║╣ │ ├─┤├┴┐│ ├┤ - * ═╩╝╚═╝╚═╝╚═╝╩╚═╩╚═╝╚═╝ ┴ ┴ ┴└─┘┴─┘└─┘ - * Describe a table and get back a normalized model schema format. - * (This is used to allow Sails to do auto-migrations) - */ - describe: async function describe(datastoreName, tableName, cb, meta) { - var datastore = registeredDatastores[datastoreName]; - spawnReadonlyConnection(datastore, async function __DESCRIBE__(client) { - // Get a list of all the tables in this database - // See: http://www.sqlite.org/faq.html#q7) - var query = `SELECT * FROM sqlite_master WHERE type="table" AND name="${tableName}" ORDER BY name`; - - try { - const schema = await wrapAsyncStatements(client.get.bind(client, query)); - if (!schema) return Promise.resolve(); - - // Query to get information about each table - // See: http://www.sqlite.org/pragma.html#pragma_table_info - var columnsQuery = `PRAGMA table_info("${schema.name}")`; - - // Query to get a list of indices for a given table - var indexListQuery = `PRAGMA index_list("${schema.name}")`; - - schema.indices = []; - schema.columns = []; - - var index = { columns: [] }; - - // Binding to the each method which takes a function that runs for every - // row returned, then a complete callback function - await wrapAsyncStatements(client.each.bind(client, indexListQuery, (err, currentIndex) => { - if (err) throw err; - // Query to get information about indices - var indexInfoQuery = - `PRAGMA index_info("${currentIndex.name}")`; - - // Retrieve detailed information for given index - client.each(indexInfoQuery, function (err, indexedCol) { - index.columns.push(indexedCol); - }); - - schema.indices.push(currentIndex); - })); - - await wrapAsyncStatements(client.each.bind(client, columnsQuery, (err, column) => { - if (err) throw err; - - // In SQLite3, AUTOINCREMENT only applies to PK columns of - // INTEGER type - column.autoIncrement = (column.type.toLowerCase() == 'integer' - && column.pk == 1); - - // By default, assume column is not indexed until we find that it - // is - column.indexed = false; - - // Search for indexed columns - schema.indices.forEach(function (idx) { - if (!column.indexed) { - index.columns.forEach(function (indexedCol) { - if (indexedCol.name == column.name) { - column.indexed = true; - if (idx.unique) column.unique = true; - } - }); - } - }); - - schema.columns.push(column); - })); - - var normalizedSchema = utils.normalizeSchema(schema); - // Set internal schema mapping - datastore.manager.schema[tableName] = normalizedSchema; - - return Promise.resolve(normalizedSchema); - } catch (err) { - return Promise.reject(err); - } - }) - .then(schema => cb(undefined, schema)) - .catch(err => cb(err)); - }, - - /** - * ╔╦╗╔═╗╔═╗╦╔╗╔╔═╗ - * ║║║╣ ╠╣ ║║║║║╣ - * ═╩╝╚═╝╚ ╩╝╚╝╚═╝ - * Build a new physical model (e.g. table/etc) to use for storing records in the database. - * - * (This is used for schema migrations.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore containing the table to define. - * @param {String} tableName The name of the table to define. - * @param {Dictionary} definition The physical model definition (not a normal Sails/Waterline model-- log this for details.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - define: async function (datastoreName, tableName, definition, done) { - - // Look up the datastore entry (manager/driver/config). - var datastore = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(datastore)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - let tableQuery; - let outerSchema - try { - const client = datastore.manager.writeClient; - const escapedTable = utils.escapeTable(tableName); - - // Iterate through each attribute, building a query string - const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); - outerSchema = _schema.schema; - - // Check for any index attributes - const indices = utils.buildIndexes(definition); - - // Build query - // const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; - tableQuery = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; - - // await wrapAsyncStatements(client.run.bind(client, query)); - await wrapAsyncStatements(client.run.bind(client, tableQuery)); - - await Promise.all(indices.map(async index => { - // Build a query to create a namespaced index tableName_key - const indexQuery = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + - tableName + ' (' + index + ');'; - - await wrapAsyncStatements(client.run.bind(client, indexQuery)); - })); - - // Replacing if it already existed - datastore.manager.schema[tableName] = _schema.schema; - - done(); - } catch (err) { - done(err); - } - - }, - - - /** - * ╔╦╗╦═╗╔═╗╔═╗ - * ║║╠╦╝║ ║╠═╝ - * ═╩╝╩╚═╚═╝╩ - * Drop a physical model (table/etc.) from the database, including all of its records. - * - * (This is used for schema migrations.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore containing the table to drop. - * @param {String} tableName The name of the table to drop. - * @param {Ref} unused Currently unused (do not use this argument.) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done Callback - * @param {Error?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - drop: async function (datastoreName, tableName, unused, done) { - - // Look up the datastore entry (manager/driver/config). - var dsEntry = registeredDatastores[datastoreName]; - - // Sanity check: - if (_.isUndefined(dsEntry)) { - return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - } - - // Build query - const query = 'DROP TABLE IF EXISTS ' + utils.escapeTable(tableName); - - - try { - const client = dsEntry.manager.writeClient; - await wrapAsyncStatements(client.run.bind(client, query)); - - delete dsEntry.manager.schema[tableName]; - done(); - } catch (err) { - done(err); - } - - }, - - - /** - * ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┌─┐ ┬ ┬┌─┐┌┐┌┌─┐┌─┐ - * ╚═╗║╣ ║ └─┐├┤ │─┼┐│ │├┤ ││││ ├┤ - * ╚═╝╚═╝ ╩ └─┘└─┘└─┘└└─┘└─┘┘└┘└─┘└─┘ - * Set a sequence in a physical model (specifically, the auto-incrementing - * counter for the primary key) to the specified value. - * - * (This is used for schema migrations.) - * - * > NOTE - removing method. SQLite can support setting a sequence on - * > primary key fields (or other autoincrement fields), however the - * > need is slim and I don't have time. - * > Leaving shell here for future developers if necessary - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {String} datastoreName The name of the datastore containing the table/etc. - * @param {String} sequenceName The name of the sequence to update. - * @param {Number} sequenceValue The new value for the sequence (e.g. 1) - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - * @param {Function} done - * @param {Error?} - * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - // setSequence: function (datastoreName, sequenceName, sequenceValue, done) { - - // // Look up the datastore entry (manager/driver/config). - // var dsEntry = registeredDatastores[datastoreName]; - - // // Sanity check: - // if (_.isUndefined(dsEntry)) { - // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); - // } - - // // Update the sequence. - // // - // // > TODO: Replace this setTimeout with real logic that calls - // // > `done()` when finished. (Or remove this method from the - // // > adapter altogether - // setTimeout(function(){ - // return done(new Error('Adapter method (`setSequence`) not implemented yet.')); - // }, 16); - - // }, -}; - -/** - * Spawns temporary connection and executes given logic. Returns promise for - * use with async/await - * @param {*} datastore - * @param {Function} logic Takes the client as its only argument. Can return a - * value or a Promise - * @param {*} cb - * @return Promise - */ -function spawnReadonlyConnection(datastore, logic) { - let client; - return new Promise((resolve, reject) => { - if (!datastore) reject(Errors.InvalidConnection); - - var datastoreConfig = datastore.config; - - // Check if we want to run in verbose mode - // Note that once you go verbose, you can't go back. - // See: https://github.com/mapbox/node-sqlite3/wiki/API - if (datastoreConfig.verbose) sqlite3.verbose(); - - // Make note whether the database already exists - exists = fs.existsSync(datastoreConfig.filename); - - // Create a new handle to our database - client = new sqlite3.Database( - datastoreConfig.filename, - sqlite3.OPEN_READONLY, - err => { - if (err) reject(err); - else resolve(client); - } - ); - }) - .then(logic) - .catch(err => { - return Promise.reject(err); //we want the user process to get this error as well - }) - .finally(() => { - if (client) client.close(); - }); -} - -/** - * Simple utility function that wraps an async function in a promise - * @param {Function} func Async function which takes 1 argument: a callback - * function that takes err, value as args (in that order) - * @return Promise - */ -function wrapAsyncStatements(func) { - return new Promise((resolve, reject) => { - func((err, value) => { - if (err) reject(err); - else resolve(value); - }); - }); -} - -/** - * Utility function that wraps an async function in a promise. In contrast - * to the above, this method specifically resolves with the `this` value - * passed to the callback function - * @param {Function} func Async function which takes 1 argument: a callback - * function that takes an err and invokes its callback with a `this` property - */ -function wrapAsyncForThis(func) { - return new Promise((resolve, reject) => { - func(function(err) { - if (err) reject(err); - else resolve(this); - }); - }) -} - -module.exports = adapter; \ No newline at end of file diff --git a/lib/adapter.js b/lib/adapter.js index 327916e..a585fb7 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -1,676 +1,1115 @@ +/* eslint-disable prefer-arrow-callback */ /*--------------------------------------------------------------- :: sails-sqlite3 -> adapter - The code here is loosely based on sails-postgres adapter. + Code refactored for Sails 1.0.0 release + + Supports Migratable interface, but (as docs on this interface + stipulate) this should only be used for dev. This adapter + does not implement the majority of possibly desired + constraints since the waterline auto-migration is intended + to be light and quick ---------------------------------------------------------------*/ +/** + * Module dependencies + */ + +const fs = require('fs'); + +const _ = require('@sailshq/lodash'); +const sqlite3 = require('sqlite3'); +const Errors = require('waterline-errors').adapter; + +const Query = require('./query'); +const utils = require('./utils'); + + +/** + * Module state + */ + +// Private var to track of all the datastores that use this adapter. In order for your adapter +// to be able to connect to the database, you'll want to expose this var publicly as well. +// (See the `registerDatastore()` method for info on the format of each datastore entry herein.) +// +// > Note that this approach of process global state will be changing in an upcoming version of +// > the Waterline adapter spec (a breaking change). But if you follow the conventions laid out +// > below in this adapter template, future upgrades should be a breeze. +var registeredDatastores = {}; + + +/** + * sails-sqlite3 + * + * Expose the adapater definition. + * + * > Most of the methods below are optional. + * > + * > If you don't need / can't get to every method, just implement + * > what you have time for. The other methods will only fail if + * > you try to call them! + * > + * > For many adapters, this file is all you need. For very complex adapters, you may need more flexiblity. + * > In any case, it's probably a good idea to start with one file and refactor only if necessary. + * > If you do go that route, it's conventional in Node to create a `./lib` directory for your private submodules + * > and `require` them at the top of this file with other dependencies. e.g.: + * > ``` + * > var updateMethod = require('./lib/update'); + * > ``` + * + * @type {Dictionary} + */ +const adapter = { + + + // The identity of this adapter, to be referenced by datastore configurations in a Sails app. + identity: 'sails-sqlite3', + + + // Waterline Adapter API Version + // + // > Note that this is not necessarily tied to the major version release cycle of Sails/Waterline! + // > For example, Sails v1.5.0 might generate apps which use sails-hook-orm@2.3.0, which might + // > include Waterline v0.13.4. And all those things might rely on version 1 of the adapter API. + // > But Waterline v0.13.5 might support version 2 of the adapter API!! And while you can generally + // > trust semantic versioning to predict/understand userland API changes, be aware that the maximum + // > and/or minimum _adapter API version_ supported by Waterline could be incremented between major + // > version releases. When possible, compatibility for past versions of the adapter spec will be + // > maintained; just bear in mind that this is a _separate_ number, different from the NPM package + // > version. sails-hook-orm verifies this adapter API version when loading adapters to ensure + // > compatibility, so you should be able to rely on it to provide a good error message to the Sails + // > applications which use this adapter. + adapterApiVersion: 1, + + + // Default datastore configuration. + defaults: { + // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an + // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, + // their contents are lost. + filename: "", + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, + verbose: false + }, + + + // ╔═╗═╗ ╦╔═╗╔═╗╔═╗╔═╗ ┌─┐┬─┐┬┬ ┬┌─┐┌┬┐┌─┐ + // ║╣ ╔╩╦╝╠═╝║ ║╚═╗║╣ ├─┘├┬┘│└┐┌┘├─┤ │ ├┤ + // ╚═╝╩ ╚═╩ ╚═╝╚═╝╚═╝ ┴ ┴└─┴ └┘ ┴ ┴ ┴ └─┘ + // ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐┌─┐ + // ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ └─┐ + // ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘└─┘ + // This allows outside access to this adapter's internal registry of datastore entries, + // for use in datastore methods like `.leaseConnection()`. + datastores: registeredDatastores, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██╗ ██╗███████╗███████╗ ██████╗██╗ ██╗ ██████╗██╗ ███████╗ // + // ██║ ██║██╔════╝██╔════╝██╔════╝╚██╗ ██╔╝██╔════╝██║ ██╔════╝ // + // ██║ ██║█████╗ █████╗ ██║ ╚████╔╝ ██║ ██║ █████╗ // + // ██║ ██║██╔══╝ ██╔══╝ ██║ ╚██╔╝ ██║ ██║ ██╔══╝ // + // ███████╗██║██║ ███████╗╚██████╗ ██║ ╚██████╗███████╗███████╗ // + // ╚══════╝╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝╚══════╝ // + // // + // Lifecycle adapter methods: // + // Methods related to setting up and tearing down; registering/un-registering datastores. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * ╦═╗╔═╗╔═╗╦╔═╗╔╦╗╔═╗╦═╗ ┌┬┐┌─┐┌┬┐┌─┐┌─┐┌┬┐┌─┐┬─┐┌─┐ + * ╠╦╝║╣ ║ ╦║╚═╗ ║ ║╣ ╠╦╝ ││├─┤ │ ├─┤└─┐ │ │ │├┬┘├┤ + * ╩╚═╚═╝╚═╝╩╚═╝ ╩ ╚═╝╩╚═ ─┴┘┴ ┴ ┴ ┴ ┴└─┘ ┴ └─┘┴└─└─┘ + * Register a new datastore with this adapter. This usually involves creating a new + * connection manager (e.g. MySQL pool or MongoDB client) for the underlying database layer. + * + * > Waterline calls this method once for every datastore that is configured to use this adapter. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Dictionary} datastoreConfig Dictionary (plain JavaScript object) of configuration options for this datastore (e.g. host, port, etc.) + * @param {Dictionary} physicalModelsReport Experimental: The physical models using this datastore (keyed by "tableName"-- NOT by `identity`!). This may change in a future release of the adapter spec. + * @property {Dictionary} * [Info about a physical model using this datastore. WARNING: This is in a bit of an unusual format.] + * @property {String} primaryKey [the name of the primary key attribute (NOT the column name-- the attribute name!)] + * @property {Dictionary} definition [the physical-layer report from waterline-schema. NOTE THAT THIS IS NOT A NORMAL MODEL DEF!] + * @property {String} tableName [the model's `tableName` (same as the key this is under, just here for convenience)] + * @property {String} identity [the model's `identity`] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done A callback to trigger after successfully registering this datastore, or if an error is encountered. + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + registerDatastore: async function (datastoreConfig, physicalModelsReport, done) { + + // Grab the unique name for this datastore for easy access below. + var datastoreName = datastoreConfig.identity; + + // Some sanity checks: + if (!datastoreName) { + return done(new Error('Consistency violation: A datastore should contain an "identity" property: a special identifier that uniquely identifies it across this app. This should have been provided by Waterline core! If you are seeing this message, there could be a bug in Waterline, or the datastore could have become corrupted by userland code, or other code in this adapter. If you determine that this is a Waterline bug, please report this at https://sailsjs.com/bugs.')); + } + if (registeredDatastores[datastoreName]) { + return done(new Error('Consistency violation: Cannot register datastore: `' + datastoreName + '`, because it is already registered with this adapter! This could be due to an unexpected race condition in userland code (e.g. attempting to initialize Waterline more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } -// Dependencies -var sqlite3 = require('sqlite3'), - async = require('async'), - fs = require('fs'), - _ = require('lodash'), - Query = require('./query'), - utils = require('./utils'); - Errors = require('waterline-errors').adapter - -module.exports = (function() { - - var connections = {}; - var dbs = {}; - - // Determines whether the database file already exists - var exists = false; - - var adapter = { - identity: 'sails-sqlite3', - - // Set to true if this adapter supports (or requires) things like data - // types, validations, keys, etc. If true, the schema for models using this - // adapter will be automatically synced when the server starts. - // Not terribly relevant if not using a non-SQL / non-schema-ed data store - syncable: true, - - // Default configuration for collections - // (same effect as if these properties were included at the top level of the - // model definitions) - defaults: { - - // Valid values are filenames, ":memory:" for an anonymous in-memory database and an empty string for an - // anonymous disk-based database. Anonymous databases are not persisted and when closing the database handle, - // their contents are lost. - filename: "", - mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, - verbose: false - }, - - /*************************************************************************/ - /* Public Methods for Sails/Waterline Adapter Compatibility */ - /*************************************************************************/ - - /** - * This method runs when a model is initially registered - * at server-start-time. This is the only required method. - * - * @param {[type]} connection [description] - * @param {[type]} collection [description] - * @param {Function} cb [description] - * @return {[type]} [description] - */ - registerConnection: function(connection, collections, cb) { - var self = this; - - if (!connection.identity) return cb(Errors.IdentityMissing); - if (connections[connection.identity]) return cb(Errors.IdentityDuplicate); - - connections[connection.identity] = { - config: connection, - collections: collections - }; - - async.map(Object.keys(collections), function(columnName, cb) { - self.describe(connection.identity, columnName, cb); - }, cb); - }, - - - /** - * Fired when a model is unregistered, typically when the server - * is killed. Useful for tearing-down remaining open connections, - * etc. - * - * @param {[type]} connectionId [description] - * @param {Function} cb [description] - * @return {[type]} [description] - */ - teardown: function(connectionId, cb) { - if (!connections[connectionId]) return cb(); - delete connections[connectionId]; - cb(); - }, - - /** - * This method returns attributes and is required when integrating with a - * schemaful database. - * - * @param {[type]} connectionId [description] - * @param {[type]} collection [description] - * @param {[Function]} cb [description] - * @return {[type]} [description] - */ - describe: function(connectionId, table, cb) { - var self = this; - - spawnConnection(connectionId, function __DESCRIBE__(client, cb) { - - var connection = connections[connectionId]; - var collection = connection.collections[table]; - - // Get a list of all the tables in this database - // See: http://www.sqlite.org/faq.html#q7) - var query = 'SELECT * FROM sqlite_master WHERE type="table" AND name="' + table + '" ORDER BY name'; - - client.get(query, function (err, schema) { - if (err || !schema) return cb(); - // Query to get information about each table - // See: http://www.sqlite.org/pragma.html#pragma_table_info - var columnsQuery = "PRAGMA table_info(" + schema.name + ")"; - - // Query to get a list of indices for a given table - var indexListQuery = 'PRAGMA index_list("' + schema.name + '")'; - - schema.indices = []; - schema.columns = []; - - var index = { columns: [] }; - - client.each(indexListQuery, function(err, currentIndex) { - // Query to get information about indices - var indexInfoQuery = - 'PRAGMA index_info("' + currentIndex.name + '")'; - - // Retrieve detailed information for given index - client.each(indexInfoQuery, function(err, indexedCol) { - index.columns.push(indexedCol); - }); - - schema.indices.push(currentIndex); - }, function(err, resultCount) { - if (err) return cb(err); - - client.each(columnsQuery, function(err, column) { - - // In SQLite3, AUTOINCREMENT only applies to PK columns of - // INTEGER type - column.autoIncrement = (column.type.toLowerCase() == 'integer' - && column.pk == 1); - - // By default, assume column is not indexed until we find that it - // is - column.indexed = false; - - // Search for indexed columns - schema.indices.forEach(function(idx) { - if (!column.indexed) { - index.columns.forEach(function(indexedCol) { - if (indexedCol.name == column.name) { - column.indexed = true; - if (idx.unique) column.unique = true; - } - }); - } - }); + let writeClient; + try { + writeClient = await wrapAsyncStatements((cb) => { + if (datastoreConfig.verbose) sqlite3.verbose(); + const writeClient = new sqlite3.Database( + datastoreConfig.filename, + datastoreConfig.mode, + err => { + if (!err) { + //set write client to serialize mode + writeClient.serialize(); + } - schema.columns.push(column); - }, function(err, resultCount) { - - // This callback function is fired when all the columns have been - // iterated by the .each() function - if (err) { - console.error("Error while retrieving column information."); - console.error(err); - return cb(err); - } - var normalizedSchema = utils.normalizeSchema(schema); - try { - // Set internal schema mapping - collection.schema = normalizedSchema; - - } catch(e){ - console.log(e); - } - - - // Fire the callback with the normalized schema - cb(null, normalizedSchema); - }); - }); - }); - }, cb); - }, + cb(err, writeClient); + } + ); + }); + } catch (err) { + return done(err); + } + // To maintain the spirit of this repository, this implementation will + // continue to spin up and tear down a connection to the Sqlite db on every + // request. + // TODO: Consider creating the connection and maintaining through the life + // of the sails app. (This would lock it from changes outside sails) + registeredDatastores[datastoreName] = { + config: datastoreConfig, + manager: { + models: physicalModelsReport, //for reference + schema: {}, + foreignKeys: utils.buildForeignKeyMap(physicalModelsReport), + writeClient, + }, + // driver: undefined // << TODO: include driver here (if relevant) + }; + + try { + for (let tableName in physicalModelsReport) { + await wrapAsyncStatements(this.describe.bind(this, datastoreName, tableName)); + } + } catch (err) { + return done(err); + } - /** - * Creates a new table in the database when defining a model. - * - * @param {[type]} connectionId [description] - * @param {[type]} table [description] - * @param {[type]} definition [description] - * @param {[Function]} cb [description] - * @return {[type]} [description] - */ - define: function(connectionId, table, definition, cb) { + return done(); + }, + + + /** + * ╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗╦ ╦╔╗╔ + * ║ ║╣ ╠═╣╠╦╝ ║║║ ║║║║║║║ + * ╩ ╚═╝╩ ╩╩╚══╩╝╚═╝╚╩╝╝╚╝ + * Tear down (un-register) a datastore. + * + * Fired when a datastore is unregistered. Typically called once for + * each relevant datastore when the server is killed, or when Waterline + * is shut down after a series of tests. Useful for destroying the manager + * (i.e. terminating any remaining open connections, etc.). + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The unique name (identity) of the datastore to un-register. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + teardown: function (datastoreName, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Attempting to tear down a datastore (`' + datastoreName + '`) which is not currently registered with this adapter. This is usually due to a race condition in userland code (e.g. attempting to tear down the same ORM instance more than once), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - var describe = function(err, result) { - if (err) return cb(err); + // Close write client + dsEntry.manager.writeClient.close(); + delete registeredDatastores[datastoreName]; + + // Inform Waterline that we're done, and that everything went as expected. + return done(); + + }, + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ███╗ ███╗██╗ // + // ██╔══██╗████╗ ████║██║ // + // ██║ ██║██╔████╔██║██║ // + // ██║ ██║██║╚██╔╝██║██║ // + // ██████╔╝██║ ╚═╝ ██║███████╗ // + // ╚═════╝ ╚═╝ ╚═╝╚══════╝ // + // (D)ata (M)anipulation (L)anguage // + // // + // DML adapter methods: // + // Methods related to manipulating records stored in the database. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ + * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ + * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ + * Create a new record. + * + * (e.g. add a new row to a SQL table, or a new document to a MongoDB collection.) + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the physical record that was + * > created (a dictionary) as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Dictionary?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + create: async function (datastoreName, query, done) { + + // normalize newRecords property + query.newRecords = [query.newRecord]; + delete query.newRecord; + + try { + const record = await wrapAsyncStatements( + adapter.createEach.bind(adapter, datastoreName, query)); + + if (record && record.length >>> 0 > 0) { + done(undefined, record[0]); + } + } catch (err) { + done(err); + } + }, + + + /** + * ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔═╗╦ ╦ + * ║ ╠╦╝║╣ ╠═╣ ║ ║╣ ║╣ ╠═╣║ ╠═╣ + * ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚═╝╩ ╩ + * Create multiple new records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were created as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + createEach: async function (datastoreName, query, done) { + let verbose = false; + + // Look up the datastore entry (manager/driver/config). + const dsEntry = registeredDatastores[datastoreName]; + const manager = dsEntry.manager; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - adapter.describe(connectionId, table.replace(/["']/g, ""), cb); - }; + try { + const client = manager.writeClient; + const tableName = query.using; + const escapedTable = utils.escapeTable(tableName); + + const attributeSets = utils.mapAllAttributes(query.newRecords, manager.schema[tableName]); + + const columnNames = attributeSets.keys.join(', '); + + const paramValues = attributeSets.paramLists.map((paramList) => { + return `( ${paramList.join(', ')} )`; + }).join(', '); + + // Build query + var insertQuery = `INSERT INTO ${escapedTable} (${columnNames}) values ${paramValues}`; + var selectQuery = `SELECT * FROM ${escapedTable} ORDER BY rowid DESC LIMIT ${query.newRecords.length}`; + + // first insert values + await wrapAsyncStatements( + client.run.bind(client, insertQuery, attributeSets.values)); + + // get the last inserted rows if requested + const model = manager.models[tableName]; + let newRows; + if (query.meta && query.meta.fetch) { + newRows = []; + const queryObj = new Query(tableName, manager.schema[tableName], model); + + await wrapAsyncStatements(client.each.bind(client, selectQuery, (err, row) => { + if (err) throw err; + + newRows.push(queryObj.castRow(row)); + })); + + // resort for the order we were given the records. + // we can guarantee that the first records will be given the + // first available row IDs (even if some were deleted creating gaps), + // so it's as easy as a sort using the primary key as the comparator + let pkName = model.definition[model.primaryKey].columnName; + newRows.sort((lhs, rhs) => { + if (lhs[pkName] < rhs[pkName]) return -1; + if (lhs[pkName] > rhs[pkName]) return 1; + return 0; + }); + } - spawnConnection(connectionId, function __DEFINE__(client, cb) { + done(undefined, newRows); + } catch (err) { + done(err); + } - // Escape table name - table = utils.escapeTable(table); + }, + + + + /** + * ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗ + * ║ ║╠═╝ ║║╠═╣ ║ ║╣ + * ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝ + * Update matching records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were updated as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + update: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Iterate through each attribute, building a query string - var _schema = utils.buildSchema(definition); + try { + const client = dsEntry.manager.writeClient; + const tableName = query.using; + const tableSchema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const _query = new Query(tableName, tableSchema, model); + const updateQuery = _query.update(query.criteria, query.valuesToSet); + + /* TODO: See below note and fix so that we do not query the db twice where unnecessary + * Note: The sqlite driver we're using does not return changed values + * on an update. If we are expected to fetch, we need to deterministically + * be able to fetch the exact records that we updated. + * We cannot simply query off the same criteria because it is possible + * (nay likely) that one of the criteria is based on a field that is + * changed in the update call. In most cases, acquiring the primary key + * value before the update and then re-querying that key after the update + * will be sufficient. However, it is possible to update the primary key + * itself. So we will construct 2 cases: + * 1: Query the primary key for all records that will be updated. Then + * craft a new where object based on only those primary keys to + * query again after the update executes + * 2: craft a new where object based on what the primary key is changing + * to. + * + * Note that option 1 sucks. However, an analysis of the where criteria to + * determine the optimal *post-update* where criteria is more work than + * I have time to do, so option 1 it is. + */ + + let newQuery; + if (query.meta && query.meta.fetch) { + const pkCol = model.definition[model.primaryKey].columnName; + let newWhere = {}; + newQuery = _.cloneDeep(query); + newQuery.criteria = newQuery.criteria || {}; + + if (query.valuesToSet[pkCol]) { + newWhere[pkCol] = query.valuesToSet[pkCol]; + } else { + newQuery.criteria.select = [pkCol]; + + const rows = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, newQuery)); + + delete newQuery.criteria.select; + + const inSet = { in: rows.map(row => row[pkCol]) }; + newWhere[pkCol] = inSet; + } - // Check for any index attributes - var indices = utils.buildIndexes(definition); + newQuery.criteria.where = newWhere; + } - // Build query - var query = 'CREATE TABLE ' + table + ' (' + _schema + ')'; - - client.run(query, function(err) { - if (err) return cb(err); + const statement = await wrapAsyncForThis( + client.run.bind(client, updateQuery.query, updateQuery.values)); - // Build indices - function buildIndex(name, cb) { + let results; + if (query.meta && query.meta.fetch) { + if (statement.changes === 0) { + results = []; + } else { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, newQuery)); + } + } - // Strip slashes from tablename, used to namespace index - var cleanTable = table.replace(/['"]/g, ''); + done(undefined, results); + } catch (err) { + done(err); + } - // Build a query to create a namespaced index tableName_key - var query = 'CREATE INDEX ' + cleanTable + '_' + name + ' on ' + - table + ' (' + name + ');'; + }, + + + /** + * ╔╦╗╔═╗╔═╗╔╦╗╦═╗╔═╗╦ ╦ + * ║║║╣ ╚═╗ ║ ╠╦╝║ ║╚╦╝ + * ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝ ╩ + * Destroy one or more records. + * + * > Note that depending on the value of `query.meta.fetch`, + * > you may be expected to return the array of physical records + * > that were destroyed as the second argument to the callback. + * > (Otherwise, exclude the 2nd argument or send back `undefined`.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + destroy: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Run query - client.run(query, function(err) { - if (err) return cb(err); - cb(); - }); - } - async.eachSeries(indices, buildIndex, cb); - }); - }, describe); - }, - - - /** - * Drops a table corresponding to the model. - * - * @param {[type]} connectionId [description] - * @param {[type]} table [description] - * @param {[type]} relations [description] - * @param {[Function]} cb [description] - * @return {[type]} [description] - */ - drop: function(connectionId, table, relations, cb) { - - if (typeof relations == 'function') { - cb = relations; - relations = []; + try { + const client = dsEntry.manager.writeClient; + const tableName = query.using; + const tableSchema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + + const _query = new Query(tableName, tableSchema, model); + const queryObj = _query.destroy(query.criteria); + + let results; + if (query.meta && query.meta.fetch) { + results = await wrapAsyncStatements( + adapter.find.bind(adapter, datastoreName, query)); } - spawnConnection(connectionId, function __DROP__(client, cb) { + await wrapAsyncStatements( + client.run.bind(client, queryObj.query, queryObj.values)); - function dropTable(item, next) { + done(undefined, results); + } catch (err) { + done(err); + } - // Build query - var query = 'DROP TABLE ' + utils.escapeTable(table); + }, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ██████╗ ██╗ // + // ██╔══██╗██╔═══██╗██║ // + // ██║ ██║██║ ██║██║ // + // ██║ ██║██║▄▄ ██║██║ // + // ██████╔╝╚██████╔╝███████╗ // + // ╚═════╝ ╚══▀▀═╝ ╚══════╝ // + // (D)ata (Q)uery (L)anguage // + // // + // DQL adapter methods: // + // Methods related to fetching information from the database (e.g. finding stored records). // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + + /** + * ╔═╗╦╔╗╔╔╦╗ + * ╠╣ ║║║║ ║║ + * ╚ ╩╝╚╝═╩╝ + * Find matching records. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array} [matching physical records] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + find: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Run the query - client.run(query, function(err) { - cb(null, null); - }); - } + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; + const queryObj = new Query(tableName, schema, model); + const queryStatement = queryObj.find(query.criteria); - async.eachSeries(relations, dropTable, function(err) { - if (err) return cb(err); - dropTable(table, cb); - }); - }, cb); - }, - - - /** - * Add a column to the table. - * - * @param {[type]} connectionId [description] - * @param {[type]} table [description] - * @param {[type]} attrName [description] - * @param {[type]} attrDef [description] - * @param {[Function]} cb [description] - * @return {[type]} [description] - */ - addAttribute: function(connectionId, table, attrName, attrDef, cb) { - spawnConnection(connectionId, function __ADD_ATTRIBUTE__(client, cb) { - - // Escape table name - table = utils.escapeTable(table); - - // Set up a schema definition - var attrs = {}; - attrs[attrName] = attrDef; - - var _schema = utils.buildSchema(attrs); - - // Build query - var query = 'ALTER TABLE ' + table + ' ADD COLUMN ' + _schema; - - // Run query - client.run(query, function(err) { - if (err) return cb(err); - cb(null, this); - }); - }, cb); - }, - - - /** - * Remove attribute from table. - * In SQLite3, this is tricky since there's no support for DROP COLUMN - * in ALTER TABLE. We'll have to rename the old table, create a new table - * with the same name minus the column and copy all the data over. - */ - removeAttribute: function(connectionId, table, attrName, cb) { - spawnConnection(connectionId, function __REMOVE_ATTRIBUTE__(client, cb) { - - // NOTE on this method just so I don't forget: Below is a pretty hackish - // way to remove attributes. Proper SQLite way would be to write all of - // the logic below into a single SQL statement wrapped in BEGIN TRANSAC- - // TION and COMMIT block like this: - // - // BEGIN TRANSACTION; - // ALTER TABLE table RENAME TO table_old_; - // CREATE TABLE table(attrName1, ...); - // INSERT INTO table SELECT attrName1, ... FROM table_old_; - // DROP TABLE table_old_; - // COMMIT; - // - // This will ensure that removing attribute would be atomic. For now, - // hacking it cause I'm actually feeling lazy. - - var oldTable = table + '_old_'; - - // Build query to rename table - var renameQuery = 'ALTER TABLE ' + utils.escapeTable(table) + - ' RENAME TO ' + utils.escapeTable(oldTable); - - // Run query - client.run(query, function(err) { - if (err) return cb(err); - - // Get the attributes - adapter.describe(connectionId, oldTable, function(err, schema) { - if (err) return cb(err); - - // Deep copy the schema and remove the attribute - var newAttributes = _.clone(schema); - delete newAttributes[attrName]; - - // Recreate the table - adapter.define(connectionId, table, newAttributes, - function (err, schema) { - if (err) return cb(err); - - // Copy data back from old table to new table - var copyQuery = 'INSERT INTO ' + utils.escapeTable(table) + - ' SELECT rowid, '; - - Object.keys(newAttributes).forEach( - function(colName, idx, columns) { - copyQuery += colName; - if (idx < keys.length) - copyQuery += ', ' - } - ); - - copyQuery += ' FROM ' + utils.escapeTable(oldTable); - - client.run(copyQuery, function(err) { - if (err) return cb(err); - - var dropQuery = 'DROP TABLE ' + utils.escapeTable(oldTable); - - client.run(dropQuery, function(err) { - if (err) return cb(err); - - // End of operation! - cb(); - }); - }); - } - ); - }); - }); - }, cb); - }, - - - /** - * Finds and returns an instance of a model that matches search criteria. - */ - // REQUIRED method if users expect to call Model.find(), Model.findAll() or related methods - // You're actually supporting find(), findAll(), and other methods here - // but the core will take care of supporting all the different usages. - // (e.g. if this is a find(), not a findAll(), it will only send back a single model) - find: function(connectionId, table, options, cb) { - spawnConnection(connectionId, function __FIND__(client, cb) { - - // Check if this is an aggregate query and that there's something to return - if (options.groupBy || options.sum || options.average || options.min || - options.max) { - if (!options.sum && !options.average && !options.min && - !options.max) { - return cb(Errors.InvalidGroupBy); - } - } + try { + await spawnReadonlyConnection(dsEntry, async function __FIND__(client) { + const values = []; + let resultCount = await wrapAsyncStatements( + client.each.bind(client, queryStatement.query, queryStatement.values, (err, row) => { + if (err) throw err; - var connection = connections[connectionId]; - var collection = connection.collections[table]; + values.push(queryObj.castRow(row)); + })); - // Grab connection schema - var schema = getSchema(connection.collections); - - // Build query - var queryObj = new Query(collection.definition, schema); - var query = queryObj.find(table, options); + done(undefined, values); + }); + } catch (err) { + done(err); + } + }, + + + /** + * ╦╔═╗╦╔╗╔ + * ║║ ║║║║║ + * ╚╝╚═╝╩╝╚╝ + * ┌─ ┌─┐┌─┐┬─┐ ┌┐┌┌─┐┌┬┐┬┬ ┬┌─┐ ┌─┐┌─┐┌─┐┬ ┬┬ ┌─┐┌┬┐┌─┐ ─┐ + * │─── ├┤ │ │├┬┘ │││├─┤ │ │└┐┌┘├┤ ├─┘│ │├─┘│ ││ ├─┤ │ ├┤ ───│ + * └─ └ └─┘┴└─ ┘└┘┴ ┴ ┴ ┴ └┘ └─┘ ┴ └─┘┴ └─┘┴─┘┴ ┴ ┴ └─┘ ─┘ + * Perform a "find" query with one or more native joins. + * + * > NOTE: If you don't want to support native joins (or if your database does not + * > support native joins, e.g. Mongo) remove this method completely! Without this method, + * > Waterline will handle `.populate()` using its built-in join polyfill (aka "polypopulate"), + * > which sends multiple queries to the adapter and joins the results in-memory. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Array} [matching physical records, populated according to the join instructions] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + /**************************************** + * NOTE: Intention is to support joins. + * Ignoring for the time being since + * waterline polyfills a join in memory + ***************************************/ + // join: function (datastoreName, query, done) { + + // // Look up the datastore entry (manager/driver/config). + // var dsEntry = registeredDatastores[datastoreName]; + + // // Sanity check: + // if (_.isUndefined(dsEntry)) { + // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + // } + + // // Perform the query and send back a result. + // // + // // > TODO: Replace this setTimeout with real logic that calls + // // > `done()` when finished. (Or remove this method from the + // // > adapter altogether + // setTimeout(function(){ + // return done(new Error('Adapter method (`join`) not implemented yet.')); + // }, 16); + + // }, + + + /** + * ╔═╗╔═╗╦ ╦╔╗╔╔╦╗ + * ║ ║ ║║ ║║║║ ║ + * ╚═╝╚═╝╚═╝╝╚╝ ╩ + * Get the number of matching records. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the number of matching records] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + count: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Cast special values - var values = []; + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; - // Run query - client.each(query.query, query.values, function(err, row) { - if (err) return cb(err); + const countQuery = new Query(tableName, schema, model); + const statement = countQuery.count(query.criteria, 'count_alias'); - values.push(queryObj.cast(row)); - }, function(err, resultCount) { - var _values = options.joins ? utils.group(values) : values; + await spawnReadonlyConnection(dsEntry, async function __COUNT__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); - cb(null, values); - }); - }, cb); - }, - - - /** - * Add a new row to the table - */ - // REQUIRED method if users expect to call Model.create() or any methods - create: function(connectionId, table, data, cb) { - spawnConnection(connectionId, function __CREATE__(client, cb) { - - // Grab Connection Schema - var connection = connections[connectionId]; - var collection = connection.collections[table]; - // Grab connection schema - var schema = getSchema(connection.collections); - - // Build query - var _query = new Query(collection.schema, schema); - - // Escape table name - table = utils.escapeTable(table); - - // Transform the data object into arrays used in parametrized query - var attributes = utils.mapAttributes(data), - columnNames = attributes.keys.join(', '), - paramValues = attributes.params.join(', '); - - - // Build query - var insertQuery = 'INSERT INTO ' + table + ' (' + columnNames + ') values (' + paramValues + ')'; - var selectQuery = 'SELECT * FROM ' + table + ' ORDER BY rowid DESC LIMIT 1'; - - // First insert the values - client.run(insertQuery, attributes.values, function(err) { - if (err) return cb(err); - - // Get the last inserted row - client.get(selectQuery, function(err, row) { - if (err) return cb(err); - - var values = _query.cast(row); - - cb(null, values); - }); - }); - }, cb); - }, + if (!row) throw new Error('No rows returned by count query?'); + done(undefined, row.count_alias); + }); + } catch (err) { + done(err); + } + }, + + + /** + * ╔═╗╦ ╦╔╦╗ + * ╚═╗║ ║║║║ + * ╚═╝╚═╝╩ ╩ + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the sum] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + sum: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Raw query interface - query: function(table, query, data, cb) { - if (_.isFunction(data)) { - cb = data; - data = null; - } + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; - spawnConnection(function __QUERY__(client, cb) { - if (data) client.all(query, data, cb); - client.all(query, cb); - }, dbs[table].config, cb); - }, - - - // REQUIRED method if users expect to call Model.update() - update: function(connectionId, table, options, data, cb) { - spawnConnection(connectionId, function __UPDATE__(client, cb) { - - // Grab Connection Schema - var connection = connections[connectionId]; - var collection = connection.collections[table]; - // Grab connection schema - var schema = getSchema(connection.collections); - - // Build query - var _query = new Query(collection.schema, schema); - - // Build a query for the specific query strategy - var selectQuery = _query.find(table, options); - var updateQuery = _query.update(table, options, data); - var primaryKeys = []; - - client.serialize(function() { - - // Run query - client.run(updateQuery.query, updateQuery.values, function(err) { - if (err) { console.error(err); return cb(err); } - - // Build a query to return updated rows - if (this.changes > 0) { - - adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { - if (err) { console.error(err); return cb(err); } - var values = []; - - models.forEach(function(item) { - values.push(_query.cast(item)); - }); - - cb(null, values); - }); - } else { - console.error('WARNING: No rows updated.'); - cb(null); - } - }); - }); - }, cb); - }, - - // REQUIRED method if users expect to call Model.destroy() - destroy: function(connectionId, table, options, cb) { - - spawnConnection(connectionId, function __DELETE__(client, cb) { - - var connection = connections[connectionId]; - var collection = connection.collections[table]; - var _schema = utils.buildSchema(collection.definition); - - // Build a query for the specific query strategy - var _query = new Query(_schema); - var query = _query.destroy(table, options); - - // Run Query - adapter.find(connectionId, table.replace(/["']/g, ""), options, function(err, models) { - if (err) { return cb(err); } - - var values = []; - models.forEach(function(model) { - values.push(_query.cast(model)); - }); - - client.run(query.query, query.values, function __DELETE__(err, result) { - if(err) return cb(handleQueryError(err)); - cb(null, values); - }); - }); - - }, cb); - }, - - - - // REQUIRED method if users expect to call Model.stream() - stream: function(table, options, stream) { - // options is a standard criteria/options object (like in find) - - // stream.write() and stream.end() should be called. - // for an example, check out: - // https://github.com/balderdashy/sails-dirty/blob/master/DirtyAdapter.js#L247 - if (dbs[table].config.verbose) sqlite3 = sqlite3.verbose(); - - var client = new sqlite3.Database(dbs[table].config.filename, dbs[table].config.mode, function(err) { - if (err) return cb(err); - - // Escape table name - table = utils.escapeTable(table); - - // Build query - var query = new Query(dbs[table.replace(/["']/g, "")].schema).find(table, options); - - // Run the query - client.each(query.query, query.values, function(err, row) { - if (err) { - stream.end(); - client.close(); - } + const sumQuery = new Query(tableName, schema, model); + const statement = sumQuery.sum(query.criteria, query.numericAttrName, 'sum_alias'); - stream.write(row); - }, function(err, resultCount) { - stream.end(); - client.close(); - }); + await spawnReadonlyConnection(dsEntry, async function __SUM__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); + + if (!row) throw new Error('No rows returned by sum query?'); + + done(undefined, row.sum_alias); }); + } catch (err) { + done(err); + } + + }, + + + /** + * ╔═╗╦ ╦╔═╗ + * ╠═╣╚╗╔╝║ ╦ + * ╩ ╩ ╚╝ ╚═╝ + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore to perform the query on. + * @param {Dictionary} query The stage-3 query to perform. + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * @param {Number} [the average ("mean")] + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + avg: async function (datastoreName, query, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); } - }; - /***************************************************************************/ - /* Private Methods - /***************************************************************************/ + try { + const tableName = query.using; + const schema = dsEntry.manager.schema[tableName]; + const model = dsEntry.manager.models[tableName]; - function getSchema(collections){ - var schema = {}; - Object.keys(collections).forEach(function(collectionId) { - schema[collectionId] = collections[collectionId].schema; + const avgQuery = new Query(tableName, schema, model); + const statement = avgQuery.avg(query.criteria, query.numericAttrName, 'avg_alias'); + + await spawnReadonlyConnection(dsEntry, async function __AVG__(client) { + const row = await wrapAsyncStatements( + client.get.bind(client, statement.query, statement.values)); + + if (!row) throw new Error('No rows returned by avg query?'); + + done(undefined, row.avg_alias); }); - return schema; - } - - function spawnConnection(connectionName, logic, cb) { - var connectionObject = connections[connectionName]; - if (!connectionObject) return cb(Errors.InvalidConnection); + } catch (err) { + done(err); + } - var connectionConfig = connectionObject.config; + }, + + + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // ██████╗ ██████╗ ██╗ // + // ██╔══██╗██╔══██╗██║ // + // ██║ ██║██║ ██║██║ // + // ██║ ██║██║ ██║██║ // + // ██████╔╝██████╔╝███████╗ // + // ╚═════╝ ╚═════╝ ╚══════╝ // + // (D)ata (D)efinition (L)anguage // + // // + // DDL adapter methods: // + // Methods related to modifying the underlying structure of physical models in the database. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + + /* ╔╦╗╔═╗╔═╗╔═╗╦═╗╦╔╗ ╔═╗ ┌┬┐┌─┐┌┐ ┬ ┌─┐ + * ║║║╣ ╚═╗║ ╠╦╝║╠╩╗║╣ │ ├─┤├┴┐│ ├┤ + * ═╩╝╚═╝╚═╝╚═╝╩╚═╩╚═╝╚═╝ ┴ ┴ ┴└─┘┴─┘└─┘ + * Describe a table and get back a normalized model schema format. + * (This is used to allow Sails to do auto-migrations) + */ + describe: async function describe(datastoreName, tableName, cb, meta) { + var datastore = registeredDatastores[datastoreName]; + spawnReadonlyConnection(datastore, async function __DESCRIBE__(client) { + // Get a list of all the tables in this database + // See: http://www.sqlite.org/faq.html#q7) + var query = `SELECT * FROM sqlite_master WHERE type="table" AND name="${tableName}" ORDER BY name`; + + try { + const schema = await wrapAsyncStatements(client.get.bind(client, query)); + if (!schema) return Promise.resolve(); + + // Query to get information about each table + // See: http://www.sqlite.org/pragma.html#pragma_table_info + var columnsQuery = `PRAGMA table_info("${schema.name}")`; + + // Query to get a list of indices for a given table + var indexListQuery = `PRAGMA index_list("${schema.name}")`; + + schema.indices = []; + schema.columns = []; + + var index = { columns: [] }; + + // Binding to the each method which takes a function that runs for every + // row returned, then a complete callback function + await wrapAsyncStatements(client.each.bind(client, indexListQuery, (err, currentIndex) => { + if (err) throw err; + // Query to get information about indices + var indexInfoQuery = + `PRAGMA index_info("${currentIndex.name}")`; + + // Retrieve detailed information for given index + client.each(indexInfoQuery, function (err, indexedCol) { + index.columns.push(indexedCol); + }); - // Check if we want to run in verbose mode - // Note that once you go verbose, you can't go back. - // See: https://github.com/mapbox/node-sqlite3/wiki/API - if (connectionConfig.verbose) sqlite3 = sqlite3.verbose(); - - // Make note whether the database already exists - exists = fs.existsSync(connectionConfig.filename); + schema.indices.push(currentIndex); + })); - // Create a new handle to our database - var client = new sqlite3.Database( - connectionConfig.filename, - connectionConfig.mode, - function(err) { - after(err, client); - } - ); + await wrapAsyncStatements(client.each.bind(client, columnsQuery, (err, column) => { + if (err) throw err; + + // In SQLite3, AUTOINCREMENT only applies to PK columns of + // INTEGER type + column.autoIncrement = (column.type.toLowerCase() == 'integer' + && column.pk == 1); + + // By default, assume column is not indexed until we find that it + // is + column.indexed = false; + + // Search for indexed columns + schema.indices.forEach(function (idx) { + if (!column.indexed) { + index.columns.forEach(function (indexedCol) { + if (indexedCol.name == column.name) { + column.indexed = true; + if (idx.unique) column.unique = true; + } + }); + } + }); - function after(err, client) { - if (err) { - console.error("Error creating/opening SQLite3 database: " + err); + schema.columns.push(column); + })); - // Close the db instance on error - if (client) client.close(); + var normalizedSchema = utils.normalizeSchema(schema); + // Set internal schema mapping + datastore.manager.schema[tableName] = normalizedSchema; - return cb(err); + return Promise.resolve(normalizedSchema); + } catch (err) { + return Promise.reject(err); } + }) + .then(schema => cb(undefined, schema)) + .catch(err => cb(err)); + }, + + /** + * ╔╦╗╔═╗╔═╗╦╔╗╔╔═╗ + * ║║║╣ ╠╣ ║║║║║╣ + * ═╩╝╚═╝╚ ╩╝╚╝╚═╝ + * Build a new physical model (e.g. table/etc) to use for storing records in the database. + * + * (This is used for schema migrations.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table to define. + * @param {String} tableName The name of the table to define. + * @param {Dictionary} definition The physical model definition (not a normal Sails/Waterline model-- log this for details.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + define: async function (datastoreName, tableName, definition, done) { + + // Look up the datastore entry (manager/driver/config). + var datastore = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(datastore)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } - // Run the logic - logic(client, function(err, result) { + let tableQuery; + let outerSchema + try { + const client = datastore.manager.writeClient; + const escapedTable = utils.escapeTable(tableName); - // Close db instance after it's done running - client.close(); - return cb(err, result); - }); + // Iterate through each attribute, building a query string + const _schema = utils.buildSchema(definition, datastore.manager.foreignKeys[tableName]); + outerSchema = _schema.schema; + + // Check for any index attributes + const indices = utils.buildIndexes(definition); + + // Build query + // const query = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; + tableQuery = 'CREATE TABLE ' + escapedTable + ' (' + _schema.declaration + ')'; + + // await wrapAsyncStatements(client.run.bind(client, query)); + await wrapAsyncStatements(client.run.bind(client, tableQuery)); + + await Promise.all(indices.map(async index => { + // Build a query to create a namespaced index tableName_key + const indexQuery = 'CREATE INDEX ' + tableName + '_' + index + ' on ' + + tableName + ' (' + index + ');'; + + await wrapAsyncStatements(client.run.bind(client, indexQuery)); + })); + + // Replacing if it already existed + datastore.manager.schema[tableName] = _schema.schema; + + done(); + } catch (err) { + done(err); + } + + }, + + + /** + * ╔╦╗╦═╗╔═╗╔═╗ + * ║║╠╦╝║ ║╠═╝ + * ═╩╝╩╚═╚═╝╩ + * Drop a physical model (table/etc.) from the database, including all of its records. + * + * (This is used for schema migrations.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table to drop. + * @param {String} tableName The name of the table to drop. + * @param {Ref} unused Currently unused (do not use this argument.) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done Callback + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + drop: async function (datastoreName, tableName, unused, done) { + + // Look up the datastore entry (manager/driver/config). + var dsEntry = registeredDatastores[datastoreName]; + + // Sanity check: + if (_.isUndefined(dsEntry)) { + return done(new Error('Consistency violation: Cannot do that with datastore (`' + datastoreName + '`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + } + + // Build query + const query = 'DROP TABLE IF EXISTS ' + utils.escapeTable(tableName); + + + try { + const client = dsEntry.manager.writeClient; + await wrapAsyncStatements(client.run.bind(client, query)); + + delete dsEntry.manager.schema[tableName]; + done(); + } catch (err) { + done(err); } - } - return adapter; -})(); + }, + + + /** + * ╔═╗╔═╗╔╦╗ ┌─┐┌─┐┌─┐ ┬ ┬┌─┐┌┐┌┌─┐┌─┐ + * ╚═╗║╣ ║ └─┐├┤ │─┼┐│ │├┤ ││││ ├┤ + * ╚═╝╚═╝ ╩ └─┘└─┘└─┘└└─┘└─┘┘└┘└─┘└─┘ + * Set a sequence in a physical model (specifically, the auto-incrementing + * counter for the primary key) to the specified value. + * + * (This is used for schema migrations.) + * + * > NOTE - removing method. SQLite can support setting a sequence on + * > primary key fields (or other autoincrement fields), however the + * > need is slim and I don't have time. + * > Leaving shell here for future developers if necessary + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {String} datastoreName The name of the datastore containing the table/etc. + * @param {String} sequenceName The name of the sequence to update. + * @param {Number} sequenceValue The new value for the sequence (e.g. 1) + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + * @param {Function} done + * @param {Error?} + * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + */ + // setSequence: function (datastoreName, sequenceName, sequenceValue, done) { + + // // Look up the datastore entry (manager/driver/config). + // var dsEntry = registeredDatastores[datastoreName]; + + // // Sanity check: + // if (_.isUndefined(dsEntry)) { + // return done(new Error('Consistency violation: Cannot do that with datastore (`'+datastoreName+'`) because no matching datastore entry is registered in this adapter! This is usually due to a race condition (e.g. a lifecycle callback still running after the ORM has been torn down), or it could be due to a bug in this adapter. (If you get stumped, reach out at https://sailsjs.com/support.)')); + // } + + // // Update the sequence. + // // + // // > TODO: Replace this setTimeout with real logic that calls + // // > `done()` when finished. (Or remove this method from the + // // > adapter altogether + // setTimeout(function(){ + // return done(new Error('Adapter method (`setSequence`) not implemented yet.')); + // }, 16); + + // }, +}; + +/** + * Spawns temporary connection and executes given logic. Returns promise for + * use with async/await + * @param {*} datastore + * @param {Function} logic Takes the client as its only argument. Can return a + * value or a Promise + * @param {*} cb + * @return Promise + */ +function spawnReadonlyConnection(datastore, logic) { + let client; + return new Promise((resolve, reject) => { + if (!datastore) reject(Errors.InvalidConnection); + + var datastoreConfig = datastore.config; + + // Check if we want to run in verbose mode + // Note that once you go verbose, you can't go back. + // See: https://github.com/mapbox/node-sqlite3/wiki/API + if (datastoreConfig.verbose) sqlite3.verbose(); + + // Make note whether the database already exists + exists = fs.existsSync(datastoreConfig.filename); + + // Create a new handle to our database + client = new sqlite3.Database( + datastoreConfig.filename, + sqlite3.OPEN_READONLY, + err => { + if (err) reject(err); + else resolve(client); + } + ); + }) + .then(logic) + .catch(err => { + return Promise.reject(err); //we want the user process to get this error as well + }) + .finally(() => { + if (client) client.close(); + }); +} + +/** + * Simple utility function that wraps an async function in a promise + * @param {Function} func Async function which takes 1 argument: a callback + * function that takes err, value as args (in that order) + * @return Promise + */ +function wrapAsyncStatements(func) { + return new Promise((resolve, reject) => { + func((err, value) => { + if (err) reject(err); + else resolve(value); + }); + }); +} + +/** + * Utility function that wraps an async function in a promise. In contrast + * to the above, this method specifically resolves with the `this` value + * passed to the callback function + * @param {Function} func Async function which takes 1 argument: a callback + * function that takes an err and invokes its callback with a `this` property + */ +function wrapAsyncForThis(func) { + return new Promise((resolve, reject) => { + func(function (err) { + if (err) reject(err); + else resolve(this); + }); + }) +} + +module.exports = adapter; \ No newline at end of file diff --git a/lib/query.js b/lib/query.js index 01c841e..7055bee 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3,8 +3,7 @@ */ var _ = require('@sailshq/lodash'), - utils = require('./utils'), - hop = utils.object.hasOwnProperty; + utils = require('./utils'); /** * Query Builder for creating parameterized queries for use @@ -429,26 +428,6 @@ Query.prototype.sort = function(options) { this._query += sortItems.join(', '); }; -/** - * Specify a `group by` condition - */ - -Query.prototype.group = function(options) { - var self = this; - - this._query += ' GROUP BY '; - - // Normalize to array - if(!Array.isArray(options)) options = [options]; - - options.forEach(function(key) { - self._query += key + ', '; - }); - - // Remove trailing comma - this._query = this._query.slice(0, -2); -}; - /** * Cast special values to proper types. * diff --git a/lib/utils.js b/lib/utils.js index 31e5806..f3741c0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,4 @@ -const _ = require("underscore"); +const _ = require('@sailshq/lodash'); const utils = module.exports = {}; @@ -98,23 +98,6 @@ utils.buildForeignKeyMap = physicalModelsReport => { return foreignKeyMap; } -/** -* Safe hasOwnProperty -*/ -utils.object = {}; -/** -* Safer helper for hasOwnProperty checks -* -* @param {Object} obj -* @param {String} prop -* @return {Boolean} -* @api public -*/ -var hop = Object.prototype.hasOwnProperty; - utils.object.hasOwnProperty = function(obj, prop) { - return hop.call(obj, prop); -}; - /** * Escape Name * @@ -138,90 +121,6 @@ utils.buildIndexes = function(obj) { return indexes; }; - -/** - * Group Results into an Array - * - * Groups values returned from an association query into a single result. - * For each collection association the object returned should have an array - * under the user defined key with the joined results. - * - * @param {Array} results returned from a query - * @return {Object} a single values object - */ -utils.group = function(values) { - - var self = this; - var joinKeys = []; - var _value; - - if (!Array.isArray(values)) return values; - - // Grab all the keys needed to be grouped - var associationKeys = []; - - values.forEach(function(value) { - Object.keys(value).forEach(function(key) { - key = key.split('__'); - if (key.length === 2) associationKeys.push(key[0].toLowerCase()); - }); - }); - - associationKeys = _.unique(associationKeys); - - // Store the values to be grouped by id - var groupings = {}; - - values.forEach(function(value) { - - // Add to groupings - if (!groupings[value.id]) groupings[value.id] = {}; - - associationKeys.forEach(function(key) { - if(!Array.isArray(groupings[value.id][key])) - groupings[value.id][key] = []; - var props = {}; - - Object.keys(value).forEach(function(prop) { - var attr = prop.split('__'); - if (attr.length === 2 && attr[0] === key) { - props[attr[1]] = value[prop]; - delete value[prop]; - } - }); - - // Don't add empty records that come from a left join - var empty = true; - - Object.keys(props).forEach(function(prop) { - if (props[prop] !== null) empty = false; - }); - - if (!empty) groupings[value.id][key].push(props); - }); - }); - - var _values = []; - - values.forEach(function(value) { - var unique = true; - - _values.forEach(function(_value) { - if (_value.id === value.id) unique = false; - }); - - if (!unique) return; - - Object.keys(groupings[value.id]).forEach(function(key) { - value[key] = _.uniq(groupings[value.id][key], 'id'); - }); - - _values.push(value); - }); - - return _values; -}; - /** * Map data from a stage-3 query to its representation * in Sqlite. Collects a list of columns and processes @@ -394,13 +293,3 @@ utils.sqlTypeCast = function(type, columnName) { return 'TEXT'; } }; - -/** - * JS Date to UTC Timestamp - * - * Dates should be stored in Postgres with UTC timestamps - * and then converted to local time on the client. - */ -utils.toSqlDate = function(date) { - return date.toUTCString(); -}; diff --git a/package-lock.json b/package-lock.json index 4f6ed65..84d3231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.0.1", + "version": "0.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -934,60 +934,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "should": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", - "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", - "dev": true, - "requires": { - "should-equal": "^2.0.0", - "should-format": "^3.0.3", - "should-type": "^1.4.0", - "should-type-adaptors": "^1.0.1", - "should-util": "^1.0.0" - } - }, - "should-equal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", - "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", - "dev": true, - "requires": { - "should-type": "^1.4.0" - } - }, - "should-format": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", - "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-type-adaptors": "^1.0.1" - } - }, - "should-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", - "dev": true - }, - "should-type-adaptors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", - "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", - "dev": true, - "requires": { - "should-type": "^1.3.0", - "should-util": "^1.0.0" - } - }, - "should-util": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", - "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", - "dev": true - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -1102,11 +1048,6 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, - "underscore": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz", - "integrity": "sha1-EzXF5PXm0zu7SwBrqMhqAPVW3gg=" - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", diff --git a/package.json b/package.json index 64b432b..2622e8c 100755 --- a/package.json +++ b/package.json @@ -1,38 +1,43 @@ { "name": "sails-sqlite3", - "version": "0.0.1", - "description": "Boilerplate adapter for Sails.js", - "main": "index.js", - "scripts": { - "test": "make test" - }, + "version": "0.1.0", + "description": "Waterline Adapter for SQLite in Sails.js", + "main": "lib/adapter.js", "repository": { "type": "git", - "url": "https://github.com/balderdashy/sails-adapter-boilerplate.git" + "url": "https://github.com/kcappieg/sails-sqlite3.git" }, "keywords": [ "orm", "waterline", "sails", "sailsjs", - "sails.js" + "sails.js", + "sqlite" + ], + "contributors": [ + { + "name": "Andrew Jo", + "email": "andrewjo@gmail.com>" + }, + { + "name": "Kevin C. Gall", + "email": "kcg245@gmail.com" + } ], - "author": "Andrew Jo ", "license": "MIT", "readmeFilename": "README.md", "dependencies": { "@sailshq/lodash": "^3.10.3", "sqlite3": "~4.0.x", - "underscore": "1.5.2", "waterline-errors": "^0.10.1" }, "devDependencies": { "mocha": "^5.2.0", - "should": "*", "waterline-adapter-tests": "^1.0.1" }, "scripts": { - "test": "node test/integration/runner.js" + "test": "node test/runner.js" }, "sails": { "adapter": { diff --git a/test/register.js b/test/register.js deleted file mode 100644 index b4fcbb7..0000000 --- a/test/register.js +++ /dev/null @@ -1,14 +0,0 @@ -describe('registerCollection', function () { - - it('should not hang or encounter any errors', function (cb) { - var adapter = require('../index.js'); - adapter.registerCollection({ - identity: 'foo' - }, cb); - }); - - // e.g. - // it('should create a mysql connection pool', function () {}) - // it('should create an HTTP connection pool', function () {}) - // ... and so on. -}); \ No newline at end of file diff --git a/test/integration/runner.js b/test/runner.js similarity index 91% rename from test/integration/runner.js rename to test/runner.js index b9cdf2d..a22f854 100644 --- a/test/integration/runner.js +++ b/test/runner.js @@ -1,6 +1,6 @@ var tests = require('waterline-adapter-tests'), sqlite3 = require('sqlite3'), - adapter = require('../../index'), + adapter = require('../lib/adapter'), mocha = require('mocha'); /** diff --git a/test/unit/adapter.avg.js b/test/unit/adapter.avg.js deleted file mode 100644 index e3fcff0..0000000 --- a/test/unit/adapter.avg.js +++ /dev/null @@ -1,47 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * AVG - * - * Adds a AVG select parameter to a sql statement - */ - - describe('.avg()', function() { - - describe('with array', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - average: ['age'] - }; - - it('should use the AVG aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, CAST(AVG(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - describe('with string', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - average: 'age' - }; - - it('should use the AVG aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, CAST(AVG(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.create.js b/test/unit/adapter.create.js deleted file mode 100644 index f4239aa..0000000 --- a/test/unit/adapter.create.js +++ /dev/null @@ -1,80 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_create', done); - }); - - after(function(done) { - support.Teardown('test_create', done); - }); - - // Attributes for the test table - var attributes = { - field_1: 'foo', - field_2: 'bar' - }; - - /** - * CREATE - * - * Insert a row into a table - */ - - describe('.create()', function() { - - // Insert a record - it('should insert a single record', function(done) { - adapter.create('test_create', attributes, function(err, result) { - - // Check record was actually inserted - support.Client(function(err, client, close) { - client.all('SELECT * FROM "test_create"', function(err, rows) { - - // Test 1 row is returned - rows.length.should.eql(1); - - // close client - client.close(); - - done(); - }); - }); - }); - }); - - // Create Auto-Incremented ID - it('should create an auto-incremented ID field', function(done) { - adapter.create('test_create', attributes, function(err, result) { - - // Should have an ID of 2 - result.id.should.eql(2); - - done(); - }); - }); - - it('should keep case', function(done) { - var attributes = { - field_1: 'Foo', - field_2: 'bAr' - }; - - adapter.create('test_create', attributes, function(err, result) { - - result.field_1.should.eql('Foo'); - result.field_2.should.eql('bAr'); - - done(); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.define.js b/test/unit/adapter.define.js deleted file mode 100644 index 92678f9..0000000 --- a/test/unit/adapter.define.js +++ /dev/null @@ -1,106 +0,0 @@ -var adapter = require('../../lib/adapter'), - _ = require('underscore'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - after(function(done) { - support.Teardown('test_define', done); - }); - - // Attributes for the test table - var definition = { - id : { - type: 'serial', - autoIncrement: true - }, - name : 'string', - email : 'string', - title : 'string', - phone : 'string', - type : 'string', - favoriteFruit : { - defaultsTo: 'blueberry', - type: 'string' - }, - age : 'integer' - }; - - /** - * DEFINE - * - * Create a new table with a defined set of attributes - */ - - describe('.define()', function() { - - describe('basic usage', function() { - - // Register the collection - before(function(done) { - var collection = _.extend({ config: support.Config }, { - identity: 'test_define' - }); - - adapter.registerCollection(collection, done); - }); - - // Build Table from attributes - it('should build the table', function(done) { - - adapter.define('test_define', definition, function(err) { - adapter.describe('test_define', function(err, result) { - Object.keys(result).length.should.eql(8); - done(); - }); - }); - - }); - - }); - - describe('reserved words', function() { - - // Register the collection - before(function(done) { - var collection = _.extend({ config: support.Config }, { - identity: 'user' - }); - - adapter.registerCollection(collection, done); - }); - - after(function(done) { - support.Client(function(err, client) { - var query = 'DROP TABLE "user";'; - client.run(query, function(err) { - - // close client - client.close(); - - done(); - }); - }); - }); - - // Build Table from attributes - it('should escape reserved words', function(done) { - - adapter.define('user', definition, function(err) { - adapter.describe('user', function(err, result) { - Object.keys(result).length.should.eql(8); - done(); - }); - }); - - }); - - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.describe.js b/test/unit/adapter.describe.js deleted file mode 100644 index f603674..0000000 --- a/test/unit/adapter.describe.js +++ /dev/null @@ -1,42 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_describe', done); - }); - - after(function(done) { - support.Teardown('test_describe', done); - }); - - /** - * DESCRIBE - * - * Similar to MySQL's Describe method this should list the - * properties of a table. - */ - - describe('.describe()', function() { - - // Output Column Names - it('should output the column names', function(done) { - adapter.describe('test_describe', function(err, results) { - Object.keys(results).length.should.eql(3); - - should.exist(results.id); - should.exist(results.field_1); - should.exist(results.field_2); - - done(); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.destroy.js b/test/unit/adapter.destroy.js deleted file mode 100644 index 0b47d8c..0000000 --- a/test/unit/adapter.destroy.js +++ /dev/null @@ -1,55 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_destroy', done); - }); - - after(function(done) { - support.Teardown('test_destroy', done); - }); - - /** - * DESTROY - * - * Remove a row from a table - */ - - describe('.destroy()', function() { - - describe('with options', function() { - - before(function(done) { - support.Seed('test_destroy', done); - }); - - it('should destroy the record', function(done) { - adapter.destroy('test_destroy', { where: { id: 1 }}, function(err, result) { - - // Check record was actually removed - support.Client(function(err, client, close) { - client.all('SELECT * FROM "test_destroy"', function(err, rows) { - - // Test no rows are returned - rows.length.should.eql(0); - - // close client - client.close(); - - done(); - }); - }); - - }); - }); - - }); - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.drop.js b/test/unit/adapter.drop.js deleted file mode 100644 index 46ecbe5..0000000 --- a/test/unit/adapter.drop.js +++ /dev/null @@ -1,35 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_drop', done); - }); - - /** - * DROP - * - * Drop a table and all it's records. - */ - - describe('.drop()', function() { - - // Drop the Test table - it('should drop the table', function(done) { - - adapter.drop('test_drop', function(err, result) { - adapter.describe('test_drop', function(err, result) { - should.not.exist(result); - done(); - }); - }); - - }); - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.find.js b/test/unit/adapter.find.js deleted file mode 100644 index 4d91ee1..0000000 --- a/test/unit/adapter.find.js +++ /dev/null @@ -1,87 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_find', done); - }); - - after(function(done) { - support.Teardown('test_find', done); - }); - - /** - * FIND - * - * Returns an array of records from a SELECT query - */ - - describe('.find()', function() { - - describe('WHERE clause', function() { - - before(function(done) { - support.Seed('test_find', done); - }); - - describe('key/value attributes', function() { - - it('should return the record set', function(done) { - adapter.find('test_find', { where: { field_1: 'foo' } }, function(err, results) { - results.length.should.eql(1); - results[0].id.should.eql(1); - done(); - }); - }); - - }); - - describe('comparators', function() { - - // Insert a unique record to test with - before(function(done) { - var query = [ - 'INSERT INTO "test_find" (field_1, field_2)', - "values ('foobar', 'AR)H$daxx');" - ].join(''); - - support.Client(function(err, client, close) { - client.run(query, function(err) { - - // close client - client.close(); - - done(); - }); - }); - }); - - it('should support endsWith', function(done) { - - var criteria = { - where: { - field_2: { - endsWith: 'AR)H$daxx' - } - } - }; - - adapter.find('test_find', criteria, function(err, results) { - results.length.should.eql(1); - results[0].field_2.should.eql('AR)H$daxx'); - done(); - }); - }); - - }); - - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.groupBy.js b/test/unit/adapter.groupBy.js deleted file mode 100644 index 91b906f..0000000 --- a/test/unit/adapter.groupBy.js +++ /dev/null @@ -1,47 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * groupBy - * - * Adds a Group By statement to a sql statement - */ - - describe('.groupBy()', function() { - - describe('with array', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - groupBy: ['name'] - }; - - it('should append a Group By clause to the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, name FROM test WHERE LOWER("name") = $1 GROUP BY name'); - }); - }); - - describe('with string', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - groupBy: 'name' - }; - - it('should use the MAX aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, name FROM test WHERE LOWER("name") = $1 GROUP BY name'); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.index.js b/test/unit/adapter.index.js deleted file mode 100644 index 0d92f73..0000000 --- a/test/unit/adapter.index.js +++ /dev/null @@ -1,60 +0,0 @@ -var adapter = require('../../lib/adapter'), - _ = require('underscore'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Teardown - */ - - after(function(done) { - support.Teardown('test_index', done); - }); - - // Attributes for the test table - var definition = { - id: { - type: 'serial', - autoIncrement: true - }, - name: { - type: 'string', - index: true - } - }; - - /** - * Indexes - * - * Ensure Indexes get created correctly - */ - - describe('Index Attributes', function() { - - before(function(done) { - var collection = _.extend({ config: support.Config }, { - identity: 'test_index' - }); - - adapter.registerCollection(collection, function(err) { - if(err) return cb(err); - adapter.define('test_index', definition, done); - }); - }); - - // Build Indicies from definition - it('should add indicies', function(done) { - - adapter.define('test_index', definition, function(err) { - adapter.describe('test_index', function(err, result) { - result.name.indexed.should.eql(true); - done(); - }); - }); - - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.max.js b/test/unit/adapter.max.js deleted file mode 100644 index d8eb5a4..0000000 --- a/test/unit/adapter.max.js +++ /dev/null @@ -1,47 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * MAX - * - * Adds a MAX select parameter to a sql statement - */ - - describe('.max()', function() { - - describe('with array', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - max: ['age'] - }; - - it('should use the max aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, MAX(age) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - describe('with string', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - max: 'age' - }; - - it('should use the MAX aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, MAX(age) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.min.js b/test/unit/adapter.min.js deleted file mode 100644 index 503c623..0000000 --- a/test/unit/adapter.min.js +++ /dev/null @@ -1,47 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * MIN - * - * Adds a MIN select parameter to a sql statement - */ - - describe('.min()', function() { - - describe('with array', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - min: ['age'] - }; - - it('should use the min aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, MIN(age) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - describe('with string', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - min: 'age' - }; - - it('should use the MIN aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, MIN(age) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.sum.js b/test/unit/adapter.sum.js deleted file mode 100644 index b6076a7..0000000 --- a/test/unit/adapter.sum.js +++ /dev/null @@ -1,47 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * SUM - * - * Adds a SUM select parameter to a sql statement - */ - - describe('.sum()', function() { - - describe('with array', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - sum: ['age'] - }; - - it('should use the SUM aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, CAST(SUM(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - describe('with string', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - sum: 'age' - }; - - it('should use the SUM aggregate option in the select statement', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, CAST(SUM(age) AS float) AS age FROM test WHERE LOWER("name") = $1'); - }); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/adapter.update.js b/test/unit/adapter.update.js deleted file mode 100644 index 9809b12..0000000 --- a/test/unit/adapter.update.js +++ /dev/null @@ -1,53 +0,0 @@ -var adapter = require('../../lib/adapter'), - should = require('should'), - support = require('./support/bootstrap'); - -describe('adapter', function() { - - /** - * Setup and Teardown - */ - - before(function(done) { - support.Setup('test_update', done); - }); - - after(function(done) { - support.Teardown('test_update', done); - }); - - /** - * UPDATE - * - * Update a row in a table - */ - - describe('.update()', function() { - - describe('with options', function() { - - before(function(done) { - support.Seed('test_update', done); - }); - - it('should update the record', function(done) { - - adapter.update('test_update', { where: { id: 1 }}, { field_1: 'foobar' }, function(err, result) { - result[0].field_1.should.eql('foobar'); - done(); - }); - - }); - - it('should keep case', function(done) { - - adapter.update('test_update', { where: { id: 1 }}, { field_1: 'FooBar' }, function(err, result) { - result[0].field_1.should.eql('FooBar'); - done(); - }); - - }); - - }); - }); -}); \ No newline at end of file diff --git a/test/unit/query.cast.js b/test/unit/query.cast.js deleted file mode 100644 index 3ce827d..0000000 --- a/test/unit/query.cast.js +++ /dev/null @@ -1,25 +0,0 @@ -var Query = require('../../lib/query'), - assert = require('assert'); - -describe('query', function() { - - /** - * CAST - * - * Cast values to proper types - */ - - describe('.cast()', function() { - - describe('Array', function() { - - it('should cast to values to array', function() { - var values = new Query({ list: { type: 'array' }}).cast({ list: "[0,1,2,3]" }); - assert(Array.isArray(values.list)); - assert(values.list.length === 4); - }); - - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/query.limit.js b/test/unit/query.limit.js deleted file mode 100644 index 3b0d56f..0000000 --- a/test/unit/query.limit.js +++ /dev/null @@ -1,29 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * LIMIT - * - * Adds a LIMIT parameter to a sql statement - */ - - describe('.limit()', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - limit: 1 - }; - - it('should append the LIMIT clause to the query', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 LIMIT 1'); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/query.skip.js b/test/unit/query.skip.js deleted file mode 100644 index ae44b47..0000000 --- a/test/unit/query.skip.js +++ /dev/null @@ -1,28 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * SKIP - * - * Adds an OFFSET parameter to a sql statement - */ - - describe('.skip()', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - skip: 1 - }; - - it('should append the SKIP clause to the query', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 OFFSET 1'); - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/query.sort.js b/test/unit/query.sort.js deleted file mode 100644 index 59dc63a..0000000 --- a/test/unit/query.sort.js +++ /dev/null @@ -1,69 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * SORT - * - * Adds an ORDER BY parameter to a sql statement - */ - - describe('.sort()', function() { - - it('should append the ORDER BY clause to the query', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - sort: { - name: 1 - } - }; - - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC'); - - }); - - it('should sort by multiple columns', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - sort: { - name: 1, - age: 1 - } - }; - - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC, "age" ASC'); - - }); - - it('should allow desc and asc ordering', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo' - }, - sort: { - name: 1, - age: -1 - } - }; - - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 ORDER BY "name" ASC, "age" DESC'); - - }); - - }); - -}); \ No newline at end of file diff --git a/test/unit/query.where.js b/test/unit/query.where.js deleted file mode 100644 index b67f66c..0000000 --- a/test/unit/query.where.js +++ /dev/null @@ -1,179 +0,0 @@ -var Query = require('../../lib/query'), - should = require('should'); - -describe('query', function() { - - /** - * WHERE - * - * Build the WHERE part of an sql statement from a js object - */ - - describe('.where()', function() { - - describe('`AND` criteria', function() { - - describe('case insensitivity', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'Foo', - age: 1 - } - }; - - it('should build a SELECT statement using LOWER() on strings', function() { - var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" = $2'); - query.values[0].should.eql('foo'); - }); - }); - - describe('criteria is simple key value lookups', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo', - age: 27 - } - }; - - it('should build a simple SELECT statement', function() { - var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" = $2'); - query.values.length.should.eql(2); - }); - - }); - - describe('has multiple comparators', function() { - - // Lookup criteria - var criteria = { - where: { - name: 'foo', - age: { - '>' : 27, - '<' : 30 - } - } - }; - - it('should build a SELECT statement with comparators', function() { - var query = new Query({ name: { type: 'text' }, age: { type: 'integer'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") = $1 AND "age" > $2 AND "age" < $3'); - query.values.length.should.eql(3); - }); - - }); - - }); - - describe('`LIKE` criteria', function() { - - // Lookup criteria - var criteria = { - where: { - like: { - type: '%foo%', - name: 'bar%' - } - } - }; - - it('should build a SELECT statement with ILIKE', function() { - var query = new Query({ type: { type: 'text' }, name: { type: 'text'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("type") ILIKE $1 AND LOWER("name") ILIKE $2'); - query.values.length.should.eql(2); - }); - - }); - - describe('`OR` criteria', function() { - - // Lookup criteria - var criteria = { - where: { - or: [ - { like: { foo: '%foo%' } }, - { like: { bar: '%bar%' } } - ] - } - }; - - it('should build a SELECT statement with multiple like statements', function() { - var query = new Query({ foo: { type: 'text' }, bar: { type: 'text'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("foo") ILIKE $1 OR LOWER("bar") ILIKE $2'); - query.values.length.should.eql(2); - }); - - }); - - describe('`IN` criteria', function() { - - // Lookup criteria - var criteria = { - where: { - name: [ - 'foo', - 'bar', - 'baz' - ] - } - }; - - var camelCaseCriteria = { - where: { - myId: [ - 1, - 2, - 3 - ] - } - }; - - it('should build a SELECT statement with an IN array', function() { - var query = new Query({ name: { type: 'text' }}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE LOWER("name") IN ($1, $2, $3)'); - query.values.length.should.eql(3); - }); - - it('should build a SELECT statememnt with an IN array and camel case column', function() { - var query = new Query({ myId: { type: 'integer' }}).find('test', camelCaseCriteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE "myId" IN ($1, $2, $3)'); - query.values.length.should.eql(3); - }); - - }); - - describe('`NOT` criteria', function() { - - // Lookup criteria - var criteria = { - where: { - age: { - not: 40 - } - } - }; - - it('should build a SELECT statement with an NOT clause', function() { - var query = new Query({age: { type: 'integer'}}).find('test', criteria); - - query.query.should.eql('SELECT rowid, * FROM test WHERE "age" <> $1'); - query.values.length.should.eql(1); - }); - - }); - - }); -}); \ No newline at end of file diff --git a/test/unit/support/bootstrap.js b/test/unit/support/bootstrap.js deleted file mode 100644 index 63fb7c8..0000000 --- a/test/unit/support/bootstrap.js +++ /dev/null @@ -1,91 +0,0 @@ -var sqlite3 = require('sqlite3').verbose(), - adapter = require('../../../lib/adapter'); - -var Support = module.exports = {}; - -Support.Config = { - filename: 'sailssqlite.db', - mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, - verbose: true -}; - -Support.Definition = { - field_1: { type: 'string' }, - field_2: { type: 'string' }, - id: { - type: 'integer', - autoIncrement: true, - defaultsTo: 'AUTO_INCREMENT', - primaryKey: true - } -}; - -Support.Client = function(cb) { - var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { - cb(err, client); - }); -}; - -Support.Collection = function(name) { - return { - identity: name, - config: Support.Config, - definition: Support.Definition - }; -}; - -// Seed a record to use for testing -Support.Seed = function(tableName, cb) { - var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { - createRecord(tableName, client, function(err) { - if(err) { - client.close(); - return cb(err); - } - - client.close(); - cb(); - }); - }); -}; - -// Register and define a collection -Support.Setup = function(tableName, cb) { - adapter.registerCollection(Support.Collection(tableName), function(err) { - if (err) return cb(err); - adapter.define(tableName, Support.Definition, cb); - }); -}; - -// Remove a table -Support.Teardown = function(tableName, cb) { - var client = new sqlite3.Database(Support.Config.filename, Support.Config.mode, function(err) { - dropTable(tableName, client, function(err) { - if (err) { - client.close(); - return cb(err); - } - - client.close(); - return cb(); - }); - }); -}; - -function dropTable(table, client, cb) { - table = '"' + table + '"'; - - var query = "DROP TABLE " + table; - client.run(query, cb); -} - -function createRecord(table, client, cb) { - table = '"' + table + '"'; - - var query = [ - "INSERT INTO " + table + ' (field_1, field_2)', - " values ('foo', 'bar');" - ].join(''); - - client.run(query, cb); -} \ No newline at end of file From ffb22bfdf878dae3a20d16be80ee5948da7f1982 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 5 Jan 2019 15:39:18 -0500 Subject: [PATCH 75/81] readme updates --- README.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 87f61ea..f24d22d 100755 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ A [Waterline](https://github.com/balderdashy/waterline) adapter for SQLite3. May ## Disclaimers - SQLite3 adapter is not optimized for performance. Native joins are not implemented (among other issues). -- This codebase contains no unit tests, though all integration tests with the waterline api (v1) pass. +- This codebase contains no unit tests, though all integration tests with the waterline api (v1) pass. (See below for supported interfaces) -#### People who should use this package right now: -Those prototyping apps with sailsjs and looking to use sqlite for a test database. +#### People who can use this package as is: +Those prototyping apps with sailsjs 1.x and looking to use sqlite for a test database. For anyone looking to use this adapter in production, contributions welcome! @@ -25,12 +25,25 @@ In your `config\datastores.js` file, add a property with your datastore name. Su default: { adapter: 'sails-sqlite3', filename: '[YOUR DATABASE].db', - mode: AS PER sqlite3 MODE OPTIONS, + mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, verbose: false } ``` -For more information on the `mode` configuration property, see the [driver documentation](https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback) +For more information on the `mode` configuration property, see the [driver documentation](https://github.com/mapbox/node-sqlite3/wiki/API#new-sqlite3databasefilename-mode-callback). + +#### Example with Modes +To use different database modes, import the `sqlite3` module, which is a dependency of this pacakge: + +```js +const sqlite3 = require('sqlite3'); + +const config = { + filename: 'testdb.db', + mode: sqlite3.OPEN_READONLY, + verbose: true +}; +``` ## Testing From 737b278661e7ea4910212c646e8ff6b8ac0c4d6a Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 12 Jan 2019 14:34:21 -0500 Subject: [PATCH 76/81] update repo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2622e8c..5fd12af 100755 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "lib/adapter.js", "repository": { "type": "git", - "url": "https://github.com/kcappieg/sails-sqlite3.git" + "url": "https://github.com/AndrewJo/sails-sqlite3.git" }, "keywords": [ "orm", From 2716f2a06412694eaec7a801a39d04ec02bba340 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 12 Jan 2019 14:34:27 -0500 Subject: [PATCH 77/81] 0.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5fd12af..05071c3 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.1.0", + "version": "0.1.1", "description": "Waterline Adapter for SQLite in Sails.js", "main": "lib/adapter.js", "repository": { From 9965eb1ca5a398e81dd55f58052a1dbc9c9c4943 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 21 Jan 2019 14:57:18 -0500 Subject: [PATCH 78/81] bug fix create method --- lib/adapter.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/adapter.js b/lib/adapter.js index a585fb7..b2e51e6 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -280,12 +280,14 @@ const adapter = { delete query.newRecord; try { - const record = await wrapAsyncStatements( + const recordList = await wrapAsyncStatements( adapter.createEach.bind(adapter, datastoreName, query)); - if (record && record.length >>> 0 > 0) { - done(undefined, record[0]); + let record; + if (recordList && recordList.length >>> 0 > 0) { + record = recordList[0]; } + done(undefined, record); } catch (err) { done(err); } From ca04debe21ca951108a41ff28a7e2b09e333ee6b Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Mon, 21 Jan 2019 14:59:56 -0500 Subject: [PATCH 79/81] 0.1.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84d3231..1a9849b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 05071c3..d024ee9 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.1.1", + "version": "0.1.2", "description": "Waterline Adapter for SQLite in Sails.js", "main": "lib/adapter.js", "repository": { From 13e27cd5687a0f5f836de903312b2b98cb76401f Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 17 Aug 2019 16:50:47 -0400 Subject: [PATCH 80/81] update package versions --- package-lock.json | 942 ++++++++++++++++++++++++++++++++++++++++------ package.json | 4 +- 2 files changed, 827 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a9849b..3f8f9e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -35,11 +35,26 @@ "validator": "5.7.0" } }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -54,6 +69,15 @@ "readable-stream": "^2.0.6" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -116,35 +140,116 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { - "delayed-stream": "~1.0.0" + "color-name": "1.1.3" } }, - "commander": { - "version": "2.15.1", - "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -160,6 +265,19 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -169,18 +287,40 @@ } }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -211,6 +351,12 @@ "safer-buffer": "^2.1.0" } }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, "encrypted-attr": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/encrypted-attr/-/encrypted-attr-1.0.6.tgz", @@ -220,12 +366,67 @@ "lodash": "^4.17.4" } }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -246,6 +447,24 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, "flaverr": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/flaverr/-/flaverr-1.9.2.tgz", @@ -284,9 +503,9 @@ } }, "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", + "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", "requires": { "minipass": "^2.2.1" } @@ -296,6 +515,12 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -311,6 +536,21 @@ "wide-align": "^1.1.0" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -364,21 +604,36 @@ "har-schema": "^2.0.0" } }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "http-signature": { @@ -426,6 +681,30 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "dev": true + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -434,6 +713,30 @@ "number-is-nan": "^1.0.0" } }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -444,11 +747,27 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -504,10 +823,29 @@ "graceful-fs": "^4.1.9" } }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "dev": true, + "requires": { + "invert-kv": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "lodash._baseassign": { @@ -584,19 +922,54 @@ "lodash.isarray": "^3.0.0" } }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "requires": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + } + }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.37.0" + "mime-db": "1.40.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -620,9 +993,9 @@ } }, "minizlib": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.1.tgz", - "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", "requires": { "minipass": "^2.2.1" } @@ -636,73 +1009,77 @@ } }, "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz", + "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==", "dev": true, "requires": { + "ansi-colors": "3.2.3", "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", + "debug": "3.2.6", "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.2", + "find-up": "3.0.0", + "glob": "7.1.3", "growl": "1.10.5", - "he": "1.1.1", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "2.2.0", "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "ms": "2.1.1", + "node-environment-flags": "1.0.5", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.2.2", + "yargs-parser": "13.0.0", + "yargs-unparser": "1.5.0" } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true }, "nan": { - "version": "2.10.0", - "resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, "needle": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.4.tgz", - "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", "requires": { - "debug": "^2.1.2", + "debug": "^3.2.6", "iconv-lite": "^0.4.4", "sax": "^1.2.4" } }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-environment-flags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", + "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + } + }, "node-pre-gyp": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -726,19 +1103,28 @@ } }, "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==" }, "npm-packlist": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz", - "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz", + "integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==", "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -765,6 +1151,34 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", + "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -778,6 +1192,17 @@ "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "dev": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -792,6 +1217,48 @@ "os-tmpdir": "^1.0.0" } }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "parley": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/parley/-/parley-3.8.0.tgz", @@ -803,25 +1270,47 @@ "flaverr": "^1.5.1" } }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", + "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "punycode": { "version": "2.1.1", @@ -892,6 +1381,18 @@ "uuid": "^3.3.2" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -925,34 +1426,55 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sqlite3": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.4.tgz", - "integrity": "sha512-CO8vZMyUXBPC+E3iXOCc7Tz2pAdq5BWfLcQmOokCOZW5S5sZ/paijiPOCdvzpdP83RroWHYa5xYlVqCxSqpnQg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz", + "integrity": "sha512-RvqoKxq+8pDHsJo7aXxsFR18i+dU2Wp5o12qAJOV5LNcDt+fgJsc2QKKg3sIRfXrN9ZjzY1T7SNe/DFVqAXjaw==", "requires": { - "nan": "~2.10.0", - "node-pre-gyp": "^0.10.3", + "nan": "^2.12.1", + "node-pre-gyp": "^0.11.0", "request": "^2.87.0" } }, "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -991,32 +1513,38 @@ "ansi-regex": "^2.0.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { "has-flag": "^3.0.0" } }, "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" + "yallist": "^3.0.3" } }, "tough-cookie": { @@ -1120,7 +1648,7 @@ "async": "2.0.1", "bluebird": "3.2.1", "mocha": "3.0.2", - "waterline": "github:balderdashy/waterline#90b8a0a9132862faaa3b6851a8c4db1f8c41b23c", + "waterline": "github:balderdashy/waterline", "waterline-utils": "^1.3.2" }, "dependencies": { @@ -1277,6 +1805,21 @@ } } }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -1285,15 +1828,180 @@ "string-width": "^1.0.2 || 2" } }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + }, + "yargs": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", + "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", + "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.11", + "yargs": "^12.0.5" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs": { + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "dev": true, + "requires": { + "cliui": "^4.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" + } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } } } } diff --git a/package.json b/package.json index d024ee9..85653d2 100755 --- a/package.json +++ b/package.json @@ -29,11 +29,11 @@ "readmeFilename": "README.md", "dependencies": { "@sailshq/lodash": "^3.10.3", - "sqlite3": "~4.0.x", + "sqlite3": "^4.1.0", "waterline-errors": "^0.10.1" }, "devDependencies": { - "mocha": "^5.2.0", + "mocha": "^6.2.0", "waterline-adapter-tests": "^1.0.1" }, "scripts": { From f7c196817cf297161141ecedd0e13987fd94c798 Mon Sep 17 00:00:00 2001 From: "Kevin C. Gall" Date: Sat, 17 Aug 2019 16:50:51 -0400 Subject: [PATCH 81/81] 0.1.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3f8f9e3..a4ae65c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 85653d2..d68816a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sails-sqlite3", - "version": "0.1.2", + "version": "0.1.3", "description": "Waterline Adapter for SQLite in Sails.js", "main": "lib/adapter.js", "repository": {