UNPKG

nqm-minimongo

Version:

Client-side mongo database with server sync over http

296 lines (252 loc) 10.4 kB
_ = require 'lodash' async = require 'async' createUid = require('./utils').createUid processFind = require('./utils').processFind compileSort = require('./selector').compileSort # Do nothing callback for success doNothing = -> return module.exports = class WebSQLDb constructor: (options, success, error) -> @collections = {} # Create database # TODO escape name @db = window.openDatabase 'minimongo_' + options.namespace, '', 'Minimongo:' + options.namespace, 5 * 1024 * 1024 if not @db return error("Failed to create database") createTables = (tx) -> tx.executeSql(''' CREATE TABLE IF NOT EXISTS docs ( col TEXT NOT NULL, id TEXT NOT NULL, state TEXT NOT NULL, doc TEXT, PRIMARY KEY (col, id));''', [], doNothing, error) # Create tables @db.transaction createTables, error, => if success then success(this) 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, error) , 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)) , error , error upsert: (doc, success, error) -> # Android 2.x requires error callback error = error or -> return # Handle both single and multiple upsert items = doc if not _.isArray(items) items = [items] for item in items if not item._id item._id = createUid() @db.transaction (tx) => for item in items tx.executeSql "INSERT OR REPLACE INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, item._id, "upserted", JSON.stringify(item)], doNothing, error , error , -> if success then success(doc) remove: (id, success, error) -> # 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) , error else tx.executeSql "INSERT INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, id, "removed", JSON.stringify({_id: id})], -> if success then success(id) , error , error , 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() , error else callback() else callback() , callback, error , (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() , error else callback() , callback, error , (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 JSON.parse(row.doc) if success? then success(docs) , error , 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) , error , error resolveUpsert: (doc, success, error) -> # Android 2.x requires error callback error = error or -> return # Handle both single and multiple resolve items = doc if not _.isArray(items) items = [items] # Find records @db.transaction (tx) => async.eachSeries items, (item, cb) => tx.executeSql "SELECT * FROM docs WHERE col = ? AND id = ?", [@name, item._id], (tx, results) => if results.rows.length > 0 # Only safely remove upsert if doc is the same if results.rows.item(0).state == "upserted" and _.isEqual(JSON.parse(results.rows.item(0).doc), item) tx.executeSql 'UPDATE docs SET state="cached" WHERE col = ? AND id = ?', [@name, item._id], doNothing, error cb() else cb() else # Upsert removed, which is fine cb() , error , (err) -> if err return error(err) # Success if success then success(doc) , 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) , error , error # Add but do not overwrite or record as upsert seed: (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 if results.rows.length == 0 tx.executeSql "INSERT INTO docs (col, id, state, doc) VALUES (?, ?, ?, ?)", [@name, doc._id, "cached", JSON.stringify(doc)], -> if success then success(doc) , error else if success then success(doc) , error , 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) , error else if success then success(doc) else if success then success(doc) , error , error