UNPKG

loopback-connector-couch

Version:
415 lines (349 loc) 13.6 kB
{_} = require 'lodash' debug = require('debug')('loopback:connector:couch') # api exports.initialize = (dataSource, callback) -> connector = new CouchConnector dataSource return callback && process.nextTick callback #Constructor and useful reference functions class CouchConnector constructor: (dataSource) -> @dataSource = dataSource dataSource.connector = this settings = dataSource.settings or {} @settings = settings helpers.optimizeSettings settings design = views: by_model: map: 'function (doc) { if (doc.loopbackModel) return emit(doc.loopbackModel, null); }' if settings.auth?.reader @_nanoReader = require('nano')(@buildAuthUrl(settings.auth.reader)) if settings.auth?.writer @_nanoWriter = require('nano')(@buildAuthUrl(settings.auth.writer)) if settings.auth?.admin @_nanoAdmin = require('nano')(@buildAuthUrl(settings.auth.admin)) @_nanoReader = require('nano')(@buildAuthUrl(settings.auth)) if not @_nanoReader @_nanoWriter = require('nano')(@buildAuthUrl(settings.auth)) if not @_nanoWriter @_nanoAdmin = require('nano')(@buildAuthUrl(settings.auth)) if not @_nanoAdmin helpers.updateDesign @_nanoAdmin, '_design/loopback', design @_models = {} @name = 'couchdb' if settings.views and _.isArray settings.views @DataAccessObject = () -> #add existing methods if dataSource.constructor.DataAccessObject for k,v of dataSource.constructor.DataAccessObject @DataAccessObject[k] = v for k,v of dataSource.constructor.DataAccessObject.prototype @DataAccessObject.prototype[k] = v #then add connector method viewFn = @buildViewEndpoint settings.views @DataAccessObject.queryView = viewFn dataSource.queryView = viewFn return this relational: false getDefaultIdType: () -> return String getTypes: () -> return ['db', 'nosql', 'couchdb'] getMetadata: () -> unless @_metaData @_metaData = types: @getTypes() defaultIdType: @getDefaultIdType() isRelational: @isRelational schemaForSettings: {} return @_metaData define: (descr) -> modelName = descr.model.modelName @_models[modelName] = descr descr.properties._rev = type: String # Add index views for schemas that have indexes design = views: {} hasIndexes = false for propName, value of descr.properties if value.index hasIndexes = true viewName = helpers.viewName propName design.views[viewName] = map: 'function (doc) { if (doc.loopbackModel === \'' + modelName + '\' && doc.'+propName + ') return emit(doc.' + propName + ', null); }' if hasIndexes designName = '_design/' + helpers.designName modelName helpers.updateDesign @_nanoAdmin, designName, design #Loopback.io prototype functions create: (model, data, callback) -> debug 'CouchDB create' @save model, data, callback save: (model, data, callback) -> debug 'CouchDB save' return callback and callback "Cannot create an empty document in the database" if not data delete data._deleted # Prevents accidental deletion via save command @_nanoWriter.insert @forDB(model, data), (err, rsp) => return callback err if err # Undo the effects of savePrep as data object is the only one # that the Loopback.io can access. helpers.undoPrep data # Update the data object with the revision returned by CouchDb. data._rev = rsp.rev return callback and callback null, rsp.id, rsp.rev updateOrCreate: (model, data, callback) -> debug 'CouchDB updateOrCreate' delete data._deleted # Prevents accidental deletion return @save model, data, (err, id, rev) -> return callback and callback err if err data.id = id data._rev = rev return callback and callback null, data update: (model, where, data, callback) -> debug 'CouchDB update' delete data._deleted # Prevents accidental deletion @all model, {where}, (err, docsFromDb) => return callback and callback err if err helpers.merge(docsFromDb, data) if (not _.isArray docsFromDb) docsFromDb = [docsFromDb] docs = (@forDB model, doc for doc in docsFromDb) debug docs @_nanoWriter.bulk {docs}, (err, rsp) -> return callback and callback err, rsp updateAttributes: (model, id, attributes, callback) -> debug 'CouchDB updateAttributes' delete attributes._deleted #prevent accidental deletion @_nanoReader.get id, (err, doc) => return callback and callback err if err @save model, helpers.merge(doc, attributes), (err, rsp) -> return callback and callback err if err doc._rev = rsp.rev return callback and callback null, doc destroyAll: (model, where, callback) -> debug 'CouchDB destroyAll' @all model, {where}, (err, docs) => return callback and callback err if err debug docs docs = for doc in docs {_id: doc.id, _rev: doc._rev, _deleted: yes} @_nanoWriter.bulk {docs}, (err, rsp) -> return callback and callback err, rsp count: (model, callback, where) -> debug 'CouchDB count' @all model, {where}, (err, docs) => return callback and callback err if err callback and callback null, docs.length all: (model, filter, callback) -> debug 'CouchDB all' debug filter # Consider first the easy case that a specific id is requested if id = filter?.where?.id debug '...moving to findById from all' return @findById(model, id, callback) params = keys: [model] include_docs: yes params.skip = filter.offset if filter.offset and not filter.where params.limit = filter.limit if filter.limit and not filter.where #if you have a where clause and a limit first get all the data and then limit them # We always fallback on loopback/by_model view as it allows us # to iterate over all the docs for a model. But check if # there is a specialized view for one of the where conditions. designName = 'loopback' viewName = 'by_model' if where = filter?.where props = @_models[model].properties for propName, value of where # We can use an optimal view when a where "clause" uses an indexed property if value and props[propName]? and props[propName].index # Use the design and view for the model and propName designName = helpers.designName model viewName = helpers.viewName propName # CouchDb stores dates as Unix time params.key = if _.isDate value then value.getTime() else value # We don't want to use keys - we now have a key property delete params.keys break @_nanoReader.view designName, viewName, params, (err, body) => return callback and callback err if err docs = for row in body.rows row.doc.id = row.doc._id delete row.doc._id row.doc debug "CouchDB all: docs before where" debug docs if where = filter?.where for k, v of where # CouchDb stores dates as Unix time where[k] = v.getTime() if _.isDate v docs = _.where docs, where debug "CouchDB all: docs after where" debug docs if orders = filter?.order orders = [orders] if _.isString orders sorting = (a, b) -> for item, i in @ ak = a[@[i].key]; bk = b[@[i].key]; rev = @[i].reverse if ak > bk then return 1 * rev if ak < bk then return -1 * rev 0 for key, i in orders orders[i] = reverse: helpers.reverse key key: helpers.stripOrder key docs.sort sorting.bind orders if filter?.limit and filter?.where maxDocsNum = filter.limit else maxDocsNum = docs.length if filter?.offset and filter?.where startDocsNum = filter.offset else startDocsNum = 0 docs = docs.slice startDocsNum, maxDocsNum output = (@fromDB model, doc for doc in docs) return callback null, output forDB: (model, data = {}) -> helpers.savePrep model, data props = @_models[model].properties for k, v of props if data[k] and props[k].type.name is 'Date' and data[k].getTime? data[k] = data[k].getTime() data fromDB: (model, data) -> return data unless data helpers.undoPrep data props = @_models[model].properties for k, v of props if data[k]? and props[k].type.name is 'Date' date = new Date data[k] date.setTime data[k] data[k] = date data exists: (model, id, callback) -> debug 'CouchdDB exists' @_nanoReader.head id, (err, _, headers) -> return callback and callback null, 0 if err callback && callback null, 1 getLatestRevision: (model, id, callback) -> @_nanoReader.head id, (err, _, headers) -> return callback and callback err if err rev = headers.etag.substr(1, headers.etag.length - 2) return callback and callback null, rev destroy: (model, id, callback) -> debug 'CouchDB destroy' @getLatestRevision model, id, (err, rev) => return callback and callback err if err @_nanoWriter.destroy id, rev, (err, rsp) => return callback and callback err, rsp findById: (model, id, callback) -> debug 'CouchDB findById' @_nanoReader.get id, (err, doc) => debug err, doc return callback and callback null, [] if err and err.statusCode is 404 return callback and callback err if err return callback and callback null, [(@fromDB model, doc)] # Uses array as this function is called by all who needs to return array viewFunction: (model, ddoc, viewname, keys, callback) -> ddoc = if ddoc then ddoc else @settings.database or @settings.db view = _.findWhere @_availableViews, {ddoc: ddoc, name: viewname} if not view return callback and callback "The requested view is not available in the datasource" params = keys if typeof keys is 'function' callback = keys params = {} if typeof keys is 'string' params = keys: [keys] if _.isArray keys params = keys: keys debug model, ddoc, viewname, params @_nanoReader.view ddoc, viewname, params, (err, rsp) => return callback and callback err if err docs = _.pluck rsp.rows, 'value' return callback and callback null, (@fromDB model, doc for doc in docs) buildViewEndpoint: (views) -> @_availableViews = views fn = _.bind @viewFunction, @ fn.accepts = [ { arg: 'modelName' type: "string" description: "The current model name" required: false http: (ctx) -> return ctx.method.sharedClass.name }, { arg: 'ddoc', type: "string", description: "The design document name for the requested view. Defaults to CouchDB database name used for this data.", required: false, http: {source: 'query'}}, { arg: 'viewname', type: "string", description: "The view name requested.", required: true, http: {source: 'query'}}, { arg: 'keys', type: "object", description: "The index(es) requested to narrow view results. Parameter can be a string, array of strings or object with 'key' or with 'startkey' and 'endkey', as per CouchDB. Use the object version for complex keys querying.", required: false, http: {source: 'query'}} ] fn.returns = { arg: 'items', type: "array"} fn.shared = true fn.http = path: '/queryView', verb: 'get' fn.description = "Query a CouchDB view based on design document name, view name and keys." return fn buildAuthUrl: (auth) -> if auth and (auth.username or auth.user) and (auth.password or auth.pass) authString = (auth.username || auth.user) + ':' + (auth.password || auth.pass) + '@' else authString = '' url = @settings.protocol + '://' + authString + @settings.hostname + ':' + @settings.port + '/' + @settings.database return url # helpers helpers = optimizeSettings: (settings) -> settings.hostname = settings.hostname or settings.host or '127.0.0.1' settings.protocol = settings.protocol or 'http' settings.port = settings.port or 5984 settings.database = settings.database or settings.db if (not settings.database) throw new Error("Database name must be specified in dataSource for CouchDB connector") merge: (base, update) -> return update unless base if not _.isArray base _.extend base, update else _.each base, (doc) -> _.extend doc, update base reverse: (key) -> if hasOrder = key.match(/\s+(A|DE)SC$/i) return -1 if hasOrder[1] is "DE" return 1 stripOrder: (key) -> key.replace(/\s+(A|DE)SC/i, "") savePrep: (model, data) -> if id = data.id data._id = id.toString() delete data.id if data._rev is null delete data._rev if model data.loopbackModel = model return undoPrep: (data) -> if _id = data._id data.id = _id.toString() delete data._id delete data.loopbackModel return designName: (modelName) -> 'loopback_' + modelName viewName: (propName) -> 'by_' + propName invokeCallbackOrLogError: (callback, err, res) -> # When callback exists let it handle the error and result if callback callback and callback err, res else if err # Without a callback we can at least log the error console.log err updateDesign: (db, designName, design, callback) -> # Add the design document to the database or update it if it already exists. db.get designName, (err, designDoc) => if err && err.error != 'not_found' return helpers.invokeCallbackOrLogError callback, err, designDoc # Update the design doc if !designDoc designDoc = design else # We only update the design when its views have changed - this avoids rebuilding the views. if _.isEqual(designDoc.views, design.views) return helpers.invokeCallbackOrLogError callback, null, designDoc designDoc.views = design.views # Insert the design doc into the database. db.insert designDoc, designName, (err, insertedDoc) => return helpers.invokeCallbackOrLogError callback, err, insertedDoc