721 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			721 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| function lowerFirst(str) {
 | |
|   return str.charAt(0).toLowerCase() + str.substr(1);
 | |
| }
 | |
| function snakeCase(str) {
 | |
|   return lowerFirst(str).replace(/([A-Z])/g, function (match) {
 | |
|     return "_" + match.toLowerCase();
 | |
|   });
 | |
| }
 | |
| function camelCase(str) {
 | |
|   return str.replace(/_([a-z])/g, function (match) {
 | |
|     return match[1].toUpperCase();
 | |
|   });
 | |
| }
 | |
| function upperCamelCase(str) {
 | |
|   var camel = camelCase(str);
 | |
|   return camel.charAt(0).toUpperCase() + camel.substr(1);
 | |
| }
 | |
| 
 | |
| var searchConditions = {
 | |
|   '=':  true,
 | |
|   '==': true,
 | |
|   '!=': true,
 | |
|   '<>': true,
 | |
|   '<':  true,
 | |
|   '<=': true,
 | |
|   '!<': true,
 | |
|   '>':  true,
 | |
|   '>=': true,
 | |
|   '!>': true,
 | |
|   'IS': true,
 | |
|   'IS NOT': true,
 | |
| 
 | |
|   'IN':          true,
 | |
|   'NOT IN':      true,
 | |
|   'LIKE':        true,
 | |
|   'NOT LIKE':    true,
 | |
|   'GLOB':        true,
 | |
|   'NOT GLOB':    true,
 | |
|   'BETWEEN':     true,
 | |
|   'NOT BETWEEN': true,
 | |
| };
 | |
| 
 | |
