abstract-nosql
Version:
An abstract prototype for nosql database with sync and streamable (LevelDOWN API)
618 lines (559 loc) • 17.9 kB
text/coffeescript
# Copyright (c) 2013 Rod Vagg, MIT License
# Copyright (c) 2014 Riceball LEE, MIT License
xtend = require("xtend")
AbstractObject = require("abstract-object")
Codec = require("buffer-codec")
utf8ByteLength = Codec.getByteLen
Errors = require("./abstract-error")
try AbstractIterator = require("abstract-iterator")
AbstractChainedBatch = require("./abstract-chained-batch")
setImmediate = global.setImmediate or process.nextTick
AbstractError = Errors.AbstractError
NotImplementedError = Errors.NotImplementedError
InvalidArgumentError = Errors.InvalidArgumentError
OpenError = Errors.OpenError
CloseError = Errors.CloseError
inherits = require("inherits-ex/lib/inherits")
isString = require("util-ex/lib/is/type/string")
isFunction = require("util-ex/lib/is/type/function")
isArray = require("util-ex/lib/is/type/array")
module.exports = class AbstractNoSQL
inherits AbstractNoSQL, AbstractObject
constructor: ->
super
initialize:(location) ->
#not all database have the location argument.
if location and typeof location isnt "string"
throw new InvalidArgumentError("constructor requires a location string argument")
@location = location
finalize: ->
if @_opened
if @_closeSync then @closeSync()
else @closeAsync()
@_options = null
@::__defineGetter__ "opened", ->
!!@_opened
setOpened: (aValue, options)->
if aValue
@_opened = true
@_options = options if options
else
@_opened = false
#the optimal low-level sync functions:
isExistsSync: (key, options) ->
options = {} unless options?
key = String(key) unless @_isBuffer(key)
if @_isExistsSync
result = @_isExistsSync(key, options)
return result
else if @_getSync
try
@_getSync key, options
return true
catch err
if AbstractError.isNotFound(err)
return false
else
throw err
throw new NotImplementedError()
isExistSync: @::isExistsSync
getSync: (key, options) ->
if @_getSync
options = {} unless options?
throw err if err = @_checkKey(key, "key")
key = String(key) unless @_isBuffer(key) #TODO should move to low-level db.
result = @_getSync(key, options)
return result
throw new NotImplementedError()
getBufferSync: (key, destBuffer, options) ->
if @_getBufferSync
options = {} unless options?
options.offset = 0 unless options.offset?
result = @_getBufferSync(key, destBuffer, options)
return result
throw new NotImplementedError()
mGetSync: (keys, options) ->
if @_mGetSync
options = {} unless options?
options.raiseError = options.raiseError isnt false
needKeyName = options.keys
arr = @_mGetSync(keys, options)
i = 0
result = []
while i < arr.length
if needKeyName isnt false
result.push
key: arr[i]
value: arr[++i]
else
result.push arr[i]
i++
return result
throw new NotImplementedError()
putSync: (key, value, options) ->
if @_putSync
options = {} unless options?
result = @_putSync(key, value, options)
return result
throw new NotImplementedError()
delSync: (key, options) ->
if @_delSync
options = {} unless options?
result = @_delSync(key, options)
return result
throw new NotImplementedError()
batchSync: (operations, options) ->
if @_batchSync
options = {} unless options?
unless isArray(operations)
throw new InvalidArgumentError("batch(operations) requires an array argument")
for e in operations
continue unless typeof e is "object"
throw err if err = @_checkKey(e.type, "type")
throw err if err = @_checkKey(e.key, "key")
result = @_batchSync(operations, options)
return result
throw new NotImplementedError()
approximateSizeSync: (start, end) ->
if @_approximateSizeSync
if not start? or not end?
throw new InvalidArgumentError "
approximateSize() requires valid `start`, `end` arguments"
start = String(start) unless @_isBuffer(start)
end = String(end) unless @_isBuffer(end)
result = @_approximateSizeSync(start, end)
return result
throw new NotImplementedError()
openSync: (options) ->
if @_openSync
options = @_options || {} unless options?
options.createIfMissing = options.createIfMissing isnt false
options.errorIfExists = !!options.errorIfExists
result = @_openSync(options)
@setOpened true, options if result
result = @ if result
return result
throw new NotImplementedError()
#if successful should return true.
closeSync: ->
if @_closeSync
result = @_closeSync()
@setOpened false if result
return result
throw new NotImplementedError()
#the async methods simulated by sync methods:
#the derived class can override these methods to implement
#the real async methods for better performance.
_open: (options, callback) ->
that = this
if @_openSync
setImmediate ->
result = undefined
try
result = that._openSync(options)
catch err
callback err
return
if result
callback null, result
else
callback new OpenError("can not open database.")
else
setImmediate callback
_close: (callback) ->
that = this
if @_closeSync
setImmediate ->
result = undefined
try
result = that._closeSync()
catch err
callback err
return
if result
callback null, result
else
callback new CloseError("can not close database.")
else
setImmediate callback
_isExists: (key, options, callback) ->
that = this
if @_isExistsSync
setImmediate ->
result = undefined
try
result = that._isExistsSync(key, options)
catch err
callback err
return
callback null, result
else
@_get key, options, (err, value) ->
if err
if AbstractError.isNotFound(err)
callback null, false
else
callback err
else
callback null, true
_getBuffer: (key, destBuffer, options, callback) ->
that = this
if @_getSync or @_getBufferSync isnt AbstractNoSQL::_getBufferSync
setImmediate ->
result = undefined
try
result = that._getBufferSync(key, destBuffer, options)
catch err
callback err
return
callback null, result
else if @_get
@_get key, options, (err, value)->
return callback(err) if err
result = utf8ByteLength(value)
if destBuffer
result = Math.min(result, destBuffer.length)
result = destBuffer.write(value, options.offset, result) if result
callback null, result
else
setImmediate callback
_getBufferSync: (key, destBuffer, options) ->
if @_getSync
value = @_getSync(key, options)
result = utf8ByteLength(value)
if destBuffer
result = Math.min(result, destBuffer.length)
result = destBuffer.write(value, options.offset, result) if result
return result
else
throw new NotImplementedError('_mGetSync: _getSync is not implemented.')
_mGetSync: (keys, options) ->
if @_getSync
result = []
needKeyName = options.keys
raiseError = options.raiseError
options.asBuffer = options.asBuffer is true
for key in keys
try
value = @_getSync(key, options)
catch err
throw err if raiseError
value = undefined
if needKeyName isnt false
result.push key, value
else
result.push value
return result
else
throw new NotImplementedError('_mGetSync: _getSync is not implemented.')
_mGet: (keys, options, callback) ->
that = this
if @_getSync or @_mGetSync isnt AbstractNoSQL::_mGetSync
setImmediate ->
result = undefined
try
result = that._mGetSync keys, options
catch err
callback err
return
callback null, result
else if keys.length > 0 and @_get
result = []
i = 0
needKeyName = options.keys
raiseError = options.raiseError
readNext = (err, value)->
return callback(err) if err and raiseError
if needKeyName isnt false
result.push keys[i], value
else
result.push value
i++
return callback(null, result) if i >= keys.length
that._get keys[i], options, readNext
@_get keys[i], options, readNext
else
setImmediate callback
_get: (key, options, callback) ->
that = this
if @_getSync
setImmediate ->
result = undefined
try
result = that._getSync(key, options)
catch err
callback err
return
callback null, result
else
setImmediate callback
_put: (key, value, options, callback) ->
that = this
if @_putSync
setImmediate ->
result = undefined
try
result = that._putSync(key, value, options)
catch err
callback err
return
callback null, result
else
setImmediate callback
_del: (key, options, callback) ->
that = this
if @_delSync
setImmediate ->
result = undefined
try
result = that._delSync(key, options)
catch err
callback err
return
callback null, result
else
setImmediate callback
_batch: (array, options, callback) ->
that = this
if @_batchSync
setImmediate ->
result = undefined
try
result = that._batchSync(array, options)
catch err
callback err
return
callback null, result
else
setImmediate callback
#TODO: remove from here, not a necessary primitive
_approximateSize: (start, end, callback) ->
that = this
if @_approximateSizeSync
setImmediate ->
result = undefined
try
result = that._approximateSizeSync(start, end)
catch err
callback err
return
callback null, result
else
setImmediate callback
#slower impl:
#
#_exec: (fn, args, callback) ->
# that = this
# if fn then setImmediate ->
# result
# try
# result = fn.apply(that, args)
# catch (err)
# callback(err)
# return
#
# callback(null, result)
# else
# setImmediate(callback)
#
#_open: (options, callback) ->
# this._exec(this._openSync, [options], callback)
#
#
openAsync: (options, callback) ->
options = {} unless options?
options.createIfMissing = options.createIfMissing isnt false
options.errorIfExists = !!options.errorIfExists
that = this
@_open options, (err, result) ->
that.setOpened true, options if not err?
callback err, result
open: (options, callback) ->
if isFunction options
callback = options
options = undefined
if callback
@openAsync options, callback
else
@openSync options
closeAsync: (callback) ->
that = this
callback = undefined unless isFunction callback
@_close (err, result) ->
return callback err if err
that.setOpened false
callback null, result if callback
close: (callback) ->
if callback
@closeAsync callback
else
@closeSync()
isExistsAsync: (key, options, callback) ->
options = {} unless options?
key = String(key) unless @_isBuffer(key)
@_isExists key, options, callback
isExists: (key, options, callback) ->
if isFunction options
callback = options
options = {}
else
if callback
@isExistsAsync key, options, callback
else
@isExistsSync key, options
isExist: @::isExists
getBufferAsync: (key, destBuffer, options, callback) ->
options = {} unless options?
options.offset = 0 unless options.offset?
@_getBuffer key, destBuffer, options, callback
getBuffer: (key, destBuffer, options, callback) ->
err = undefined
if isFunction options
callback = options
options = {}
if callback
@getBufferAsync key, destBuffer, options, callback
else
@getBufferSync key, destBuffer, options
mGetAsync: (keys, options, callback) ->
options = {} unless options?
options.asBuffer = options.asBuffer is true
options.raiseError = options.raiseError isnt false
needKeyName = options.keys isnt false
@_mGet keys, options, (err, arr)->
return callback(err) if err
if needKeyName
i = 0
result = []
while i < arr.length
result.push
key: arr[i]
value: arr[++i]
i++
else
result = arr
callback null, result
mGet: (keys, options, callback) ->
err = undefined
if isFunction options
callback = options
options = {}
else
if callback
@mGetAsync keys, options, callback
else
@mGetSync keys, options
getAsync: (key, options, callback) ->
options = {} unless options?
return callback(err) if err = @_checkKey(key, "key")
key = String(key) unless @_isBuffer(key)
options.asBuffer = options.asBuffer is true
@_get key, options, callback
get: (key, options, callback) ->
err = undefined
if isFunction options
callback = options
options = {}
if callback
@getAsync key, options, callback
else
@getSync key, options
putAsync: (key, value, options, callback) ->
options = {} unless options?
return callback(err) if err = @_checkKey(key, "key", @_isBuffer)
key = String(key) unless @_isBuffer(key)
# coerce value to string in node, don't touch it in browser
# (indexeddb can store any JS type)
value = String(value) if value? and not @_isBuffer(value) and not process.browser
@_put key, value, options, callback
put: (key, value, options, callback) ->
err = undefined
if isFunction options
callback = options
options = {}
if callback
@putAsync key, value, options, callback
else
@putSync key, value, options
delAsync: (key, options, callback) ->
options = {} unless options?
return callback(err) if err = @_checkKey(key, "key", @_isBuffer)
key = String(key) unless @_isBuffer(key)
@_del key, options, callback
del: (key, options, callback) ->
err = undefined
if isFunction options
callback = options
options = {}
if callback
@delAsync key, options, callback
else
@delSync key, options
batchAsync: (array, options, callback) ->
options = {} unless options?
unless isArray(array)
vError = new InvalidArgumentError("batch(array) requires an array argument")
return callback(vError)
for e in array
continue unless typeof e is "object"
return callback(err) if err = @_checkKey(e.type, "type")
return callback(err) if err = @_checkKey(e.key, "key")
@_batch array, options, callback
batch: (array, options, callback) ->
return @_chainedBatch() unless arguments.length
if isFunction options
callback = options
options = {}
callback = array if isFunction array
if callback
@batchAsync array, options, callback
else
@batchSync array, options
#TODO: remove from here, not a necessary primitive
approximateSizeAsync: (start, end, callback) ->
start = String(start) unless @_isBuffer(start)
end = String(end) unless @_isBuffer(end)
@_approximateSize start, end, callback
approximateSize: (start, end, callback) ->
if not start? or not end? or isFunction(start) or isFunction(end)
throw new InvalidArgumentError "
approximateSize() requires valid `start`, `end` and `callback`(for async) arguments"
if callback
@approximateSizeAsync start, end, callback
else
@approximateSizeSync start, end
#should override this to test sync or if you do not wanna
#implement the _iterator function.
IteratorClass: AbstractIterator
iterator: (options) ->
options = {} unless typeof options is "object"
if @IteratorClass
return new @IteratorClass(this, options)
else if isFunction @_iterator
console.error "_iterator is deprecated. please use the IteratorClass instead."
return @_iterator(options)
throw new NotImplementedError()
_chainedBatch: ->
new AbstractChainedBatch(this)
_isBuffer: (obj) ->
Buffer.isBuffer obj
_checkKey: (obj, type) ->
if not obj?
return new InvalidArgumentError(type + " cannot be `null` or `undefined`")
if @_isBuffer(obj)
if obj.length is 0
return new InvalidArgumentError(type + " cannot be an empty Buffer")
else if String(obj) is ""
return new InvalidArgumentError(type + " cannot be an empty String")
isOpen: ->
!!@_opened
module.exports.AbstractNoSQL = AbstractNoSQL
module.exports.__defineGetter__ "AbstractLevelDOWN", ->
console.error "AbstractLevelDOWN is deprecated. use AbstractNoSQL instead."
AbstractNoSQL
module.exports.__defineGetter__ "AbstractIterator", ->
console.error "AbstractIterator is deprecated. it's moved to abstract-iterator."
console.error "first `npm install abstract-iterator`" unless AbstractIterator
AbstractIterator
#module.exports.AbstractIterator = AbstractIterator
module.exports.AbstractChainedBatch = AbstractChainedBatch