UNPKG

nqm-minimongo

Version:

Client-side mongo database with server sync over http

382 lines (329 loc) 13.8 kB
_ = require 'lodash' async = require 'async' utils = require('./utils') processFind = require('./utils').processFind compileSort = require('./selector').compileSort # Do nothing callback for success doNothing = -> return module.exports = class WebSQLDb constructor: (options, success, error) -> @collections = {} try # Create database # TODO escape name @db = window.openDatabase 'minimongo_' + options.namespace, '', 'Minimongo:' + options.namespace, 5 * 1024 * 1024 if not @db return error(new Error("Failed to create database")) catch ex if error error(ex) return migrateToV1 = (tx) -> tx.executeSql(''' CREATE TABLE docs ( col TEXT NOT NULL, id TEXT NOT NULL, state TEXT NOT NULL, doc TEXT, PRIMARY KEY (col, id));''', [], doNothing, ((tx, err) -> error(err))) migrateToV2 = (tx) -> tx.executeSql(''' ALTER TABLE docs ADD COLUMN base TEXT;''', [], doNothing, ((tx, err) -> error(err))) # Check if at v2 version checkV2 = => if @db.version == "1.0" @db.changeVersion "1.0", "2.0", migrateToV2, error, => if success then success(this) else if @db.version != "2.0" return error("Unknown db version " + @db.version) else if success then success(this) if not @db.version @db.changeVersion "", "1.0", migrateToV1, error, checkV2 else checkV2() return @db addCollection: (name, success, error) -> collection = new Collection(name, @db) @[name] = collection @collections[name] = collection if success success() removeCollection: (name, success, error) -> delete @[name] delete @collections[name] # Remove all documents of collection @db.transaction (tx) -> tx.executeSql("DELETE FROM docs WHERE col = ?", [name], success, ((tx, err) -> error(err))) , error # Stores data in indexeddb store class Collection constructor: (name, db) -> @name = name @db = db find: (selector, options) -> return fetch: (success, error) => @_findFetch(selector, options, success, error) findOne: (selector, options, success, error) -> if _.isFunction(options) [options, success, error] = [{}, options, success] @find(selector, options).fetch (results) -> if success? then success(if results.length>0 then results[0] else null) , error _findFetch: (selector, options, success, error) -> # Android 2.x requires error callback error = error or -> return # Get all docs from collection @db.readTransaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ?", [@name], (tx, results) -> docs = [] for i in [0...results.rows.length] row = results.rows.item(i) if row.state != "removed" docs.push JSON.parse(row.doc) if success? then success(processFind(docs, selector, options)) , ((tx, err) -> error(err)) , error upsert: (docs, bases, success, error) -> [items, success, error] = utils.regularizeUpsert(docs, bases, success, error) # Android 2.x requires error callback error = error or -> return @db.transaction (tx) => ids = _.map(items, (item) -> item.doc._id) # Get bases bases = {} async.eachSeries ids, (id, callback) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, id], (tx2, results) -> tx = tx2 if results.rows.length > 0 row = results.rows.item(0) if row.state == "upserted" bases[row.id] = if row.base then JSON.parse(row.base) else null else if row.state == "cached" bases[row.id] = JSON.parse(row.doc) callback() , ((tx, err) -> error(err)) , => for item in items id = item.doc._id # Prefer explicit base if item.base != undefined base = item.base else if bases[id] base = bases[id] else base = null tx.executeSql "INSERT OR REPLACE INTO docs (col, id, state, doc, base) VALUES (?, ?, ?, ?, ?)", [@name, item.doc._id, "upserted", JSON.stringify(item.doc), JSON.stringify(base)], doNothing, ((tx, err) -> error(err)) , error , -> if success then success(docs) remove: (id, success, error) -> # Special case for filter-type remove if _.isObject(id) @find(id).fetch (rows) => async.each rows, (row, cb) => @remove(row._id, (=> cb()), cb) , => success() , error return # Android 2.x requires error callback error = error or -> return # Find record @db.transaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, id], (tx, results) => if results.rows.length > 0 # Change to removed tx.executeSql 'UPDATE docs SET state="removed" WHERE col = ? AND id = ?', [@name, id], -> if success then success(id) , ((tx, err) -> error(err)) else tx.executeSql "INSERT INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, id, "removed", JSON.stringify({_id: id})], -> if success then success(id) , ((tx, err) -> error(err)) , ((tx, err) -> error(err)) , error cache: (docs, selector, options, success, error) -> # Android 2.x requires error callback error = error or -> return @db.transaction (tx) => # Add all non-local that are not upserted or removed async.eachSeries docs, (doc, callback) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, doc._id], (tx, results) => # Check if present and not upserted/deleted if results.rows.length == 0 or results.rows.item(0).state == "cached" existing = if results.rows.length > 0 then JSON.parse(results.rows.item(0).doc) else null # If _rev present, make sure that not overwritten by lower _rev if not existing or not doc._rev or not existing._rev or doc._rev >= existing._rev # Upsert tx.executeSql "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, doc._id, "cached", JSON.stringify(doc)], -> callback() , ((tx, err) -> error(err)) else callback() else callback() , ((tx, err) -> error(err)) , (err) => if err if error then error(err) return # Rows have been cached, now look for stale ones to remove docsMap = _.object(_.pluck(docs, "_id"), docs) if options.sort sort = compileSort(options.sort) # Perform query, removing rows missing in docs from local db @find(selector, options).fetch (results) => @db.transaction (tx) => async.eachSeries results, (result, callback) => # If not present in docs and is present locally and not upserted/deleted tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, result._id], (tx, rows) => if not docsMap[result._id] and rows.rows.length > 0 and rows.rows.item(0).state == "cached" # If past end on sorted limited, ignore if options.sort and options.limit and docs.length == options.limit if sort(result, _.last(docs)) >= 0 return callback() # Item is gone from server, remove locally tx.executeSql "DELETE FROM docs WHERE col = ? AND id = ?", [@name, result._id], -> callback() , ((tx, err) -> error(err)) else callback() , ((tx, err) -> error(err)) , (err) -> if err? if error? then error(err) return if success? then success() , error , error , error pendingUpserts: (success, error) -> # Android 2.x requires error callback error = error or -> return @db.readTransaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND state = ?", [@name, "upserted"], (tx, results) -> docs = [] for i in [0...results.rows.length] row = results.rows.item(i) docs.push { doc: JSON.parse(row.doc), base: if row.base then JSON.parse(row.base) else null } if success? then success(docs) , ((tx, err) -> error(err)) , error pendingRemoves: (success, error) -> # Android 2.x requires error callback error = error or -> return @db.readTransaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND state = ?", [@name, "removed"], (tx, results) -> docs = [] for i in [0...results.rows.length] row = results.rows.item(i) docs.push JSON.parse(row.doc)._id if success? then success(docs) , ((tx, err) -> error(err)) , error resolveUpserts: (upserts, success, error) -> # Android 2.x requires error callback error = error or -> return # Find records @db.transaction (tx) => async.eachSeries upserts, (upsert, cb) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, upsert.doc._id], (tx, results) => if results.rows.length > 0 and results.rows.item(0).state == "upserted" # Only safely remove upsert if doc is the same if _.isEqual(JSON.parse(results.rows.item(0).doc), upsert.doc) tx.executeSql 'UPDATE docs SET state="cached" WHERE col = ? AND id = ?', [@name, upsert.doc._id], doNothing, ((tx, err) -> error(err)) cb() else tx.executeSql 'UPDATE docs SET base=? WHERE col = ? AND id = ?', [JSON.stringify(upsert.doc), @name, upsert.doc._id], doNothing, ((tx, err) -> error(err)) cb() else # Upsert removed, which is fine cb() , ((tx, err) -> error(err)) , (err) -> if err return error(err) # Success if success then success() , error resolveRemove: (id, success, error) -> # Android 2.x requires error callback error = error or -> return # Find record @db.transaction (tx) => # Only safely remove if removed state tx.executeSql 'DELETE FROM docs WHERE state="removed" AND col = ? AND id = ?', [@name, id], -> if success then success(id) , ((tx, err) -> error(err)) , error # Add but do not overwrite or record as upsert seed: (docs, success, error) -> if not _.isArray(docs) docs = [docs] # Android 2.x requires error callback error = error or -> return @db.transaction (tx) => # Add all non-local that are not upserted or removed async.eachSeries docs, (doc, callback) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, doc._id], (tx, results) => # Check if present if results.rows.length == 0 # Upsert tx.executeSql "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, doc._id, "cached", JSON.stringify(doc)], -> callback() , ((tx, err) -> error(err)) else callback() , ((tx, err) -> error(err)) , (err) => if err if error then error(err) else if success then success() , error # Add but do not overwrite upsert/removed and do not record as upsert cacheOne: (doc, success, error) -> # Android 2.x requires error callback error = error or -> return @db.transaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, doc._id], (tx, results) => # Only insert if not present or cached if results.rows.length == 0 or results.rows.item(0).state == "cached" existing = if results.rows.length > 0 then JSON.parse(results.rows.item(0).doc) else null # If _rev present, make sure that not overwritten by lower _rev if not existing or not doc._rev or not existing._rev or doc._rev >= existing._rev tx.executeSql "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, doc._id, "cached", JSON.stringify(doc)], -> if success then success(doc) , ((tx, err) -> error(err)) else if success then success(doc) else if success then success(doc) , ((tx, err) -> error(err)) , error uncache: (selector, success, error) -> compiledSelector = utils.compileDocumentSelector(selector) # Android 2.x requires error callback error = error or -> return @db.transaction (tx) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND state = ?", [@name, "cached"], (tx, results) => # Determine which to remove toRemove = [] for i in [0...results.rows.length] row = results.rows.item(i) doc = JSON.parse(row.doc) if compiledSelector(doc) toRemove.push(doc._id) # Add all non-local that are not upserted or removed async.eachSeries toRemove, (id, callback) => # Only safely remove if removed state tx.executeSql 'DELETE FROM docs WHERE state="cached" AND col = ? AND id = ?', [@name, id], -> callback() , ((tx, err) -> error(err)) , (err) => if err if error then error(err) else if success then success() , ((tx, err) -> error(err)) , error