├── .npmignore ├── .gitignore ├── dist ├── types.js ├── pull.js ├── pull.d.ts ├── promises.d.ts ├── push.d.ts ├── index.d.ts ├── indexeddb-pull.d.ts ├── index.js ├── push.js ├── db.d.ts ├── promises.js ├── indexeddb-pull.js ├── store.d.ts ├── db.js ├── types.d.ts └── store.js ├── tsconfig.json ├── src ├── pull.ts ├── index.ts ├── promises.ts ├── push.ts ├── indexeddb-pull.ts ├── db.ts ├── types.ts └── store.ts ├── test ├── fixtures │ ├── send-request.sh │ ├── api.js │ ├── samples.js │ ├── custom-sync-sample.js │ └── sync-server.js ├── custom-sync.js ├── local-sync.js ├── crud.js └── select.js ├── Makefile ├── package.json ├── examples ├── upgrade.js └── select-multiple-fields.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | test.js 3 | example 4 | examples 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/pull.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class Pull { 4 | receive(updates, callback) { } 5 | } 6 | exports.default = Pull; 7 | -------------------------------------------------------------------------------- /dist/pull.d.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | export default class Pull implements types.IPull { 3 | receive(updates: types.IUpdate[] | types.IUpdate, callback: types.ICallback): void; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dist/promises.d.ts: -------------------------------------------------------------------------------- 1 | export declare function returnResult(err: any, request: any, callback: any, resolve?: any, reject?: any, push?: any): void; 2 | export declare function createPromise(args: any, fn: any): any; 3 | -------------------------------------------------------------------------------- /src/pull.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types" 2 | 3 | export default class Pull implements types.IPull { 4 | receive( 5 | updates: types.IUpdate[] | types.IUpdate, 6 | callback: types.ICallback 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/send-request.sh: -------------------------------------------------------------------------------- 1 | curl -XPOST -H "Content-type: application/json" -d '[{ "action": "add", "store": "people", "id": "azer@roadbeats.com", "doc": {"name":"azer","email":"azer@roadbeats.com","age":29,"tags":["software","travel"]} }]' http://localhost:3000 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN=$(shell pwd)/node_modules/.bin 2 | 3 | compile: clean 4 | @echo " > Compiling..." 5 | @$(BIN)/tsc 6 | 7 | watch: compile 8 | @echo " > Watching for changes..." 9 | @yolo -i src -c "make compile" 10 | 11 | clean: 12 | @echo " > Cleaning..." 13 | @rm -rf ./dist 14 | -------------------------------------------------------------------------------- /dist/push.d.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | export default class Push implements types.IPush { 3 | targets: types.IDB[]; 4 | constructor(); 5 | hook(target: types.IDB): void; 6 | publish(updates: types.IUpdate | types.IUpdate[], callback: types.IMultipleErrorsCallback): void; 7 | } 8 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import DB from "./db"; 2 | import Push from "./push"; 3 | import Pull from "./pull"; 4 | import IndexedDBPull from "./indexeddb-pull"; 5 | import * as types from "./types"; 6 | export { Push, Pull, IndexedDBPull, types }; 7 | export * from "./types"; 8 | export default function createDB(name: string, options: types.IDBOptions): DB; 9 | export declare function createTestingDB(options?: types.IDBOptions): DB; 10 | -------------------------------------------------------------------------------- /dist/indexeddb-pull.d.ts: -------------------------------------------------------------------------------- 1 | import Pull from "./pull"; 2 | import * as types from "./types"; 3 | export default class IndexedDBPull extends Pull implements types.IIndexedDBPull { 4 | db: types.IDB; 5 | private _stores; 6 | constructor(db: types.IDB); 7 | stores(): types.IStoreMap; 8 | receive(updates: types.IUpdate[] | types.IUpdate, callback: types.ICallback): void; 9 | copyUpdate(update: types.IUpdate, callback: types.ICallback): void; 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indexeddb", 3 | "version": "0.1.0", 4 | "description": "Minimalistic wrapper for HTML5 IndexedDB API", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "test": "node test -b" 8 | }, 9 | "repository": { 10 | "url": "git@github.com:azer/indexeddb.git", 11 | "type": "git" 12 | }, 13 | "author": "azer", 14 | "license": "BSD", 15 | "devDependencies": { 16 | "prova": "github:azer/prova", 17 | "typescript": "^3.1.1" 18 | }, 19 | "dependencies": { 20 | "pubsub": "github:azer/pubsub" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DB from "./db" 2 | import Store from "./store" 3 | import Push from "./push" 4 | import Pull from "./pull" 5 | import IndexedDBPull from "./indexeddb-pull" 6 | import * as types from "./types" 7 | 8 | export { Push, Pull, IndexedDBPull, types } 9 | export * from "./types" 10 | 11 | export default function createDB(name: string, options: types.IDBOptions) { 12 | return new DB(name, options) 13 | } 14 | 15 | export function createTestingDB(options?: types.IDBOptions) { 16 | return new DB( 17 | `testing-${Math.floor(Math.random() * 9999999)}`, 18 | options || { 19 | version: 1 20 | } 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/promises.ts: -------------------------------------------------------------------------------- 1 | export function returnResult(err, request, callback, resolve?, reject?, push?) { 2 | if (err) { 3 | ;(callback || reject)(err) 4 | } 5 | 6 | request.onerror = e => { 7 | if (callback) { 8 | callback(e.target.error) 9 | } else { 10 | reject(e.target.error) 11 | } 12 | } 13 | 14 | request.onsuccess = e => { 15 | if (callback) { 16 | callback(undefined, e.target.result) 17 | } else { 18 | resolve(e.target.result) 19 | } 20 | 21 | if (push) push(e.target.result) 22 | } 23 | } 24 | 25 | export function createPromise(args, fn) { 26 | const cb = args[args.length - 1] 27 | if (typeof cb === "function") return fn(cb) 28 | 29 | return new Promise((resolve, reject) => fn(undefined, resolve, reject)) 30 | } 31 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const db_1 = require("./db"); 4 | const push_1 = require("./push"); 5 | exports.Push = push_1.default; 6 | const pull_1 = require("./pull"); 7 | exports.Pull = pull_1.default; 8 | const indexeddb_pull_1 = require("./indexeddb-pull"); 9 | exports.IndexedDBPull = indexeddb_pull_1.default; 10 | const types = require("./types"); 11 | exports.types = types; 12 | function createDB(name, options) { 13 | return new db_1.default(name, options); 14 | } 15 | exports.default = createDB; 16 | function createTestingDB(options) { 17 | return new db_1.default(`testing-${Math.floor(Math.random() * 9999999)}`, options || { 18 | version: 1 19 | }); 20 | } 21 | exports.createTestingDB = createTestingDB; 22 | -------------------------------------------------------------------------------- /dist/push.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class Push { 4 | constructor() { 5 | this.targets = []; 6 | } 7 | hook(target) { 8 | this.targets.push(target); 9 | } 10 | publish(updates, callback) { 11 | const errors = []; 12 | var self = this; 13 | next(0); 14 | function next(i) { 15 | if (i >= self.targets.length) { 16 | callback(errors.length ? errors : undefined); 17 | return; 18 | } 19 | self.targets[i].pull.receive(updates, err => { 20 | if (err) { 21 | errors.push(err); 22 | } 23 | next(i + 1); 24 | }); 25 | } 26 | } 27 | } 28 | exports.default = Push; 29 | -------------------------------------------------------------------------------- /dist/db.d.ts: -------------------------------------------------------------------------------- 1 | import Store from "./store"; 2 | import * as types from "./types"; 3 | export default class DB implements types.IDB { 4 | idb: IDBDatabase | null; 5 | name: string; 6 | version: number; 7 | stores: types.IStore[]; 8 | push: types.IPush; 9 | pull: types.IPull; 10 | constructor(name: string, options: types.IDBOptions); 11 | close(): void; 12 | delete(): Promise; 13 | onUpgradeNeeded(event: Event): void; 14 | open(callback: (error?: Error, db?: IDBDatabase) => void): void; 15 | ready(callback: (error?: Error, db?: IDBDatabase) => void): void; 16 | store(name: string, options: types.IStoreDefinition): Store; 17 | sync(target: types.IDB): void; 18 | transaction(storeNames: string[], type: IDBTransactionMode, callback: (error?: Error, transaction?: IDBTransaction) => void): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/push.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types" 2 | 3 | export default class Push implements types.IPush { 4 | public targets: types.IDB[] 5 | constructor() { 6 | this.targets = [] 7 | } 8 | 9 | hook(target: types.IDB) { 10 | this.targets.push(target) 11 | } 12 | 13 | publish( 14 | updates: types.IUpdate | types.IUpdate[], 15 | callback: types.IMultipleErrorsCallback 16 | ) { 17 | const errors: Error[] = [] 18 | var self = this 19 | 20 | next(0) 21 | 22 | function next(i) { 23 | if (i >= self.targets.length) { 24 | callback(errors.length ? errors : undefined) 25 | return 26 | } 27 | 28 | self.targets[i].pull.receive(updates, err => { 29 | if (err) { 30 | errors.push(err) 31 | } 32 | 33 | next(i + 1) 34 | }) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dist/promises.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function returnResult(err, request, callback, resolve, reject, push) { 4 | if (err) { 5 | ; 6 | (callback || reject)(err); 7 | } 8 | request.onerror = e => { 9 | if (callback) { 10 | callback(e.target.error); 11 | } 12 | else { 13 | reject(e.target.error); 14 | } 15 | }; 16 | request.onsuccess = e => { 17 | if (callback) { 18 | callback(undefined, e.target.result); 19 | } 20 | else { 21 | resolve(e.target.result); 22 | } 23 | if (push) 24 | push(e.target.result); 25 | }; 26 | } 27 | exports.returnResult = returnResult; 28 | function createPromise(args, fn) { 29 | const cb = args[args.length - 1]; 30 | if (typeof cb === "function") 31 | return fn(cb); 32 | return new Promise((resolve, reject) => fn(undefined, resolve, reject)); 33 | } 34 | exports.createPromise = createPromise; 35 | -------------------------------------------------------------------------------- /examples/upgrade.js: -------------------------------------------------------------------------------- 1 | const createDB = require("../"); 2 | const version = Number(localStorage['version'] || 1) 3 | localStorage['version'] = version + 1 4 | 5 | console.log('Current version: ', version) 6 | 7 | const db = window.db = createDB('upgrade-test', { 8 | version 9 | }) 10 | 11 | const fruits = db.store('fruits', { 12 | key: { autoIncrement: true, keyPath: 'id' }, 13 | upgrade: upgrade 14 | }) 15 | 16 | function upgrade (store) { 17 | console.log('upgrading') 18 | store.createIndex('name', { unique : false }) 19 | } 20 | 21 | fruits.add({ name: prompt('Add a fruit: ') }, (error, id) => { 22 | if (error) return console.error(error) 23 | 24 | console.log('added', id) 25 | 26 | console.log('Rows =>') 27 | 28 | fruits.selectRange('name', { from: 'a', to: 'z' }, (error, result) => { 29 | if (error) return console.error(error) 30 | if (result === null) return console.error(error, result) 31 | 32 | console.log(' Id:', result.value.id) 33 | console.log(' Name:', result.value.name) 34 | 35 | console.log(result) 36 | result.continue() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/fixtures/api.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get, 3 | post, 4 | put, 5 | delete: rdelete 6 | } 7 | 8 | function sendJSON (method, url, data, callback) { 9 | var xmlhttp = new XMLHttpRequest(); 10 | xmlhttp.open(method, url); 11 | 12 | if (data) { 13 | xmlhttp.setRequestHeader("Content-Type", "application/json"); 14 | xmlhttp.send(JSON.stringify(data)); 15 | } else { 16 | xmlhttp.send(null); 17 | } 18 | 19 | xmlhttp.onreadystatechange = () => { 20 | if (xmlhttp.readyState !== 4) { 21 | return 22 | } 23 | 24 | const parsed = JSON.parse(xmlhttp.responseText) 25 | if (xmlhttp.status >= 300 && parsed && parsed.error) { 26 | return callback(new Error(parsed.error)) 27 | } 28 | 29 | if (xmlhttp.status >= 300) { 30 | return callback(new Error('Request error: ' + xmlhttp.status)) 31 | } 32 | 33 | callback(undefined, parsed) 34 | } 35 | } 36 | 37 | function get (url, callback) { 38 | sendJSON('GET', url, null, callback) 39 | } 40 | 41 | function post (url, data, callback) { 42 | sendJSON('POST', url, data, callback) 43 | } 44 | 45 | function put (url, data, callback) { 46 | sendJSON('PUT', url, data, callback) 47 | } 48 | 49 | function rdelete (url, data, callback) { 50 | sendJSON('DELETE', url, data, callback) 51 | } 52 | -------------------------------------------------------------------------------- /examples/select-multiple-fields.js: -------------------------------------------------------------------------------- 1 | document.write('see the console') 2 | 3 | const db = window.db = require("../")('./select-test', { version: 1 }) 4 | 5 | const people = db.store('people', { 6 | key: { autoIncrement: true, keyPath: 'id' }, 7 | indexes: [ 8 | 'name', 9 | 'age', 10 | 'country', 11 | { name: 'age+country', fields: ['age', 'country'] } 12 | ] 13 | }) 14 | 15 | ready(function () { 16 | 17 | people.select('age+country', [20, 'jamaika'], function (error, result) { 18 | if (error) throw error 19 | if (!result) return console.log('done') 20 | 21 | console.log(result.value) 22 | result.continue() 23 | }) 24 | 25 | }) 26 | 27 | 28 | function ready (cb) { 29 | if (localStorage['ready']) return cb() 30 | 31 | people.add({ name: 'foo', country: 'jamaika', age: 20 }, error => { 32 | if (error) throw error 33 | 34 | people.add({ name: 'foo', country: 'korea', age: 20 }, error => { 35 | if (error) throw error 36 | 37 | people.add({ name: 'bar', country: 'jamaika', age: 25 }, error => { 38 | if (error) throw error 39 | 40 | people.add({ name: 'qux', country: 'jamaika', age: 20 }, error => { 41 | if (error) throw error 42 | 43 | console.log('created') 44 | localStorage['ready'] = true 45 | cb() 46 | }) 47 | }) 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/custom-sync.js: -------------------------------------------------------------------------------- 1 | const test = require("prova") 2 | const samples = require("./fixtures/samples") 3 | const APIHook = require("./fixtures/custom-sync-sample").APIHook 4 | const api = require("./fixtures/api") 5 | 6 | test("should send update objects to remote server", t => { 7 | t.plan(13) 8 | 9 | const people = samples.store() 10 | const hook = new APIHook() 11 | 12 | people.db.sync(hook) 13 | 14 | people.add({ name: "azer", email: "azer@roadbeats.com" }) 15 | people.add({ name: "nova", email: "nova@roadbeats.com" }) 16 | people.add({ name: "yolo", email: "yolo@roadbeats.com" }) 17 | people.delete("3") 18 | 19 | setTimeout(() => { 20 | t.ok(hook.lastSyncedAt > 0) 21 | 22 | api.get("/sync-api", (error, result) => { 23 | t.error(error) 24 | t.equal(result.length, 4) 25 | t.equal(result[0].action, "add") 26 | t.equal(result[0].doc.email, "azer@roadbeats.com") 27 | t.equal(result[1].action, "add") 28 | t.equal(result[1].doc.email, "nova@roadbeats.com") 29 | t.equal(result[2].action, "delete") 30 | t.equal(result[2].documentId, "3") 31 | t.equal(result[3].doc.email, "pablo@roadbeats.com") 32 | t.equal(result[3].action, "add") 33 | }) 34 | 35 | people.get("4", (error, doc) => { 36 | t.error(error) 37 | t.equal(doc.email, "pablo@roadbeats.com") 38 | }) 39 | }, 1250) 40 | }) 41 | -------------------------------------------------------------------------------- /test/fixtures/samples.js: -------------------------------------------------------------------------------- 1 | const createDB = require("../../dist/").createTestingDB 2 | 3 | const data = [ 4 | { 5 | name: "azer", 6 | email: "azer@roadbeats.com", 7 | age: 29, 8 | tags: ["software", "travel"] 9 | }, 10 | { name: "aziz", email: "aziz@roadbeats.com", age: 30, tags: ["fun"] }, 11 | { name: "ammar", email: "ammar@roadbeats.com", age: 23, tags: ["finance"] }, 12 | { 13 | name: "nova", 14 | email: "nova@roadbeats.com", 15 | age: 25, 16 | tags: ["photography", "travel"] 17 | }, 18 | { 19 | name: "apo", 20 | email: "apo@roadbeats.com", 21 | age: 40, 22 | tags: ["documentary", "videography"] 23 | }, 24 | { 25 | name: "foo", 26 | email: "foo@roadbeats.com", 27 | age: 10, 28 | tags: ["software", "testing"] 29 | } 30 | ] 31 | 32 | module.exports = { 33 | store: store, 34 | createData: createData 35 | } 36 | 37 | function store(options) { 38 | return createDB(options).store("people", { 39 | indexes: [ 40 | { name: "email", options: { unique: true } }, 41 | { name: "tags", options: { multiEntry: true, unique: false } }, 42 | { name: "name+age", fields: ["name", "age"] }, 43 | "name", 44 | "age" 45 | ] 46 | }) 47 | } 48 | 49 | function createData(store, callback) { 50 | Promise.all(data.map(row => store.add(row))) 51 | .catch(callback) 52 | .then(() => callback()) 53 | } 54 | -------------------------------------------------------------------------------- /src/indexeddb-pull.ts: -------------------------------------------------------------------------------- 1 | import Pull from "./pull" 2 | import * as types from "./types" 3 | 4 | export default class IndexedDBPull extends Pull 5 | implements types.IIndexedDBPull { 6 | public db: types.IDB 7 | private _stores: types.IStoreMap | undefined 8 | constructor(db: types.IDB) { 9 | super() 10 | this.db = db 11 | } 12 | 13 | stores(): types.IStoreMap { 14 | if (this._stores) return this._stores 15 | 16 | this._stores = {} 17 | 18 | var i = this.db.stores.length 19 | while (i--) { 20 | this._stores[this.db.stores[i].name] = this.db.stores[i] 21 | } 22 | 23 | return this._stores 24 | } 25 | 26 | receive(updates: types.IUpdate[] | types.IUpdate, callback: types.ICallback) { 27 | if (!Array.isArray(updates)) { 28 | return this.copyUpdate(updates, callback) 29 | } 30 | 31 | const self = this 32 | next(0) 33 | 34 | function next(i) { 35 | if (i >= (updates as types.IUpdate[]).length) 36 | return callback && callback() 37 | 38 | self.copyUpdate(updates[i], err => { 39 | if (err) return callback(err) 40 | next(i + 1) 41 | }) 42 | } 43 | } 44 | 45 | copyUpdate(update: types.IUpdate, callback: types.ICallback) { 46 | const stores = this.stores() 47 | const store = stores[update.store] 48 | 49 | if (!store) return callback(new Error("Unknown store: " + update.store)) 50 | 51 | if (update.action === "add") { 52 | update.doc.id = update.documentId 53 | store._add(update.doc, callback) 54 | return 55 | } 56 | 57 | if (update.action === "update") { 58 | store._update(update.doc, callback) 59 | return 60 | } 61 | 62 | if (update.action === "delete") { 63 | store._delete(update.documentId, callback) 64 | return 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dist/indexeddb-pull.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pull_1 = require("./pull"); 4 | class IndexedDBPull extends pull_1.default { 5 | constructor(db) { 6 | super(); 7 | this.db = db; 8 | } 9 | stores() { 10 | if (this._stores) 11 | return this._stores; 12 | this._stores = {}; 13 | var i = this.db.stores.length; 14 | while (i--) { 15 | this._stores[this.db.stores[i].name] = this.db.stores[i]; 16 | } 17 | return this._stores; 18 | } 19 | receive(updates, callback) { 20 | if (!Array.isArray(updates)) { 21 | return this.copyUpdate(updates, callback); 22 | } 23 | const self = this; 24 | next(0); 25 | function next(i) { 26 | if (i >= updates.length) 27 | return callback && callback(); 28 | self.copyUpdate(updates[i], err => { 29 | if (err) 30 | return callback(err); 31 | next(i + 1); 32 | }); 33 | } 34 | } 35 | copyUpdate(update, callback) { 36 | const stores = this.stores(); 37 | const store = stores[update.store]; 38 | if (!store) 39 | return callback(new Error("Unknown store: " + update.store)); 40 | if (update.action === "add") { 41 | update.doc.id = update.documentId; 42 | store._add(update.doc, callback); 43 | return; 44 | } 45 | if (update.action === "update") { 46 | store._update(update.doc, callback); 47 | return; 48 | } 49 | if (update.action === "delete") { 50 | store._delete(update.documentId, callback); 51 | return; 52 | } 53 | } 54 | } 55 | exports.default = IndexedDBPull; 56 | -------------------------------------------------------------------------------- /test/local-sync.js: -------------------------------------------------------------------------------- 1 | const test = require("prova") 2 | const samples = require("./fixtures/samples") 3 | 4 | test("syncing three different indexeddb databases", function(t) { 5 | t.plan(17) 6 | 7 | const a = samples.store() 8 | const b = samples.store() 9 | const c = samples.store() 10 | 11 | a.db.sync(b.db) 12 | a.db.sync(c.db) 13 | 14 | a.add({ name: "azer", email: "azer@roadbeats.com" }, (error, id) => { 15 | t.error(error) 16 | t.equal(id, 1) 17 | 18 | setTimeout(function() { 19 | b.get(id, (error, doc) => { 20 | t.error(error) 21 | t.ok(doc) 22 | t.equal(doc.name, "azer") 23 | t.equal(doc.email, "azer@roadbeats.com") 24 | 25 | c.update( 26 | { id: 1, name: "nova", email: "nova@roadbeats.com" }, 27 | error => { 28 | t.error(error) 29 | setTimeout(function() { 30 | a.get(id, (error, doc) => { 31 | t.error(error) 32 | t.equal(doc.name, "nova") 33 | t.equal(doc.email, "nova@roadbeats.com") 34 | 35 | a.delete(id, function(error) { 36 | t.error(error) 37 | 38 | setTimeout(function() { 39 | a.get(id, (error, doc) => { 40 | t.error(error) 41 | t.notOk(doc) 42 | }) 43 | 44 | b.get(id, (error, doc) => { 45 | t.error(error) 46 | t.notOk(doc) 47 | }) 48 | 49 | c.get(id, (error, doc) => { 50 | t.error(error) 51 | t.notOk(doc) 52 | }) 53 | 54 | a.db.delete() 55 | b.db.delete() 56 | c.db.delete() 57 | }, 100) 58 | }) 59 | }) 60 | }, 100) 61 | } 62 | ) 63 | }) 64 | }, 100) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/fixtures/custom-sync-sample.js: -------------------------------------------------------------------------------- 1 | const createDB = require("../../dist/").createTestingDB 2 | const Push = require("../../dist/").Push 3 | const Pull = require("../../dist/").Pull 4 | const api = require("./api") 5 | 6 | class APIHook { 7 | constructor() { 8 | this.lastSyncedAt = 0 9 | this.pull = new APIPull(this) 10 | this.push = new APIPush(this) 11 | } 12 | } 13 | 14 | class APIPull extends Pull { 15 | constructor(hook) { 16 | super() 17 | this.parent = hook 18 | this.intervalMS = 1000 19 | } 20 | 21 | receive(updates) { 22 | if (!Array.isArray(updates)) { 23 | updates = [updates] 24 | } 25 | 26 | api.post("/sync-api", updates, (error, resp) => { 27 | if (error) { 28 | return this.onError(error) 29 | } 30 | 31 | this.parent.lastSyncedAt = resp.time 32 | }) 33 | } 34 | 35 | onError(err) { 36 | console.error("screwed up", err) 37 | } 38 | } 39 | 40 | class APIPush extends Push { 41 | constructor(hook) { 42 | super() 43 | this.parent = hook 44 | this.intervalMS = 1000 45 | this.scheduledAt = 0 46 | this.schedule() 47 | } 48 | 49 | onPublish(errors) { 50 | if (errors) { 51 | console.error("Errors occurred on publish", errors) 52 | return 53 | } 54 | 55 | console.log("Received updates from API") 56 | } 57 | 58 | schedule() { 59 | if (this.scheduledAt > 0) { 60 | // already scheduled 61 | return 62 | } 63 | 64 | this.scheduledAt = Date.now() 65 | this.scheduler = setTimeout(() => this.checkForUpdates(), this.intervalMS) 66 | } 67 | 68 | checkForUpdates() { 69 | api.get(`/sync-api?ts=${this.parent.lastSyncedAt}`, (error, updates) => { 70 | if (error) return this.onError(error) 71 | this.publish(updates, this.onPublish) 72 | }) 73 | } 74 | 75 | onError(err) { 76 | console.error("screwed up > > >", err) 77 | } 78 | } 79 | 80 | module.exports = { 81 | APIHook: APIHook, 82 | APIPull: APIPull, 83 | APIPush: APIPush 84 | } 85 | -------------------------------------------------------------------------------- /dist/store.d.ts: -------------------------------------------------------------------------------- 1 | import * as types from "./types"; 2 | export default class Store implements types.IStore { 3 | name: string; 4 | db: types.IDB; 5 | idbStore: IDBObjectStore; 6 | isSimpleStore: boolean; 7 | key: string | types.IKeyDefinition; 8 | indexes: types.IIndexDefinition[]; 9 | isTestStore: boolean; 10 | onChange: any; 11 | customUpgradeFn: (store: types.IStore) => void | undefined; 12 | constructor(name: string, options: types.IStoreDefinition); 13 | create(db: IDBDatabase, event: Event): void; 14 | createIndex(index: types.IIndexDefinition): void; 15 | upgrade(event: Event): void; 16 | mode(type: string, callback: types.ICallback): IDBTransaction; 17 | readWrite(callback: types.ICallback): IDBTransaction; 18 | readOnly(callback: types.ICallback): IDBTransaction; 19 | all(optionalCallback?: types.ICallback): Promise; 20 | add(doc: object, optionalCallback?: types.ICallback): Promise; 21 | get(key: any, optionalCallback?: types.ICallback): Promise; 22 | getByIndex(indexName: any, indexValue: any, optionalCallback?: types.ICallback): any; 23 | cursor(range: types.IRange, direction: string, callback: types.ICallback): void; 24 | indexCursor(name: string, range: types.IRange, direction: string, callback: types.ICallback): void; 25 | onPublish(errors: Error[]): void; 26 | select(indexName: string, rangeOptions: null | types.IRange, directionOrCallback: string | types.ICallback, optionalCallback?: types.ICallback): void; 27 | update(doc: object, optionalCallback?: types.ICallback): any; 28 | delete(id: any, callback?: types.ICallback): any; 29 | count(optionalCallback?: types.ICallback): Promise; 30 | range(options: types.IRange): types.IRange | IDBKeyRange; 31 | testing(optionalDB?: types.IDB): types.IStore; 32 | _add(doc: object, callback?: types.ICallback, resolve?: types.IResolveFn, reject?: types.IRejectFn, push?: types.IPushFn): void; 33 | _delete(id: any, callback: types.ICallback, resolve: types.IResolveFn, reject: types.IRejectFn, push: types.IPushFn): void; 34 | _update(doc: object, callback?: types.ICallback, resolve?: types.IResolveFn, reject?: types.IRejectFn, push?: types.IPushFn): void; 35 | } 36 | -------------------------------------------------------------------------------- /test/crud.js: -------------------------------------------------------------------------------- 1 | const test = require("prova") 2 | const samples = require("./fixtures/samples") 3 | 4 | test("add + get", function(t) { 5 | const people = samples.store() 6 | t.plan(5) 7 | 8 | people.add({ name: "azer", email: "azer@roadbeats.com" }, (error, id) => { 9 | t.error(error) 10 | 11 | t.equal(id, 1) 12 | 13 | people.get(id, (error, doc) => { 14 | t.error(error) 15 | t.equal(doc.name, "azer") 16 | t.equal(doc.email, "azer@roadbeats.com") 17 | 18 | people.db.delete() 19 | }) 20 | }) 21 | }) 22 | 23 | test("update", function(t) { 24 | const people = samples.store() 25 | t.plan(5) 26 | 27 | people.add({ name: "azer", email: "azer@roadbeats.com" }, (error, id) => { 28 | t.error(error) 29 | 30 | people.update( 31 | { id: 1, name: "nova", email: "nova@roadbeats.com" }, 32 | error => { 33 | t.error(error) 34 | 35 | people.get(id, (error, doc) => { 36 | t.error(error) 37 | t.equal(doc.name, "nova") 38 | t.equal(doc.email, "nova@roadbeats.com") 39 | 40 | people.db.delete() 41 | }) 42 | } 43 | ) 44 | }) 45 | }) 46 | 47 | test("delete", function(t) { 48 | const people = samples.store() 49 | t.plan(4) 50 | 51 | people.add({ name: "azer", email: "azer@roadbeats.com" }, (error, id) => { 52 | t.error(error) 53 | 54 | people.delete(id, error => { 55 | t.error(error) 56 | 57 | people.get(id, (error, doc) => { 58 | t.error(error) 59 | t.notOk(doc) 60 | 61 | people.db.delete() 62 | }) 63 | }) 64 | }) 65 | }) 66 | 67 | test("getByIndex", function(t) { 68 | const people = samples.store() 69 | 70 | t.plan(5) 71 | samples.createData(people, error => { 72 | t.error(error) 73 | 74 | people.getByIndex("name", "azer", (error, result) => { 75 | t.error(error) 76 | 77 | t.equal(result.id, 1) 78 | t.equal(result.name, "azer") 79 | t.equal(result.email, "azer@roadbeats.com") 80 | 81 | people.db.delete() 82 | }) 83 | }) 84 | }) 85 | 86 | test("count", function(t) { 87 | const people = samples.store() 88 | t.plan(3) 89 | 90 | samples.createData(people, error => { 91 | t.error(error) 92 | 93 | people.count((error, count) => { 94 | t.error(error) 95 | t.equal(count, 6) 96 | 97 | people.db.delete() 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /dist/db.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const idb = typeof window === "undefined" 4 | ? null 5 | : window.indexedDB || 6 | window.webkitIndexedDB || 7 | window.mozIndexedDB || 8 | window.OIndexedDB || 9 | window.msIndexedDB; 10 | const store_1 = require("./store"); 11 | const push_1 = require("./push"); 12 | const indexeddb_pull_1 = require("./indexeddb-pull"); 13 | const promises_1 = require("./promises"); 14 | class DB { 15 | constructor(name, options) { 16 | this.idb = null; 17 | this.name = name; 18 | this.version = options.version; 19 | this.stores = options.stores || []; 20 | this.push = new push_1.default(); 21 | this.pull = new indexeddb_pull_1.default(this); 22 | } 23 | close() { 24 | this.idb.close(); 25 | } 26 | delete() { 27 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 28 | promises_1.returnResult(null, idb.deleteDatabase(this.name), callback, resolve, reject); 29 | }); 30 | } 31 | onUpgradeNeeded(event) { 32 | this.stores.forEach(store => store.create(event.target.result, event)); 33 | } 34 | open(callback) { 35 | const request = idb.open(this.name, this.version); 36 | request.onupgradeneeded = event => { 37 | this.onUpgradeNeeded(event); 38 | }; 39 | request.onsuccess = event => { 40 | this.idb = request.result; 41 | callback(undefined, this.idb); 42 | }; 43 | request.onerror = event => { 44 | callback(new Error("Can not open DB. Error: " + JSON.stringify(request.error))); 45 | }; 46 | request.onblocked = event => { 47 | callback(new Error(this.name + 48 | " can not be opened because it's still open somewhere else.")); 49 | }; 50 | } 51 | ready(callback) { 52 | if (this.idb) 53 | return callback(); 54 | this.open(callback); 55 | } 56 | store(name, options) { 57 | var s = new store_1.default(name, options); 58 | s.db = this; 59 | this.stores.push(s); 60 | return s; 61 | } 62 | sync(target) { 63 | this.push.hook(target); 64 | target.push.hook(this); 65 | } 66 | transaction(storeNames, type, callback) { 67 | this.ready(error => { 68 | if (error) 69 | return callback(error); 70 | callback(undefined, this.idb.transaction(storeNames, type)); 71 | }); 72 | } 73 | } 74 | exports.default = DB; 75 | -------------------------------------------------------------------------------- /test/fixtures/sync-server.js: -------------------------------------------------------------------------------- 1 | const http = require("http") 2 | const url = require("url") 3 | 4 | const hostname = "127.0.0.1" 5 | const port = 3000 6 | const server = http.createServer(onRequest) 7 | const stores = {} 8 | 9 | server.listen(port, hostname, () => { 10 | console.log(`Server running at http://${hostname}:${port}/`) 11 | }) 12 | 13 | function onRequest(req, resp) { 14 | const query = url.parse(req.url, true).query 15 | 16 | console.log("request", req.url) 17 | 18 | if (req.method !== "POST") { 19 | return resp.end(JSON.stringify(filter(query))) 20 | } 21 | 22 | var body = "" 23 | 24 | req.on("data", function(chunk) { 25 | body += chunk 26 | }) 27 | 28 | req.on("end", function() { 29 | sync(JSON.parse(body)) 30 | 31 | if (stores.people[4] == undefined) { 32 | stores.people[4] = { 33 | name: "pablo", 34 | email: "pablo@roadbeats.com", 35 | age: 40, 36 | tags: ["foobar"], 37 | createdAt: Date.now() + 1000 38 | } 39 | } 40 | }) 41 | 42 | resp.end('{ "done": true, "time": ' + Date.now() + " }") 43 | 44 | if (stores.people) { 45 | delete stores.people[4] 46 | } 47 | } 48 | 49 | function sync(updates) { 50 | updates.forEach(u => { 51 | if (!stores[u.store]) { 52 | stores[u.store] = {} 53 | } 54 | 55 | if (u.action === "add") { 56 | u.doc.createdAt = Date.now() 57 | stores[u.store][u.documentId] = u.doc 58 | console.log("added ", JSON.stringify(u.doc)) 59 | } 60 | 61 | if (u.action === "update") { 62 | u.doc.updatedAt = Date.now() 63 | stores[u.store][u.documentId] = u.doc 64 | console.log("updated ", JSON.stringify(u.doc)) 65 | } 66 | 67 | if (u.action === "delete") { 68 | stores[u.store][u.documentId] = { deleted: true, deletedAt: Date.now() } 69 | console.log("deleted ", u.documentId) 70 | } 71 | }) 72 | } 73 | 74 | function filter(options) { 75 | const ts = Number(options.ts || 0) 76 | const updates = [] 77 | 78 | var name 79 | var id 80 | for (name in stores) { 81 | for (id in stores[name]) { 82 | if (stores[name][id].createdAt > ts) { 83 | updates.push({ 84 | action: "add", 85 | store: name, 86 | documentId: id, 87 | doc: stores[name][id] 88 | }) 89 | } 90 | 91 | if (stores[name][id].lastUpdatedAt > ts) { 92 | updates.push({ 93 | action: "update", 94 | store: name, 95 | documentId: id, 96 | doc: stores[name][id] 97 | }) 98 | } 99 | 100 | if (stores[name][id].deletedAt > ts) { 101 | updates.push({ 102 | action: "delete", 103 | store: name, 104 | documentId: id 105 | }) 106 | } 107 | } 108 | } 109 | 110 | return updates 111 | } 112 | -------------------------------------------------------------------------------- /test/select.js: -------------------------------------------------------------------------------- 1 | const test = require("prova") 2 | const samples = require("./fixtures/samples") 3 | 4 | test("select range", function(t) { 5 | const people = samples.store() 6 | t.plan(6) 7 | 8 | samples.createData(people, error => { 9 | t.error(error) 10 | 11 | people.select("name", { from: "b", to: "g" }, (error, result) => { 12 | t.error(error) 13 | 14 | if (!result) people.db.delete() 15 | 16 | t.equal(result.value.id, 6) 17 | t.equal(result.value.name, "foo") 18 | t.equal(result.value.email, "foo@roadbeats.com") 19 | 20 | result.continue() 21 | }) 22 | }) 23 | }) 24 | 25 | test("searching by tag", function(t) { 26 | const people = samples.store() 27 | t.plan(8) 28 | 29 | var ctr = -1 30 | var expected = [{ id: 1, name: "azer" }, { id: 6, name: "foo" }] 31 | 32 | samples.createData(people, error => { 33 | t.error(error) 34 | 35 | people.select("tags", { only: "software" }, (error, result) => { 36 | t.error(error) 37 | 38 | if (!result) return people.db.delete() 39 | 40 | ctr++ 41 | t.equal(result.value.id, expected[ctr].id) 42 | t.equal(result.value.name, expected[ctr].name) 43 | 44 | result.continue() 45 | }) 46 | }) 47 | }) 48 | 49 | test("sorting by index", function(t) { 50 | const people = samples.store() 51 | t.plan(27) 52 | 53 | var desc = [40, 30, 29, 25, 23, 10] 54 | var asc = [10, 23, 25, 29, 30, 40] 55 | var dctr = -1 56 | var actr = -1 57 | 58 | samples.createData(people, error => { 59 | t.error(error) 60 | 61 | people.select("age", null, "prev", function(error, result) { 62 | t.error(error) 63 | if (!result) return people.db.delete() 64 | 65 | dctr++ 66 | t.equal(result.value.age, desc[dctr]) 67 | result.continue() 68 | }) 69 | 70 | people.select("age", null, "next", function(error, result) { 71 | t.error(error) 72 | if (!result) return people.db.delete() 73 | 74 | actr++ 75 | t.equal(result.value.age, asc[actr]) 76 | result.continue() 77 | }) 78 | }) 79 | }) 80 | 81 | test("selecting range with multiple indexes", function(t) { 82 | const people = samples.store() 83 | 84 | t.plan(9) 85 | 86 | samples.createData(people, error => { 87 | t.error(error) 88 | 89 | const expected1 = [1] 90 | let ctr1 = -1 91 | 92 | people.select("name+age", ["azer", 29], (error, result) => { 93 | t.error(error) 94 | if (!result) return people.db.delete() 95 | t.equal(result.value.id, expected1[++ctr1]) 96 | result.continue() 97 | }) 98 | 99 | const expected2 = [3, 5] 100 | let ctr2 = -1 101 | people.select( 102 | "name+age", 103 | { from: ["a", 20], to: ["ap" + "\uffff", 30] }, 104 | (error, result) => { 105 | t.error(error) 106 | if (!result) return people.db.delete() 107 | t.equal(result.value.id, expected2[++ctr2]) 108 | result.continue() 109 | } 110 | ) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | const idb: IDBFactory | null = 2 | typeof window === "undefined" 3 | ? self && self.indexedDB 4 | : (window as any).indexedDB || 5 | (window as any).webkitIndexedDB || 6 | (window as any).mozIndexedDB || 7 | (window as any).OIndexedDB || 8 | (window as any).msIndexedDB 9 | 10 | import Store from "./store" 11 | import Push from "./push" 12 | import Pull from "./indexeddb-pull" 13 | import { returnResult, createPromise } from "./promises" 14 | import * as types from "./types" 15 | 16 | export default class DB implements types.IDB { 17 | public idb: IDBDatabase | null 18 | public name: string 19 | public version: number 20 | public stores: types.IStore[] 21 | public push: types.IPush 22 | public pull: types.IPull 23 | constructor(name: string, options: types.IDBOptions) { 24 | this.idb = null 25 | this.name = name 26 | this.version = options.version 27 | this.stores = options.stores || [] 28 | this.push = new Push() 29 | this.pull = new Pull(this) 30 | } 31 | 32 | close() { 33 | this.idb.close() 34 | } 35 | 36 | delete(): Promise { 37 | return createPromise(arguments, (callback, resolve, reject) => { 38 | returnResult( 39 | null, 40 | idb.deleteDatabase(this.name), 41 | callback, 42 | resolve, 43 | reject 44 | ) 45 | }) 46 | } 47 | 48 | onUpgradeNeeded(event: Event) { 49 | this.stores.forEach(store => 50 | store.create((event.target as any).result, event) 51 | ) 52 | } 53 | 54 | open(callback: (error?: Error, db?: IDBDatabase) => void) { 55 | const request = idb.open(this.name, this.version) 56 | 57 | request.onupgradeneeded = event => { 58 | this.onUpgradeNeeded(event) 59 | } 60 | 61 | request.onsuccess = event => { 62 | this.idb = request.result 63 | callback(undefined, this.idb) 64 | } 65 | 66 | request.onerror = event => { 67 | callback( 68 | new Error("Can not open DB. Error: " + JSON.stringify(request.error)) 69 | ) 70 | } 71 | 72 | request.onblocked = event => { 73 | callback( 74 | new Error( 75 | this.name + 76 | " can not be opened because it's still open somewhere else." 77 | ) 78 | ) 79 | } 80 | } 81 | 82 | ready(callback: (error?: Error, db?: IDBDatabase) => void) { 83 | if (this.idb) return callback() 84 | this.open(callback) 85 | } 86 | 87 | store(name: string, options: types.IStoreDefinition) { 88 | var s = new Store(name, options) 89 | s.db = this 90 | this.stores.push(s) 91 | return s 92 | } 93 | 94 | sync(target: types.IDB) { 95 | this.push.hook(target) 96 | target.push.hook(this) 97 | } 98 | 99 | transaction( 100 | storeNames: string[], 101 | type: IDBTransactionMode, 102 | callback: (error?: Error, transaction?: IDBTransaction) => void 103 | ) { 104 | this.ready(error => { 105 | if (error) return callback(error) 106 | callback(undefined, this.idb.transaction(storeNames, type)) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IDB { 2 | idb: IDBDatabase | null; 3 | name: string; 4 | version: number; 5 | stores: IStore[]; 6 | push: IPush; 7 | pull: IPull; 8 | close(): any; 9 | delete(): Promise; 10 | onUpgradeNeeded(event: Event): any; 11 | open(callback: (error?: Error, db?: IDBDatabase) => void): any; 12 | ready(callback: (error?: Error, db?: IDBDatabase) => void): any; 13 | sync(target: ISynchronizable): any; 14 | store(name: string, options: IStoreDefinition): any; 15 | transaction(storeNames: string[], type: string, callback: (error?: Error, transaction?: IDBTransaction) => void): any; 16 | } 17 | export interface IDBOptions { 18 | version: number; 19 | stores?: IStore[]; 20 | } 21 | export interface ISynchronizable { 22 | push: IPush; 23 | pull: IPull; 24 | } 25 | export interface IPull { 26 | receive(updates: IUpdate[] | IUpdate, callback: ICallback): any; 27 | } 28 | export interface IPush { 29 | targets: IDB[]; 30 | hook(pullTarget: IDB): any; 31 | publish(updates: IUpdate | IUpdate[], callback: IMultipleErrorsCallback): any; 32 | } 33 | export interface IStoreMap { 34 | [name: string]: IStore; 35 | } 36 | export interface IIndexedDBPull { 37 | db: IDB; 38 | stores(): IStoreMap; 39 | receive(updates: IUpdate[] | IUpdate, callback: ICallback): any; 40 | } 41 | export declare type IIndexDefinition = string | IStrictIndexDefinition; 42 | export interface IStrictIndexDefinition { 43 | name: string; 44 | path?: string; 45 | paths?: string[]; 46 | field?: string; 47 | fields?: string[]; 48 | options?: object; 49 | } 50 | export interface IStoreDefinition { 51 | key?: string | IKeyDefinition; 52 | indexes: IIndexDefinition[]; 53 | upgrade?: (store: IStore) => void; 54 | testing?: boolean; 55 | } 56 | export interface IStore { 57 | db: IDB; 58 | name: string; 59 | idbStore: IDBObjectStore; 60 | isSimpleStore: boolean; 61 | key: string | IKeyDefinition; 62 | indexes: any[]; 63 | isTestStore: boolean; 64 | onChange: any; 65 | customUpgradeFn: (store: IStore) => void | undefined; 66 | add(doc: object, optionalCallback?: ICallback): Promise; 67 | all(optionalCallback?: ICallback): Promise; 68 | create(db: IDBDatabase, event: Event): any; 69 | createIndex(index: IIndexDefinition): any; 70 | count(optionalCallback?: ICallback): Promise; 71 | cursor(range: IRange, direction: string, callback: ICallback): any; 72 | delete(id: any, optionalCallback?: ICallback): Promise; 73 | get(key: any, optionalCallback?: ICallback): Promise; 74 | getByIndex(indexName: any, indexValue: any, optionalCallback?: ICallback): any; 75 | mode(type: string, callback: ICallback): IDBTransaction; 76 | range(options: IRange): IRange | IDBKeyRange; 77 | readWrite(callback: ICallback): IDBTransaction; 78 | readOnly(callback: ICallback): IDBTransaction; 79 | update(doc: object, optionalCallback?: ICallback): any; 80 | upgrade(event: Event): any; 81 | indexCursor(name: string, range: IRange, direction: string, callback: ICallback): any; 82 | select(indexName: string, rangeOptions: null | IRange, directionOrCallback: string | ICallback, optionalCallback?: ICallback): any; 83 | _add(doc: object, callback?: ICallback, resolve?: IResolveFn, reject?: IRejectFn, push?: IPushFn): any; 84 | _delete(id: any, callback: ICallback, resolve?: IResolveFn, reject?: IRejectFn, push?: IPushFn): any; 85 | _update(doc: object, callback?: ICallback, resolve?: IResolveFn, reject?: IRejectFn, push?: IPushFn): any; 86 | } 87 | export interface IKeyDefinition { 88 | keyPath: string; 89 | autoIncrement?: boolean; 90 | } 91 | export interface IRange { 92 | from?: any; 93 | to?: any; 94 | only?: any; 95 | } 96 | export interface IUpdate { 97 | action: string; 98 | store: string; 99 | id?: any; 100 | documentId?: any; 101 | doc?: any; 102 | } 103 | export interface IResolveFn { 104 | (result: any): void; 105 | } 106 | export interface IRejectFn { 107 | (error: Error): void; 108 | } 109 | export interface ICallback { 110 | (error?: Error, result?: any): void; 111 | } 112 | export interface IMultipleErrorsCallback { 113 | (errors?: Error[], result?: any): void; 114 | } 115 | export interface IPushFn { 116 | (result: any): void; 117 | } 118 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface IDB { 2 | idb: IDBDatabase | null 3 | name: string 4 | version: number 5 | stores: IStore[] 6 | push: IPush 7 | pull: IPull 8 | 9 | close() 10 | delete(): Promise 11 | onUpgradeNeeded(event: Event) 12 | open(callback: (error?: Error, db?: IDBDatabase) => void) 13 | ready(callback: (error?: Error, db?: IDBDatabase) => void) 14 | sync(target: ISynchronizable) 15 | store(name: string, options: IStoreDefinition) 16 | transaction( 17 | storeNames: string[], 18 | type: string, 19 | callback: (error?: Error, transaction?: IDBTransaction) => void 20 | ) 21 | } 22 | 23 | export interface IDBOptions { 24 | version: number 25 | stores?: IStore[] 26 | } 27 | 28 | export interface ISynchronizable { 29 | push: IPush 30 | pull: IPull 31 | } 32 | 33 | export interface IPull { 34 | receive(updates: IUpdate[] | IUpdate, callback: ICallback) 35 | } 36 | 37 | export interface IPush { 38 | targets: IDB[] 39 | hook(pullTarget: IDB) 40 | publish(updates: IUpdate | IUpdate[], callback: IMultipleErrorsCallback) 41 | } 42 | 43 | export interface IStoreMap { 44 | [name: string]: IStore 45 | } 46 | 47 | export interface IIndexedDBPull { 48 | db: IDB 49 | stores(): IStoreMap 50 | receive(updates: IUpdate[] | IUpdate, callback: ICallback) 51 | } 52 | 53 | export type IIndexDefinition = string | IStrictIndexDefinition 54 | 55 | export interface IStrictIndexDefinition { 56 | name: string 57 | path?: string 58 | paths?: string[] 59 | field?: string 60 | fields?: string[] 61 | options?: object 62 | } 63 | 64 | export interface IStoreDefinition { 65 | key?: string | IKeyDefinition 66 | indexes: IIndexDefinition[] 67 | upgrade?: (store: IStore) => void 68 | testing?: boolean 69 | } 70 | 71 | export interface IStore { 72 | db: IDB 73 | name: string 74 | idbStore: IDBObjectStore 75 | isSimpleStore: boolean 76 | key: string | IKeyDefinition 77 | indexes: any[] 78 | isTestStore: boolean 79 | onChange: any 80 | customUpgradeFn: (store: IStore) => void | undefined 81 | 82 | add(doc: object, optionalCallback?: ICallback): Promise 83 | all(optionalCallback?: ICallback): Promise 84 | create(db: IDBDatabase, event: Event) 85 | createIndex(index: IIndexDefinition) 86 | count(optionalCallback?: ICallback): Promise 87 | cursor(range: IRange, direction: string, callback: ICallback) 88 | delete(id: any, optionalCallback?: ICallback): Promise 89 | get(key: any, optionalCallback?: ICallback): Promise 90 | getByIndex(indexName: any, indexValue: any, optionalCallback?: ICallback) 91 | mode(type: string, callback: ICallback): IDBTransaction 92 | range(options: IRange): IRange | IDBKeyRange 93 | readWrite(callback: ICallback): IDBTransaction 94 | readOnly(callback: ICallback): IDBTransaction 95 | update(doc: object, optionalCallback?: ICallback) 96 | upgrade(event: Event) 97 | 98 | indexCursor( 99 | name: string, 100 | range: IRange, 101 | direction: string, 102 | callback: ICallback 103 | ) 104 | 105 | select( 106 | indexName: string, 107 | rangeOptions: null | IRange, 108 | directionOrCallback: string | ICallback, 109 | optionalCallback?: ICallback 110 | ) 111 | 112 | _add( 113 | doc: object, 114 | callback?: ICallback, 115 | resolve?: IResolveFn, 116 | reject?: IRejectFn, 117 | push?: IPushFn 118 | ) 119 | _delete( 120 | id: any, 121 | callback: ICallback, 122 | resolve?: IResolveFn, 123 | reject?: IRejectFn, 124 | push?: IPushFn 125 | ) 126 | _update( 127 | doc: object, 128 | callback?: ICallback, 129 | resolve?: IResolveFn, 130 | reject?: IRejectFn, 131 | push?: IPushFn 132 | ) 133 | } 134 | 135 | export interface IKeyDefinition { 136 | keyPath: string 137 | autoIncrement?: boolean 138 | } 139 | 140 | export interface IRange { 141 | from?: any 142 | to?: any 143 | only?: any 144 | } 145 | 146 | export interface IUpdate { 147 | action: string 148 | store: string 149 | id?: any 150 | documentId?: any 151 | doc?: any 152 | } 153 | 154 | export interface IResolveFn { 155 | (result: any): void 156 | } 157 | 158 | export interface IRejectFn { 159 | (error: Error): void 160 | } 161 | 162 | export interface ICallback { 163 | (error?: Error, result?: any): void 164 | } 165 | 166 | export interface IMultipleErrorsCallback { 167 | (errors?: Error[], result?: any): void 168 | } 169 | 170 | export interface IPushFn { 171 | (result: any): void 172 | } 173 | -------------------------------------------------------------------------------- /dist/store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const pubsub = require("pubsub"); 4 | const promises_1 = require("./promises"); 5 | const index_1 = require("./index"); 6 | const READ_WRITE = "readwrite"; 7 | const READ_ONLY = "readonly"; 8 | const DEFAULT_KEY = { keyPath: "id", autoIncrement: true }; 9 | class Store { 10 | constructor(name, options) { 11 | this.name = name; 12 | this.idbStore = null; 13 | if (!options) { 14 | this.isSimpleStore = true; 15 | } 16 | else { 17 | this.key = options.key || DEFAULT_KEY; 18 | this.indexes = options.indexes || []; 19 | this.customUpgradeFn = options.upgrade; 20 | this.isTestStore = !!options.testing; 21 | } 22 | this.onChange = pubsub(); 23 | } 24 | create(db, event) { 25 | if (db.objectStoreNames.contains(this.name)) { 26 | return this.upgrade(event); 27 | } 28 | if (this.isSimpleStore) { 29 | this.idbStore = db.createObjectStore(this.name); 30 | return; 31 | } 32 | const key = typeof this.key === "string" ? { keyPath: this.key } : this.key; 33 | this.idbStore = db.createObjectStore(this.name, key); 34 | this.indexes.forEach(index => this.createIndex(index)); 35 | } 36 | createIndex(index) { 37 | if (typeof index === "string") { 38 | index = { name: index }; 39 | } 40 | const field = index.path || index.paths || index.field || index.fields || index.name; 41 | const options = index.options || { unique: false }; 42 | this.idbStore.createIndex(index.name, field, options); 43 | } 44 | upgrade(event) { 45 | if (!this.customUpgradeFn) 46 | return; 47 | const target = event.currentTarget; 48 | this.idbStore = target.transaction.objectStore(this.name); 49 | this.customUpgradeFn(this); 50 | } 51 | mode(type, callback) { 52 | return this.db.transaction([this.name], type, (error, tx) => { 53 | if (error) 54 | return callback(error); 55 | callback(undefined, tx.objectStore(this.name)); 56 | }); 57 | } 58 | readWrite(callback) { 59 | return this.mode(READ_WRITE, callback); 60 | } 61 | readOnly(callback) { 62 | return this.mode(READ_ONLY, callback); 63 | } 64 | all(optionalCallback) { 65 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 66 | this.readOnly((error, ro) => { 67 | promises_1.returnResult(error, ro.openCursor(), callback, resolve, reject); 68 | }); 69 | }); 70 | } 71 | add(doc, optionalCallback) { 72 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 73 | this._add(doc, callback, resolve, reject, id => { 74 | this.db.push.publish({ 75 | action: "add", 76 | store: this.name, 77 | documentId: id, 78 | doc 79 | }, this.onPublish); 80 | }); 81 | }); 82 | } 83 | get(key, optionalCallback) { 84 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 85 | this.readOnly((error, ro) => { 86 | promises_1.returnResult(error, ro.get(key), callback, resolve, reject); 87 | }); 88 | }); 89 | } 90 | getByIndex(indexName, indexValue, optionalCallback) { 91 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 92 | this.readOnly((error, ro) => { 93 | promises_1.returnResult(error, ro.index(indexName).get(indexValue), callback, resolve, reject); 94 | }); 95 | }); 96 | } 97 | cursor(range, direction, callback) { 98 | this.readOnly((error, ro) => { 99 | promises_1.returnResult(error, ro.openCursor(this.range(range), direction), callback); 100 | }); 101 | } 102 | indexCursor(name, range, direction, callback) { 103 | this.readOnly((error, ro) => { 104 | promises_1.returnResult(error, ro.index(name).openCursor(this.range(range), direction), callback); 105 | }); 106 | } 107 | onPublish(errors) { 108 | if (errors) { 109 | console.error("Error(s) happened on publishing changes", errors); 110 | } 111 | } 112 | select(indexName, rangeOptions, directionOrCallback, optionalCallback) { 113 | let range = rangeOptions ? this.range(rangeOptions) : null; 114 | let direction = directionOrCallback; 115 | let callback = optionalCallback; 116 | if (arguments.length === 3 && typeof directionOrCallback === "function") { 117 | direction = undefined; 118 | callback = directionOrCallback; 119 | } 120 | this.readOnly((error, ro) => { 121 | promises_1.returnResult(error, ro.index(indexName).openCursor(range, direction), callback); 122 | }); 123 | } 124 | update(doc, optionalCallback) { 125 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 126 | this._update(doc, callback, resolve, reject, id => { 127 | this.db.push.publish({ 128 | action: "update", 129 | store: this.name, 130 | documentId: id, 131 | doc: doc 132 | }, this.onPublish); 133 | }); 134 | }); 135 | } 136 | delete(id, callback) { 137 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 138 | this._delete(id, callback, resolve, reject, () => { 139 | this.db.push.publish({ 140 | action: "delete", 141 | store: this.name, 142 | documentId: id 143 | }, this.onPublish); 144 | }); 145 | }); 146 | } 147 | count(optionalCallback) { 148 | return promises_1.createPromise(arguments, (callback, resolve, reject) => { 149 | this.readOnly((error, ro) => { 150 | promises_1.returnResult(error, ro.count(), callback, resolve, reject); 151 | }); 152 | }); 153 | } 154 | range(options) { 155 | if (options.from !== undefined && options.to !== undefined) { 156 | return IDBKeyRange.bound(options.from, options.to); 157 | } 158 | if (options.to !== undefined && options.from === undefined) { 159 | return IDBKeyRange.upperBound(options.to); 160 | } 161 | if (options.from !== undefined) { 162 | return IDBKeyRange.lowerBound(options.from); 163 | } 164 | if (options.only) { 165 | return IDBKeyRange.only(options.only); 166 | } 167 | return options; 168 | } 169 | testing(optionalDB) { 170 | const db = optionalDB || index_1.createTestingDB(); 171 | return db.store(this.name, this.isSimpleStore 172 | ? null 173 | : { 174 | key: this.key, 175 | indexes: this.indexes, 176 | upgrade: this.customUpgradeFn, 177 | testing: true 178 | }); 179 | } 180 | _add(doc, callback, resolve, reject, push) { 181 | this.readWrite((error, rw) => { 182 | promises_1.returnResult(error, rw.add(doc), callback, resolve, reject, push); 183 | this.onChange.publish(); 184 | }); 185 | } 186 | _delete(id, callback, resolve, reject, push) { 187 | this.readWrite((error, rw) => { 188 | promises_1.returnResult(error, rw.delete(id), callback, resolve, reject, push); 189 | this.onChange.publish(); 190 | }); 191 | } 192 | _update(doc, callback, resolve, reject, push) { 193 | this.readWrite((error, rw) => { 194 | promises_1.returnResult(error, rw.put(doc), callback, resolve, reject, push); 195 | this.onChange.publish(); 196 | }); 197 | } 198 | } 199 | exports.default = Store; 200 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import * as pubsub from "pubsub" 2 | import { returnResult, createPromise } from "./promises" 3 | import * as types from "./types" 4 | import { createTestingDB } from "./index" 5 | 6 | const READ_WRITE = "readwrite" 7 | const READ_ONLY = "readonly" 8 | const DEFAULT_KEY = { keyPath: "id", autoIncrement: true } 9 | 10 | export default class Store implements types.IStore { 11 | public name: string 12 | public db: types.IDB 13 | public idbStore: IDBObjectStore 14 | public isSimpleStore: boolean 15 | public key: string | types.IKeyDefinition 16 | public indexes: types.IIndexDefinition[] 17 | public isTestStore: boolean 18 | public onChange: any 19 | public customUpgradeFn: (store: types.IStore) => void | undefined 20 | 21 | constructor(name: string, options: types.IStoreDefinition) { 22 | this.name = name 23 | this.idbStore = null 24 | 25 | if (!options) { 26 | this.isSimpleStore = true 27 | } else { 28 | this.key = options.key || DEFAULT_KEY 29 | this.indexes = options.indexes || [] 30 | this.customUpgradeFn = options.upgrade 31 | this.isTestStore = !!options.testing 32 | } 33 | 34 | this.onChange = pubsub() 35 | } 36 | 37 | create(db: IDBDatabase, event: Event) { 38 | if (db.objectStoreNames.contains(this.name)) { 39 | return this.upgrade(event) 40 | } 41 | 42 | if (this.isSimpleStore) { 43 | this.idbStore = db.createObjectStore(this.name) 44 | return 45 | } 46 | 47 | const key = typeof this.key === "string" ? { keyPath: this.key } : this.key 48 | this.idbStore = db.createObjectStore(this.name, key) 49 | this.indexes.forEach(index => this.createIndex(index)) 50 | } 51 | 52 | createIndex(index: types.IIndexDefinition) { 53 | if (typeof index === "string") { 54 | index = { name: index } 55 | } 56 | 57 | const field = 58 | index.path || index.paths || index.field || index.fields || index.name 59 | const options = index.options || { unique: false } 60 | 61 | this.idbStore.createIndex(index.name, field, options) 62 | } 63 | 64 | upgrade(event: Event) { 65 | if (!this.customUpgradeFn) return 66 | 67 | const target = event.currentTarget as any 68 | 69 | this.idbStore = (target.transaction as IDBTransaction).objectStore( 70 | this.name 71 | ) 72 | this.customUpgradeFn(this) 73 | } 74 | 75 | mode(type: string, callback: types.ICallback): IDBTransaction { 76 | return this.db.transaction([this.name], type, (error, tx) => { 77 | if (error) return callback(error) 78 | callback(undefined, tx.objectStore(this.name)) 79 | }) 80 | } 81 | 82 | readWrite(callback: types.ICallback): IDBTransaction { 83 | return this.mode(READ_WRITE, callback) 84 | } 85 | 86 | readOnly(callback: types.ICallback): IDBTransaction { 87 | return this.mode(READ_ONLY, callback) 88 | } 89 | 90 | all(optionalCallback?: types.ICallback): Promise { 91 | return createPromise(arguments, (callback, resolve, reject) => { 92 | this.readOnly((error, ro) => { 93 | returnResult(error, ro.openCursor(), callback, resolve, reject) 94 | }) 95 | }) 96 | } 97 | 98 | add(doc: object, optionalCallback?: types.ICallback): Promise { 99 | return createPromise(arguments, (callback, resolve, reject) => { 100 | this._add(doc, callback, resolve, reject, id => { 101 | this.db.push.publish( 102 | { 103 | action: "add", 104 | store: this.name, 105 | documentId: id, 106 | doc 107 | }, 108 | this.onPublish 109 | ) 110 | }) 111 | }) 112 | } 113 | 114 | get(key: any, optionalCallback?: types.ICallback): Promise { 115 | return createPromise(arguments, (callback, resolve, reject) => { 116 | this.readOnly((error, ro) => { 117 | returnResult(error, ro.get(key), callback, resolve, reject) 118 | }) 119 | }) 120 | } 121 | 122 | getByIndex( 123 | indexName: any, 124 | indexValue: any, 125 | optionalCallback?: types.ICallback 126 | ) { 127 | return createPromise(arguments, (callback, resolve, reject) => { 128 | this.readOnly((error, ro) => { 129 | returnResult( 130 | error, 131 | ro.index(indexName).get(indexValue), 132 | callback, 133 | resolve, 134 | reject 135 | ) 136 | }) 137 | }) 138 | } 139 | 140 | cursor(range: types.IRange, direction: string, callback: types.ICallback) { 141 | this.readOnly((error, ro) => { 142 | returnResult(error, ro.openCursor(this.range(range), direction), callback) 143 | }) 144 | } 145 | 146 | indexCursor( 147 | name: string, 148 | range: types.IRange, 149 | direction: string, 150 | callback: types.ICallback 151 | ) { 152 | this.readOnly((error, ro) => { 153 | returnResult( 154 | error, 155 | ro.index(name).openCursor(this.range(range), direction), 156 | callback 157 | ) 158 | }) 159 | } 160 | 161 | onPublish(errors: Error[]) { 162 | if (errors) { 163 | console.error("Error(s) happened on publishing changes", errors) 164 | } 165 | } 166 | 167 | select( 168 | indexName: string, 169 | rangeOptions: null | types.IRange, 170 | directionOrCallback: string | types.ICallback, 171 | optionalCallback?: types.ICallback 172 | ) { 173 | let range = rangeOptions ? this.range(rangeOptions) : null 174 | let direction = directionOrCallback 175 | let callback = optionalCallback 176 | 177 | if (arguments.length === 3 && typeof directionOrCallback === "function") { 178 | direction = undefined 179 | callback = directionOrCallback 180 | } 181 | 182 | this.readOnly((error, ro) => { 183 | returnResult( 184 | error, 185 | ro.index(indexName).openCursor(range, direction), 186 | callback 187 | ) 188 | }) 189 | } 190 | 191 | update(doc: object, optionalCallback?: types.ICallback) { 192 | return createPromise(arguments, (callback, resolve, reject) => { 193 | this._update(doc, callback, resolve, reject, id => { 194 | this.db.push.publish( 195 | { 196 | action: "update", 197 | store: this.name, 198 | documentId: id, 199 | doc: doc 200 | }, 201 | this.onPublish 202 | ) 203 | }) 204 | }) 205 | } 206 | 207 | delete(id: any, callback?: types.ICallback) { 208 | return createPromise(arguments, (callback, resolve, reject) => { 209 | this._delete(id, callback, resolve, reject, () => { 210 | this.db.push.publish( 211 | { 212 | action: "delete", 213 | store: this.name, 214 | documentId: id 215 | }, 216 | this.onPublish 217 | ) 218 | }) 219 | }) 220 | } 221 | 222 | count(optionalCallback?: types.ICallback): Promise { 223 | return createPromise(arguments, (callback, resolve, reject) => { 224 | this.readOnly((error, ro) => { 225 | returnResult(error, ro.count(), callback, resolve, reject) 226 | }) 227 | }) 228 | } 229 | 230 | range(options: types.IRange): types.IRange | IDBKeyRange { 231 | if (options.from !== undefined && options.to !== undefined) { 232 | return IDBKeyRange.bound(options.from, options.to) 233 | } 234 | 235 | if (options.to !== undefined && options.from === undefined) { 236 | return IDBKeyRange.upperBound(options.to) 237 | } 238 | 239 | if (options.from !== undefined) { 240 | return IDBKeyRange.lowerBound(options.from) 241 | } 242 | 243 | if (options.only) { 244 | return IDBKeyRange.only(options.only) 245 | } 246 | 247 | return options 248 | } 249 | 250 | testing(optionalDB?: types.IDB): types.IStore { 251 | const db = optionalDB || createTestingDB() 252 | return db.store( 253 | this.name, 254 | this.isSimpleStore 255 | ? null 256 | : { 257 | key: this.key, 258 | indexes: this.indexes, 259 | upgrade: this.customUpgradeFn, 260 | testing: true 261 | } 262 | ) 263 | } 264 | 265 | _add( 266 | doc: object, 267 | callback?: types.ICallback, 268 | resolve?: types.IResolveFn, 269 | reject?: types.IRejectFn, 270 | push?: types.IPushFn 271 | ) { 272 | this.readWrite((error, rw) => { 273 | returnResult(error, rw.add(doc), callback, resolve, reject, push) 274 | this.onChange.publish() 275 | }) 276 | } 277 | 278 | _delete( 279 | id: any, 280 | callback: types.ICallback, 281 | resolve: types.IResolveFn, 282 | reject: types.IRejectFn, 283 | push: types.IPushFn 284 | ) { 285 | this.readWrite((error, rw) => { 286 | returnResult(error, rw.delete(id), callback, resolve, reject, push) 287 | this.onChange.publish() 288 | }) 289 | } 290 | 291 | _update( 292 | doc: object, 293 | callback?: types.ICallback, 294 | resolve?: types.IResolveFn, 295 | reject?: types.IRejectFn, 296 | push?: types.IPushFn 297 | ) { 298 | this.readWrite((error, rw) => { 299 | returnResult(error, rw.put(doc), callback, resolve, reject, push) 300 | this.onChange.publish() 301 | }) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## indexeddb 2 | 3 | Well-tested, low-level wrapper around the IndexedDB API. [It can sync, too](#synchronization). 4 | 5 | See `test.js` for examples. 6 | 7 | ## Install 8 | 9 | ```bash 10 | $ yarn add azer/indexeddb # or npm i azer/indexeddb 11 | ``` 12 | 13 | ## API 14 | 15 | * [Stores](#stores) 16 | * [.add](#add) 17 | * [.all](#all) 18 | * [.get](#get) 19 | * [.getByIndex](#getByIndex) 20 | * [.select](#select) 21 | * [.update](#update) 22 | * [.delete](#delete) 23 | * [.count](#count) 24 | * [.upgrade](#upgrade) 25 | * [.onChange](#onChange) 26 | * [Promises](#promises) 27 | * [Synchronization](#synchronization) 28 | * [Local](#local) 29 | * [Remote](#remote) 30 | 31 | ## Stores 32 | 33 | Define a store for each data type. Stores will have following methods; 34 | 35 | ```js 36 | const db = require('indexeddb')('mydb', { 37 | version: 1 38 | }) 39 | 40 | // Create your stores before opening the connection 41 | const people = db.store('people', { 42 | key: 'email', // you can choose custom key optionally by: "key: { autoIncrement: true, keyPath: 'id' }" 43 | indexes: [ 44 | { name: 'email', options: { unique: true } }, 45 | 'age', 46 | 'country' 47 | ] 48 | }) 49 | ``` 50 | 51 | #### `.add` 52 | 53 | Store method to add documents. 54 | 55 | Parameters: `store.add(document, callback)` 56 | 57 | ```js 58 | people.add({ name: 'foo', email: 'bar@qux.com' }, error => console.log(error)) 59 | ``` 60 | 61 | #### `.all` 62 | 63 | Store method to iterate all documents in the store. 64 | 65 | ```js 66 | people.all((err, row) => { 67 | if (err) return console.error(err) 68 | 69 | if (row) { 70 | console.log(row.value) 71 | row.continue() 72 | } 73 | }) 74 | ``` 75 | 76 | #### `.get` 77 | 78 | Store method to get a document by key value. 79 | 80 | Parameters: `store.get(key, callback)` 81 | 82 | ```js 83 | people.get(1, (error, result) => console.log(error, result)) 84 | ``` 85 | 86 | #### `.getByIndex` 87 | 88 | Store method to get document(s) matching given index key and value. 89 | 90 | Parameters: `store.getByIndex(indexName, indexValue, callback)` 91 | 92 | ```js 93 | people.getByIndex('email', 'bar@qux.com' }, (error, result) => console.log(error, result)) 94 | ``` 95 | 96 | #### `.select` 97 | 98 | Store method to get document(s) selecting by index, range and/or expected values. 99 | 100 | Parameters: `store.select(indexName, rangeOptions, direction , callback)` 101 | 102 | Range options can be expected values or have an object with following properties; 103 | * `from` 104 | * `to` 105 | * `only` 106 | 107 | ```js 108 | people.select('name', { from: 'a', to: 'e' }, (error, row) => { 109 | console.log(error, result) 110 | row.continue() 111 | }) 112 | ``` 113 | 114 | You can optionally choose direction parameter for getting results sorted. Direction paramters are: 115 | * `prev` (descending) 116 | * `next` (ascending) 117 | 118 | ```js 119 | people.select('name', { from: 'a', to: 'e' }, 'prev', (error, row) => { 120 | console.log(error, result) 121 | row.continue() 122 | }) 123 | ``` 124 | 125 | Direction parameters can be useful when you need to iterate by a numeric field. For example, we can iterate the people store by `age` in descending order: 126 | 127 | ```js 128 | people.select('age', null, 'prev', (error, row) => { 129 | console.log(error, result) 130 | row.continue() 131 | }) 132 | ``` 133 | 134 | Range options can be field keys, too. You can select by matching multiple fields at a time. Make sure having an index for the combination of the indexes though; 135 | 136 | ```js 137 | const people = db.store('people', { 138 | key: { autoIncrement: true, keyPath: 'id' }, 139 | indexes: [ 140 | { name: 'age+country', fields: ['age', 'country'] } 141 | ] 142 | }) 143 | ``` 144 | 145 | Now we can select people by age and country: 146 | 147 | ```js 148 | people.select('age+country', [20, 'jamaika'], (error, row) => { 149 | console.log(error, result) 150 | row.continue() 151 | }) 152 | ``` 153 | 154 | `from` and `to` options provides us more flexibility here: 155 | 156 | ```js 157 | people.select('age+country', { from: [20, 'jamaika'], to: [30, 'jamaika'] }, (error, result) => { 158 | console.log(error, result) 159 | result.continue() 160 | }) 161 | ``` 162 | 163 | #### `.update` 164 | 165 | Store method to update a document. 166 | 167 | Parameters: `store.update(document, callback)` 168 | 169 | ```js 170 | people.update({ id: 1, name: 'foo', email: 'hola@yolo.com' }, error => console.log(error)) 171 | ``` 172 | 173 | #### `.delete` 174 | 175 | Store method to delete a record. 176 | 177 | Parameters: `store.delete(id, callback)` 178 | 179 | ```js 180 | people.delete(1, error => console.log(error)) 181 | ``` 182 | 183 | #### `.count` 184 | 185 | Store method to count all records. 186 | 187 | ```js 188 | people.count(error, count => console.log(error, count)) 189 | ``` 190 | 191 | #### `.upgrade` 192 | 193 | Store option to perform upgrade when there is a version change. It's an optional method. 194 | 195 | ```js 196 | const people = db.store('people', { 197 | key: { autoIncrement: true, keyPath: 'id' }, 198 | upgrade: upgrade 199 | }) 200 | 201 | function upgrade () { 202 | people.createIndex('name', { unique: false }) 203 | } 204 | ``` 205 | 206 | #### `.onChange` 207 | 208 | This is an optimized pubsub object where you can subscribe for changes. Here is an example; 209 | 210 | ```js 211 | people.onChange(() => console.log('people store has changed')) 212 | ``` 213 | 214 | You can subscribe for once, too; 215 | 216 | ```js 217 | people.onChange.once(() => console.log('he-yo!')) 218 | ``` 219 | 220 | There is unsubscribe methods, as well. See full API reference for this particular object at [pubsub](https://github.com/azer/pubsub) repository. 221 | 222 | #### Promises 223 | 224 | If callback is not passed, a `Promise` will be returned. Here is an example; 225 | 226 | ```js 227 | const rows = [{ name: 'foo' }, { name: 'bar' }, { name: 'qux' }] 228 | 229 | Promise.all(rows.map(row => people.add(row)) 230 | ``` 231 | 232 | Supported methods: 233 | 234 | * add 235 | * get 236 | * getByIndex 237 | * update 238 | * delete 239 | 240 | ## Synchronization 241 | 242 | ### Local 243 | 244 | You can use the sync method to keep multiple local indexeddb database instances easily. 245 | 246 | ```js 247 | const a = require('indexeddb')('local', { 248 | version: 1 249 | }) 250 | 251 | const b = require('indexeddb')('local', { 252 | version: 1 253 | }) 254 | 255 | const c = require('indexeddb')('local', { 256 | version: 1 257 | }) 258 | 259 | a.sync(b) 260 | a.sync(c) 261 | ``` 262 | 263 | That's it! Now all three local databases will be in sync automatically. 264 | 265 | ### Remote 266 | 267 | You can sync your IndexedDB remotely. To accomplish this, you'll need to 268 | customize *Push* and *Pull* classes. Both of these classes pass eachother update objects. Here is an example update object; 269 | 270 | ```js 271 | { 272 | action: 'add', 273 | store: 'articles', 274 | id: 1, 275 | doc: { 276 | title: 'hello world', 277 | content: 'lorem ipsum ...' 278 | } 279 | } 280 | ``` 281 | 282 | #### Customizing `Push` 283 | 284 | The Push class takes updates from itself and sends them to the target. 285 | If we are syncing an IndexedDB instance with a remote API, then we'll tweak 286 | this class to take updates from the API and simply pass it to the `publish` method. 287 | 288 | ```js 289 | const Push = require('indexeddb/lib/push') 290 | 291 | class PushFromAPI extends Push { 292 | constructor() { 293 | super() 294 | this.checkForUpdates() 295 | } 296 | 297 | checkForUpdates() { 298 | myapi.get("/sync", resp => { 299 | this.publish(resp.updates) 300 | setTimeout(() => this.checkForUpdates(), 1000) // keep checking for updates 301 | }) 302 | } 303 | } 304 | ``` 305 | 306 | #### Customizing `Pull` 307 | 308 | The Pull class takes updates from a foreign source and copies to itself. 309 | In this example, we'll customize the `receive` method to post updates to our API server; 310 | 311 | ```js 312 | const Pull = require('indexeddb/lib/pull') 313 | 314 | class PullIntoAPI extends Pull { 315 | receive(updates) { 316 | if (!Array.isArray(updates)) updates = [updates] // We may get one update or an array of updates, depending on what we sync with 317 | 318 | updates.forEach(function (update) { 319 | if (update.action == "add" && update.store == "articles") { 320 | myapi.put("/articles", options.doc, function (error) {}) 321 | } 322 | }) 323 | } 324 | } 325 | ``` 326 | 327 | #### Custom Synchronization 328 | 329 | After we defined the custom Push & Pull functions, we are ready to hook them. 330 | Simply create an object of these two and pass it to the sync method of the database 331 | that you want to sync with. Check the example below; 332 | 333 | ```js 334 | const local = require('indexeddb')('local', { 335 | version: 1 336 | }) 337 | 338 | local.sync({ 339 | pull: new APIPush, 340 | push: new APIPull 341 | }) 342 | ``` 343 | 344 | ## Examples 345 | 346 | See `test` folder. 347 | 348 | ## Tests 349 | 350 | I use [prova](https://github.com/azer/prova) for testing. After running the following commands, open up `localhost:7559` see the results: 351 | 352 | * Simple create/update tests: `node test/crud.js -b` 353 | * Select queries: `node test/select.js -b` 354 | * Sync: 355 | * Start sample sync backend: `node test/fixtures/sync-server.js` 356 | * Run frontend tests: `node test/custom-sync.js -b -y "/sync-api=http://localhost:3000"` 357 | --------------------------------------------------------------------------------