LocalStorageDB

LocalStorageDB Script

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/14976/94706/LocalStorageDB.js

/*
	Kailash Nadh (http://nadh.in)

	localStorageDB v 2.3.1
	A simple database layer for localStorage

	v 2.3.1 Mar 2015
	v 2.3 Feb 2014 Contribution: Christian Kellner (http://orange-coding.net)
	v 2.2 Jan 2014 Contribution: Andy Hawkins (http://a904guy.com) 
	v 2.1 Nov 2013
	v 2.0 June 2013
	v 1.9 Nov 2012

	License	:	MIT License
*/

!(function (_global, undefined) {
	function localStorageDB(db_name, engine) {
		var db_prefix = 'db_',
			db_id = db_prefix + db_name,
			db_new = false,	// this flag determines whether a new database was created during an object initialisation
			db = null;

			try {
				var storage = (engine == sessionStorage ? sessionStorage: localStorage);
			} catch(e) { // ie8 hack
				var storage = engine;
			}

		// if the database doesn't exist, create it
		db = storage[ db_id ];
		if( !( db && (db = JSON.parse(db)) && db.tables && db.data ) ) {
			if(!validateName(db_name)) {
				error("The name '" + db_name + "' contains invalid characters");
			} else {
				db = {tables: {}, data: {}};
				commit();
				db_new = true;
			}
		}


		// ______________________ private methods

		// _________ database functions
		// drop the database
		function drop() {
			if(storage.hasOwnProperty(db_id)) {
				delete storage[db_id];
			}
			db = null;
		}

		// number of tables in the database
		function tableCount() {
			var count = 0;
			for(var table in db.tables) {
				if( db.tables.hasOwnProperty(table) ) {
					count++;
				}
			}
			return count;
		}

		// _________ table functions

		// returns all fields in a table.
		function tableFields(table_name) {
			return db.tables[table_name].fields;
		}

		// check whether a table exists
		function tableExists(table_name) {
			return db.tables[table_name] ? true : false;
		}

		// check whether a table exists, and if not, throw an error
		function tableExistsWarn(table_name) {
			if(!tableExists(table_name)) {
				error("The table '" + table_name + "' does not exist");
			}
		}

		// check whether a table column exists
		function columnExists(table_name, field_name) {
			var exists = false;
			var table_fields = db.tables[table_name].fields;
			for(var field in table_fields){
				if(table_fields[field] == field_name)
				{
					exists = true;
					break;
				}
			}
			return exists;
		}

		// create a table
		function createTable(table_name, fields) {
			db.tables[table_name] = {fields: fields, auto_increment: 1};
			db.data[table_name] = {};
		}

		// drop a table
		function dropTable(table_name) {
			delete db.tables[table_name];
			delete db.data[table_name];
		}

		// empty a table
		function truncate(table_name) {
			db.tables[table_name].auto_increment = 1;
			db.data[table_name] = {};
		}

		//alter a table
		function alterTable(table_name, new_fields, default_values){
			db.tables[table_name].fields = db.tables[table_name].fields.concat(new_fields);

			// insert default values in existing table
			if(typeof default_values != "undefined") {
				// loop through all the records in the table
				for(var ID in db.data[table_name]) {
					if( !db.data[table_name].hasOwnProperty(ID) ) {
						continue;
					}
					for(var field in new_fields) {
						if(typeof default_values == "object") {
							db.data[table_name][ID][new_fields[field]] = default_values[new_fields[field]];
						} else {
							db.data[table_name][ID][new_fields[field]] = default_values;
						}
					}
				}
			}
		}

		// number of rows in a table
		function rowCount(table_name) {
			var count = 0;
			for(var ID in db.data[table_name]) {
				if( db.data[table_name].hasOwnProperty(ID) ) {
					count++;
				}
			}
			return count;
		}

		// insert a new row
		function insert(table_name, data) {
			data.ID = db.tables[table_name].auto_increment;
			db.data[table_name][ db.tables[table_name].auto_increment ] = data;
			db.tables[table_name].auto_increment++;
			return data.ID;
		}

		// select rows, given a list of IDs of rows in a table
		function select(table_name, ids, start, limit, sort, distinct) {
			var ID = null, results = [], row = null;

			for(var i=0; i<ids.length; i++) {
				ID = ids[i];
				row = db.data[table_name][ID];
				results.push( clone(row) );
			}

			// there are sorting params
			if(sort && sort instanceof Array) {
				for(var i=0; i<sort.length; i++) {
					results.sort(sort_results(sort[i][0], sort[i].length > 1 ? sort[i][1] : null));
				}
			}
			
			// distinct params
			if(distinct && distinct instanceof Array) {
				for(var j=0; j<distinct.length; j++) {
					var seen = {}, d = distinct[j];

					for(var i=0; i<results.length; i++) {
						if(results[i] === undefined) {
							continue;
						}

						if(results[i].hasOwnProperty(d) && seen.hasOwnProperty(results[i][d])) {
							delete(results[i]);
						} else {
							seen[results[i][d]] = 1;
						}
					}
				}

				// can't use .filter(ie8)
				var new_results = [];
				for(var i=0; i<results.length; i++) {
					if(results[i] !== undefined) {
						new_results.push(results[i]);
					}
				}

				results = new_results;
			}

			// limit and offset
			start = start && typeof start === "number" ? start : null;
			limit = limit && typeof limit === "number" ? limit : null;

			if(start && limit) {
				results = results.slice(start, start+limit);
			} else if(start) {
				results = results.slice(start);
			} else if(limit) {
				results = results.slice(start, limit);
			}

			return results;
		}

		// sort a result set
		function sort_results(field, order) {
			return function(x, y) {
				// case insensitive comparison for string values
				var v1 = typeof(x[field]) === "string" ? x[field].toLowerCase() : x[field],
					v2 = typeof(y[field]) === "string" ? y[field].toLowerCase() : y[field];

				if(order === "DESC") {
					return v1 == v2 ? 0 : (v1 < v2 ? 1 : -1);
				} else {
					return v1 == v2 ? 0 : (v1 > v2 ? 1 : -1);
				}
			};
		}

		// select rows in a table by field-value pairs, returns the IDs of matches
		function queryByValues(table_name, data) {
			var result_ids = [],
				exists = false,
				row = null;

			// loop through all the records in the table, looking for matches
			for(var ID in db.data[table_name]) {
				if( !db.data[table_name].hasOwnProperty(ID) ) {
					continue;
				}

				row = db.data[table_name][ID];
				exists = true;

				for(var field in data) {
					if( !data.hasOwnProperty(field) ) {
						continue;
					}

					if(typeof data[field] == 'string') {	// if the field is a string, do a case insensitive comparison
						if( row[field] === null || row[field].toString().toLowerCase() != data[field].toString().toLowerCase() ) {
							exists = false;
							break;
						}
					} else {
						if(row[field] != data[field]) {
							exists = false;
							break;
						}
					}
				}
				if(exists) {
					result_ids.push(ID);
				}
			}

			return result_ids;
		}

		// select rows in a table by a function, returns the IDs of matches
		function queryByFunction(table_name, query_function) {
			var result_ids = [],
				exists = false,
				row = null;

			// loop through all the records in the table, looking for matches
			for(var ID in db.data[table_name]) {
				if( !db.data[table_name].hasOwnProperty(ID) ) {
					continue;
				}

				row = db.data[table_name][ID];

				if( query_function( clone(row) ) == true ) {	// it's a match if the supplied conditional function is satisfied
					result_ids.push(ID);
				}
			}

			return result_ids;
		}

		// return all the IDs in a table
		function getIDs(table_name) {
			var result_ids = [];

			for(var ID in db.data[table_name]) {
				if( db.data[table_name].hasOwnProperty(ID) ) {
					result_ids.push(ID);
				}
			}
			return result_ids;
		}

		// delete rows, given a list of their IDs in a table
		function deleteRows(table_name, ids) {
			for(var i=0; i<ids.length; i++) {
				if( db.data[table_name].hasOwnProperty(ids[i]) ) {
					delete db.data[table_name][ ids[i] ];
				}
			}
			return ids.length;
		}

		// update rows
		function update(table_name, ids, update_function) {
			var ID = '', num = 0;

			for(var i=0; i<ids.length; i++) {
				ID = ids[i];

				var updated_data = update_function( clone(db.data[table_name][ID]) );

				if(updated_data) {
					delete updated_data['ID']; // no updates possible to ID

					var new_data = db.data[table_name][ID];
					// merge updated data with existing data
					for(var field in updated_data) {
						if( updated_data.hasOwnProperty(field) ) {
							new_data[field] = updated_data[field];
						}
					}

					db.data[table_name][ID] = validFields(table_name, new_data);
					num++;
				}
			}
			return num;
		}

		// commit the database to localStorage
		function commit() {
			try {
				storage.setItem(db_id, JSON.stringify(db));
				return true;
			} catch(e) {
				return false;
			}
		}

		// serialize the database
		function serialize() {
			return JSON.stringify(db);
		}

		// throw an error
		function error(msg) {
			throw new Error(msg);
		}

		// clone an object
		function clone(obj) {
			var new_obj = {};
			for(var key in obj) {
				if( obj.hasOwnProperty(key) ) {
					new_obj[key] = obj[key];
				}
			}
			return new_obj;
		}

		// validate db, table, field names (alpha-numeric only)
		function validateName(name) {
			return name.toString().match(/[^a-z_0-9]/ig) ? false : true;
		}

		// given a data list, only retain valid fields in a table
		function validFields(table_name, data) {
			var field = '', new_data = {};

			for(var i=0; i<db.tables[table_name].fields.length; i++) {
				field = db.tables[table_name].fields[i];

				if (data[field] !== undefined) {
					new_data[field] = data[field];
				}
			}
			return new_data;
		}

		// given a data list, populate with valid field names of a table
		function validateData(table_name, data) {
			var field = '', new_data = {};
			for(var i=0; i<db.tables[table_name].fields.length; i++) {
				field = db.tables[table_name].fields[i];
				new_data[field] = (data[field] === null || data[field] === undefined) ? null : data[field];
			}
			return new_data;
		}

		// ______________________ public methods

		return {
			// commit the database to localStorage
			commit: function() {
				return commit();
			},

			// is this instance a newly created database?
			isNew: function() {
				return db_new;
			},

			// delete the database
			drop: function() {
				drop();
			},

			// serialize the database
			serialize: function() {
				return serialize();
			},

			// check whether a table exists
			tableExists: function(table_name) {
				return tableExists(table_name);
			},

			// list of keys in a table
			tableFields: function(table_name) {
				return tableFields(table_name);
			},

			// number of tables in the database
			tableCount: function() {
				return tableCount();
			},

			columnExists: function(table_name, field_name){
				return columnExists(table_name, field_name);
			},

			// create a table
			createTable: function(table_name, fields) {
				var result = false;
				if(!validateName(table_name)) {
					error("The database name '" + table_name + "' contains invalid characters.");
				} else if(this.tableExists(table_name)) {
					error("The table name '" + table_name + "' already exists.");
				} else {
					// make sure field names are valid
					var is_valid = true;
					for(var i=0; i<fields.length; i++) {
						if(!validateName(fields[i])) {
							is_valid = false;
							break;
						}
					}

					if(is_valid) {
						// cannot use indexOf due to <IE9 incompatibility
						// de-duplicate the field list
						var fields_literal = {};
						for(var i=0; i<fields.length; i++) {
							fields_literal[ fields[i] ] = true;
						}
						delete fields_literal['ID']; // ID is a reserved field name

						fields = ['ID'];
						for(var field in fields_literal) {
							if( fields_literal.hasOwnProperty(field) ) {
								fields.push(field);
							}
						}

						createTable(table_name, fields);
						result = true;
					} else {
						error("One or more field names in the table definition contains invalid characters");
					}
				}

				return result;
			},

			// Create a table using array of Objects @ [{k:v,k:v},{k:v,k:v},etc]
			createTableWithData: function(table_name, data) {
				if(typeof data !== 'object' || !data.length || data.length < 1) {
					error("Data supplied isn't in object form. Example: [{k:v,k:v},{k:v,k:v} ..]");
				}

				var fields = Object.keys(data[0]);

				// create the table
				if( this.createTable(table_name, fields) ) {
					this.commit();

					// populate
					for (var i=0; i<data.length; i++) {
						if( !insert(table_name, data[i]) ) {
							error("Failed to insert record: [" + JSON.stringify(data[i]) + "]");
						}
					}
					this.commit();
				}
				return true;
			},

			// drop a table
			dropTable: function(table_name) {
				tableExistsWarn(table_name);
				dropTable(table_name);
			},

			// empty a table
			truncate: function(table_name) {
				tableExistsWarn(table_name);
				truncate(table_name);
			},

			// alter a table
			alterTable: function(table_name, new_fields, default_values) {
				var result = false;
				if(!validateName(table_name)) {
					error("The database name '" + table_name + "' contains invalid characters");
				} else {
					if(typeof new_fields == "object") {
						// make sure field names are valid
						var is_valid = true;
						for(var i=0; i<new_fields.length; i++) {
							if(!validateName(new_fields[i])) {
								is_valid = false;
								break;
							}
						}

						if(is_valid) {
							// cannot use indexOf due to <IE9 incompatibility
							// de-duplicate the field list
							var fields_literal = {};
							for(var i=0; i<new_fields.length; i++) {
								fields_literal[ new_fields[i] ] = true;
							}
							delete fields_literal['ID']; // ID is a reserved field name

							new_fields = [];
							for(var field in fields_literal) {
								if( fields_literal.hasOwnProperty(field) ) {
									new_fields.push(field);
								}
							}

							alterTable(table_name, new_fields, default_values);
							result = true;
						} else {
							error("One or more field names in the table definition contains invalid characters");
						}
					} else if(typeof new_fields == "string") {
						if(validateName(new_fields)) {
							var new_fields_array = [];
							new_fields_array.push(new_fields);
							alterTable(table_name, new_fields_array, default_values);
							result = true;
						} else {
							error("One or more field names in the table definition contains invalid characters");
						}
					}
				}

				return result;
			},

			// number of rows in a table
			rowCount: function(table_name) {
				tableExistsWarn(table_name);
				return rowCount(table_name);
			},

			// insert a row
			insert: function(table_name, data) {
				tableExistsWarn(table_name);
				return insert(table_name, validateData(table_name, data) );
			},

			// insert or update based on a given condition
			insertOrUpdate: function(table_name, query, data) {
				tableExistsWarn(table_name);

				var result_ids = [];
				if(!query) {
					result_ids = getIDs(table_name);				// there is no query. applies to all records
				} else if(typeof query == 'object') {				// the query has key-value pairs provided
					result_ids = queryByValues(table_name, validFields(table_name, query));
				} else if(typeof query == 'function') {				// the query has a conditional map function provided
					result_ids = queryByFunction(table_name, query);
				}

				// no existing records matched, so insert a new row
				if(result_ids.length == 0) {
					return insert(table_name, validateData(table_name, data) );
				} else {
					var ids = [];
					for(var n=0; n<result_ids.length; n++) {
						update(table_name, result_ids, function(o) {
							ids.push(o.ID);
							return data;
						});
					}

					return ids;
				}
			},

			// update rows
			update: function(table_name, query, update_function) {
				tableExistsWarn(table_name);

				var result_ids = [];
				if(!query) {
					result_ids = getIDs(table_name);				// there is no query. applies to all records
				} else if(typeof query == 'object') {				// the query has key-value pairs provided
					result_ids = queryByValues(table_name, validFields(table_name, query));
				} else if(typeof query == 'function') {				// the query has a conditional map function provided
					result_ids = queryByFunction(table_name, query);
				}
				return update(table_name, result_ids, update_function);
			},

			// select rows
			query: function(table_name, query, limit, start, sort, distinct) {
				tableExistsWarn(table_name);

				var result_ids = [];
				if(!query) {
					result_ids = getIDs(table_name, limit, start); // no conditions given, return all records
				} else if(typeof query == 'object') {			// the query has key-value pairs provided
					result_ids = queryByValues(table_name, validFields(table_name, query), limit, start);
				} else if(typeof query == 'function') {		// the query has a conditional map function provided
					result_ids = queryByFunction(table_name, query, limit, start);
				}

				return select(table_name, result_ids, start, limit, sort, distinct);
			},

			// alias for query() that takes a dict of params instead of positional arrguments
			queryAll: function(table_name, params) {
				if(!params) {
					return this.query(table_name)
				} else {
					return this.query(table_name,
						params.hasOwnProperty('query') ? params.query : null,
						params.hasOwnProperty('limit') ? params.limit : null,
						params.hasOwnProperty('start') ? params.start : null,
						params.hasOwnProperty('sort') ? params.sort : null,
						params.hasOwnProperty('distinct') ? params.distinct : null
					);
				}
			},

			// delete rows
			deleteRows: function(table_name, query) {
				tableExistsWarn(table_name);

				var result_ids = [];
				if(!query) {
					result_ids = getIDs(table_name);
				} else if(typeof query == 'object') {
					result_ids = queryByValues(table_name, validFields(table_name, query));
				} else if(typeof query == 'function') {
					result_ids = queryByFunction(table_name, query);
				}
				return deleteRows(table_name, result_ids);
			}
		}
	}

	// make amd compatible
	if(typeof define === 'function' && define.amd) {
		define(function() {
			return localStorageDB;
		});
	} else {
		_global['localStorageDB'] = localStorageDB;
	}

}(window));