├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── adapter.js ├── error.js └── util.js ├── package.json └── test └── integration └── runner.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | *.sqlite 3 | *.db 4 | *.sw* 5 | .tmp 6 | dist 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 33 | node_modules 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "indent": 2, 4 | "maxdepth": 6, 5 | "maxlen": 120, 6 | "expr": true, 7 | "trailing": true, 8 | "node": true, 9 | "esnext": true 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waterlinejs/sqlite3-adapter/dd8269cc105d9498b873a722a6eab615e50e7403/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 5 5 | - 6 6 | 7 | # install gcc 4.8 on linux using apt 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - gcc-4.8 14 | - g++-4.8 15 | 16 | env: 17 | - CXX=g++-4.8 18 | 19 | compiler: 20 | - gcc 21 | 22 | email: 23 | notifications: false 24 | 25 | deploy: 26 | provider: npm 27 | email: waterlinejs@balderdash.io 28 | api_key: 29 | secure: FzB9VNOz5PshMdU+Yk2w5j79pOcgTdlWVVOntYIMGYOeyPSDVeywu6adT3CWnHYiYnNkqD5DOlxKwTUY1mxx0MqDfdwSnuVtRRnG/q6CdxuYh8NUsybCi/erc3p0N373Y9FxMHLMqmVtbRucHdiWhhPM54CdSMZTu+AOBarDn9/ioPIR0WxdSFtXiywteTTRgc2JKOUAfLrQql48pVaryquidOb0wYvjvV17/I/bV2rPSPTnHde/l+JbmDpxiej15a5du5cpxcI6eljr9XkytE2kB9cyWX8bq3RWOX1/uPdzhNSkHEyWV18kzKNUAKqoPVZhb9+xMotkBwaGCQ/82JqRjC5gJGWEOElSbFnG+nLGGAKel4/c11ei4wKCilADRbDWcxoulm5Dki+P3g8aSZ20FI+dMKuSULpuUj42ZiTou8U3Xq9mIwYPRg64RWAkyGVDfnaSmVBrW4KY4aZwykq8nzeMwVw6CIMsnTqpEvRUEqUhIIHw4UkBqw0nOeU2Ah5cOQlXUHCmeylwNwVd9Wyk+v1NiKHNykzlJeuCOgq6GTbcIksmqcgmBBYm9EIE5x9e4V84Ad8NKlKva7IEqTb4SaJsObQjmw2KvBm/V3bbwG5vmijCogVFHgk3iOK3prbGz4l+zzeIFzH8uwrFURIgB4ZWmyXnIokt9gHIMdc= 30 | on: 31 | tags: true 32 | repo: waterlinejs/sqlite3-adapter 33 | all_branches: true 34 | node: 6 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 waterline.js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Waterline SQLite3 Adapter 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][ci-image]][ci-url] 5 | [![Dependency Status][daviddm-image]][daviddm-url] 6 | [![Code Climate][codeclimate-image]][codeclimate-url] 7 | 8 | A [Waterline](https://github.com/waterlinejs) adapter for 9 | [SQLite3](https://www.sqlite.org/). 10 | 11 | ## Features 12 | - Fully compatible with SQLite3 13 | - Supports Waterline Associations 14 | - Uses [knex.js](http://knexjs.org/) for query building 15 | - Written in ES6 16 | 17 | ## Compatibility 18 | - [Waterline](http://waterline.js.org) v0.10 and newer 19 | - [Trails](http://trailsjs.io) v1.0 and newer 20 | - Node 4 or newer 21 | 22 | ## Install 23 | 24 | ```sh 25 | $ npm install waterline-sqlite3 --save 26 | ``` 27 | 28 | ## Configuration 29 | 30 | #### `config/connections.js` 31 | 32 | ```js 33 | module.exports.connections = { 34 | sqlitedb: { 35 | /** 36 | * Database instance type. Specify whether to store the database on disk 37 | * or in memory. 38 | */ 39 | adapter: 'waterline-sqlite3', // or 'memory' 40 | 41 | /** 42 | * Location of file if type='disk' 43 | */ 44 | filename: './tmp/db.sqlite', 45 | 46 | /** 47 | * Set to true to output SQL queries 48 | */ 49 | debug: false 50 | } 51 | } 52 | ``` 53 | 54 | ## License 55 | MIT 56 | 57 | ## Maintained By 58 | [](http://langa.io) 59 | 60 | 61 | [npm-image]: https://img.shields.io/npm/v/waterline-sqlite3.svg?style=flat-square 62 | [npm-url]: https://npmjs.org/package/waterline-sqlite3 63 | [ci-image]: https://img.shields.io/travis/waterlinejs/sqlite3-adapter/master.svg?style=flat-square 64 | [ci-url]: https://travis-ci.org/waterlinejs/sqlite3-adapter 65 | [daviddm-image]: http://img.shields.io/david/waterlinejs/sqlite3-adapter.svg?style=flat-square 66 | [daviddm-url]: https://david-dm.org/waterlinejs/sqlite3-adapter 67 | [codeclimate-image]: https://img.shields.io/codeclimate/github/waterlinejs/sqlite3-adapter.svg?style=flat-square 68 | [codeclimate-url]: https://codeclimate.com/github/waterlinejs/sqlite3-adapter 69 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var babel = require('gulp-babel'); 3 | 4 | gulp.task('default', function () { 5 | return gulp.src([ 'lib/**' ]) 6 | .pipe(babel()) 7 | .pipe(gulp.dest('dist')); 8 | }); 9 | -------------------------------------------------------------------------------- /lib/adapter.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import sqlite3 from 'sqlite3' 3 | import Knex from 'knex' 4 | import _ from 'lodash' 5 | import path from 'path' 6 | import WaterlineSequel from 'waterline-sequel' 7 | import WaterlineError from 'waterline-errors' 8 | import WaterlineCursor from 'waterline-cursor' 9 | 10 | import Util from './util' 11 | import AdapterError from './error' 12 | 13 | const Adapter = { 14 | 15 | identity: 'waterline-sqlite3', 16 | 17 | wlSqlOptions: { 18 | parameterized: true, 19 | caseSensitive: false, 20 | escapeCharacter: '"', 21 | wlNext: false, 22 | casting: true, 23 | canReturnValues: false, 24 | escapeInserts: true, 25 | declareDeleteAlias: false 26 | }, 27 | 28 | /** 29 | * Local connections store 30 | */ 31 | connections: new Map(), 32 | 33 | pkFormat: 'integer', 34 | syncable: true, 35 | 36 | /** 37 | * Adapter default configuration 38 | */ 39 | defaults: { 40 | schema: true, 41 | debug: false, 42 | type: 'disk', 43 | filename: '.tmp/db.sqlite', 44 | }, 45 | 46 | /** 47 | * This method runs when a connection is initially registered 48 | * at server-start-time. This is the only required method. 49 | * 50 | * @param {[type]} connection 51 | * @param {[type]} collection 52 | * @param {Function} cb 53 | * @return {[type]} 54 | */ 55 | registerConnection (connection, collections, cb) { 56 | if (!connection.identity) { 57 | return cb(WaterlineError.adapter.IdentityMissing) 58 | } 59 | if (this.connections.get(connection.identity)) { 60 | return cb(WaterlineError.adapter.IdentityDuplicate) 61 | } 62 | 63 | _.defaults(connection, this.defaults) 64 | 65 | // Where are we writing the DB to? 66 | let filename = connection.filename || connection.path + '/' + connection.identity + '.sqlite' 67 | 68 | // Check if it's an in memory instance. 69 | if (connection.type == 'memory') { 70 | if (!_.isEmpty(filename) && filename != ':memory:' && filename != this.defaults.filename) { 71 | console.error(` 72 | WARNING: 73 | The connection config for the sqlite3 connection ${connection.identity} 74 | specifies the filename "${filename}" but specifies type="memory". The 75 | file will not be used, and the data will not be persistent. 76 | `) 77 | } 78 | filename = ':memory:' 79 | } 80 | 81 | // Get the folder for the 82 | const filePath = filename !== ":memory:" ? path.dirname(filename) : false 83 | 84 | if (filePath) 85 | try { 86 | fs.mkdirSync(path.resolve(filePath)) 87 | } catch (error) { 88 | if (error.code !== "EEXIST") 89 | throw error 90 | } 91 | 92 | this.connections.set(connection.identity, { 93 | identity: connection.identity, 94 | schema: this.buildSchema(connection, collections), 95 | collections: collections, 96 | knex: Knex({ 97 | client: 'sqlite3', 98 | connection: { 99 | filename: filename + (filename.endsWith('.sqlite') ? '' : '.sqlite') 100 | }, 101 | debug: process.env.WATERLINE_DEBUG_SQL || connection.debug 102 | }) 103 | }) 104 | 105 | cb() 106 | }, 107 | 108 | /** 109 | * Construct the waterline schema for the given connection. 110 | * 111 | * @param connection 112 | * @param collections[] 113 | */ 114 | buildSchema (connection, collections) { 115 | return _.chain(collections) 116 | .map((model, modelName) => { 117 | let definition = _.get(model, [ 'waterline', 'schema', model.identity ]) 118 | return _.defaults(definition, { 119 | attributes: { }, 120 | tableName: modelName 121 | }) 122 | }) 123 | .indexBy('tableName') 124 | .value() 125 | }, 126 | 127 | /** 128 | * Describe a table. List all columns and their properties. 129 | * 130 | * @see http://www.sqlite.org/pragma.html#pragma_table_info 131 | * @see http://www.sqlite.org/faq.html#q7 132 | * @see https://github.com/AndrewJo/sails-sqlite3/blob/master/lib/adapter.js#L156 133 | * 134 | * @param connectionName 135 | * @param tableName 136 | */ 137 | describe (connectionName, tableName, cb) { 138 | let cxn = this.connections.get(connectionName) 139 | 140 | return Promise.all([ 141 | cxn.knex.raw(`pragma table_info("${tableName}")`), 142 | cxn.knex.raw(`pragma index_list("${tableName}")`) 143 | ]) 144 | .then(([ tableInfo = [ ], indexList = [ ] ]) => { 145 | return Promise.all(indexList.map(index => { 146 | return cxn.knex.raw(`pragma index_info("${index.name}")`) 147 | .then(([ indexInfo = { } ]) => { 148 | let indexResult = _.extend(indexInfo, index) 149 | return indexResult 150 | }) 151 | })) 152 | .then(indexes => { 153 | return Util.transformTableInfo(tableInfo, _.flatten(indexes)) 154 | }) 155 | }) 156 | .then(result => { 157 | if (_.isEmpty(result)) return cb() 158 | 159 | _.isFunction(cb) && cb(null, result) 160 | return result 161 | }) 162 | .catch(AdapterError.wrap(cb)) 163 | }, 164 | 165 | /** 166 | * Drop a table 167 | */ 168 | drop (connectionName, tableName, relations = [ ], cb = relations) { 169 | let cxn = Adapter.connections.get(connectionName) 170 | 171 | return cxn.knex.schema.dropTableIfExists(tableName) 172 | .then(result => { 173 | _.isFunction(cb) && cb() 174 | }) 175 | .catch(AdapterError.wrap(cb)) 176 | }, 177 | 178 | /** 179 | * Create a new table 180 | * 181 | * @param connectionName 182 | * @param tableName 183 | * @param definition - the waterline schema definition for this model 184 | * @param cb 185 | */ 186 | define (connectionName, tableName, definition, cb) { 187 | let cxn = this.connections.get(connectionName) 188 | 189 | return cxn.knex.schema 190 | .createTable(tableName, table => { 191 | _.each(definition, (definition, attributeName) => { 192 | let newColumn = Util.toKnexColumn(table, attributeName, definition) 193 | Util.applyColumnConstraints(newColumn, definition) 194 | }) 195 | Util.applyTableConstraints(table, definition) 196 | }) 197 | .then(result => { 198 | _.isFunction(cb) && cb() 199 | }) 200 | .catch(AdapterError.wrap(cb)) 201 | }, 202 | 203 | /** 204 | * Add a column to a table 205 | */ 206 | addAttribute (connectionName, tableName, attributeName, definition, cb) { 207 | let cxn = this.connections.get(connectionName) 208 | 209 | return cxn.knex.schema 210 | .table(tableName, table => { 211 | let newColumn = Util.toKnexColumn(table, attributeName, definition) 212 | return Util.applyColumnConstraints(newColumn, definition) 213 | }) 214 | .then(() => { 215 | _.isFunction(cb) && cb() 216 | }) 217 | .catch(AdapterError.wrap(cb)) 218 | }, 219 | 220 | /** 221 | * Remove a column from a table 222 | */ 223 | removeAttribute (connectionName, tableName, attributeName, cb) { 224 | let cxn = this.connections.get(connectionName) 225 | 226 | return cxn.knex.schema 227 | .table(tableName, table => { 228 | table.dropColumn(attributeName) 229 | }) 230 | .then(result => { 231 | _.isFunction(cb) && cb(null, result) 232 | return result 233 | }) 234 | .catch(AdapterError.wrap(cb)) 235 | }, 236 | 237 | /** 238 | * Perform a direct SQL query on the database 239 | * 240 | * @param connectionName 241 | * @param tableName 242 | * @param queryString 243 | * @param data 244 | */ 245 | query (connectionName, tableName, queryString, args = [ ], cb = args) { 246 | let cxn = this.connections.get(connectionName) 247 | let query = cxn.knex.raw(Util.toKnexRawQuery(queryString), Util.castValues(args)) 248 | 249 | return query 250 | .then(rows => { 251 | let result = _.map(rows, row => { 252 | return Util.castSqlValues(row, cxn.collections[tableName]) 253 | }) 254 | _.isFunction(cb) && cb(null, result) 255 | return result 256 | }) 257 | }, 258 | 259 | /** 260 | * Create a new record 261 | * 262 | * @param connectionName {String} 263 | * @param tableName {String} 264 | * @param record {Object} 265 | * @param cb {Function} 266 | */ 267 | create (connectionName, tableName, record, cb) { 268 | let cxn = this.connections.get(connectionName) 269 | let pk = this.getPrimaryKey(cxn, tableName) 270 | 271 | return cxn.knex.transaction(txn => { 272 | return txn.insert(Util.castRecord(record)) 273 | .into(tableName) 274 | .then(([ rowid ]) => { 275 | return txn.select().from(tableName).where('rowid', rowid) 276 | }) 277 | .then(([ created ]) => { 278 | let record = Util.castSqlValues(created, cxn.collections[tableName]) 279 | _.isFunction(cb) && cb(null, record) 280 | return record 281 | }) 282 | .catch(AdapterError.wrap(cb)) 283 | }) 284 | }, 285 | 286 | /** 287 | * Find records 288 | * 289 | * @param connectionName {String} 290 | * @param tableName {String} 291 | * @param options {Object} 292 | * @param cb {Function} 293 | */ 294 | find (connectionName, tableName, options, cb) { 295 | let cxn = this.connections.get(connectionName) 296 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 297 | 298 | if (options.select && !options.select.length) { 299 | delete options.select 300 | } 301 | 302 | // Remove any empty where clauses, they seem to cause 303 | // issues. 304 | // @link https://github.com/trailsjs/trailpack-waterline/issues/25 305 | // @link https://github.com/waterlinejs/sqlite3-adapter/issues/1 306 | if (options.where) { 307 | Object.keys(options.where).forEach(column => { 308 | if (Array.isArray(options.where[column]) && options.where[column].length === 0) 309 | delete options.where[column] 310 | }) 311 | 312 | if (Object.keys(options.where).length === 0) 313 | options.where = null 314 | } 315 | 316 | return new Promise((resolve, reject) => { 317 | resolve(wlsql.find(tableName, options)) 318 | }) 319 | .then(({ query: [query], values: [values] }) => { 320 | return this.query(connectionName, tableName, query, values) 321 | }) 322 | .then((rows = [ ]) => { 323 | _.isFunction(cb) && cb(null, rows) 324 | return rows 325 | }) 326 | .catch(AdapterError.wrap(cb)) 327 | }, 328 | 329 | /** 330 | * Update a record 331 | * 332 | * @param connectionName {String} 333 | * @param tableName {String} 334 | * @param options {Object} 335 | * @param data {Object} 336 | * @param cb {Function} 337 | */ 338 | update (connectionName, tableName, options, data, cb) { 339 | let cxn = this.connections.get(connectionName) 340 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 341 | let pk = this.getPrimaryKey(cxn, tableName) 342 | let updateRows 343 | 344 | return cxn.knex.transaction(txn => { 345 | return new Promise((resolve, reject) => { 346 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 347 | resolve(wlsql.simpleWhere(tableName, _.pick(options, 'where'))) 348 | }) 349 | .then(({ query: where, values }) => { 350 | let [ $, whereClause ] = where.split('WHERE') 351 | 352 | return txn 353 | .select('rowid') 354 | .from(tableName) 355 | .whereRaw(txn.raw(Util.toKnexRawQuery(whereClause), values)) 356 | }) 357 | .then(rows => { 358 | updateRows = _.compact(_.pluck(rows, pk)) 359 | // TODO cleanup updateRows 360 | if (updateRows.length === 0) { 361 | updateRows = _.compact(_.pluck(rows, 'rowid')) 362 | } 363 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 364 | return wlsql.update(tableName, options, data) 365 | }) 366 | .then(({ query: _query, values }) => { 367 | let [ $, setClause ] = _query.split('SET') 368 | let query = `UPDATE "${tableName}" SET ` + setClause 369 | 370 | return txn.raw(Util.toKnexRawQuery(query), Util.castValues(values)) 371 | }) 372 | .then(() => { 373 | return txn 374 | .select() 375 | .from(tableName) 376 | .whereIn('rowid', updateRows) 377 | }) 378 | }) 379 | .then(rows => { 380 | let result = _.map(rows, row => { 381 | return Util.castSqlValues(row, cxn.collections[tableName]) 382 | }) 383 | _.isFunction(cb) && cb(null, result) 384 | }) 385 | .catch(AdapterError.wrap(cb)) 386 | }, 387 | 388 | /** 389 | * Destroy a record 390 | * 391 | * @param connectionName {String} 392 | * @param tableName {String} 393 | * @param options {Object} 394 | * @param cb {Function} 395 | */ 396 | destroy (connectionName, tableName, options, cb) { 397 | let cxn = this.connections.get(connectionName) 398 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 399 | let found 400 | 401 | return this.find(connectionName, tableName, options) 402 | .then(_found => { 403 | found = _found 404 | return wlsql.simpleWhere(tableName, _.pick(options, 'where')) 405 | }) 406 | .then(({ query: where, values }) => { 407 | let query = `DELETE FROM "${tableName}" ` + where 408 | return this.query(connectionName, tableName, query, values) 409 | }) 410 | .then(rows => { 411 | _.isFunction(cb) && cb(null, found) 412 | return found 413 | }) 414 | .catch(AdapterError.wrap(cb)) 415 | }, 416 | 417 | /** 418 | * Count the number of records 419 | * 420 | * @param connectionName {String} 421 | * @param tableName {String} 422 | * @param options {Object} 423 | * @param cb {Function} 424 | */ 425 | count (connectionName, tableName, options, cb) { 426 | let cxn = this.connections.get(connectionName) 427 | let wlsql = new WaterlineSequel(cxn.schema, this.wlSqlOptions) 428 | 429 | return new Promise((resolve, reject) => { 430 | resolve(wlsql.count(tableName, options)) 431 | }) 432 | .then(({ query: [_query], values: [values] }) => { 433 | let [ query, asClause ] = _query.split('AS') 434 | return this.query(connectionName, tableName, query.trim(), values) 435 | }) 436 | .then(([ row ]) => { 437 | let count = Number(row.count) 438 | _.isFunction(cb) && cb(null, count) 439 | return count 440 | }) 441 | .catch(AdapterError.wrap(cb)) 442 | }, 443 | 444 | /** 445 | * Populate record associations 446 | * 447 | * @param connectionName {String} 448 | * @param tableName {String} 449 | * @param options {Object} 450 | * @param cb {Function} 451 | */ 452 | join (connectionName, tableName, options, cb) { 453 | let cxn = this.connections.get(connectionName) 454 | 455 | WaterlineCursor({ 456 | instructions: options, 457 | parentCollection: tableName, 458 | 459 | $find (tableName, criteria, next) { 460 | return Adapter.find(connectionName, tableName, criteria, next) 461 | }, 462 | 463 | $getPK (tableName) { 464 | if (!tableName) return 465 | return Adapter.getPrimaryKey(cxn, tableName) 466 | } 467 | 468 | }, cb) 469 | }, 470 | 471 | /** 472 | * Get the primary key column of a table 473 | * 474 | * @param cxn 475 | * @param tableName 476 | */ 477 | getPrimaryKey ({ collections }, tableName) { 478 | let definition = collections[tableName].definition 479 | 480 | if (!definition._pk) { 481 | let pk = _.findKey(definition, (attr, name) => { 482 | return attr.primaryKey === true 483 | }) 484 | definition._pk = pk || 'id' 485 | } 486 | 487 | return definition._pk 488 | }, 489 | 490 | /** 491 | * Fired when a model is unregistered, typically when the server 492 | * is killed. Useful for tearing-down remaining open connections, 493 | * etc. 494 | * 495 | * @param {Function} cb [description] 496 | * @return {[type]} [description] 497 | */ 498 | teardown (conn, cb = conn) { 499 | let connections = conn ? [ this.connections.get(conn) ] : this.connections.values() 500 | let promises = [ ] 501 | 502 | for (let cxn of connections) { 503 | if (!cxn) continue 504 | 505 | promises.push(new Promise(resolve => { 506 | cxn.knex.destroy(resolve) 507 | this.connections.delete(cxn.identity) 508 | })) 509 | } 510 | 511 | return Promise.all(promises) 512 | .then(() => cb()) 513 | .catch(cb) 514 | } 515 | } 516 | 517 | _.bindAll(Adapter) 518 | export default Adapter 519 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const Errors = { 4 | SQLITE_CONSTRAINT (sqliteError) { 5 | return { 6 | code: 'E_UNIQUE', 7 | message: sqliteError.message, 8 | invalidAttributes: [ ] 9 | } 10 | } 11 | } 12 | 13 | const AdapterError = { 14 | wrap(cb, txn) { 15 | return function (sqliteError) { 16 | let errorWrapper = Errors[sqliteError.code] || sqliteError 17 | let error = sqliteError 18 | 19 | if (_.isFunction(errorWrapper)) { 20 | error = errorWrapper(sqliteError) 21 | } 22 | 23 | _.isFunction(cb) && cb(error) 24 | } 25 | } 26 | } 27 | 28 | export default AdapterError 29 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import _ from 'lodash' 3 | import Adapter from './adapter' 4 | import CriteriaParser from 'waterline-sequel/sequel/lib/criteriaProcessor' 5 | 6 | const Util = { 7 | 8 | /** 9 | * Create a column for Knex from a Waterline atribute definition 10 | * https://www.sqlite.org/datatype3.html 11 | */ 12 | toKnexColumn (table, _name, attrDefinition) { 13 | let attr = _.isObject(attrDefinition) ? attrDefinition : { type: attrDefinition } 14 | let type = attr.autoIncrement ? 'serial' : attr.type 15 | let name = attr.columnName || _name 16 | 17 | switch (type.toLowerCase()) { 18 | case 'text': 19 | case 'mediumtext': 20 | case 'longtext': 21 | case 'string': 22 | case 'json': 23 | case 'array': 24 | return table.text(name, type) 25 | 26 | /** 27 | * table.integer(name) 28 | * Adds an integer column. 29 | */ 30 | case 'boolean': 31 | case 'serial': 32 | case 'smallserial': 33 | case 'bigserial': 34 | case 'int': 35 | case 'integer': 36 | case 'smallint': 37 | case 'bigint': 38 | case 'biginteger': 39 | case 'datestamp': 40 | case 'datetime': 41 | case 'date': 42 | return table.integer(name) 43 | 44 | /** 45 | * table.float(column, [precision], [scale]) 46 | * Adds a float column, with optional precision and scale. 47 | */ 48 | case 'real': 49 | case 'float': 50 | case 'double': 51 | case 'decimal': 52 | return table.specificType(name, 'REAL') 53 | 54 | case 'binary': 55 | case 'bytea': 56 | return table.binary(name) 57 | 58 | case 'sqltype': 59 | case 'sqlType': 60 | return table.specificType(name, type) 61 | 62 | default: 63 | console.error('Unregistered type given for attribute. name=', name, '; type=', type) 64 | return table.text(name) 65 | } 66 | }, 67 | 68 | /** 69 | * Apply a primary key constraint to a table 70 | * 71 | * @param table - a knex table object 72 | * @param definition - a waterline attribute definition 73 | */ 74 | applyPrimaryKeyConstraints (table, definition) { 75 | let primaryKeys = _.keys(_.pick(definition, attribute => { 76 | return attribute.primaryKey 77 | })) 78 | 79 | return table.primary(primaryKeys) 80 | }, 81 | 82 | applyTableConstraints(table, definition) { 83 | return this.applyPrimaryKeyConstraints(table, definition) 84 | }, 85 | 86 | applyColumnConstraints (column, definition) { 87 | if (_.isString(definition)) { 88 | return 89 | } 90 | return _.map(definition, (value, key) => { 91 | if (key == 'defaultsTo' && definition.autoIncrement && value == 'AUTO_INCREMENT') { 92 | return 93 | } 94 | 95 | return this.applyParticularColumnConstraint(column, key, value, definition) 96 | }) 97 | }, 98 | 99 | applyParticularColumnConstraint (column, constraintName, value, definition) { 100 | if (!value) return 101 | 102 | switch (constraintName) { 103 | 104 | case 'index': 105 | return column.index(_.get(value, 'indexName'), _.get(value, 'indexType')) 106 | 107 | /** 108 | * Acceptable forms: 109 | * attr: { unique: true } 110 | * attr: { 111 | * unique: { 112 | * unique: true, // or false 113 | * composite: [ 'otherAttr' ] 114 | * } 115 | * } 116 | */ 117 | case 'unique': 118 | if ((value === true || _.get(value, 'unique') === true) && !definition.primaryKey) { 119 | column.unique() 120 | } 121 | return 122 | 123 | case 'notNull': 124 | return column.notNullable() 125 | 126 | case 'defaultsTo': 127 | return column.defaultTo(value) 128 | 129 | case 'type': return 130 | case 'primaryKey': return 131 | case 'autoIncrement': return 132 | case 'on': return 133 | case 'via': return 134 | case 'foreignKey': return 135 | case 'references': return 136 | case 'model': return 137 | case 'alias': return 138 | 139 | default: 140 | console.error('Unknown constraint [', constraintName, '] on column') 141 | } 142 | }, 143 | 144 | /** 145 | * Convert a paramterized waterline query into a knex-compatible query string 146 | */ 147 | toKnexRawQuery (sql = '') { 148 | return sql.replace(/\$\d+/g, '?') 149 | }, 150 | 151 | castSqlValues (values, model) { 152 | return _.mapValues(values, (value, attr) => { 153 | let definition = model.definition[attr] 154 | if (!definition) { 155 | return value 156 | } 157 | if (_.contains([ 'date', 'datetime', 'datestamp'], definition.type)) { 158 | return new Date(value) 159 | } 160 | if (definition.type == 'json' && _.isString(value)) { 161 | return JSON.parse(value) 162 | } 163 | if (definition.type == 'array' && _.isString(value)) { 164 | return JSON.parse(value) 165 | } 166 | 167 | return value 168 | }) 169 | }, 170 | 171 | castRecord (record) { 172 | return _.object(_.keys(record), this.castValues(record)) 173 | }, 174 | 175 | /** 176 | * Cast values to the correct type 177 | */ 178 | castValues (values) { 179 | return _.map(values, value => { 180 | if (_.isArray(value)) { 181 | return JSON.stringify(value) 182 | } 183 | if (_.isPlainObject(value)) { 184 | return JSON.stringify(value) 185 | } 186 | if (_.isNumber(value)) { 187 | return Number(value) 188 | } 189 | if (Buffer.isBuffer(value)) { 190 | return value 191 | } 192 | if (_.isString(value)) { 193 | let stripped = value.replace(/\"/g, '') 194 | if (moment(stripped, moment.ISO_8601, true).isValid()) { 195 | return new Date(stripped).valueOf() 196 | } 197 | } 198 | if (_.isDate(value)) { 199 | return value.valueOf() 200 | } 201 | 202 | return value 203 | }) 204 | }, 205 | 206 | transformTableInfo (tableInfo, indexes) { 207 | return _.chain(tableInfo) 208 | .map(column => { 209 | let index = _.findWhere(indexes, { cid: column.cid }) 210 | 211 | return { 212 | columnName: column.name, 213 | primaryKey: !!column.pk, 214 | type: column.type, 215 | indexed: !!(column.pk || index), 216 | unique: !!(column.pk || (index && index.unique)) 217 | } 218 | }) 219 | .indexBy('columnName') 220 | .value() 221 | } 222 | } 223 | 224 | _.bindAll(Util) 225 | export default Util 226 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waterline-sqlite3", 3 | "description": "Waterline Adapter for SQLite3", 4 | "version": "1.0.4", 5 | "author": "Travis Webb ", 6 | "url": "http://github.com/waterlinejs/sqlite3-adapter", 7 | "keywords": [ 8 | "sqlite", 9 | "sqlite3", 10 | "node orm", 11 | "orm", 12 | "driver", 13 | "es6" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/waterlinejs/sqlite3-adapter.git" 18 | }, 19 | "dependencies": { 20 | "knex": "^0.9", 21 | "lodash": "^3.10.0", 22 | "moment": "^2.10.6", 23 | "sqlite3": "^3.1.4", 24 | "waterline-cursor": "0.0.6", 25 | "waterline-errors": "^0.10.1", 26 | "waterline-sequel": "^0.5.0" 27 | }, 28 | "devDependencies": { 29 | "babel": "5.8.38", 30 | "gulp": "3.9.1", 31 | "gulp-babel": "5.2.0", 32 | "mocha": "latest", 33 | "waterline-adapter-tests": "^0.12.0" 34 | }, 35 | "bundledDependencies": [ 36 | "knex", 37 | "lodash", 38 | "moment", 39 | "waterline-cursor", 40 | "waterline-errors", 41 | "waterline-sequel" 42 | ], 43 | "scripts": { 44 | "test": "gulp && rm -rf .tmp/ ; NODE_ENV=test node test/integration/runner.js", 45 | "prepublish": "gulp", 46 | "postinstall": "npm run prepublish" 47 | }, 48 | "main": "dist/adapter", 49 | "license": "MIT", 50 | "bugs": "https://github.com/waterlinejs/sqlite3-adapter/issues", 51 | "waterlineAdapter": { 52 | "waterlineVersion": ">0.10.0", 53 | "interfaces": [ 54 | "associations", 55 | "migratable", 56 | "queryable", 57 | "semantic", 58 | "sql" 59 | ], 60 | "features": [ 61 | "autoIncrement", 62 | "crossAdapter", 63 | "unique" 64 | ] 65 | }, 66 | "engines": { 67 | "node": ">= 4.0.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/integration/runner.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var mocha = require('mocha'); 3 | var TestRunner = require('waterline-adapter-tests'); 4 | var Adapter = require('../../dist/adapter'); 5 | 6 | // Grab targeted interfaces from this adapter's `package.json` file: 7 | var pkg = { }; 8 | var interfaces = [ ]; 9 | var features = [ ]; 10 | 11 | try { 12 | pkg = require('../../package.json'); 13 | interfaces = pkg.waterlineAdapter.interfaces; 14 | features = pkg.waterlineAdapter.features; 15 | } 16 | catch (e) { 17 | throw new Error( 18 | '\n' + 19 | 'Could not read supported interfaces from `waterlineAdapter.interfaces`' + '\n' + 20 | 'in this adapter\'s `package.json` file ::' + '\n' + 21 | util.inspect(e) 22 | ); 23 | } 24 | 25 | console.log('Testing `' + pkg.name + '`, a Sails/Waterline adapter.'); 26 | console.log('Running `waterline-adapter-tests` against ' + interfaces.length + ' interfaces...'); 27 | console.log('( ' + interfaces.join(', ') + ' )'); 28 | console.log(); 29 | console.log('Latest draft of Waterline adapter interface spec:'); 30 | console.log('http://links.sailsjs.org/docs/plugins/adapters/interfaces'); 31 | console.log(); 32 | 33 | /** 34 | * Integration Test Runner 35 | * 36 | * Uses the `waterline-adapter-tests` module to 37 | * run mocha tests against the specified interfaces 38 | * of the currently-implemented Waterline adapter API. 39 | */ 40 | new TestRunner({ 41 | mocha: { bail: false }, 42 | failOnError: true, 43 | interfaces: interfaces, 44 | features: features, 45 | 46 | adapter: Adapter, 47 | config: { 48 | type: 'disk' 49 | } 50 | }); 51 | --------------------------------------------------------------------------------