Nedomas/databound

View on GitHub
src/databound.coffee

Summary

Maintainability
Test Coverage
_ = require 'lodash'
jQuery = require 'jquery'

# You can specify scope for the connection.
#
# ```coffeescript
#   User = new Databound '/users', city: 'New York'
#
#   User.where(name: 'John').then (users) ->
#     alert 'You are a New Yorker called John'
#
#   User.create(name: 'Peter').then (new_user) ->
#     # I am from New York
#     alert "I am from #{new_user.city}"
# ```
class Databound
  constructor: (@endpoint, @scope = {}, @options = {}) ->
    @extra_where_scopes = @options.extra_where_scopes or []
    @records = []
    @seeds = []
    @properties = []

  # ## Start of Configs
  # Functions ``request`` and ``promise`` are overritable
  Databound.API_URL = ""

  # Does a POST request and returns a ``promise``
  request: (action, params) ->
    jQuery.post @url(action), @data(params), 'json'

  # Returns a ``promise`` which resolves with ``result``
  promise: (result) ->
    deferred = jQuery.Deferred()
    deferred.resolve result
    deferred.promise()
  # ## End of Configs

  where: (params) ->
    _this = @

    @wrappedRequest('where', params).then (resp) ->
      records = JSON.parse(resp.records).concat(_this.seeds)
      _this.records = _.sortBy(records, 'id')
      _this.promise _this.records

  all: ->
    @where()

  # Return a single record by ``id``
  #
  # ```coffeescript
  # User.find(15).then (user) ->
  #   alert "Yo, #{user.name}"
  # ```
  find: (id) ->
    @checkUndefinedId('find', id)

    _this = @
    @where(id: id).then ->
      record = _this.take(id)

      unless record
        throw new DataboundError("Couldn't find record with id: #{id}")

      _this.promise record

  # Return a single record by ``params``
  #
  # ```coffeescript
  # User.findBy(name: 'John', city: 'New York').then (user) ->
  #   alert "I'm John from New York"
  # ```
  findBy: (params) ->
    _this = @

    @where(params).then (resp) ->
      _this.promise _.first(_.values(resp))

  create: (params) ->
    @requestAndRefresh 'create', params

  # Specify ``id`` when updating or destroying the record.
  #
  # ```coffeescript
  #   User = new Databound '/users'
  #
  #   User.update(id: 15, name: 'Saint John').then (updated_user) ->
  #     alert updated_user
  #
  #   User.destroy(15).then (resp) ->
  #     alert resp.success
  # ```
  update: (params) ->
    @requestAndRefresh 'update', params

  destroy: (id) ->
    @checkUndefinedId('destroy', id)
    @requestAndRefresh 'destroy', id: id

  # Just take already dowloaded records
  take: (id) ->
    _.detect @records, (record) ->
      id.toString() == record.id.toString()

  takeAll: ->
    @records

  # f.e. Have default records
  injectSeedRecords: (records) ->
    @seeds = records

  requestAndRefresh: (action, params) ->
    _this = @

    # backend responds with:
    #
    # ```javascript
    #   {
    #     success: true,
    #     id: record.id,
    #     scoped_records: []
    #   }
    # ```
    @wrappedRequest(action, params).then (resp) ->
      records = JSON.parse(resp.scoped_records)
      records_with_seeds = records.concat(_this.seeds)
      _this.records = _.sortBy(records_with_seeds, 'id')

      if resp.id
        _this.promise _this.take(resp.id)
      else
        _this.promise resp.success

  url: (action) ->
    if _.isEmpty(Databound.API_URL)
      "#{@endpoint}/#{action}"
    else
      "#{Databound.API_URL}/#{@endpoint}/#{action}"

  data: (params) ->
    scope: JSON.stringify(@scope)
    extra_where_scopes: JSON.stringify(@extra_where_scopes)
    data: JSON.stringify(params)

  wrappedRequest: (args...) ->
    @request(args...).then(_.bind(@handleSuccess, @)).fail(@handleFailure)

  handleSuccess: (resp) ->
    throw new Error 'Error in the backend' unless resp?.success

    @promise(resp)

  handleFailure: (e) ->
    if e.status == DataboundError.STATUS
      throw new DataboundError(e.responseJSON.message)
    else
      throw new Error "Error in the backend with status #{e.status}"

  checkUndefinedId: (action, id) ->
    return unless _.isUndefined(id)

    throw new DataboundError("Couldn't #{action} a record without an id")

class DataboundError
  constructor: (text) ->
    @message = "Databound: #{text}"

  @STATUS: 405

DataboundError:: = new Error()

module.exports = Databound