var fs = require('fs'); var async = require("async"); module.exports = function(s,config){ s.onBeforeDatabaseLoadExtensions.forEach(function(extender){ extender(config) }) //sql/database connection with knex s.databaseOptions = { client: config.databaseType, connection: config.db, } if(s.databaseOptions.client.indexOf('sqlite')>-1){ s.databaseOptions.client = 'sqlite3' s.databaseOptions.useNullAsDefault = true try{ require('sqlite3') }catch(err){ console.log('Installing SQlite3 Module...') require('child_process').execSync('npm install sqlite3 --unsafe-perm') } } if(s.databaseOptions.client === 'sqlite3' && s.databaseOptions.connection.filename === undefined){ s.databaseOptions.connection.filename = s.mainDirectory+"/shinobi.sqlite" } s.mergeQueryValues = function(query,values){ if(!values){values=[]} var valuesNotFunction = true; if(typeof values === 'function'){ var values = []; valuesNotFunction = false; } if(values&&valuesNotFunction){ var splitQuery = query.split('?') var newQuery = '' splitQuery.forEach(function(v,n){ newQuery += v var value = values[n] if(value){ if(isNaN(value) || value instanceof Date){ newQuery += "'"+value+"'" }else{ newQuery += value } } }) }else{ newQuery = query } return newQuery } s.getUnixDate = function(value){ newValue = new Date(value).valueOf() return newValue } s.stringToSqlTime = function(value){ newValue = new Date(value.replace('T',' ')) return newValue } var runQuery = async.queue(function(data, callback) { s.databaseEngine .raw(data.query,data.values) .asCallback(callback) }, 4); const cleanSqlWhereObject = (where) => { const newWhere = {} Object.keys(where).forEach((key) => { if(key !== '__separator'){ const value = where[key] newWhere[key] = value } }) return newWhere } const processSimpleWhereCondition = (dbQuery,where,didOne) => { var whereIsArray = where instanceof Array; if(where[0] === 'or' || where.__separator === 'or'){ if(whereIsArray){ where.shift() dbQuery.orWhere(...where) }else{ where = cleanSqlWhereObject(where) dbQuery.orWhere(where) } }else if(!didOne){ didOne = true whereIsArray ? dbQuery.where(...where) : dbQuery.where(where) }else{ whereIsArray ? dbQuery.andWhere(...where) : dbQuery.andWhere(where) } } const processWhereCondition = (dbQuery,where,didOne) => { var whereIsArray = where instanceof Array; if(!where[0])return; if(where[0] && where[0] instanceof Array){ dbQuery.where(function() { var _this = this var didOneInsideGroup = false where.forEach((whereInsideGroup) => { processWhereCondition(_this,whereInsideGroup,didOneInsideGroup) }) }) }else if(where[0] && where[0] instanceof Object){ dbQuery.where(function() { var _this = this var didOneInsideGroup = false where.forEach((whereInsideGroup) => { processSimpleWhereCondition(_this,whereInsideGroup,didOneInsideGroup) }) }) }else{ processSimpleWhereCondition(dbQuery,where,didOne) } } const knexError = (dbQuery,options,err) => { console.error('knexError----------------------------------- START') if(config.debugLogVerbose && config.debugLog === true){ s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) s.debugLog('STACK TRACE, NOT AN ',new Error()) } console.error(err) console.error(dbQuery.toString()) console.error('knexError----------------------------------- END') } const knexQuery = (options,callback) => { try{ if(!s.databaseEngine)return// console.log('Database Not Set'); // options = { // action: "", // columns: "", // table: "" // } var dbQuery switch(options.action){ case'select': options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); dbQuery = s.databaseEngine.select(...options.columns).from(options.table) break; case'count': options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); dbQuery = s.databaseEngine(options.table) dbQuery.count(options.columns) break; case'update': dbQuery = s.databaseEngine(options.table).update(options.update) break; case'delete': dbQuery = s.databaseEngine(options.table) break; case'insert': dbQuery = s.databaseEngine(options.table).insert(options.insert) break; } if(options.where){ var didOne = false; options.where.forEach((where) => { processWhereCondition(dbQuery,where,didOne) }) } if(options.action === 'delete'){ dbQuery.del() } if(options.orderBy){ dbQuery.orderBy(...options.orderBy) } if(options.groupBy){ dbQuery.groupBy(options.groupBy) } if(options.limit){ if(`${options.limit}`.indexOf(',') === -1){ dbQuery.limit(options.limit) }else{ const limitParts = `${options.limit}`.split(',') dbQuery.limit(limitParts[0]).offset(limitParts[1]) } } if(config.debugLog === true){ console.log(dbQuery.toString()) } if(callback || options.update || options.insert){ dbQuery.asCallback(function(err,r) { if(err){ knexError(dbQuery,options,err) } if(callback)callback(err,r) if(config.debugLogVerbose && config.debugLog === true){ s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) s.debugLog('s.knexQuery RESPONSE',JSON.stringify(r,null,3)) s.debugLog('STACK TRACE, NOT AN ',new Error()) } }) } return dbQuery }catch(err){ if(callback)callback(err,[]) knexError(dbQuery,options,err) } } const getDatabaseRows = function(options,callback){ //current cant handle `end` time var whereQuery = [ ['ke','=',options.groupKey], ] const monitorRestrictions = options.monitorRestrictions var frameLimit = parseInt(options.limit) || 500 const endIsStartTo = options.endIsStartTo const chosenDate = options.date const startDate = options.startDate ? s.stringToSqlTime(options.startDate) : null const endDate = options.endDate ? s.stringToSqlTime(options.endDate) : null const startOperator = options.startOperator || '>=' const endOperator = options.endOperator || '<=' const rowType = options.rowType || 'rows' if(chosenDate){ if(chosenDate.indexOf('-') === -1 && !isNaN(chosenDate)){ chosenDate = parseInt(chosenDate) } var selectedDate = chosenDate if(typeof chosenDate === 'string' && chosenDate.indexOf('.') > -1){ selectedDate = chosenDate.split('.')[0] } selectedDate = new Date(selectedDate) var utcSelectedDate = new Date(selectedDate.getTime() + selectedDate.getTimezoneOffset() * 60000) startDate = moment(utcSelectedDate).format('YYYY-MM-DD HH:mm:ss') var dayAfter = utcSelectedDate dayAfter.setDate(dayAfter.getDate() + 1) endDate = moment(dayAfter).format('YYYY-MM-DD HH:mm:ss') } if(startDate){ if(endDate){ whereQuery.push(['time',startOperator,startDate]) whereQuery.push([endIsStartTo ? 'time' : 'end',endOperator,endDate]) }else{ whereQuery.push(['time',startOperator,startDate]) } } if(monitorRestrictions && monitorRestrictions.length > 0){ whereQuery.push(monitorRestrictions) } if(options.archived){ whereQuery.push(['details','LIKE',`%"archived":"1"%`]) } if(options.filename){ whereQuery.push(['filename','=',options.filename]) frameLimit = "1"; } options.orderBy = options.orderBy ? options.orderBy : ['time','desc'] if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0] knexQuery({ action: options.count ? "count" : "select", columns: options.columns || "*", table: options.table, where: whereQuery, orderBy: options.orderBy, groupBy: options.groupBy, limit: frameLimit || '500' },(err,r) => { if(err){ callback({ ok: false, total: 0, limit: frameLimit, [rowType]: [] }) }else{ r.forEach(function(file){ file.details = s.parseJSON(file.details) }) callback({ ok: true, total: r.length, limit: frameLimit, [rowType]: r }) } }) } s.knexQuery = knexQuery s.getDatabaseRows = getDatabaseRows s.sqlQuery = function(query,values,onMoveOn,hideLog){ if(!values){values=[]} if(typeof values === 'function'){ var onMoveOn = values; var values = []; } if(!onMoveOn){onMoveOn=function(){}} // if(s.databaseOptions.client === 'pg'){ // query = query // .replace(/ NOT LIKE /g," NOT ILIKE ") // .replace(/ LIKE /g," ILIKE ") // } if(config.debugLog === true){ var mergedQuery = s.mergeQueryValues(query,values) s.debugLog('s.sqlQuery QUERY',mergedQuery) } if(!s.databaseEngine || !s.databaseEngine.raw){ s.connectDatabase() } return runQuery.push({ query: query, values: values },function(err,r){ if(err && !hideLog){ console.log('s.sqlQuery QUERY ERRORED',query) console.log('s.sqlQuery ERROR',err) } if(onMoveOn && typeof onMoveOn === 'function'){ switch(s.databaseOptions.client){ case'sqlite3': if(!r)r=[] break; default: if(r)r=r[0] break; } onMoveOn(err,r) } }) } s.connectDatabase = function(){ s.databaseEngine = require('knex')(s.databaseOptions) } s.preQueries = function(){ var knex = s.databaseEngine var mySQLtail = '' if(config.databaseType === 'mysql'){ mySQLtail = ' ENGINE=InnoDB DEFAULT CHARSET=utf8' //add Presets table and modernize var createPresetsTableQuery = 'CREATE TABLE IF NOT EXISTS `Presets` ( `ke` varchar(50) DEFAULT NULL, `name` text, `details` text, `type` varchar(50) DEFAULT NULL)' s.sqlQuery( createPresetsTableQuery + mySQLtail + ';',[],function(err){ if(err)console.error(err) if(config.databaseType === 'sqlite3'){ var aQuery = "ALTER TABLE Presets RENAME TO _Presets_old;" aQuery += createPresetsTableQuery aQuery += "INSERT INTO Presets (`ke`, `name`, `details`, `type`) SELECT `ke`, `name`, `details`, `type` FROM _Presets_old;COMMIT;DROP TABLE _Presets_old;" }else{ s.sqlQuery('ALTER TABLE `Presets` CHANGE COLUMN `type` `type` VARCHAR(50) NULL DEFAULT NULL AFTER `details`;',[],function(err){ if(err)console.error(err) },true) } },true) //add Schedules table, will remove in future s.sqlQuery("CREATE TABLE IF NOT EXISTS `Schedules` (`ke` varchar(50) DEFAULT NULL,`name` text,`details` text,`start` varchar(10) DEFAULT NULL,`end` varchar(10) DEFAULT NULL,`enabled` int(1) NOT NULL DEFAULT '1')" + mySQLtail + ';',[],function(err){ if(err)console.error(err) },true) //add Timelapses and Timelapse Frames tables, will remove in future s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapses` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`date` date NOT NULL,`time` timestamp NOT NULL,`end` timestamp NOT NULL,`size` int(11)NOT NULL)" + mySQLtail + ';',[],function(err){ if(err)console.error(err) },true) s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)" + mySQLtail + ';',[],function(err){ if(err)console.error(err) },true) //Add index to Videos table s.sqlQuery('CREATE INDEX `videos_index` ON Videos(`time`);',[],function(err){ if(err && err.code !== 'ER_DUP_KEYNAME'){ console.error(err) } },true) //Add index to Events table s.sqlQuery('CREATE INDEX `events_index` ON Events(`ke`, `mid`, `time`);',[],function(err){ if(err && err.code !== 'ER_DUP_KEYNAME'){ console.error(err) } },true) //Add index to Logs table s.sqlQuery('CREATE INDEX `logs_index` ON Logs(`ke`, `mid`, `time`);',[],function(err){ if(err && err.code !== 'ER_DUP_KEYNAME'){ console.error(err) } },true) //Add index to Monitors table s.sqlQuery('CREATE INDEX `monitors_index` ON Monitors(`ke`, `mode`, `type`, `ext`);',[],function(err){ if(err && err.code !== 'ER_DUP_KEYNAME'){ console.error(err) } },true) //Add index to Timelapse Frames table s.sqlQuery('CREATE INDEX `timelapseframes_index` ON `Timelapse Frames`(`ke`, `mid`, `time`);',[],function(err){ if(err && err.code !== 'ER_DUP_KEYNAME'){ console.error(err) } },true) //add Cloud Videos table, will remove in future s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\',`details` text)' + mySQLtail + ';',[],function(err){ if(err)console.error(err) },true) //add Events Counts table, will remove in future s.sqlQuery('CREATE TABLE IF NOT EXISTS `Events Counts` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext NOT NULL,`time` timestamp NOT NULL DEFAULT current_timestamp(),`end` timestamp NOT NULL DEFAULT current_timestamp(),`count` int(10) NOT NULL DEFAULT 1,`tag` varchar(30) DEFAULT NULL)' + mySQLtail + ';',[],function(err){ if(err && err.code !== 'ER_TABLE_EXISTS_ERROR'){ console.error(err) } s.sqlQuery('ALTER TABLE `Events Counts` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `details`;',[],function(err){ // console.error(err) },true) },true) //add Cloud Timelapse Frames table, will remove in future s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`href` text NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)' + mySQLtail + ';',[],function(err){ if(err)console.error(err) },true) //create Files table var createFilesTableQuery = "CREATE TABLE IF NOT EXISTS `Files` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`name` tinytext NOT NULL,`size` float NOT NULL DEFAULT '0',`details` text NOT NULL,`status` int(1) NOT NULL DEFAULT '0',`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP)" s.sqlQuery(createFilesTableQuery + mySQLtail + ';',[],function(err){ if(err)console.error(err) //add time column to Files table if(config.databaseType === 'sqlite3'){ var aQuery = "ALTER TABLE Files RENAME TO _Files_old;" aQuery += createPresetsTableQuery aQuery += "INSERT INTO Files (`ke`, `mid`, `name`, `details`, `size`, `status`, `time`) SELECT `ke`, `mid`, `name`, `details`, `size`, `status`, `time` FROM _Files_old;COMMIT;DROP TABLE _Files_old;" }else{ s.sqlQuery('ALTER TABLE `Files` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`;',[],function(err){ if(err && err.sqlMessage && err.sqlMessage.indexOf('Duplicate') === -1)console.error(err) },true) } },true) } delete(s.preQueries) } s.sqlQueryBetweenTimesWithPermissions = (options,callback) => { // options = { // table: 'Events Counts', // user: user, // monitorId: req.params.id, // startTime: req.query.start, // endTime: req.query.end, // startTimeOperator: req.query.startOperator, // endTimeOperator: req.query.endOperator, // limit: req.query.limit, // archived: req.query.archived, // endIsStartTo: !!req.query.endIsStartTo, // parseRowDetails: true, // rowName: 'counts' // } const rowName = options.rowName || 'rows' const preliminaryValidationFailed = options.preliminaryValidationFailed || false if(preliminaryValidationFailed){ if(options.noFormat){ callback([]); }else{ callback({ ok: true, [rowName]: [], }) } return } const user = options.user const groupKey = options.groupKey const monitorId = options.monitorId const archived = options.archived const theTableSelected = options.table const endIsStartTo = options.endIsStartTo const userDetails = user.details var endTime = options.endTime var startTimeOperator = options.startTimeOperator var endTimeOperator = options.endTimeOperator var startTime = options.startTime var limitString = `${options.limit}` const monitorRestrictions = s.getMonitorRestrictions(options.user.details,monitorId) getDatabaseRows({ monitorRestrictions: monitorRestrictions, table: theTableSelected, groupKey: groupKey, startDate: startTime, endDate: endTime, startOperator: startTimeOperator, endOperator: endTimeOperator, limit: options.limit, archived: archived, rowType: rowName, endIsStartTo: endIsStartTo },(response) => { const limit = response.limit const r = response[rowName]; if(!r){ callback({ total: 0, limit: response.limit, skip: 0, [rowName]: [] }); return } if(options.parseRowDetails){ r.forEach((row) => { row.details = JSON.parse(row.details) }) } if(options.noCount){ if(options.noFormat){ callback(r) }else{ callback({ ok: true, limit: response.limit, [rowName]: r, endIsStartTo: endIsStartTo }) } }else{ getDatabaseRows({ monitorRestrictions: monitorRestrictions, columns: 'time', count: true, table: theTableSelected, groupKey: groupKey, startDate: startTime, endDate: endTime, startOperator: startTimeOperator, endOperator: endTimeOperator, archived: archived, type: 'count', endIsStartTo: endIsStartTo },(response) => { console.log('count') console.log(response) const count = response.count var skipOver = 0 if(limitString.indexOf(',') > -1){ skipOver = parseInt(limitString.split(',')[0]) limitString = parseInt(limitString.split(',')[1]) }else{ limitString = parseInt(limitString) } callback({ total: response['count(*)'], limit: response.limit, skip: skipOver, [rowName]: r, endIsStartTo: endIsStartTo }) }) } }) } }