@hoodie/client
Version:
Client API for the Hoodie server
1,916 lines (1,515 loc) • 407 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Hoodie = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
module.exports = Hoodie
var getState = require('./lib/get-state')
var getApi = require('./lib/get-api')
function Hoodie (options) {
var state = getState(options)
var api = getApi(state)
return api
}
},{"./lib/get-api":3,"./lib/get-state":4}],2:[function(require,module,exports){
module.exports = {
on: on,
one: one,
off: off,
trigger: trigger
}
/**
* add a listener to an event
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function on (state, eventName, handler) {
state.emitter.on(eventName, handler)
return this
}
/**
* adds a one time listener to an event
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function one (state, eventName, handler) {
state.emitter.once(eventName, handler)
return this
}
/**
* removes a listener for the specified event
*
* It will unsubscribe at most, one instance of a listener for a particular event.
* If any single listener has subcribed multiple times to the same event,
* then `off` must be called multiple times.
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function off (state, eventName, handler) {
state.emitter.removeListener(eventName, handler)
return this
}
/**
* trigger a specified event
*
* @param {String} eventName Name of event
* @param {...*} [options] Options
*/
function trigger (state, eventName) {
var args = [].slice.call(arguments, 1)
state.emitter.emit.apply(state.emitter, args)
return this
}
},{}],3:[function(require,module,exports){
module.exports = getApi
var defaultsDeep = require('lodash/defaultsDeep')
var internals = module.exports.internals = {}
internals.Store = require('@hoodie/store-client')
internals.Account = require('@hoodie/account-client')
internals.ConnectionStatus = require('@hoodie/connection-status/client')
internals.Log = require('@hoodie/log/client')
internals.pouchdbDocApi = require('pouchdb-doc-api')
internals.init = require('./init')
function getApi (state) {
var url = state.url + '/hoodie'
state.PouchDB.plugin(internals.pouchdbDocApi)
var hoodieDb = new state.PouchDB('hoodie')
var hoodieAccount = mergeOptionsAndCreate(internals.Account, {
cache: hoodieDb.doc('_local/account'),
url: url + '/account/api'
}, state.account)
var hoodieConnectionStatus = mergeOptionsAndCreate(internals.ConnectionStatus, {
cache: hoodieDb.doc('_local/connection-status'),
url: url
}, state.connectionStatus)
var log = mergeOptionsAndCreate(internals.Log, { prefix: 'hoodie' }, state.log)
var hoodieStore = new internals.Store('store', {
PouchDB: state.PouchDB,
get remote () {
return hoodieAccount.get(['id', 'session.id']).then(function (properties) {
var headers = { authorization: 'Session ' + properties.session.id }
return new state.PouchDB(url + '/store/api/' + encodeURIComponent('user/' + properties.id), {
headers: headers, // for PouchDB v7
ajax: { headers: headers } // for PouchDB v6
})
})
}
})
var api = {
get url () {
return state.url + '/hoodie'
},
// core modules
account: hoodieAccount,
store: hoodieStore,
connectionStatus: hoodieConnectionStatus,
// helpers
request: require('./request').bind(null, state),
log: log,
// events
on: require('./events').on.bind(null, state),
one: require('./events').one.bind(null, state),
off: require('./events').off.bind(null, state),
trigger: require('./events').trigger.bind(null, state)
}
api.plugin = require('./plugin').bind(null, api, state)
internals.init(api)
return api
}
function mergeOptionsAndCreate (ObjectConstructor, defaultOptions, stateOptions) {
var options = defaultsDeep(defaultOptions, stateOptions || {})
return new ObjectConstructor(options)
}
},{"./events":2,"./init":5,"./plugin":6,"./request":7,"@hoodie/account-client":9,"@hoodie/connection-status/client":28,"@hoodie/log/client":40,"@hoodie/store-client":49,"lodash/defaultsDeep":264,"pouchdb-doc-api":302}],4:[function(require,module,exports){
module.exports = getState
var EventEmitter = require('events').EventEmitter
var defaultsDeep = require('lodash/defaultsDeep')
function getState (options) {
options = options || {}
if (typeof options.url !== 'string') {
throw new TypeError('options.url is required (see https://github.com/hoodiehq/hoodie-client#constructor)')
}
if (typeof options.PouchDB !== 'function') {
throw new TypeError('options.PouchDB is required (see https://github.com/hoodiehq/hoodie-client#constructor)')
}
var requiredProperties = {
emitter: (options && options.emitter) || new EventEmitter()
}
return defaultsDeep(requiredProperties, options)
}
},{"events":101,"lodash/defaultsDeep":264}],5:[function(require,module,exports){
module.exports = init
function init (hoodie) {
// In order to prevent data loss, we want to move all data that has been
// created without an account (e.g. while offline) to the user’s account
// on signin. So before the signin happens, we store the user account’s id
// and data and store it again after the signin
hoodie.account.hook.before('signin', function (options) {
return Promise.all([
hoodie.account.get('id'),
hoodie.store.findAll()
]).then(function (results) {
options.beforeSignin = {
accountId: results[0],
docs: results[1]
}
})
})
hoodie.account.hook.after('signin', function (account, options) {
// when signing in to a newly created account, the account.id does not
// change. The same is true when the user changed their username. In both
// cases there is no need to migrate local data
if (options.beforeSignin.accountId === account.id) {
return hoodie.store.connect()
}
return hoodie.store.reset()
.then(function () {
function migrate (doc) {
doc.createdBy = account.id
delete doc._rev
return doc
}
return hoodie.store.add(options.beforeSignin.docs.map(migrate))
})
.then(function () {
return hoodie.store.connect()
})
})
hoodie.account.hook.before('signout', function () {
return hoodie.store.push()
.catch(function (error) {
if (error.status !== 401) {
throw error
}
error.message = 'Local changes could not be synced, sign in first'
throw error
})
})
hoodie.account.hook.after('signout', function (options) {
return hoodie.store.reset()
})
hoodie.account.on('unauthenticate', hoodie.store.disconnect)
hoodie.account.on('reauthenticate', hoodie.store.connect)
// handle connection status changes
hoodie.connectionStatus.on('disconnect', function () {
hoodie.account.get('session')
.then(function (session) {
if (session) {
hoodie.store.disconnect()
}
})
})
hoodie.connectionStatus.on('reconnect', function () {
hoodie.account.get('session')
.then(function (session) {
if (session) {
hoodie.store.connect()
}
})
})
hoodie.account.get('session')
.then(function (session) {
// signed out
if (!session) {
return
}
// signed in, but session was invalid
if (session.invalid) {
return
}
// hoodie.connectionStatus.ok is false if there is a connection issue
if (hoodie.connectionStatus.ok === false) {
return
}
hoodie.store.connect()
})
}
},{}],6:[function(require,module,exports){
module.exports = function pluginMethod (hoodie, options, plugin) {
if (typeof plugin === 'function') {
plugin(hoodie, options)
}
if (typeof plugin === 'object') {
Object.keys(plugin).forEach(function (key) {
hoodie[key] = plugin[key]
})
}
return hoodie
}
},{}],7:[function(require,module,exports){
module.exports = request
var Promise = require('lie')
var internals = module.exports.internals = {}
internals.request = require('./utils/request')
function request (state, options) {
if (!state) {
return Promise.reject(new Error('hoodie.request: state must be defined'))
}
if (!options) {
return Promise.reject(new Error('hoodie.request: URL or options argument must be defined'))
}
var requestOptions = {
url: options.url || options
}
if ((/^\/([^/]|$)/).test(requestOptions.url)) {
requestOptions.url = state.url + requestOptions.url
}
if (options.data) {
requestOptions.body = JSON.stringify(options.data)
}
requestOptions.method = options.method
requestOptions.headers = options.headers
return internals.request(requestOptions)
}
},{"./utils/request":8,"lie":110}],8:[function(require,module,exports){
module.exports = request
var nets = require('nets')
var Promise = require('lie')
function request (options) {
options.encoding = undefined
return new Promise(function (resolve, reject) {
nets(options, function (error, response) {
if (error) {
return reject(error)
}
if (response.statusCode >= 400) {
error = new Error('HTTP error ' + response.statusCode)
error.name = 'RequestError'
error.code = response.statusCode
error.body = response.body
return reject(error)
}
resolve(response)
})
})
}
},{"lie":110,"nets":298}],9:[function(require,module,exports){
module.exports = Account
var events = require('./lib/events')
var getState = require('./utils/get-state')
function Account (options) {
if (!(this instanceof Account)) {
return new Account(options)
}
var state = getState(options)
var api = {
signUp: require('./lib/sign-up').bind(null, state),
signIn: require('./lib/sign-in').bind(null, state),
signOut: require('./lib/sign-out').bind(null, state),
destroy: require('./lib/destroy').bind(null, state),
get: require('./lib/get').bind(null, state),
update: require('./lib/update').bind(null, state),
profile: {
get: require('./lib/profile-get').bind(null, state),
update: require('./lib/profile-update').bind(null, state)
},
request: require('./lib/request').bind(null, state),
on: events.on.bind(null, state),
one: events.one.bind(null, state),
off: events.off.bind(null, state),
hook: state.hook.api,
validate: require('./lib/validate').bind(null, state)
}
return api
}
},{"./lib/destroy":10,"./lib/events":11,"./lib/get":12,"./lib/profile-get":13,"./lib/profile-update":14,"./lib/request":15,"./lib/sign-in":16,"./lib/sign-out":17,"./lib/sign-up":18,"./lib/update":19,"./lib/validate":20,"./utils/get-state":25}],10:[function(require,module,exports){
module.exports = destroy
var omit = require('lodash/omit')
var internals = module.exports.internals = {}
internals.request = require('../utils/request')
internals.get = require('./get')
function destroy (state) {
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
var promise = Promise.resolve()
if (internals.get(state, 'session')) {
promise = promise.then(function () {
internals.request({
method: 'DELETE',
url: state.url + '/session/account',
headers: {
authorization: 'Session ' + cache.session.id
}
})
})
}
return promise.then(function () {
state.cache.unset()
var account = omit(cache, 'session')
state.emitter.emit('signout', account)
state.emitter.emit('destroy', account)
return account
})
})
}
},{"../utils/request":26,"./get":12,"lodash/omit":291}],11:[function(require,module,exports){
module.exports = {
on: on,
one: one,
off: off
}
/**
* add a listener to an event
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function on (state, eventName, handler) {
state.emitter.on(eventName, handler)
return this
}
/**
* adds a one time listener to an event
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function one (state, eventName, handler) {
state.emitter.once(eventName, handler)
return this
}
/**
* removes a listener for the specified event
*
* It will unsubscribe at most, one instance of a listener for a particular event.
* If any single listener has subcribed multiple times to the same event,
* then `off` must be called multiple times.
*
* @param {String} eventName Name of event
* @param {Function} handler callback for event
*/
function off (state, eventName, handler) {
state.emitter.removeListener(eventName, handler)
return this
}
},{}],12:[function(require,module,exports){
module.exports = accountGet
var set = require('lodash/set')
var merge = require('lodash/merge')
var internals = module.exports.internals = {}
internals.getProperties = require('../utils/get-properties')
internals.fetchProperties = require('../utils/fetch-properties')
function accountGet (state, path, options) {
if (typeof path === 'object' && !Array.isArray(path)) {
options = path
path = undefined
}
return state.setup
.then(state.cache.get)
.then(function (cachedProperties) {
if ((options && options.local) || !cachedProperties.session || pathIsLocalOnly(path)) {
return internals.getProperties(cachedProperties, path)
}
return internals.fetchProperties({
url: state.url + '/session/account',
sessionId: cachedProperties.session.id,
path: path
})
.then(function (result) {
if (typeof path === 'string') {
set(cachedProperties, path, result)
} else if (Array.isArray(path)) {
merge(cachedProperties, result, {
session: cachedProperties.session
})
} else {
result.session = cachedProperties.session
cachedProperties = result
}
// reauthenticate an expired session
if (cachedProperties.session.invalid) {
delete cachedProperties.session.invalid
state.emitter.emit('reauthenticate')
}
return state.cache.set(cachedProperties)
.then(function () {
return internals.getProperties(cachedProperties, path)
})
})
.catch(function (error) {
if (error.statusCode !== 401) {
throw error
}
cachedProperties.session.invalid = true
state.emitter.emit('unauthenticate')
return state.cache.set(cachedProperties)
.then(function () {
throw error
})
})
})
}
function pathIsLocalOnly (path) {
if (!path) {
return false
}
if (typeof path === 'string') {
return isLocalPath(path)
}
return path.filter(isLocalPath).length === path.length
}
function isLocalPath (path) {
return /^(id|session)\b/.test(path)
}
},{"../utils/fetch-properties":22,"../utils/get-properties":24,"lodash/merge":289,"lodash/set":293}],13:[function(require,module,exports){
module.exports = profileGet
var set = require('lodash/set')
var internals = module.exports.internals = {}
internals.getProperties = require('../utils/get-properties')
internals.fetchProperties = require('../utils/fetch-properties')
function profileGet (state, path, options) {
if (typeof path === 'object' && !Array.isArray(path)) {
options = path
path = undefined
}
return state.setup
.then(state.cache.get)
.then(function (cachedProperties) {
if (!cachedProperties.session) {
return internals.getProperties(cachedProperties.profile || {}, path)
}
if (options && options.local) {
return internals.getProperties(cachedProperties.profile || {}, path)
}
return internals.fetchProperties({
url: state.url + '/session/account/profile',
sessionId: cachedProperties.session.id,
path: path
})
.then(function (result) {
if (typeof path === 'string') {
set(cachedProperties.profile, path, result)
} else {
cachedProperties.profile = result
}
// reauthenticate an expired session
if (cachedProperties.session.invalid) {
delete cachedProperties.session.invalid
state.emitter.emit('reauthenticate')
}
return state.cache.set(cachedProperties)
.then(function () {
return internals.getProperties(cachedProperties.profile || {}, path)
})
})
.catch(function (error) {
if (error.statusCode !== 401) {
throw error
}
cachedProperties.session.invalid = true
state.emitter.emit('unauthenticate')
return state.cache.set(cachedProperties)
.then(function () {
throw error
})
})
})
}
},{"../utils/fetch-properties":22,"../utils/get-properties":24,"lodash/set":293}],14:[function(require,module,exports){
module.exports = updateProfile
var clone = require('lodash/clone')
var merge = require('lodash/merge')
var Promise = require('lie')
var internals = module.exports.internals = {}
internals.request = require('../utils/request')
internals.serialise = require('../utils/serialise')
function updateProfile (state, options) {
if (!options) {
return Promise.reject(new Error('Please specify a profile property to update or add.'))
}
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
return internals.request({
method: 'PATCH',
url: state.url + '/session/account/profile',
headers: {
authorization: 'Session ' + cache.session.id
},
body: internals.serialise('profile', options, cache.id + '-profile')
})
.then(function () {
if (!cache.profile) {
cache.profile = {}
}
merge(cache.profile, options)
state.cache.set(cache)
state.emitter.emit('update', clone(cache))
return cache.profile
})
.catch(function (error) {
if (error.statusCode === 401) {
cache.session.invalid = true
state.emitter.emit('unauthenticate')
state.cache.set(cache)
}
throw error
})
})
}
},{"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/clone":262,"lodash/merge":289}],15:[function(require,module,exports){
module.exports = request
var Promise = require('lie')
var internals = module.exports.internals = {}
internals.deserialise = require('../utils/deserialise')
internals.request = require('../utils/request')
internals.serialise = require('../utils/serialise')
function request (state, options) {
if (!options || !options.type) {
return Promise.reject(new Error('account.request: options.type must be passed'))
}
return state.setup
.then(function () {
return internals.request({
url: state.url + '/requests',
method: 'POST',
body: internals.serialise('request', options)
})
})
.then(function (response) {
var data = internals.deserialise(response.body)
state.emitter.emit(options.type, data)
return data
})
}
},{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110}],16:[function(require,module,exports){
module.exports = signIn
var Promise = require('lie')
var omit = require('lodash/omit')
var internals = module.exports.internals = {}
internals.deserialise = require('../utils/deserialise')
internals.request = require('../utils/request')
internals.serialise = require('../utils/serialise')
function signIn (state, options) {
if (!options) {
options = {}
}
var usernameOrPasswordUnset = !options.username || !options.password
if (usernameOrPasswordUnset && !options.token) {
return Promise.reject(new Error('options.username/options.password or options.token required'))
}
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
// If a different user is signed in than the one trying to signIn, throw an error
if (cache.session && cache.username !== options.username) {
return Promise.reject(new Error('You must sign out before signing in'))
}
return state.hook('signin', options, function (options) {
return internals.request({
url: state.url + '/session',
method: 'PUT',
body: internals.serialise('session', options)
})
.then(function (response) {
var data = internals.deserialise(response.body, {
include: 'account'
})
// admins don’t have an account
if (!data.account) {
data.account = {
username: options.username
}
}
// If the username hasn’t changed, emit 'reauthenticate' instead of 'signin'
var emitEvent = cache.username === data.account.username
? 'reauthenticate'
: 'signin'
cache = {
username: data.account.username,
session: {
id: data.id
}
}
if (data.account.id) {
cache.id = data.account.id
}
state.cache.set(cache)
state.emitter.emit(emitEvent, cache)
return omit(cache, 'session')
})
})
})
}
},{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/omit":291}],17:[function(require,module,exports){
module.exports = signOut
var omit = require('lodash/omit')
var internals = module.exports.internals = {}
internals.request = require('../utils/request')
internals.get = require('./get')
internals.generateId = require('../utils/generate-id')
function signOut (state) {
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
if (!cache.session) {
throw new Error('UnauthenticatedError: Not signed in')
}
return state.hook('signout', function () {
return internals.request({
method: 'DELETE',
url: state.url + '/session',
headers: {
authorization: 'Session ' + cache.session.id
}
})
.then(function () {
return state.cache.unset()
})
.then(function () {
return state.cache.set({
id: internals.generateId()
})
.then(function () {
var account = omit(cache, 'session')
state.emitter.emit('signout', account)
return account
})
})
})
})
}
},{"../utils/generate-id":23,"../utils/request":26,"./get":12,"lodash/omit":291}],18:[function(require,module,exports){
module.exports = signUp
var Promise = require('lie')
var internals = module.exports.internals = {}
internals.request = require('../utils/request')
internals.deserialise = require('../utils/deserialise')
internals.serialise = require('../utils/serialise')
function signUp (state, options) {
if (!options || !options.username || !options.password) {
return Promise.reject(new Error('options.username and options.password is required'))
}
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
state.validate(options)
if (options.profile) {
throw new Error('SignUp with profile data not yet implemented. Please see https://github.com/hoodiehq/hoodie-account-client/issues/11.')
}
options.createdAt = cache.createdAt
return internals.request({
url: state.url + '/session/account',
method: 'PUT',
body: internals.serialise('account', options, cache.id)
})
.then(function (response) {
var account = internals.deserialise(response.body, {
include: 'profile'
})
state.emitter.emit('signup', account)
return account
})
})
}
},{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110}],19:[function(require,module,exports){
module.exports = update
var merge = require('lodash/merge')
var omit = require('lodash/omit')
var Promise = require('lie')
var internals = module.exports.internals = {}
internals.deserialise = require('../utils/deserialise')
internals.request = require('../utils/request')
internals.serialise = require('../utils/serialise')
function update (state, options) {
if (!options) {
return Promise.reject(new Error('Specify an account property to update'))
}
return state.setup
.then(function () {
return state.cache.get()
})
.then(function (cache) {
return internals.request({
method: 'PATCH',
url: state.url + '/session/account',
headers: {
authorization: 'Session ' + cache.session.id
},
body: internals.serialise('account', options, cache.id)
})
.then(function (response) {
// when a username changes, the session ID gets recalculated, as it’s based
// on the username, see npm.im/couchdb-calculate-session-id. In that case
// the server sets x-set-session with the new session id.
if (response.headers['x-set-session']) {
cache.session.id = response.headers['x-set-session']
}
merge(cache, omit(options, ['password']))
state.cache.set(cache)
var account = omit(cache, 'session')
state.emitter.emit('update', account)
return account
})
.catch(function (error) {
if (error.statusCode === 401) {
cache.session.invalid = true
state.emitter.emit('unauthenticate')
state.cache.set(cache)
}
throw error
})
})
}
},{"../utils/deserialise":21,"../utils/request":26,"../utils/serialise":27,"lie":110,"lodash/merge":289,"lodash/omit":291}],20:[function(require,module,exports){
module.exports = validate
var Promise = require('lie')
function validate (state, options) {
var self = this
return new Promise(function (resolve, reject) {
var result
try {
result = state.validate.call(self, options)
} catch (error) {
reject(error)
}
if (result && result.then) {
return result.then(resolve, reject)
}
resolve()
})
}
},{"lie":110}],21:[function(require,module,exports){
module.exports = deserialise
var merge = require('lodash/merge')
var filter = require('lodash/filter')
function deserialise (response, options) {
if (!response || !response.data) {
throw new Error('Please include a JSON API response to deserialise.')
}
return Array.isArray(response.data)
? deserialiseMany(options || {}, response)
: deserialiseOne(options || {}, response)
}
function deserialiseOne (options, response) {
var resource = response.data
var properties = {}
options = merge({}, options)
if (resource.type !== 'profile') {
properties.id = resource.id
}
if (options.include) {
var tmp = options.include.indexOf('.')
var currentInclude = options.include.substr(0, tmp)
var nextInclude = options.include.substr(tmp + 1)
if (tmp === -1) {
currentInclude = nextInclude
nextInclude = ''
}
if (resource.relationships) {
var relationship = resource.relationships[currentInclude].data
var includedResource = filter(response.included, {
type: relationship.type,
id: relationship.id
})[0]
if (includedResource) {
options.include = nextInclude
properties[currentInclude] = deserialiseOne(options, {
included: response.included,
data: includedResource
})
}
}
}
if (resource.attributes) {
Object.keys(resource.attributes).forEach(function (attribute) {
properties[attribute] = resource.attributes[attribute]
})
}
return properties
}
function deserialiseMany (options, response) {
return response.data.map(function (resource) {
return deserialiseOne(options, {
included: response.included,
data: resource
})
})
}
},{"lodash/filter":266,"lodash/merge":289}],22:[function(require,module,exports){
module.exports = fetchProperties
var deserialise = require('./deserialise')
var getProperties = require('./get-properties')
var request = require('./request')
function fetchProperties (options) {
return request({
url: options.url,
method: 'GET',
headers: {
authorization: 'Session ' + options.sessionId
}
})
.then(function (response) {
var data = deserialise(response.body)
return getProperties(data, options.path)
})
}
},{"./deserialise":21,"./get-properties":24,"./request":26}],23:[function(require,module,exports){
module.exports = generateId
// uuids consist of numbers and lowercase letters only.
// We stick to lowercase letters to prevent confusion
// and to prevent issues with CouchDB, e.g. database
// names only allow for lowercase letters.
var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split('')
var LENGTH = 7
function generateId () {
var id = ''
var radix = CHARS.length
for (var i = 0; i < LENGTH; i++) {
var rand = Math.random() * radix
var c = CHARS[Math.floor(rand)]
id += String(c).charAt(0)
}
return id
}
},{}],24:[function(require,module,exports){
module.exports = getProperties
var get = require('lodash/get')
var set = require('lodash/set')
function getProperties (baseObject, path) {
if (path === undefined) {
return baseObject
}
if (Array.isArray(path)) {
return path.reduce(function (properties, path) {
return set(properties, path, get(baseObject, path))
}, {})
}
return get(baseObject, path)
}
},{"lodash/get":268,"lodash/set":293}],25:[function(require,module,exports){
module.exports = getState
var Hook = require('before-after-hook')
var EventEmitter = require('events').EventEmitter
var LocalStorageStore = require('async-get-set-store')
var generateId = require('./generate-id')
function getState (options) {
if (!options) {
options = {}
}
if (typeof options === 'string') {
options = {url: options}
}
if (!options.url) {
throw new Error('options.url is required')
}
var cacheKey = options.cacheKey || 'account'
var cache = options.cache || new LocalStorageStore(cacheKey)
var setup = cache.get()
.then(function (storedAccount) {
if (storedAccount.id) {
if (options.id && options.id !== storedAccount.id) {
throw new Error('account.id conflict')
}
return
}
storedAccount = {
id: options.id || generateId(),
createdAt: new Date().toISOString()
}
return cache.set(storedAccount)
})
.catch(function (error) {
error.message = 'Error while initialising: ' + error.message
throw error
})
var state = {
cacheKey: cacheKey,
emitter: options.emitter || new EventEmitter(),
hook: new Hook(),
url: options.url,
validate: options.validate || function () {},
cache: cache,
setup: setup
}
return state
}
},{"./generate-id":23,"async-get-set-store":92,"before-after-hook":94,"events":101}],26:[function(require,module,exports){
module.exports = request
var nets = require('nets')
var Promise = require('lie')
var set = require('lodash/set')
function request (options) {
options.encoding = undefined
return new Promise(function (resolve, reject) {
set(options, 'headers.accept', 'application/vnd.api+json')
set(options, 'headers.content-type', 'application/vnd.api+json')
options.json = true
if (options.body) {
// works around an issue where nets-xhr stringifies options.json
// if it is truthy, which overides options.body
options.json = options.body
}
nets(options, function (error, response) {
if (error) {
error.name = 'ConnectionError'
return reject(error)
}
if (response.statusCode >= 400) {
error = new Error(response.body.errors[0].detail)
error.name = response.body.errors[0].title + 'Error'
error.statusCode = parseInt(response.body.errors[0].status, 10)
return reject(error)
}
resolve(response)
})
})
}
},{"lie":110,"lodash/set":293,"nets":298}],27:[function(require,module,exports){
module.exports = serialise
function serialise (type, attributes, id) {
if (!type || !attributes) {
throw new Error('Serialisation must include a type and some attributes.')
}
var data = {
type: type,
attributes: attributes
}
if (id) {
data.id = id
}
return { data: data }
}
},{}],28:[function(require,module,exports){
module.exports = Connection
var EventEmitter = require('events').EventEmitter
var check = require('../lib/check')
var startChecking = require('../lib/start-checking')
var stopChecking = require('../lib/stop-checking')
var isChecking = require('../lib/is-checking')
var getOk = require('../lib/get-ok')
var on = require('../lib/on')
var off = require('../lib/off')
var reset = require('../lib/reset')
var parseOptions = require('../lib/utils/parse-options')
function Connection (options) {
var state = parseOptions(options)
state.emitter = new EventEmitter()
var api = {
get ready () {
return state.ready.then(function () { return api })
},
get ok () {
return getOk(state)
},
get isChecking () {
return isChecking(state)
},
check: check.bind(null, state),
stopChecking: stopChecking.bind(null, state),
startChecking: startChecking.bind(null, state),
on: on.bind(null, state),
off: off.bind(null, state),
reset: reset.bind(null, state)
}
return api
}
},{"../lib/check":29,"../lib/get-ok":30,"../lib/is-checking":31,"../lib/off":32,"../lib/on":33,"../lib/reset":34,"../lib/start-checking":35,"../lib/stop-checking":36,"../lib/utils/parse-options":38,"events":101}],29:[function(require,module,exports){
module.exports = check
var nextTick = require('next-tick')
var internals = module.exports.internals = {}
internals.cache = require('./utils/cache')
internals.request = require('./utils/request')
function check (state) {
return state.ready
.then(function () {
if (state.request) {
state.request.abort()
}
state.request = internals.request({
method: state.method,
url: state.url,
timeout: state.timeout
})
// once request finishes, remove it from state
return state.request
.then(function () {
delete state.request
state.timestamp = new Date().toISOString()
if (state.error) {
nextTick(function () {
state.emitter.emit('reconnect')
})
delete state.error
}
})
.catch(function (error) {
delete state.request
state.timestamp = new Date().toISOString()
if (!state.error) {
nextTick(function () {
state.emitter.emit('disconnect')
})
}
state.error = {
name: error.name,
message: error.message,
code: error.code
}
})
.then(function () {
return internals.cache.set(state)
})
.then(function () {
if (state.error) {
throw state.error
}
})
})
}
},{"./utils/cache":37,"./utils/request":39,"next-tick":299}],30:[function(require,module,exports){
module.exports = getOk
function getOk (state) {
if (state.timestamp === undefined) {
return undefined
}
return state.error === undefined
}
},{}],31:[function(require,module,exports){
module.exports = isChecking
function isChecking (state) {
return !!state.checkTimeout
}
},{}],32:[function(require,module,exports){
module.exports = off
function off (state, eventName, handler) {
state.emitter.removeListener(eventName, handler)
return this
}
},{}],33:[function(require,module,exports){
module.exports = on
function on (state, eventName, handler) {
state.emitter.on(eventName, handler)
return this
}
},{}],34:[function(require,module,exports){
module.exports = reset
var cache = require('./utils/cache')
function reset (state, o) {
return state.ready
.then(function () {
var options = o || {}
state.timestamp = undefined
state.error = undefined
if (typeof options.interval === 'number') {
var intervalTimeValue = options.interval
state.interval = {
connected: intervalTimeValue,
disconnected: intervalTimeValue
}
}
if (state.request) {
state.request.abort()
}
return cache.unset(state)
})
.then(function () {
state.emitter.emit('reset')
})
}
},{"./utils/cache":37}],35:[function(require,module,exports){
module.exports = startChecking
var internals = module.exports.internals = {}
internals.check = require('./check')
internals.getOk = require('./get-ok')
function startChecking (state, options) {
return state.ready
.then(function () {
options = parse(options)
if (!state.method || !state.url) {
return
}
handleInterval(state, options)
})
}
function timeoutHandler (state, options) {
var checkAgain = handleInterval.bind(null, state, options)
internals.check(state, options).then(checkAgain, checkAgain)
}
function handleInterval (state, options) {
var timeout
var ok = internals.getOk(state)
if (options.checkTimeout) {
timeout = options.checkTimeout
}
if (options.interval) {
if (typeof options.interval === 'number') {
timeout = options.interval
} else {
// if ok is undefined, then we are unsure what the state is but it is most likely connected. So only when ok
// is explicitly false do we want to use the disconnected interval
timeout = (ok === false) ? options.interval.disconnected : options.interval.connected
}
}
if (timeout) {
// we use setTimeout on purpose, we don't want to send requests each
// x seconds, but rather set a timeout for x seconds after each response
// but we use `checkTimeout` as variable as the effect is the same
state.checkTimeout = setTimeout(timeoutHandler, timeout, state, options)
return
}
options.interval = 30000
state.checkTimeout = setTimeout(timeoutHandler, 0, state, options)
}
function parse (options) {
if (!options) {
return {}
}
return options
}
},{"./check":29,"./get-ok":30}],36:[function(require,module,exports){
module.exports = stopChecking
function stopChecking (state) {
return state.ready
.then(function () {
clearTimeout(state.checkTimeout)
delete state.checkTimeout
})
}
},{}],37:[function(require,module,exports){
module.exports = {
get: getCache,
set: setCache,
unset: clearCache
}
function setCache (state) {
if (state.cache === false) {
return Promise.resolve()
}
var data = {
timestamp: state.timestamp,
error: state.error
}
return state.cache.set(data)
}
function getCache (state) {
if (state.cache === false) {
return Promise.resolve({})
}
return state.cache.get()
.then(function (data) {
return data
})
}
function clearCache (state) {
if (state.cache === false) {
return Promise.resolve()
}
return state.cache.unset()
}
},{}],38:[function(require,module,exports){
module.exports = parseOptions
var Store = require('async-get-set-store')
var nextTick = require('next-tick')
var cache = require('./cache')
var DEFAULTS = {
cache: {},
cacheTimeout: 7200000, // 2h in milliseconds
method: 'HEAD',
checkTimeout: undefined,
interval: {
connected: undefined,
disconnected: undefined
}
}
function parseOptions (options) {
var url = typeof options === 'string' ? options : options && options.url
if (!url) {
throw new TypeError('Connection: url must be set')
}
if (typeof options === 'string') {
options = {}
}
if (options.cache === undefined) {
options.cache = new Store('connection_' + url)
}
if (typeof options.interval === 'number') {
var intervalTimeValue = options.interval
options.interval = {
connected: intervalTimeValue,
disconnected: intervalTimeValue
}
}
var state = {
url: url,
cache: options.cache,
method: options.method || DEFAULTS.method,
checkTimeout: options.checkTimeout || DEFAULTS.checkTimeout,
interval: options.interval || DEFAULTS.interval,
cacheTimeout: options.cacheTimeout || DEFAULTS.cacheTimeout,
ready: cache.get({
cache: options.cache
})
.then(function (cache) {
if (cache.timestamp) {
var cachedTime = +new Date(cache.timestamp)
var currentTime = +new Date()
if (state.cacheTimeout && currentTime >= cachedTime + state.cacheTimeout) {
nextTick(function () {
state.emitter.emit('reset', cache)
})
} else {
state.error = cache.error
state.timestamp = cache.timestamp
}
}
})
.catch(function (error) {
error.name = 'SetupError'
error.message = 'Error while initialising: ' + error.message
throw error
})
}
return state
}
},{"./cache":37,"async-get-set-store":92,"next-tick":299}],39:[function(require,module,exports){
module.exports = request
var internals = module.exports.internals = {}
internals.nets = require('nets')
function request (options) {
var requestState
var _reject
var promise = new Promise(function (resolve, reject) {
_reject = reject
requestState = internals.nets({
method: options.method,
url: options.url,
timeout: options.timeout,
// Turn off the use of Buffer in nets
// in order to make this module compatible
// for Webpack builds.
// see: https://github.com/maxogden/nets
encoding: undefined
}, function (error, response, body) {
if (error) {
error.name = error.code === 'ETIMEDOUT' ? 'TimeoutError' : 'ConnectionError'
error.code = undefined
return reject(error)
}
if (response.statusCode >= 400) {
error = new Error('Server error')
error.name = 'ServerError'
error.code = response.statusCode
return reject(error)
}
resolve()
})
})
promise.abort = function () {
try {
requestState.abort()
} catch (error) {}
var error = new Error('Aborted')
error.name = 'AbortError'
error.code = 0
_reject(error)
}
return promise
}
},{"nets":298}],40:[function(require,module,exports){
module.exports = Log
Log.console = console
var log = require('../lib/log')
var debug = require('../lib/debug')
var info = require('../lib/info')
var warn = require('../lib/warn')
var error = require('../lib/error')
var parseOptions = require('../lib/utils/parse-options')
function Log (options) {
var state = parseOptions(options)
state.console = Log.console
var api = log.bind(null, state)
api.debug = debug.bind(null, state)
api.info = info.bind(null, state)
api.warn = warn.bind(null, state)
api.error = error.bind(null, state)
api.scoped = function (name) {
return Log({
prefix: state.prefix + ':' + name,
level: state.level,
styles: state.styles
})
}
Object.defineProperty(api, 'level', {
get: function () {
return state.level
},
set: function (newValue) {
if (['debug', 'error', 'info', 'warn'].indexOf(newValue) === -1) {
throw new Error('Invalid value for log.level: ' + newValue)
}
state.level = newValue
},
enumerable: true
})
Object.defineProperty(api, 'prefix', {
get: function () {
return state.prefix
},
set: function (newValue) {
throw new Error('log.prefix is read-only')
},
enumerable: true
})
return api
}
},{"../lib/debug":41,"../lib/error":42,"../lib/info":43,"../lib/log":44,"../lib/utils/parse-options":46,"../lib/warn":48}],41:[function(require,module,exports){
module.exports = debug
var logLevelIgnored = require('./utils/log-level-ignored')
var prepareLogArguments = require('./utils/prepare-log-arguments')
function debug (state) {
if (logLevelIgnored(state, 'debug')) {
return
}
var args = prepareLogArguments(state, 'debug', [].slice.call(arguments, 1))
state.console.log.apply(state.console, args)
}
},{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],42:[function(require,module,exports){
module.exports = error
var prepareLogArguments = require('./utils/prepare-log-arguments')
function error (state) {
var args = prepareLogArguments(state, 'error', [].slice.call(arguments, 1))
state.console.error.apply(state.console, args)
}
},{"./utils/prepare-log-arguments":47}],43:[function(require,module,exports){
module.exports = info
var logLevelIgnored = require('./utils/log-level-ignored')
var prepareLogArguments = require('./utils/prepare-log-arguments')
function info (state) {
if (logLevelIgnored(state, 'info')) {
return
}
var args = prepareLogArguments(state, 'info', [].slice.call(arguments, 1))
state.console.info.apply(state.console, args)
}
},{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],44:[function(require,module,exports){
module.exports = log
var prepareLogArguments = require('./utils/prepare-log-arguments')
function log (state) {
var args = prepareLogArguments(state, 'log', [].slice.call(arguments, 1))
state.console.log.apply(state.console, args)
}
},{"./utils/prepare-log-arguments":47}],45:[function(require,module,exports){
module.exports = logLevelIgnored
function logLevelIgnored (state, target) {
return LEVELS[target] < LEVELS[state.level]
}
var LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3
}
},{}],46:[function(require,module,exports){
module.exports = parseOptions
parseOptions.browserSupportsLogStyles = require('browser-supports-log-styles')
var DEFAULT_LEVEL = 'warn'
var DEFAULT_STYLES = {
default: 'color: white; padding: .2em .4em; border-radius: 1em',
debug: 'background: green',
log: 'background: gray',
info: 'background: blue',
warn: 'background: orange',
error: 'background: red',
reset: 'background: inherit; color: inherit'
}
function parseOptions (options) {
if (typeof options === 'string') {
options = {
prefix: options
}
}
if (!options || !options.prefix) {
throw new TypeError('"prefix" required for new Log(options)')
}
if (options.styles === false) {
options.styles = false
}
if (typeof options.styles === 'undefined') {
options.styles = parseOptions.browserSupportsLogStyles()
}
if (options.styles === true) {
options.styles = {
default: DEFAULT_STYLES.default,
log: DEFAULT_STYLES.log,
debug: DEFAULT_STYLES.debug,
info: DEFAULT_STYLES.info,
warn: DEFAULT_STYLES.warn,
error: DEFAULT_STYLES.error,
reset: DEFAULT_STYLES.reset
}
}
options.level = options.level || DEFAULT_LEVEL
return options
}
},{"browser-supports-log-styles":98}],47:[function(require,module,exports){
module.exports = prepareLogArguments
function prepareLogArguments (state, type, args) {
if (state.styles) {
return ['%c' + state.prefix + '%c', state.styles.default + '; ' + state.styles[type], state.styles.reset].concat(args)
}
return ['(' + state.prefix + ':' + type + ')'].concat(args)
}
},{}],48:[function(require,module,exports){
module.exports = warn
var logLevelIgnored = require('./utils/log-level-ignored')
var prepareLogArguments = require('./utils/prepare-log-arguments')
function warn (state) {
if (logLevelIgnored(state, 'warn')) {
return
}
var args = prepareLogArguments(state, 'warn', [].slice.call(arguments, 1))
state.console.warn.apply(state.console, args)
}
},{"./utils/log-level-ignored":45,"./utils/prepare-log-arguments":47}],49:[function(require,module,exports){
module.exports = Store
var EventEmitter = require('events').EventEmitter
var assign = require('lodash/assign')
var internals = Store.internals = {}
internals.handleChanges = require('./lib/helpers/handle-changes')
function Store (dbName, options) {
if (!(this instanceof Store)) return new Store(dbName, options)
if (typeof dbName !== 'string') throw new Error('Must be a valid string.')
if (!options || (!('remote' in options) && !options.remoteBaseUrl)) {
throw new Error('options.remote or options.remoteBaseUrl is required')
}
if (options.remoteBaseUrl) {
options.remoteBaseUrl = options.remoteBaseUrl.replace(/\/$/, '')
if (!options.remote) {
options.remote = dbName
}
if (!/^https?:\/\//.test(options.remote)) {
options.remote = (options.remoteBaseUrl + '/' + encodeURIComponent(options.remote))
}
}
var db = new options.PouchDB(dbName)
var emitter = new EventEmitter()
var state = {
db: db,
dbName: dbName,
PouchDB: options.PouchDB,
emitter: emitter,
validate: options.validate,
get remote () {
return options.remote
}
}
var api = {
db: state.db,
isPersistent: require('./lib/is-persistent').bind(null, state),
add: require('./lib/add').bind(null, state, null),
find: require('./lib/find').bind(null, state, null),
findAll: require('./lib/find-all').bind(null, state, null),
findOrAdd: require('./lib/find-or-add').bind(null, state, null),
update: require('./lib/update').bind(null, state, null),
updateOrAdd: require('./lib/update-or-add').bind(null, state, null),