cordova-plugin-nano-sqlite
Version:
NanoSQL SQLite Plugin: exposes a well documented, easy to use API for SQLite. Uses IndexedDB/WebSQL when testing in the browser, then uses SQLite on the device with the exact same API. Includes typescript support, an ORM and undo/redo built in.
292 lines (252 loc) • 10.7 kB
text/typescript
import { NanoSQLStorageAdapter, DBKey, DBRow, _NanoSQLStorage } from "nano-sql/lib/database/storage";
import { DataModel } from "nano-sql/lib/index";
import { setFast } from "lie-ts";
import { StdObject, hash, fastALL, fastCHAIN, splitArr, deepFreeze, uuid, timeid, _assign, generateID, isAndroid, intersect } from "nano-sql/lib/utilities";
import { DatabaseIndex } from "nano-sql/lib/database/db-idx";
declare const cordova: any;
export interface CordovaSQLiteDB {
sqlBatch: (queries: (string|any[])[], onSuccess: () => void, onFail: (err: Error) => void) => void;
executeSql: (sql: string, vars: any[], onSuccess: (result: SQLResultSet) => void, onFail: (err: Error) => void) => void;
}
export const getMode = () => {
if (typeof cordova !== "undefined" && window["sqlitePlugin"]) {
if (window["device"] && window["device"].platform && window["device"].platform !== "browser") {
return new SQLiteStore();
} else {
return "PERM";
}
} else {
return "PERM";
}
};
/**
* Handles WebSQL persistent storage
*
* @export
* @class _SyncStore
* @implements {NanoSQLStorageAdapter}
*/
// tslint:disable-next-line
export class SQLiteStore implements NanoSQLStorageAdapter {
private _pkKey: {
[tableName: string]: string;
};
private _dbIndex: {
[tableName: string]: DatabaseIndex;
};
private _pkIsNum: {
[tableName: string]: boolean;
};
private _id: string;
private _db: CordovaSQLiteDB;
constructor() {
this._pkKey = {};
this._dbIndex = {};
this._pkIsNum = {};
}
public setID(id: string) {
this._id = id;
}
public connect(complete: () => void) {
if (!window["sqlitePlugin"]) {
throw Error("SQLite plugin not installed or nanoSQL plugin called before device ready!");
}
console.log(`NanoSQL "${this._id}" using SQLite.`);
this._db = window["sqlitePlugin"].openDatabase({name: `${this._id}_db`, location: "default"});
fastALL(Object.keys(this._pkKey), (table, i, nextKey) => {
this._sql(true, `CREATE TABLE IF NOT EXISTS "${table}" (id ${this._pkIsNum[table] ? "REAL" : "TEXT"} PRIMARY KEY UNIQUE, data TEXT)`, [], () => {
this._sql(false, `SELECT id FROM "${table}"`, [], (result) => {
let idx: any[] = [];
for (let i = 0; i < result.rows.length; i++) {
idx.push(result.rows.item(i).id);
}
// SQLite doesn't sort primary keys, but the system depends on sorted primary keys
idx = idx.sort();
this._dbIndex[table].set(idx);
nextKey();
});
});
}).then(complete);
}
/**
* Table names can't be escaped easily in the queries. (or I can't find out how to)
* This function gaurantees any provided table is a valid table name being used by the system.
*
* @private
* @param {string} table
* @returns {string}
* @memberof _WebSQLStore
*/
private _chkTable(table: string): string {
if (Object.keys(this._dbIndex).indexOf(table) === -1) {
throw Error("No table " + table + " found!");
} else {
return `"${table}"`;
}
}
public makeTable(tableName: string, dataModels: DataModel[]): void {
this._dbIndex[tableName] = new DatabaseIndex();
dataModels.forEach((d) => {
if (d.props && intersect(["pk", "pk()"], d.props)) {
this._dbIndex[tableName].pkType = d.type;
this._pkKey[tableName] = d.key;
if (d.props && intersect(["ai", "ai()"], d.props) && (d.type === "int" || d.type === "number")) {
this._dbIndex[tableName].doAI = true;
}
if (["number", "float", "int"].indexOf(this._dbIndex[tableName].pkType) !== -1) {
this._pkIsNum[tableName] = true;
}
if (d.props && intersect(["ns", "ns()"], d.props) || ["uuid", "timeId", "timeIdms"].indexOf(this._dbIndex[tableName].pkType) !== -1) {
this._dbIndex[tableName].sortIndex = false;
}
}
});
}
public _sql(allowWrite: boolean, sql: string, args: any[], complete: (rows: SQLResultSet) => void): void {
this._db.executeSql(sql, args, (result) => {
complete(result);
}, (err) => {
console.error(sql, args, err);
return false;
});
}
public write(table: string, pk: DBKey | null, data: DBRow, complete: (row: DBRow) => void): void {
pk = pk || generateID(this._dbIndex[table].pkType, this._dbIndex[table].ai) as DBKey;
if (!pk) {
throw new Error("Can't add a row without a primary key!");
}
this._sql(false, `SELECT id FROM ${this._chkTable(table)} WHERE id = ?`, [pk], (result) => {
if (!result.rows.length) {
this._dbIndex[table].add(pk);
const r = {
...data,
[this._pkKey[table]]: pk,
};
this._sql(true, `INSERT into ${this._chkTable(table)} (id, data) VALUES (?, ?)`, [pk, JSON.stringify(r)], (result) => {
complete(r);
});
} else {
const r = {
...data,
[this._pkKey[table]]: pk,
};
this._sql(true, `UPDATE ${this._chkTable(table)} SET data = ? WHERE id = ?`, [JSON.stringify(r), pk], () => {
complete(r);
});
}
});
}
public delete(table: string, pk: DBKey, complete: () => void): void {
let pos = this._dbIndex[table].indexOf(pk);
if (pos !== -1) {
this._dbIndex[table].remove(pk);
}
this._sql(true, `DELETE FROM ${this._chkTable(table)} WHERE id = ?`, [pk], () => {
complete();
});
}
public read(table: string, pk: DBKey, callback: (row: DBRow) => void): void {
this._sql(false, `SELECT data FROM ${this._chkTable(table)} WHERE id = ?`, [pk], (result) => {
if (result.rows.length) {
callback(JSON.parse(result.rows.item(0).data));
} else {
callback(undefined as any);
}
});
}
public rangeRead(table: string, rowCallback: (row: DBRow, idx: number, nextRow: () => void) => void, complete: () => void, from?: any, to?: any, usePK?: boolean): void {
let keys = this._dbIndex[table].keys();
const usefulValues = [typeof from, typeof to].indexOf("undefined") === -1;
let ranges: number[] = usefulValues ? [from as any, to as any] : [];
if (!keys.length) {
complete();
return;
}
if (!(usePK && usefulValues) && this._dbIndex[table].sortIndex === false) {
keys = keys.sort();
}
if (usePK && usefulValues) {
ranges = ranges.map(r => this._dbIndex[table].getLocation(r));
}
let idx = ranges[0] || 0;
let getKeys: any[] = [];
let startIDX = ranges[0];
let stmnt = "SELECT data from " + this._chkTable(table);
// SQLite doesn't handle BETWEEN statements gracefully with primary keys, always doing a full table scan.
// So we take the index of the table (which is in js memory) and convert it into an IN statement meaning SQLite
// can go directly to the rows we need without a full table scan.
if (ranges.length) {
const t = typeof keys[startIDX] === "number";
while (startIDX <= ranges[1]) {
getKeys.push(t ? keys[startIDX] : `"${keys[startIDX]}"`);
startIDX++;
}
stmnt += ` WHERE id IN (${getKeys.map(k => "?").join(", ")})`;
}
stmnt += " ORDER BY id";
if (getKeys.length) {
this.batchRead(table, getKeys, (result: any[]) => {
let i = 0;
const getRow = () => {
if (result.length > i) {
rowCallback(result[i], idx, () => {
idx++;
i++;
i % 500 === 0 ? setFast(getRow) : getRow(); // handle maximum call stack error
});
} else {
complete();
}
};
getRow();
});
} else {
this._sql(false, stmnt, [], (result) => {
let i = 0;
const getRow = () => {
if (result.rows.length > i) {
rowCallback(JSON.parse(result.rows.item(i).data), idx, () => {
idx++;
i++;
i % 500 === 0 ? setFast(getRow) : getRow(); // handle maximum call stack error
});
} else {
complete();
}
};
getRow();
});
}
}
public batchRead(table: string, pks: any[], callback: (rows: any[]) => void) {
const useKeys = splitArr(pks, 500);
let rows: any[] = [];
fastCHAIN(useKeys, (keys, i, next) => {
this._sql(false, `SELECT data from ${this._chkTable(table)} WHERE id IN (${keys.map(p => "?").join(", ")}) ORDER BY id`, keys.map(p => typeof p === "string" ? `'${p}'` : p), (result) => {
let i = result.rows.length;
while (i--) {
rows.push(JSON.parse(result.rows.item(i).data));
}
next();
});
}).then(() => {
callback(rows);
});
}
public drop(table: string, callback: () => void): void {
let idx = new DatabaseIndex();
idx.doAI = this._dbIndex[table].doAI;
this._dbIndex[table] = idx;
this._sql(true, `DELETE FROM ${this._chkTable(table)}`, [], (rows) => {
callback();
});
}
public getIndex(table: string, getLength: boolean, complete: (index) => void): void {
complete(getLength ? this._dbIndex[table].keys().length : this._dbIndex[table].keys());
}
public destroy(complete: () => void) {
fastALL(Object.keys(this._dbIndex), (table, i, done) => {
this.drop(table, done);
}).then(complete);
}
}