| function wrap(db, dir, dbsMap) {
 | |
|   // TODO if I put a failure right here,
 | |
|   // why doesn't the unhandled promise rejection fire?
 | |
|   var PromiseA = require('bluebird');
 | |
|   var promises = [];
 | |
|   var earr = [];
 | |
|   var debug = false;
 | |
| 
 | |
|   if (!dbsMap) {
 | |
|     dbsMap = {};
 | |
|   }
 | |
| 
 | |
|   // PRAGMA schema.table_info(table-name);
 | |
|   //
 | |
|   function sqlite3GetColumns(tablename, columns, cb) {
 | |
|     var sql = "PRAGMA table_info(" + db.escape(tablename) + ")";
 | |
| 
 | |
|     db.all(sql, earr, function (err, result) {
 | |
|       if (err) {
 | |
|         console.error('[Error] query columns');
 | |
|         console.error(err.stack);
 | |
|         cb(err);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (debug) {
 | |
|         console.log('sqlite3 rows 0');
 | |
|         console.log(result);
 | |
|       }
 | |
| 
 | |
|       function alterTable() {
 | |
|         var column = columns.pop();
 | |
|         var sql;
 | |
| 
 | |
|         if (!column) {
 | |
|           cb(null);
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         if ((result.rows||result).some(function (row) {
 | |
|           return (row.column_name || row.name) === snakeCase(column.name);
 | |
|         })) {
 | |
|           alterTable();
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         sql = "ALTER TABLE " + db.escape(tablename)
 | |
|           + " ADD COLUMN "
 | |
|           + db.escape(column.name) + " " + db.escape(column.type)
 | |
|           + " DEFAULT null"
 | |
|           ;
 | |
| 
 | |
|         if (debug) {
 | |
|           console.log('sqlite3 1');
 | |
|           console.log(sql);
 | |
|         }
 | |
| 
 | |
|         db.all(sql, earr, function (err, results) {
 | |
|           if (err) {
 | |
|             console.error("[Error] add column '" + tablename + "'");
 | |
|             console.error(sql);
 | |
|             console.error(err.stack || new Error('stack').stack);
 | |
|             cb(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           if (debug) {
 | |
|             console.log('sqlite3 rows 1');
 | |
|             console.log(results);
 | |
|           }
 | |
| 
 | |
|           alterTable();
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       columns = columns.slice(0);
 | |
|       alterTable();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function normalizeColumn(col, i, arr) {
 | |
|     if ('string' === typeof col) {
 | |
|       col = arr[i] = { name: col, type: 'text' };
 | |
|     }
 | |
| 
 | |
|     if (!col.type) {
 | |
|       col.type = 'text';
 | |
|     }
 | |
| 
 | |
|     col.type = col.type.toLowerCase(); // oh postgres...
 | |
|     col.name = snakeCase(col.name);
 | |
| 
 | |
|     return col;
 | |
|   }
 | |
| 
 | |
|   function createTable(dir) {
 | |
|     if (!dir.modelname && !dir.tablename) {
 | |
|       throw new Error('Please specify dir.modelname');
 | |
|     }
 | |
| 
 | |
|     if (!dir.tablename) {
 | |
|       dir.tablename = snakeCase(dir.modelname);
 | |
|     }
 | |
| 
 | |
|     if (!dir.modelname) {
 | |
|       dir.modelname = upperCamelCase(dir.tablename);
 | |
|     }
 | |
| 
 | |
|     if (!dir.indices) {
 | |
|       dir.indices = [];
 | |
|     }
 | |
| 
 | |
|     var DB = {};
 | |
|     var tablename = (db.escape(dir.tablename || 'data'));
 | |
|     var idname = (db.escape(dir.idname || 'id'));
 | |
|     var idnameCased = (camelCase(dir.idname || 'id'));
 | |
| 
 | |
|     dir.indices.forEach(normalizeColumn);
 | |
|     DB._indices = dir.indices;
 | |
|     DB._indicesMap = {};
 | |
|     DB._indices.forEach(function (col) {
 | |
|       DB._indicesMap[col.name] = col;
 | |
|     });
 | |
| 
 | |
|     function simpleMap(rows) {
 | |
|       if (!rows) {
 | |
|         return [];
 | |
|       }
 | |
| 
 | |
|       var results = rows.map(function (row, i) {
 | |
|         // set up for garbage collection
 | |
|         rows[i] = null;
 | |
| 
 | |
|         var obj;
 | |
| 
 | |
|         if (row.json) {
 | |
|           obj = JSON.parse(row.json);
 | |
|         } else {
 | |
|           obj = {};
 | |
|         }
 | |
|         delete row.json;
 | |
| 
 | |
|         obj[idnameCased] = row[idname];
 | |
|         delete row[idname];
 | |
| 
 | |
|         Object.keys(row).forEach(function (fieldname) {
 | |
|           // Ideally it shouldn't be possible to overriding a former proper column,
 | |
|           // but when a new indexable field is added, the old value is still in json
 | |
|           // TODO one-time upgrade of all rows when a new column is added
 | |
|           if (null === row[fieldname] || 'undefined' === typeof row[fieldname] || '' === row[fieldname]) {
 | |
|             obj[camelCase(fieldname)] = row[fieldname] || obj[camelCase(fieldname)] || row[fieldname];
 | |
|           }
 | |
|           else {
 | |
|             obj[camelCase(fieldname)] = row[fieldname];
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         return obj;
 | |
|       });
 | |
|       // set up for garbage collection
 | |
|       rows.length = 0;
 | |
|       rows = null;
 | |
| 
 | |
|       return results;
 | |
|     }
 | |
| 
 | |
|     function simpleParse(row) {
 | |
|       if (!row) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       return simpleMap([row])[0] || null;
 | |
|     }
 | |
| 
 | |
|     // pull indices from object
 | |
|     function strainUpdate(id, data/*, vals*/, cb, oldId) {
 | |
|       var fieldable = [];
 | |
|       var sql;
 | |
|       var vals = [];
 | |
| 
 | |
|       ['hasOne', 'hasMany', 'hasAndBelongsToMany', 'belongsTo', 'belongsToMany'].forEach(function (relname) {
 | |
|         var rels = dir[relname];
 | |
| 
 | |
|         if (!rels) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         if (!Array.isArray(rels)) {
 | |
|           rels = [rels];
 | |
|         }
 | |
| 
 | |
|         // don't save relationships
 | |
|         rels.forEach(function (colname) {
 | |
|           delete data[colname];
 | |
|           delete data[camelCase(colname)];
 | |
|           // TODO placehold relationships on find / get?
 | |
|           // data[camelCase(colname)] = null;
 | |
|         });
 | |
|       });
 | |
| 
 | |
|       dir.indices.forEach(function (col) {
 | |
|         // We prioritze the raw name rather than the camelCase name because it's not in the object
 | |
|         // we give for retrieved entries, so if it's present then the user put it there themselves.
 | |
|         var val = data[col.name] || data[camelCase(col.name)];
 | |
| 
 | |
|         //if (col.name in data)
 | |
|         if ('undefined' !== typeof val) {
 | |
|           /*
 | |
|           fieldable.push(
 | |
|             db.escape(snakeCase(col.name))
 | |
|           + " = '" + db.escape(val) + "'"
 | |
|           );
 | |
|           */
 | |
|           fieldable.push(db.escape(snakeCase(col.name)));
 | |
|           vals.push(val);
 | |
|         }
 | |
| 
 | |
|         delete data[col.name];
 | |
|         delete data[camelCase(col.name)];
 | |
|       });
 | |
| 
 | |
|       if (!oldId) {
 | |
|         delete data[idnameCased];
 | |
|       }
 | |
| 
 | |
|       if (!fieldable.length || Object.keys(data).length) {
 | |
|         vals.push(JSON.stringify(data));
 | |
|       } else {
 | |
|         vals.push(null);
 | |
|       }
 | |
|       fieldable.push('json');
 | |
| 
 | |
|       vals.push(id);
 | |
| 
 | |
|       sql = cb(fieldable);
 | |
| 
 | |
|       if (debug) {
 | |
|         console.log('[masterquest-sqlite3] dbwrap.js');
 | |
|         console.log(sql);
 | |
|         console.log(vals);
 | |
|       }
 | |
| 
 | |
|       vals.forEach(function (val) {
 | |
|         if (null === val || 'number' === typeof val) {
 | |
|           sql = sql.replace('?', String(val));
 | |
|         } else {
 | |
|           sql = sql.replace('?', "'" + db.escape(val) + "'");
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       return sql;
 | |
|     }
 | |
| 
 | |
|     DB.migrate = function (columns) {
 | |
|       columns.forEach(normalizeColumn);
 | |
| 
 | |
|       return new PromiseA(function (resolve, reject) {
 | |
|         sqlite3GetColumns(tablename, columns, function (err) {
 | |
|           if (err) {
 | |
|             reject(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           resolve();
 | |
|         });
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB.find = function (obj, params) {
 | |
|       var err;
 | |
|       var sql = 'SELECT * FROM \'' + tablename + '\' ';
 | |
|       var keys = obj && Object.keys(obj);
 | |
| 
 | |
|       if (obj) {
 | |
|         Object.keys(obj).forEach(function (key) {
 | |
|           if (undefined === obj[key]) {
 | |
|             err = new Error("'" + key + "' was `undefined'. For security purposes you must explicitly set the value to null or ''");
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|       if (err) {
 | |
|         return PromiseA.reject(err);
 | |
|       }
 | |
| 
 | |
|       if (params && params.limit) {
 | |
|         params.limit = parseInt(params.limit, 10);
 | |
|         // remember to check for the case of NaN
 | |
|         if (!params.limit || params.limit <= 0) {
 | |
|           return PromiseA.reject(new Error('limit must be a positive integer'));
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (obj && keys.length) {
 | |
|         var conditions = keys.map(function (key) {
 | |
|           var dbKey = db.escape(snakeCase(key));
 | |
|           var value = obj[key];
 | |
|           if (null === value) {
 | |
|             return dbKey + ' IS NULL';
 | |
|           }
 | |
| 
 | |
|           var split, cmd;
 | |
|           if (typeof value === 'string') {
 | |
|             value = value.trim();
 | |
|             if (['IS NULL', 'IS NOT NULL'].indexOf(value.toUpperCase()) !== -1) {
 | |
|               return dbKey + ' ' + value.toUpperCase();
 | |
|             }
 | |
| 
 | |
|             split = value.split(' ');
 | |
|             if (searchConditions[split[0].toUpperCase()]) {
 | |
|               cmd = split[0].toUpperCase();
 | |
|               value = split.slice(1).join(' ');
 | |
|             } else if (searchConditions[split.slice(0, 2).join(' ').toUpperCase()]) {
 | |
|               cmd = split.slice(0, 2).join(' ').toUpperCase();
 | |
|               value = split.slice(2).join(' ');
 | |
|             }
 | |
|             // If we were given something like "BEGINS WITH 'something quoted'" we don't want
 | |
|             // to include the quotes (we'll quote it again later) so we strip them out here.
 | |
|             if (cmd) {
 | |
|               value = value.replace(/^(['"])(.*)\1$/, '$2');
 | |
|             }
 | |
|           }
 | |
|           if (typeof value === 'object') {
 | |
|             cmd = value.condition || value.relation || value.cmd;
 | |
|             value = value.value;
 | |
|             if (!cmd || !value) {
 | |
|               err = new Error("'"+key+"' was an object, but missing condition and/or value");
 | |
|               return;
 | |
|             }
 | |
|             if (typeof cmd !== 'string' || !searchConditions[cmd.toUpperCase()]) {
 | |
|               err = new Error("'"+key+"' tried to use invalid condition '"+cmd+"'");
 | |
|               return;
 | |
|             } else {
 | |
|               cmd = cmd.toUpperCase();
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (!cmd) {
 | |
|             cmd = '=';
 | |
|           }
 | |
|           // The IN condition is special in that we can't quote the value as a single value,
 | |
|           // so it requires a little more logic to actually work and still be sanitary.
 | |
|           if (cmd === 'IN' || cmd === 'NOT IN') {
 | |
|             if (typeof value === 'string') {
 | |
|               value = value.split((params || {}).seperator || /[\s,]+/);
 | |
|             }
 | |
|             if (!Array.isArray(value)) {
 | |
|               err = new Error("'"+key+"' has invalid value for use with 'IN'");
 | |
|               return;
 | |
|             }
 | |
|             value = value.map(function (val) {
 | |
|               return "'"+db.escape(val)+"'";
 | |
|             });
 | |
|             return dbKey + ' ' + cmd + ' (' + value.join(',') + ')';
 | |
|           }
 | |
|           // The BETWEEN condition is also special for the same reason as IN
 | |
|           if (cmd === 'BETWEEN' || cmd === 'NOT BETWEEN') {
 | |
|             if (typeof value === 'string') {
 | |
|               value = value.split((params || {}).seperator || /[\s,]+(AND\s+)?/i);
 | |
|             }
 | |
|             if (!Array.isArray(value) || value.length !== 2) {
 | |
|               err = new Error("'"+key+"' has invalid value for use with 'BETWEEN'");
 | |
|               return;
 | |
|             }
 | |
|             value = value.map(function (val) {
 | |
|               return "'"+db.escape(val)+"'";
 | |
|             });
 | |
|             return dbKey + ' ' + cmd + ' ' + value.join(' AND ');
 | |
|           }
 | |
|           // If we are supposed to compare to another field then make sure the name is correct,
 | |
|           // and that we don't try to quote the name.
 | |
|           if (typeof value === 'string' && /^[a-zA-Z0-9_]*$/.test(value)) {
 | |
|             var snake = snakeCase(value);
 | |
|             if (dir.indices.some(function (col) { return snake === col.name; })) {
 | |
|               return dbKey + ' ' + cmd + ' ' + snake;
 | |
|             }
 | |
|           }
 | |
|           return dbKey + ' ' + cmd + " '" + db.escape(value) + "'";
 | |
|         });
 | |
|         if (err) {
 | |
|           return PromiseA.reject(err);
 | |
|         }
 | |
| 
 | |
|         sql += 'WHERE ' + conditions.join(' AND ');
 | |
|       }
 | |
|       else if (null !== obj || !(params && params.limit)) {
 | |
|         return PromiseA.reject(new Error("to find all you must explicitly specify find(null, { limit: <<int>> })"));
 | |
|       }
 | |
| 
 | |
|       if (params) {
 | |
|         if (typeof params.orderByDesc === 'string' && !params.orderBy) {
 | |
|           params.orderBy = params.orderByDesc;
 | |
|           params.orderByDesc = true;
 | |
|         }
 | |
| 
 | |
|         // IMPORTANT: " is not the same to sqlite as '.
 | |
|         // // " is exact and necessary
 | |
|         if (params.orderBy) {
 | |
|           sql += " ORDER BY \"" + db.escape(snakeCase(params.orderBy)) + "\" ";
 | |
|           if (params.orderByDesc) {
 | |
|             sql += "DESC ";
 | |
|           }
 | |
|         } else if (DB._indicesMap.updated_at) {
 | |
|           sql += " ORDER BY \"updated_at\" DESC ";
 | |
|         } else if (DB._indicesMap.created_at) {
 | |
|           sql += " ORDER BY \"created_at\" DESC ";
 | |
|         }
 | |
|         if (isFinite(params.limit)) {
 | |
|           sql += " LIMIT " + parseInt(params.limit, 10);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return db.allAsync(sql, []).then(simpleMap);
 | |
|     };
 | |
| 
 | |
|     DB.get = function (id) {
 | |
|       var sql = "SELECT * FROM " + tablename + " WHERE " + idname + " = ?";
 | |
|       var values = [id];
 | |
| 
 | |
|       return db.getAsync(sql, values).then(function (rows) {
 | |
|         if (Array.isArray(rows)) {
 | |
|           if (!rows.length) {
 | |
|             return null;
 | |
|           }
 | |
| 
 | |
|           return rows[0] || null;
 | |
|         }
 | |
| 
 | |
|         return rows;
 | |
|       }).then(simpleParse);
 | |
|     };
 | |
| 
 | |
|     DB.upsert = function (id, data, oldId) {
 | |
|       if (!data) {
 | |
|         data = id;
 | |
|         id = data[idnameCased];
 | |
|       }
 | |
| 
 | |
|       return DB.set(oldId || id, data, oldId).then(function (result) {
 | |
|         var success = result.changes >= 1;
 | |
| 
 | |
|         if (success) {
 | |
|           return result;
 | |
|         }
 | |
| 
 | |
|         return DB.create(id, data);
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB.save = function (data, oldId) {
 | |
|       if (!data[idnameCased] && !oldId) {
 | |
|         // NOTE saving the id both in the object and the id for now
 | |
|         data[idnameCased] = require('uuid').v4();
 | |
|         return DB.create(data[idnameCased], data).then(function (/*stats*/) {
 | |
|           //data._rowid = stats.id;
 | |
|           return data;
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       return DB.set(oldId || data[idnameCased], data, oldId).then(function (result) {
 | |
|         var success = result.changes >= 1;
 | |
| 
 | |
|         if (success) {
 | |
|           return result;
 | |
|         } else {
 | |
|           //console.log('[debug result of set]', result.sql);
 | |
|           delete result.sql;
 | |
|         }
 | |
| 
 | |
|         return null;
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB.create = function (id, obj) {
 | |
|       if (!obj) {
 | |
|         obj = id;
 | |
|         id = obj[idnameCased];
 | |
|       }
 | |
|       if (!id) {
 | |
|         return PromiseA.reject(new Error("no id supplied"));
 | |
|       }
 | |
| 
 | |
|       obj.createdAt = Date.now();
 | |
|       obj.updatedAt = Date.now();
 | |
| 
 | |
|       return new PromiseA(function (resolve, reject) {
 | |
|         var json = JSON.stringify(obj);
 | |
|         var data = JSON.parse(json);
 | |
| 
 | |
|         var sql;
 | |
| 
 | |
|         // removes known fields from data
 | |
|         sql = strainUpdate(id, data, function sqlTpl(fieldable) {
 | |
|           return "INSERT INTO " + tablename + " (" + fieldable.join(', ') + ", " + idname + ")"
 | |
|             //+ " VALUES ('" + vals.join("', '") + "')"
 | |
|             + " VALUES (" + fieldable.map(function () { return '?'; }).join(", ") + ", ?)"
 | |
|             ;
 | |
|         });
 | |
| 
 | |
|         //console.log('[debug] DB.create() sql:', sql);
 | |
|         db.run(sql, [], function (err) {
 | |
|           if (err) {
 | |
|             reject(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // NOTE changes is 1 even if the value of the updated record stays the same
 | |
|           // (PostgreSQL would return 0 in that case)
 | |
|           // thus if changes is 0 then it failed, otherwise it succeeded
 | |
|           /*
 | |
|           console.log('[log db wrapper insert]');
 | |
|           console.log(this); // sql, lastID, changes
 | |
|           console.log(this.sql);
 | |
|           console.log('insert lastID', this.lastID); // sqlite's internal rowId
 | |
|           console.log('insert changes', this.changes);
 | |
|           */
 | |
| 
 | |
|           //this.id = id;
 | |
|           resolve(this);
 | |
|         });
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB.set = function (id, obj, oldId) {
 | |
|       obj.updatedAt = Date.now();
 | |
| 
 | |
|       var json = JSON.stringify(obj);
 | |
|       var data = JSON.parse(json);
 | |
| 
 | |
|       return new PromiseA(function (resolve, reject) {
 | |
|         function sqlTpl(fieldable) {
 | |
|           // this will always at least have one fieldable value: json
 | |
|           return "UPDATE " + tablename + " SET "
 | |
|             + (fieldable.join(' = ?, ') + " = ?")
 | |
|             + " WHERE " + idname + " = ?"
 | |
|             ;
 | |
|         }
 | |
| 
 | |
|         //var vals = [];
 | |
|         // removes known fields from data
 | |
|         data.updated_at = Date.now();
 | |
|         var sql = strainUpdate(id, data/*, vals*/, sqlTpl, oldId);
 | |
| 
 | |
|         //console.log('[debug] DB.set() sql:', sql);
 | |
|         db.run(sql, /*vals*/[], function (err) {
 | |
|           //console.log('[debug] error:', err);
 | |
|           if (err) {
 | |
|             reject(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // it isn't possible to tell if the update succeeded or failed
 | |
|           // only if the update resulted in a change or not
 | |
|           /*
 | |
|           console.log('[log db wrapper set]');
 | |
|           console.log(this); // sql, lastID, changes
 | |
|           console.log(this.sql);
 | |
|           console.log('update lastID', this.lastID); // always 0 (except on INSERT)
 | |
|           console.log('update changes', this.changes);
 | |
|           */
 | |
| 
 | |
|           resolve(this);
 | |
|         });
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB.destroy = function (id) {
 | |
|       if ('object' === typeof id) {
 | |
|         id = id[idnameCased];
 | |
|       }
 | |
| 
 | |
|       return new PromiseA(function (resolve, reject) {
 | |
|         var sql = "DELETE FROM " + tablename + " WHERE " + idname + " = ?";
 | |
|         var values = [id];
 | |
| 
 | |
|         db.run(sql, values, function (err) {
 | |
|           if (err) {
 | |
|             reject(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // it isn't possible to tell if the update succeeded or failed
 | |
|           // only if the update resulted in a change or not
 | |
|           /*
 | |
|           console.log('[log db wrapper delete]');
 | |
|           console.log(this); // sql, lastID, changes
 | |
|           console.log(this.sql);
 | |
|           console.log('delete lastID', this.lastID); // always 0 (except on INSERT)
 | |
|           console.log('delete changes', this.changes);
 | |
|           */
 | |
| 
 | |
|           resolve(this);
 | |
|         });
 | |
|       });
 | |
|     };
 | |
| 
 | |
|     DB._db = db;
 | |
| 
 | |
|     return new PromiseA(function (resolve, reject) {
 | |
|       var indexable = [idname + ' TEXT'];
 | |
|       var sql;
 | |
| 
 | |
|       dir.indices.forEach(function (col) {
 | |
|         if ('string' === typeof col) {
 | |
|           col = { name: col, type: 'TEXT' };
 | |
|         }
 | |
|         if (!col.type) {
 | |
|           col.type = 'TEXT';
 | |
|         }
 | |
|         indexable.push(
 | |
|           db.escape(snakeCase(col.name))
 | |
|         + ' ' + db.escape(col.type)
 | |
|         );
 | |
|       });
 | |
|       indexable.push('json TEXT');
 | |
| 
 | |
|       sql = "CREATE TABLE IF NOT EXISTS '" + tablename + "' "
 | |
|         + "(" + indexable.join(', ') + ", PRIMARY KEY(" + idname + "))"
 | |
|         ;
 | |
| 
 | |
|       db.runAsync(sql).then(function () {
 | |
|         sqlite3GetColumns(tablename, dir.indices, function (err) {
 | |
|           if (err) {
 | |
|             console.error('[Error] dbwrap get columns');
 | |
|             console.error(err.stack);
 | |
|             reject(err);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           resolve(DB);
 | |
|         });
 | |
|       }, reject);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   if (!db.__masterquest_init) {
 | |
|     db.__masterquest_init = true;
 | |
|     db = PromiseA.promisifyAll(db);
 | |
|     db.__masterquest_init = true;
 | |
|     db.escape = function (str) {
 | |
|       // TODO? literals for true,false,null
 | |
|       // error on undefined?
 | |
|       if (undefined === str) {
 | |
|         str = '';
 | |
|       }
 | |
|       return String(str).replace(/'/g, "''");
 | |
|     };
 | |
| 
 | |
|     if (dir && dir.verbose || db.verbose) {
 | |
|       console.log('Getting Verbose up in here');
 | |
|       db.on('trace', function (str) {
 | |
|         console.log('SQL:', str);
 | |
|       });
 | |
| 
 | |
|       db.on('profile', function (sql, ms) {
 | |
|         console.log('Profile:', ms);
 | |
|       });
 | |
|     }
 | |
|   }
 | |
|   dbsMap.sql = db;
 | |
| 
 | |
|   dir.forEach(function (dir) {
 | |
|     // TODO if directive is the same as existing dbsMap, skip it
 | |
|     promises.push(createTable(dir).then(function (dbw) {
 | |
| 
 | |
|       dbsMap[dir.modelname] = dbw;
 | |
| 
 | |
|       return dbw;
 | |
|     }));
 | |
|   });
 | |
| 
 | |
|   return PromiseA.all(promises).then(function (/*dbs*/) {
 | |
|     return dbsMap;
 | |
|   });
 | |
| }
 | |
| 
 | |
| module.exports.wrap = wrap;
 |