refactoring driver

refactoring driver

--- src/arangodb.coffee
+++ src/arangodb.coffee
@@ -1,16 +1,20 @@
 # node modules
+
+# Module dependencies
 url = require 'url'
-
-# 3rd party modules
-debug = require('debug') 'loopback:connector:arango'
 merge = require 'extend'
+async = require('async')
 _ = require 'underscore'
 Connector = require('loopback-connector').Connector
 GeoPoint = require('loopback-datasource-juggler').GeoPoint
+debug = require('debug') 'loopback:connector:arango'

 # arango
 ajs = require 'arangojs'
 qb = require 'aqb'
+
+returnVariable = 'result'
+

 generateConnObject = (settings) ->
   if settings.url
@@ -42,16 +46,16 @@

   # TODO: add more arango specifig objects for a connection

-  config = {
+  return {
     url: dbUrl
     databaseName: database
     promise: promise
   }

-  return config
-

 exports.generateConnObject = generateConnObject
+
+
 ###
   Initialize the ArangoDB connector for the given data source

@@ -109,18 +113,17 @@
     super 'arangodb', settings
     # debug
     @debug = dataSource.settings.debug or debug.enabled
-    debug 'constructor called' if @debug
-

     # link to datasource
     @dataSource = dataSource

-
     # Query builder
     @qb = require('aqb')

-    @db = require('arangojs') @settings
+    @db = ajs @settings
+
     @api = @db.route '_api'
+

   # one file per functionality
   # @extend require('./CoreMixin')
@@ -132,8 +135,10 @@

     @param callback [Function] The callback function, called the with created connection
   ###
-  connect: (callback) =>
-    debug 'connect called' if @debug
+  connect: (callback) ->
+
+    debug "ArangoDB connection is called with settings: #{JSON.stringify @settings}" if @debug
+
     process.nextTick () ->
       callback && callback null, @db

@@ -142,7 +147,7 @@

     @return [Array<String>] The types of connectors this connector belongs to
   ###
-  getTypes: () =>
+  getTypes: () ->
     return ['db', 'nosql', 'arangodb']

@@ -152,7 +157,7 @@
     @return [Object] The class to build the Id Value with

   ###
-  getDefaultIdType: () =>
+  getDefaultIdType: () ->
     return String

   ###
@@ -162,7 +167,7 @@

     @return [Object] The model class of this model
   ###
-  getModelClass: (model) =>
+  getModelClass: (model) ->
     return @_models[model]

@@ -173,9 +178,11 @@

     @return [Object] The collection name for this model
   ###
-  getCollectionName: (model) =>
-    if @getModelClass(model).settings.options?.arangodb?
-      model = @getModelClass(model).settings.options?.arangodb?.collection or model
+  getCollectionName: (model) ->
+    modelClass = @getModelClass(model)
+
+    if modelClass.settings.arangodb
+      model = modelClass.settings.arangodb?.collection or model

     return model

@@ -188,139 +195,68 @@

     @return [Object] The converted data as an JSON Object
   ###
-  fromDatabase: (model, data) =>
+  fromDatabase: (model, data) ->
     return null if not data?

-    properties = @getModelClass(model).properties
-
-    for key, val of data
-      # change _key value to an id property and then delete _id (database wide doc handle) and _key (collection wide doc handle)
-      if key in ['_key', '_id']
-        data.id = switch
-          when key is '_key' then val
-          when key is '_id' then val.split('/')[0]
-        delete data[key]
-        continue
-
-      # Buffer
-      if properties[key]?.type is Buffer and val?
-        data[key] = new Buffer(val, 'base64')
-
-      # TODO: Look how to read ArangoDB binary data into a buffer
-      # if properties[key]?.type is Buffer and val?
-      #   #from binary
-      #   if(data[p] instanceof mongodb.Binary) [
-      #     // Convert the Binary into Buffer
-      #     data[p] = data[p].read(0, data[p].length());
-      #   ]
-
-      # String
-      # if properties[key]?.type is String and val?
-      #   # from binary
-      #   if(data[p] instanceof mongodb.Binary) [
-      #     // Convert the Binary into String
-      #     data[p] = data[p].toString();
-      #   ]
+    props = @getModelClass(model).properties
+
+    for key, val of props
+      #Buffer type
+      if data[key]? and val? and val.type is Buffer
+        data[key] = new Buffer(data[key])

       # Date
-      if properties[key]?.type is Date and val?
-        data[key] = new Date val
+      if data[key]? and val? and val.type is Date
+        data[key] = new Date data[key]

       # GeoPoint
-      if properties[key]?.type is GeoPoint and val?
-        data[key] = new GeoPoint { lat: val.lat, lng: val.lng }
-
-      # TODO: still to come: Boolean, Number, Array and any arbitrary type
+      if data[key]? and val? and val.type is GeoPoint
+        data[key] = new GeoPoint { lat: data[key].lat, lng: data[key].lng }

     return data

-
-  ###
-    Converts JSON to insert into the database, based on the properties of a given model
-
-    @param model [String] The model name to look up the properties
-    @param data [Object] The JSON object to transferred to the database
-
-    @return [Object] The converted data as an Plain Javascript Object
-  ###
-  toDatabase: (model, data) =>
-    return null if not data?
-
-    properties = @getModelClass(model).properties
-
-    for key, val of data
-      # change _key value to an id property and then delete _id (database wide doc handle) and _key (collection wide doc handle)
-      if key in ['_key', '_id']
-        data.id = switch
-          when key is '_key' then val
-          when key is '_id' then val.split('/')[0]
-        delete data[key]
-        continue
-
-      # Buffer
-      if properties[key]?.type is Buffer and val?
-        data[key] = val.toString('base64')
-
-      # Date
-      if properties[key]?.type is Date and val?
-        data[key] = new Date val
-
-      # GeoPoint
-      if properties[key]?.type is GeoPoint and val?
-        data[key] = val
-
-      # TODO: still to come: Boolean, Number, Array and any arbitrary type
-
-    return data
-
-
-
-  ###
-    Transaction
-
-    @param collections [Object|Array|String] collection(s) to lock
-    @param action [String] Function to evaluate
-    @param params [Object] Parameter to call the action function with
-    @param callback [Function] The callback function, called with a (possible) error object and the transactions result
-  ###
-  transaction: (collections, action, params, callback) =>
-    # empty params by default
-    params = params or []
-
-    # evaluate action to a string, if not already one
-    # TODO: examine if function, make sure that when there is a params parameter function has the same param count in numbers
-    action = if typeof action isnt String then String(action) else action
-
-    @db.transaction collections, action, params, (err, result) ->
-      callback err if err
-      callback null, result
-
-  ###
-    Query with AQL and binded variables
+  ###
+    Execute a query with AQL and binded variables

     @param query [String|Object] The AQL query to execute
     @param bindVars [Object] The variables bound to the AQL query
     @param callback [Function] The callback function, called with a (possible) error object and the query's cursor
   ###
-  query: (query, bindVars, callback) =>
-    if typeof bindVars is 'function'
+  execute: (query, bindVars, callback) ->
+
+    self = this
+
+    if typeof bindVars is 'function' and !callback?
       callback = bindVars
       bindVars = {}

+    context =
+      req:
+        aql: query
+        params: bindVars
+
+    @notifyObserversAround 'execute', context, (context, done) ->
+      self.executeAQL context.req.aql, context.req.params, (err, result) ->
+        context.res = result and result._result
+        done err, result
+    , callback
+
+
+  executeAQL: (query, bindVars, callback) ->
     if @debug
-      if typeof query is 'object'
+      if typeof query.toAQL is 'function'
         q = query.toAQL()
       else
         q = query

-      debug 'query ->', q, '- bindVars ->', bindVars
+      debug "query: #{q} bindVars: #{JSON.stringify bindVars}"

     @db.query query, bindVars, (err, cursor) ->
       # workaround: when there is no error (e.g. wrong AQL syntax etc.) and no cursor, the authentication failed
       if not err? and cursor.length = 0
         authErr = new Error 'Authentication failed'
-        callback authErr, null
+        callback authErr
       else
         callback err, cursor

@@ -330,7 +266,7 @@

     @param callback [Function] The calback function, called with a (possible) error object and the server versio
   ###
-  getVersion: (callback) =>
+  getVersion: (callback) ->
     if @version?
       callback null, @version
     else
@@ -339,6 +275,7 @@
         @version = result.body
         callback null, @version

+
   # @extend require('./CrudMixin')
   # ==============
   # = CRUD Mixin =
@@ -350,18 +287,42 @@
     @param data [Object] The data to create
     @param callback [Function] The callback function, called with a (possible) error object and the created object's id
   ###
-  create: (model, data, callback) =>
-    debug 'create', model, data if @debug
+  create: (model, data, callback) ->
+    debug "create model #{model} with data: #{JSON.stringify data}" if @debug
+
+    self = this
+
+    idValue = @getIdValue(model, data)
+    idName = @idName(model)
+
+    if !idValue? or typeof idValue is 'undefined'
+      delete data[idName]
+    else
+      id = @getDefaultIdType()(idValue) if typeof idValue isnt @getDefaultIdType()
+      data._key = id
+      delete data[idName] if idName isnt '_key'

     aql = qb.insert('@data').in('@@collection').returnNew('inserted')
-    bindVars = {
+    bindVars =
       data: data,
-      '@collection': @getCollectionName(model)
-    }
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result._result[0]._key
+      '@collection': @getCollectionName model
+
+
+    @execute aql, bindVars, (err, result) ->
+      return callback(err) if err
+
+      idValue = result._result[0]._key
+      modelClass = self._models[model]
+      idType = modelClass.properties[idName].type
+
+      if idType is Number
+        num = Number(idValue)
+        idValue = num if !isNaN(num)
+
+      delete data._key
+      data[idName] = idValue;
+      callback err, idValue
+

   ###
     Save the model instance for the given data
@@ -370,56 +331,55 @@
     @param data [Object] The updated data to save or create
     @param callback [Function] The callback function, called with a (possible) error object and the number of affected objects
   ###
-  save: (model, data, callback) =>
-    debug 'save', model, data if @debug
-    @_updateOrCreate model, data, true, (err, result) ->
-      callback err if err
-      callback null, result.length
+  save: (model, data, options, callback) ->
+    debug "save for #{model} with data: #{JSON.stringify data}" if @debug
+    @updateOrCreate model, data, options, callback
+

   ###
     Update if the model instance exists with the same id or create a new instance
-    # TODO: change this to UPSERT AQL when 2.6 is out

     @param model [String] The model name
     @param data [Object] The model instance data
     @param callback [Function] The callback function, called with a (possible) error object and updated or created object
   ###
-  _updateOrCreate: (model, data, callback) =>
-    debug 'updateOrCreate', model, data if @debug
-
-    id = data.id
-    delete data.id
-
-    update_aql = qb.update('@id').with('@data').in('@@collection').returnNew('updated').toAQL()
-    create_aql = qb.insert('@data').in('@@collection').returnNew('inserted').toAQL()
-
-    # the trans-action ;)
-    action = (params) ->
-      db = require('internal').db
-      # try to update and return if we have an result
-      update_params = [ id: params.id, data: params.data, collection: params.collection ]
-      update_query = db._query(update_aql, update_params).toArray()
-      return update_result[0] if update_result.length > 0
-
-      # if update failed we try to create it
-      create_params = [ data: params.data, collection: params.collection ]
-      create_result = db._query(create_aql, create_params).toArray()
-
-      throw create_result if (create_result instanceof Error)
-
-      return create_result[0] if create_result.length > 0
-      return create_result
-
-
-    @transaction @getCollectionName(model), action, { id: id, data: data, collection: @getCollectionName(model) }, (err, result) ->
-      callback err if err
-      callback null, result
-
-  updateOrCreate: (model, data, callback) =>
-    @_updateOrCreate model, data, (err, result) ->
-      callback err if err
-      callback result[0] if result.length > 0
-      callback result
+  updateOrCreate: (model, data, options, callback) ->
+    debug "updateOrCreate for Model #{model} with data: #{JSON.stringify data}" if @debug
+
+    @getVersion (err, v) ->
+      version = new RegExp(/[2-9]+\.[6-9]+\.[0-9]+/).test(v.version)
+      if err or !version
+        err = ner Error('Error updateOrCreate not supported for version {#v}')
+        callback(err)
+
+
+    self = this
+    idValue = @getIdValue(model, data)
+    idName = @idName(model)
+    idValue = new String(idValue) if typeof idValue is 'number'
+    delete data[idName]
+    dataI = _.clone(data)
+    dataI._key = idValue
+
+    # RETURN statement at the moment is not supported
+    # See https://github.com/arangodb/aqbjs/issues/15 when issue is solved
+    #aql = qb.upsert({_key: '@id'}).insert('@dataI').update('@data').in('@@collection').return({doc: NEW, isNewInstance: OLD ? false : true })
+
+    aql = 'UPSERT {_key: @id} INSERT @dataI UPDATE @data IN @@collection RETURN {doc: UNSET(NEW, ["_id", "_rev"]), isNewInstance: OLD ? false : true }'
+    bindVars =
+      '@collection': @getCollectionName model
+      id: idValue
+      dataI: dataI
+      data: data
+
+    @execute aql, bindVars, (err, result) ->
+      if result and result._result[0]
+        newDoc = result._result[0].doc
+        isNewInstance = { isNewInstance: result._result[0].isNewInstance }
+        self.setIdValue(model, data, newDoc._key)
+        self.setIdValue(model, newDoc, newDoc._key)
+        if idName isnt '_key' then delete newDoc._key
+      callback err, newDoc, isNewInstance

   ###
@@ -429,12 +389,12 @@
     @param id [String] The id value
     @param callback [Function] The callback function, called with a (possible) error object and an boolean value if the specified object existed (true) or not (false)
   ###
-  exists: (model, id, callback) =>
-    debug 'create', model, data if @debug
+  exists: (model, id, callback) ->
+    debug "exists for #{model} with id: #{id}" if @debug

     @find model, id, (err, result) ->
-      callback err if err
-      callback null, (result.length > 0)
+      return callback err if err
+      callback null, result._result.length > 0

   ###
     Find a model instance by id
@@ -443,40 +403,46 @@
     @param id [String] id The id value
     @param callback [Function] The callback function, called with a (possible) error object and the found object
   ###
-  find: (model, id, callback) =>
-    debug 'find', model, id if @debug
-
-    aql = qb.for('retDoc').in('@@collection').filter(qb.eq("retDoc._key", '@id')).limit(1).return('retDoc')
-    bindVars = {
-      '@collection': @getCollectionName(model),
+  find: (model, id, callback) ->
+    debug "find for #{model} with id: #{id}" if @debug
+
+    aql = qb.for('doc').in('@@collection').filter(qb.eq('doc._key', '@id')).limit(1).return(qb.fn('UNSET') 'doc', ['"_id"','"_rev"'])
+
+    bindVars =
+      '@collection': @getCollectionName model
       id: id
-    }
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result[0] if result.length > 0
-      callback null, result
-
-  ###
-    Delete a model instance by id
-
-    @param model [String] model The model name
-    @param id [String] id The id value
-    @param callback [Function] The callback function, called with a (possible) error object and the number of affected objects
-  ###
-  destroy: (model, id, callback) =>
-    debug 'delete', model, id if @debug
-
-    aql = qb.for('removeDoc').in('@@collection').filter(qb.eq('removeDoc._key', '@id')).returnOld('removed')
-    bindVars = {
-      '@collection': @getCollectionName(model)
-      id: id
-    }
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result[0] if result.length > 0
-      callback null, result
+
+    @execute aql, bindVars, (err, result) ->
+      return callback err if err
+      return callback null, result._result[0] if result._result.length > 0
+      callback null, result._result
+
+
+  ###
+   Decide if id should be included
+   @param {Object} fields
+   @returns {Boolean}
+   @private
+  ###
+  _idIncluded: (fields, idName) ->
+    if !fields then return true
+
+    if Array.isArray(fields)
+      return fields.indexOf(idName) >= 0
+
+    if fields[idName]
+      # Included
+      return true
+
+    if idName in fields and !fields[idName]
+      # Excluded
+      return false
+
+    for f in fields
+      return !fields[f]; # If the fields has exclusion
+
+    return true
+

   # ========================
   # = Collection functions =
@@ -492,255 +458,111 @@
     @option return bindVars [Object] The variables, bound in the conditions
     @option return geoObject [Object] An query builder object containing possible parameters for a geo query
   ###
-  _filter2where: (model, filter, returnVariable) =>
-    debug "Evaluating where object #{JSON.stringify filter} for Model #{model}: " if @debug
-
-    # variable to name the result
-    returnVariable = returnVariable or 'result'
+  _buildWhere: (model, where, index) ->
+    debug "Evaluating where object #{JSON.stringify where} for Model #{model}" if @debug
+
+    self = this
+
+    if !where? or typeof where isnt 'object'
+      return
+
+    #  array holding the filter
+    aqlArray = []
     #  the object holding the assignments of conditional values to temporary variables
-    boundVars = {}
+    bindVars = {}
+
+    geoExpr = {}
+
     # index for condition parameter binding
-    index = 0
-    #  helper function to fill boundVars with the upcoming temporary variables that the where sentence will generate
+    index = index or 0
+
+    #  helper function to fill bindVars with the upcoming temporary variables that the where sentence will generate
     assignNewQueryVariable = (value) ->
       partName = 'param_' + (index++)
-      boundVars[partName] = value
+      bindVars[partName] = value
       return '@'+partName

-    #  array holding the filter
-    aqlArray = []
+    idName = @idName(model)
     ###
       the where object comes in two flavors

        - where[prop] = value: this is short for "prop" equals "value"
        - where[prop][op] = value: this is the long version and stands for "prop" "op" "value"
     ###
-    for condProp, condValue of filter.where
-
-      # correct if the conditionProperty falsely references to 'id'
-      condProp = '_key' if condProp is 'id'
-
-      #  special case: if conditionValue is a Object (instead of a string or number) we have a conditionOperator
-      if condValue and condValue.constructor.name is 'Object'
+
+    for condProp, condValue of where
+      do() ->
+        # correct if the conditionProperty falsely references to 'id'
+        if condProp is idName
+          condProp = '_key'
+          if typeof condValue is 'number' then condValue = String(condValue)
+
+        # special treatment for 'and', 'or' and 'nor' operator, since there value is an array of conditions
+        if condProp in ['and', 'or', 'nor']
+          # 'and', 'or' and 'nor' have multiple conditions so we run buildWhere recursively on their array to
+          if Array.isArray condValue
+            aql = qb
+            # condValue is an array of conditions so get the conditions from it via a recursive buildWhere call
+            for c, a of condValue
+              cond = self._buildWhere model, a, ++index
+              aql = aql[condProp] cond.aqlArray[0]
+              bindVars = merge true, bindVars, cond.bindVars
+            aqlArray.push aql
+            aql = null
+          return
+
+        #  special case: if condValue is a Object (instead of a string or number) we have a conditionOperator
+        if condValue and condValue.constructor.name is 'Object'
         #  condition operator is the only keys value, the new condition value is shifted one level deeper and can be a object with keys and values
-        condOp = Object.keys(condValue)[0]
-        condValue = condValue[condOp]
-      else
-        # condition operator is 'equals' or shortly 'eq'
-        # and condition value is stringified for the comparison
-        condOp = 'eq'
-        condValue = condValue.toString()
-
-      # special treatment for "and" and "or" operator, since there value is an array of conditions
-      if condOp in ['and', 'or']
-        # 'and' and 'or' have multiple conditions so we run buildWhere recursively on their array to
-        if _.isArray condValue
-          # condValue is an array of conditions so get the conditions from it via a recursive buildWhere call
-          conditionalPart = @_filter2where model, condValue, returnVariable
-
-          # add the condArray resp. the boundVars
-          aqlArray.push qb[condOp].apply null, conditionalPart.aqlArray
-          merge true, boundVars, conditionalPart.boundVars
-
-      # If the value is not an array, fall back to regular fields
-      switch
-        # number comparison
-        when condOp in ['lte', 'lt', 'gte', 'gt', 'eq', 'neq']
-          aqlArray.push qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-
-        # range comparison
-        when condOp is 'between'
-          aqlArray.push [qb.gte("#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[0])}"),  qb.lte("#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[1])}")]
-
-        # string comparison
-        when condOp is 'like'
-          aqlArray.push qb.not qb.LIKE "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-        when condOp is 'nlike'
-          aqlArray.push qb.LIKE "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-
-        # array comparison
-        when condOp is 'nin'
-          aqlArray.push qb.not qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-        when condOp is 'in'
-          aqlArray.push qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-
-        # geo comparison (extra object)
-        when 'near'
-          # 'near' does not create a condition in the filter part, it returnes the lat/long pair
-          # the query will be done from the querying method itself
-          [lat, long] = condValue.split(',')
-          collection = @getCollectionName model
-          if filter.limit?
-            geoExpr = qb.NEAR collection, lat, long, filter.limit
-          else
-            geoExpr = qb.NEAR collection, lat, long
-
-        #  if we don't have a matching operator or no operator at all (condOp = false) then use the equivalence operator
+          condOp = Object.keys(condValue)[0]
+          condValue = condValue[condOp]
+
+        if condOp
+          # If the value is not an array, fall back to regular fields
+          switch
+            # number comparison
+            when condOp in ['lte', 'lt', 'gte', 'gt', 'eq', 'neq']
+              aqlArray.push qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+
+            # range comparison
+            when condOp is 'between'
+              aqlArray.push [qb.gte("#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[0])}"),  qb.lte("#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[1])}")]
+
+            # string comparison
+            when condOp is 'like'
+              aqlArray.push qb.not qb.LIKE "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+            when condOp is 'nlike'
+              aqlArray.push qb.LIKE "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+
+            # array comparison
+            when condOp is 'nin'
+              aqlArray.push qb.not qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+            when condOp is 'inq'
+              #TODO fix for id and other type
+              condValue = (cond.toString() for cond in condValue)
+              aqlArray.push qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+
+            # geo comparison (extra object)
+            when condOp is 'near'
+              # 'near' does not create a condition in the filter part, it returnes the lat/long pair
+              # the query will be done from the querying method itself
+              [lat, long] = condValue.split(',')
+              collection = @getCollectionName model
+              if where.limit?
+                geoExpr = qb.NEAR collection, lat, long, where.limit
+              else
+                geoExpr = qb.NEAR collection, lat, long
+
+            #  if we don't have a matching operator or no operator at all (condOp = false) print warning
+            else
+              console.warn 'No matching operatorfor : ', condOp
         else
-          qb.eq "(#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
-
-    console.log 'aqlArray ', aqlArray
+          aqlArray.push qb.eq "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
+
     return {
       aqlArray: aqlArray
-      boundVars: boundVars
+      bindVars: bindVars
       geoExpr: geoExpr
-    }
-
-
-
-  ###
-  ###
-  _filter2fields: (model, filter, returnVariable) =>
-    # variable to name the result
-    returnVariable = returnVariable or 'result'
-    includes = []
-    excludes = []
-
-    if Array.isArray filter.fields
-      includes = filter.fields
-    else
-      _.each filter.fields, (field, key) -> if field then includes.push key else excludes.push key
-
-    # KEEP only the specified fields to include if includes not empty
-    return qb.fn('KEEP') returnVariable, includes if includes.length > 0
-
-    # remove/UNSET the
-    return qb.fn('UNSET') returnVariable, excludes if excludes.length > 0
-
-
-
-  ###
-  ###
-  _filter2order: (model, filter, returnVariable) =>
-    splitOrderClause = (order) ->
-      [prop, dir] = order.split ' '
-      dir = dir or 'ASC'
-      return [prop, dir]
-
-    if Array.isArray filter.order
-      return _.chain(filter.order).map(splitOrderClause).flatten().value()
-    else
-      return splitOrderClause(filter.order)
-
-  ###
-  ###
-  _filter2limit: (model, filter, returnVariable) =>
-    if filter.skip?
-      return qb.limit filter.skip, filter.limit
-    else
-      return qb.limit filter.limit
-
-  ###
-  ###
-  # _filter2include: (model, filter, returnVariable) =>
-  # TODO:
-  # if (filter && filter.include) [
-  #   self._models[model].model.include(objs, filter.include, callback);
-  # ] else [
-  #   callback(null, objs);
-  # ]
-
-
-  ###
-  ###
-  # TODO: create a function that can be called with the filter array only
-  _filter2parts: (model, filter, returnVariable) =>
-    # ================
-    # = filter.where =
-    # ================
-    # TODO: where part is FILTER part of query
-    # filter.where -> use buildWhere to create FILTER statement
-    if not filter.where? or typeof filter.where isnt 'object'
-      filterWhere = null
-    else
-      filterWhere = @_filter2where model, filter, returnVariable
-
-    # =================
-    # = filter.fields =
-    # =================
-    # TODO: fields part should be used in RETURN part of query
-    # filter.fields -> use AQL-KEEP or AQL-UNSET to reduce final returned object
-    if not filter.fields? or typeof filter.fields isnt 'object'
-      filterFields = null
-    else
-      filterFields = @_filter2fields model, filter, returnVariable
-
-    # ================
-    # = filter.order =
-    # ================
-    # TODO: order part should be used in ORDER part of query
-    # filter.order -> query-builder for sorting
-    if not filter.order? or typeof filter.order not in ['object', 'string']
-      filterOrder = null
-    else
-      filterOrder = @_filter2order model, filter, returnVariable
-
-    # ==============================
-    # = filter.limit & filter.skip =
-    # ==============================
-    # TODO: order part should be used in LIMIT part of query
-    # filter.limit & filter.skip -> query-builder for limit & skip
-    if not filter.limit?
-      filterLimit = null
-    else
-      filterLimit = @_filter2limit model, filter, returnVariable
-
-    # ==================
-    # = filter.include =
-    # ==================
-    # TODO:
-    # if (filter && filter.include) {
-    #   self._models[model].model.include(objs, filter.include, callback);
-    # } else {
-    #   callback(null, objs);
-    # }
-
-    return {
-      where: filterWhere
-      fields: filterFields
-      order: filterOrder
-      limit: filterLimit
-      # include: filterInclude
-    }
-
-  ###
-  ###
-  _filter2query: (model, filter) =>
-    debug '_filter2query', model, filter if @debug
-
-
-    collection = @getCollectionName model
-    collVariable = collection.charAt 0
-    returnVariable = 'result'
-
-    boundVars = {}
-
-    parts = @_filter2parts model, filter, returnVariable
-
-    # define returnVariable
-    aql = qb.for(returnVariable)
-    console.log 'aql_for ', aql
-    # 1) check where-part if we have a geo expression that we must include in the in-clause, if not use in-collection
-    coll = if parts.where.geoObject? then parts.where.geoObject else '@@collection'
-    aql = aql.in(coll)
-    console.log 'aql_in ', aql
-    # 2) use filter from where-part to filter, merge the boundVars from where into the global boundVars
-    merge true, boundVars, parts.where.boundVars
-    aql = aql.filter qb.and.apply null, parts.where.aqlArray
-
-    # 3) use order to sort result on item level
-    aql = aql.sort.apply null, parts.order if parts.order?
-
-    # 4) use limit to slice result on item level
-    aql = aql.limit.apply null, parts.limit if parts.limit?
-
-    # 5) use field to return fields-filterd result on attribute level or plain object
-    returnExpr = if parts.fields? then parts.fields else returnVariable
-    aql = aql.return returnExpr
-    console.log 'aql', aql
-    return {
-      aql: aql
-      boundVars: boundVars
     }

@@ -751,18 +573,97 @@
     @param [Object] filter The filter
     @param [Function] callback Callback with (possible) error object or list of objects
   ###
-  all: (model, filter, callback) =>
-    all_aql = @_filter2query model, filter
-
-    @query all_aql.aql, all_aql.boundVars, (err, result) ->
-      callback err if err
+  all: (model, filter, options, callback) ->
+    debug "all for #{model} with filter #{JSON.stringify filter}" if @debug
+
+    self = this
+    idName = @idName(model)
+
+    bindVars =
+      '@collection': @getCollectionName model
+
+    aql = qb.for(returnVariable).in('@@collection')
+
+    if filter.where
+      if filter.where[idName]
+        id = filter.where[idName];
+        delete filter.where[idName];
+        if typeof id is 'number' then id = String(id)
+        filter.where._key = id;
+
+      where = @_buildWhere(model, filter.where)
+      for w in where.aqlArray
+        aql = aql.filter(w)
+      merge true, bindVars, where.bindVars
+
+    if filter.order
+      if typeof filter.order is 'string' then filter.order = filter.order.split(',')
+      for order in filter.order
+        m = order.match(/\s+(A|DE)SC$/)
+        field = order.replace(/\s+(A|DE)SC$/, '').trim()
+        if field is idName
+          field = '_key'
+        if m and m[1] is 'DE'
+          aql = aql.sort(returnVariable + '.' + field, 'DESC')
+        else
+          aql = aql.sort(returnVariable + '.' + field, 'ASC')
+    else
+      aql = aql.sort(returnVariable + '._key')
+
+    if filter.limit
+      aql = aql.limit(filter.skip, filter.limit)
+
+    fields = _.clone(filter.fields)
+
+    if fields
+      indexId = fields.indexOf('id')
+      if indexId isnt -1
+        fields[indexId] = '_key'
+      fields = ( '"' + field + '"' for field in fields)
+      aql = aql.return(qb.fn('KEEP') returnVariable, fields)
+    else
+      aql = aql.return((qb.fn('UNSET') returnVariable, ['"_id"','"_rev"']))
+
+    @execute aql, bindVars, (err, result) ->
+      return callback err if err
+
+      cursorToArray = (r) ->
+        if self._idIncluded(filter.fields, idName)
+          self.setIdValue(model, r, r._key)
+        # Don't pass back _key if the fields is set
+        if idName isnt '_key' then delete r._key;
+
+        r = self.fromDatabase(model, r)
+
+      result = (cursorToArray r for r in result._result)

       # filter include
       if filter.include?
-        @getModelClass.model.include result, filter.include, callback
+        self._models[model].model.include result, filter.include, options, callback
       else
         callback null, result

+
+  ###
+  Delete a model instance by id
+
+  @param model [String] model The model name
+  @param id [String] id The id value
+  @param callback [Function] The callback function, called with a (possible) error object and the number of affected objects
+  ###
+  destroy: (model, id, callback) ->
+    debug "delete for #{model} with id #{id}" if @debug
+
+    aql = qb.for(returnVariable).in('@@collection').filter(qb.eq(returnVariable + '._key', '@id'))
+    .remove(returnVariable).in('@@collection').returnOld('removed')
+
+    bindVars =
+      '@collection': @getCollectionName model
+      id: id
+
+
+    @execute aql, bindVars, (err, result) ->
+      callback and callback err, result and result._result.length

   ###
@@ -772,32 +673,28 @@
     @param [Object] [where] The filter for where
     @param [Function] callback Callback with (possible) error object or the number of affected objects
   ###
-  destroyAll: (model, where, callback) =>
-    debug 'destroyAll', model, where if @debug
-
-    collection = @getCollectionName(model)
-    collVariable = collection.charAt 0
-
-    bindVars = {
+  destroyAll: (model, where, callback) ->
+    debug "destroyAll for #{model} with where #{JSON.stringify where}" if @debug
+
+    collection = @getCollectionName model
+
+    bindVars =
       '@collection': collection
-      model: collVariable
-    }
-
-    returnVariable = 'result'
-    # for .. in ..
-    aql = qb.for('@model').in('@@collection')
-    # filter ...
-    if where
-      whereFilter = @buildWhere model, where, returnVariable
-      aql = aql.filter qb.and.apply null, whereFilter.aqlArray
-      merge true, bindVars, whereFilter.bindVars
-
-    # remove ...
-    aql = aql.remove('@model').in('@@collection').returnOld(returnVariable)
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result.length
+
+    aql = qb.for(returnVariable).in('@@collection')
+
+    if !_.isEmpty(where)
+      where = @_buildWhere model, where
+      for w in where.aqlArray
+        aql = aql.filter(w)
+      merge true, bindVars, where.bindVars
+
+    aql = aql.remove(returnVariable).in('@@collection').returnOld('removed')
+
+    @execute aql, bindVars, (err, result) ->
+      return callback err if err
+      callback null, result._result.length
+

   ###
     Count the number of instances for the given model
@@ -806,30 +703,28 @@
     @param [Function] callback Callback with (possible) error object or the number of affected objects
     @param [Object] where The filter for where
   ###
-  count: (model, callback, where) =>
-    debug 'count', model, where if @debug
-
-    collection = @getCollectionName(model)
-    collVariable = collection.charAt 0
-
-    bindVars = {
-      collection: collection
-      model: collVariable
-    }
-
-    # for .. in ..
-    aql = qb.for('@model').in('@@collection')
-    # filter ...
-    if where
-      whereFilter = @buildWhere model, where, modelVariable
-      aql = aql.filter qb.and.apply null, whereFilter.aqlArray
-      merge true, bindVars, whereFilter.bindVars
-
-    aql = aql.return '@model'
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result.length
+  count: (model, where, options, callback) ->
+    debug "count for #{model} with where #{JSON.stringify where}" if @debug
+
+    collection = @getCollectionName model
+
+    bindVars =
+      '@collection': collection
+
+
+    aql = qb.for(returnVariable).in('@@collection')
+
+    if !_.isEmpty(where)
+      where = @_buildWhere model, where
+      for w in where.aqlArray
+        aql = aql.filter(w)
+      merge true, bindVars, where.bindVars
+
+    aql = aql.return(qb.fn('UNSET') returnVariable, ['"_id"','"_rev"'])
+
+    @execute aql, bindVars, (err, result) ->
+      return callback err if err
+      callback null, result._result.length

   ###
@@ -840,23 +735,32 @@
     @param [Object] data The model data
     @param [Function] callback Callback with (possible) error object or the updated object
   ###
-  updateAttributes: (model, id, data, cb) =>
-    debug 'updateAttributes', model, id, data if @debug
-
-    collection = @getCollectionName(model)
-    collVariable = collection.charAt 0
-
-    bindVars = {
-      collection: collection
+  updateAttributes: (model, id, data, callback) ->
+    debug "updateAttributes for #{model} with id #{id} and data #{JSON.stringify data}" if @debug
+
+    self = this
+
+    if id is Number then id = String(id)
+    idName = @idName(model)
+
+    bindVars =
+      '@collection': @getCollectionName model
       id: id
-    }
-
-    # for .. in ..
-    aql = qb.for('updateDoc').in('@@collection').filter('updateDoc._key','@id').update('updateDoc').with(data).in('@@collection').returnNew('result')
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result
+      data: data
+
+    #TODO: aqb not support _.return((qb.fn(UNSET) NEW, ['"_id"', '"_rev"']))
+    aql = qb.for(returnVariable).in('@@collection').filter(qb.eq(returnVariable + '._key', '@id')).update(returnVariable)
+    .with('@data').in('@@collection').returnNew('updated')
+
+    @execute aql, bindVars, (err, result) ->
+      if result and result._result[0]
+        result = result._result[0]
+        delete result['_id']
+        delete result['_rev']
+        self.setIdValue(model, result, id);
+        if idName isnt '_key' then delete result._key;
+      callback and callback err, result
+

   ###
     Update all matching instances
@@ -866,156 +770,166 @@
     @param [Object] data The property/value pairs to be updated
     @param [Function] callback Callback with (possible) error object or the number of affected objects
   ###
-  update: (model, where, data, cb) =>
-    return @updateAll model, where, data, cb
-
-  updateAll: (model, where, data, cb) =>
-    debug 'updateAll', model, where, data if @debug
-
-    # TODO: FOR & FILTER (from id) & UPDATE (with data)
-    collection = @getCollectionName(model)
-    collVariable = collection.charAt 0
-
-    bindVars = {
-      collection: collection
-      model: collVariable
-    }
-
-    # for .. in ..
-    aql = qb.for('@model').in('@@collection')
-    # filter ...
+  update: (model, where, data, callback) ->
+    @updateAll model, where, data, callback
+
+  updateAll: (model, where, data, callback) ->
+    debug "updateAll for #{model} with where #{JSON.stringify where} and data #{JSON.stringify data}" if @debug
+
+    collection = @getCollectionName model
+
+    bindVars =
+      '@collection': collection
+      data: data
+
+    aql = qb.for(returnVariable).in('@@collection')
+
     if where
-      whereFilter = @buildWhere model, where, modelVariable
-      aql = aql.filter qb.and.apply null, whereFilter.aqlArray
-      merge true, bindVars, whereFilter.bindVars
-
-    aql = aql.update(modelVariable).with(data).in('@@collection')
-
-    @query aql, bindVars, (err, result) ->
-      callback err if err
-      callback null, result.length
+      where = @_buildWhere(model, where)
+      for w in where.aqlArray
+        aql = aql.filter(w)
+      merge true, bindVars, where.bindVars
+
+    aql = aql.update(returnVariable).with('@data').in('@@collection')
+
+    idName = @idName(model)
+    delete data[idName]
+
+    @execute aql, bindVars, (err, result) ->
+      return callback err if err
+      callback null, {count: result.extra.stats.writesExecuted}

   # @extend require('./MigrationMixin')
   # ===================
   # = Migration Mixin =
   # ===================
-      ###
-        Perform autoupdate for the given models. It basically calls ensureIndex
-
-        @param [String[]] [models] A model name or an array of model names. If not present, apply to all models
-        @param [Function] [cb] The callback function
-      ###
-      autoupdate: (models, cb) =>
-        if @db
-          debug 'autoupdate' if @debug
-
-          if (not cb) and (typeof models is 'function')
-            cb = models
-            models = undefined
-
-          # First argument is a model name
-          models = [models] if typeof models is 'string'
-
-          models = models or Object.keys @_models
-
-          async.each( models, ((model, modelCallback) ->
-            indexes = @_models[model].settings.indexes or []
-            indexList = []
+  ###
+    Perform autoupdate for the given models. It basically calls ensureIndex
+
+    @param [String[]] [models] A model name or an array of model names. If not present, apply to all models
+    @param [Function] [cb] The callback function
+  ###
+  autoupdate: (models, cb) ->
+
+    self = this
+
+    if @db
+      debug 'autoupdate for model %s', models if @debug
+
+      if (not cb) and (typeof models is 'function')
+        cb = models
+        models = undefined
+
+      # First argument is a model name
+      models = [models] if typeof models is 'string'
+
+      models = models or Object.keys @_models
+
+      async.each( models, ((model, modelCallback) ->
+        indexes = self._models[model].settings.indexes or []
+        indexList = []
+        index = {}
+        options = {}
+
+        if typeof indexes is 'object'
+          for indexName, index of indexes
+            if index.keys
+              # the index object has keys
+              options = index.options or {}
+              options.name = options.name or indexName
+              index.options = options
+            else
+              options =
+                name: indexName
+              index =
+                keys: index
+                options: options
+
+            indexList.push index
+        else if Array.isArray indexes
+          indexList = indexList.concat indexes
+
+        for propIdx, property of @_models[model].properties
+          if property.index
             index = {}
-            options = {}
-
-            if typeof indexes is 'object'
-              for indexName, index of indexes
-                if index.keys
-                  # the index object has keys
-                  options = index.options or {}
-                  options.name = options.name or indexName
-                  index.options = options
-                else
-                  options = { name: indexName }
-                  index = {
-                    keys: index
-                    options: options
-                  }
-
-                indexList.push index
-            else if Array.isArray indexes
-              indexList = indexList.concat indexes
-
-            for propIdx, property of @_models[model].properties
-              if property.index
-                index = {}
-                index[propIdx] = 1
-
-                if typeof property.index is 'object'
-                  # If there is a arangodb key for the index, use it
-                  if typeof property.index.arangodb is 'object'
-                    options = property.index.arangodb
-                    index[propIdx] = options.kind or 1
-                    # Backwards compatibility for former type of indexes
-                    options.unique = true if property.index.uniqe is true
-                  else
-                    # If there isn't an  properties[p].index.mongodb object, we read the properties from  properties[p].index
-                    options = property.index
-
-                  options.background = true if options.background is undefined
-
-                # If properties[p].index isn't an object we hardcode the background option and check for properties[p].unique
-                else
-                  options = { background: true }
-                  options.unique = true if property.unique
-
-
-                indexList.push { keys: index, options: options }
-
-            debug 'create indexes' if @debug
-            async.each( indexList, ((index, indexCallback) ->
-              debug 'ensureIndex' if @debug
-
-              # TODO: ensureIndex for ArangoDB
-              # self.collection(model).ensureIndex(index.fields || index.keys, index.options, indexCallback);
-            ), modelCallback )
-          ), cb)
-        else
-          @dataSource.once 'connected', () -> @autoupdate models, cb
-
-
-
-      ###
-        Perform automigrate for the given models. It drops the corresponding collections and calls ensureIndex
-
-        @param [String[]] [models] A model name or an array of model names. If not present, apply to all models
-        @param [Function] [cb] The callback function
-      ###
-      automigrate: (models, cb) =>
-        if @db
-          debug 'automigrate' if @debug
-
-          if (not cb) and (typeof models is 'function')
-            cb = models
-            models = undefined
-
-          # First argument is a model name
-          models = [models] if typeof models is 'string'
-
-          async.each(models, ((model, modelCallback) ->
-            debug "drop collection: #{model}"
-            @db.dropCollection model, (err, collection) ->
-              if err
-                #  For errors other than 'ns not found' (collection doesn't exist)
-                return modelcallback err if not (err.name is 'MongoError' and err.ok is 0 and err.errmsg is 'ns not found')
-
-              # Recreate the collection
-              debug "create collection: #{model}"
-
-              @db.createCollection model, modelcallback
-
-          ), ((err) ->
-            return cb and cb err if err
-            @autoupdate models, cb
-          ))
-        else
-          @dataSource.once 'connected', () -> @automigrate models cb
+            index[propIdx] = 1
+
+            if typeof property.index is 'object'
+              # If there is a arangodb key for the index, use it
+              if typeof property.index.arangodb is 'object'
+                options = property.index.arangodb
+                index[propIdx] = options.kind or 1
+                # Backwards compatibility for former type of indexes
+                options.unique = true if property.index.uniqe is true
+              else
+                # If there isn't an  properties[p].index.mongodb object, we read the properties from  properties[p].index
+                options = property.index
+
+              options.background = true if options.background is undefined
+
+            # If properties[p].index isn't an object we hardcode the background option and check for properties[p].unique
+            else
+              options =
+                background: true
+              options.unique = true if property.unique
+
+            indexList.push { keys: index, options: options }
+
+        debug 'create indexes' if @debug
+
+        async.each( indexList, ((index, indexCallback) ->
+          debug 'createIndex: %s', index if @debug
+          self.collection(model).createIndex(index.fields || index.keys, index.options, indexCallback);
+        ), modelCallback )
+      ), cb)
+    else
+      @dataSource.once 'connected', () -> @autoupdate models, cb
+
+
+
+  ###
+    Perform automigrate for the given models. It drops the corresponding collections and calls ensureIndex
+
+    @param [String[]] [models] A model name or an array of model names. If not present, apply to all models
+    @param [Function] [cb] The callback function
+  ###
+  automigrate: (models, cb) ->
+    self = this
+
+    if @db
+      debug "automigrate for model #{models}" if @debug
+
+      if (not cb) and (typeof models is 'function')
+        cb = models
+        models = undefined
+
+      # First argument is a model name
+      models = [models] if typeof models is 'string'
+
+      models = models || Object.keys @_models
+
+      async.eachSeries(models, ((model, modelCallback) ->
+        collectionName = self.getCollectionName model
+        debug 'drop collection %s for model %s', collectionName, model
+
+        self.db.dropCollection model, (err, collection) ->
+          if err
+            if err.response.body?
+              err = err.response.body
+              #  For errors other than 'ns not found' (collection doesn't exist)
+              return modelCallback err if not (err.error is true and err.errorNum is 1203 and err.errorMessage is 'unknown collection \'' + model + '\'')
+
+          # Recreate the collection
+          debug 'create collection %s for model %s', collectionName, model
+
+          self.db.createCollection model, modelCallback
+
+      ), ((err) ->
+        return cb and cb err
+        self.autoupdate models, cb
+      ))
+    else
+      @dataSource.once 'connected', () -> @automigrate models cb

 exports.ArangoDBConnector = ArangoDBConnector


Simplified code in "setClassOnMousemove".

--- src/base/dom.coffee
+++ src/base/dom.coffee
@@ -1508,15 +1508,7 @@

 		CUI.Events.listen
 			node: opts.element
-			type: "wheel"
-			instance: opts.instance
-			call: ->
-				add_class()
-				return
-
-		CUI.Events.listen
-			node: opts.element
-			type: "mousemove"
+			type: ["mousemove", "wheel"]
 			instance: opts.instance
 			call: (ev) ->
 				add_class()


Fixes #289. Remove deprecated subscription call to TextBuffer.on

Also `beautify on save` is checked on every save,
not just on Atom Beautify activation / startup.

--- src/beautify.coffee
+++ src/beautify.coffee
@@ -43,6 +43,9 @@
   return

 beautify = ({onSave}) ->
+  # Verify if beautify on save
+  return if onSave and atom.config.get("atom-beautify.beautifyOnSave") is false
+  # Continue beautifying
   path ?= require("path")
   LoadingView ?= require "./views/loading-view"
   @loadingView ?= new LoadingView()
@@ -327,12 +330,8 @@
 handleSaveEvent = =>
   atom.workspace.observeTextEditors (editor) =>
     buffer = editor.getBuffer()
-    plugin.unsubscribe buffer
-    if atom.config.get("atom-beautify.beautifyOnSave")
-      events = "will-be-saved"
-      plugin.subscribe buffer, events, beautify.bind(@, {onSave:true})
-    return
-  return
+    disposable = buffer.onWillSave(beautify.bind(@, {onSave:true}))
+    plugin.subscribe disposable

 {Subscriber} = require path.join(atom.packages.resourcePath, 'node_modules', 'emissary')
 Subscriber.extend plugin


fix checkbox ng-model wiring

fix checkbox ng-model wiring

--- src/checkbox.coffee
+++ src/checkbox.coffee
@@ -34,12 +34,11 @@

         if ngModelCtrl
           scope.$watch 'selectedItem', ->
-            ngModelCtrl.$setViewValue(scope.selectedItem)
-            scope.activeItem = scope.selectedItem
+            ngModelCtrl.$setViewValue(scope.selectedItems)

           ngModelCtrl.$render = ->
             unless scope.disabled
-              scope.selectedItem = ngModelCtrl.$modelValue
+              scope.selectedItems = ngModelCtrl.$modelValue

         attrs.$observe 'disabled', (value) ->
           scope.disabled = value


Keep items methods close

Keep items methods close

--- src/checkout.coffee
+++ src/checkout.coffee
@@ -161,6 +161,17 @@
   sendLocale: (locale='pt-BR') =>
     @sendAttachment('clientPreferencesData', {locale: locale}, [])

+  # Sends a request to select an available gift
+  updateSelectableGifts: (list, selectedGifts, expectedOrderFormSections = @_allOrderFormSections) =>
+    updateSelectableGiftsRequest =
+      id: list
+      selectedGifts: selectedGifts
+      expectedOrderFormSections: expectedOrderFormSections
+
+    @_updateOrderForm
+      url: @_getUpdateSelectableGifts(list)
+      data: JSON.stringify(updateSelectableGiftsRequest)
+
   # Sends a request to add an offering, along with its info, to the OrderForm.
   addOfferingWithInfo: (offeringId, offeringInfo, itemIndex, expectedOrderFormSections = @_allOrderFormSections) =>
     updateItemsRequest =
@@ -186,6 +197,20 @@
       url: @_getRemoveOfferingsURL(itemIndex, offeringId)
       data: JSON.stringify(updateItemsRequest)

+  # Sends a request to add an item in the OrderForm.
+  addToCart: (items, expectedOrderFormSections = @_allOrderFormSections, salesChannel) =>
+    addToCartRequest =
+      orderItems: items
+      expectedOrderFormSections: expectedOrderFormSections
+
+    salesChannelQueryString = ''
+    if salesChannel
+      salesChannelQueryString = '?sc=' + salesChannel
+
+    @_updateOrderForm
+      url: @_getAddToCartURL() + salesChannelQueryString
+      data: JSON.stringify addToCartRequest
+
   # Sends a request to update the items in the OrderForm. Items that are omitted are not modified.
   updateItems: (items, expectedOrderFormSections = @_allOrderFormSections) =>
     updateItemsRequest =
@@ -196,49 +221,38 @@
       url: @_getUpdateItemURL()
       data: JSON.stringify(updateItemsRequest)

-  # Sends a request to select an available gift
-  updateSelectableGifts: (list, selectedGifts, expectedOrderFormSections = @_allOrderFormSections) =>
-    updateSelectableGiftsRequest =
-      id: list
-      selectedGifts: selectedGifts
-      expectedOrderFormSections: expectedOrderFormSections
-
-    @_updateOrderForm
-      url: @_getUpdateSelectableGifts(list)
-      data: JSON.stringify(updateSelectableGiftsRequest)
-
   # Sends a request to remove items from the OrderForm.
   removeItems: (items, expectedOrderFormSections = @_allOrderFormSections) =>
     item.quantity = 0 for item in items
     @updateItems(items, expectedOrderFormSections)

   # Sends a request to remove all items from the OrderForm.
-  removeAllItems: (expectedOrderFormSections = @_allOrderFormSections)=>
+  removeAllItems: (expectedOrderFormSections = @_allOrderFormSections) =>
     @getOrderForm(['items']).then (orderForm) =>
       items = orderForm.items
       item.quantity = 0 for item in items
       @updateItems(items, expectedOrderFormSections)

-  # Sends a request to add a discount coupon to the OrderForm.
-  addDiscountCoupon: (couponCode, expectedOrderFormSections = @_allOrderFormSections) =>
-    couponCodeRequest =
-      text: couponCode
-      expectedOrderFormSections: expectedOrderFormSections
-
-    @_updateOrderForm
-      url: @_getAddCouponURL()
-      data: JSON.stringify couponCodeRequest
-
-  # Sends a request to remove the discount coupon from the OrderForm.
-  removeDiscountCoupon: (expectedOrderFormSections) =>
-    @addDiscountCoupon('', expectedOrderFormSections)
-
-  # Sends a request to remove the gift registry for the current OrderForm.
-  removeGiftRegistry: (expectedFormSections = @_allOrderFormSections) =>
-    checkoutRequest = { expectedOrderFormSections: expectedFormSections }
-    @_updateOrderForm
-      url: @_getRemoveGiftRegistryURL()
-      data: JSON.stringify(checkoutRequest)
+  # Sends a request to change the price of an item, updating manualPrice on the orderForm
+  # Only possible if allowManualPrice is true
+  setManualPrice: (itemIndex, manualPrice) =>
+    setManualPriceRequest =
+      price: manualPrice
+
+    @_updateOrderForm
+      url: @_manualPriceURL(itemIndex)
+      type: 'PUT'
+      contentType: 'application/json; charset=utf-8'
+      dataType: 'json'
+      data: JSON.stringify setManualPriceRequest
+
+  # Sends a request to remove the manualPrice of an item, updating manualPrice on the orderForm
+  removeManualPrice: (itemIndex) =>
+    @_updateOrderForm
+      url: @_manualPriceURL(itemIndex)
+      type: 'DELETE'
+      contentType: 'application/json; charset=utf-8'
+      dataType: 'json'

   # Sends a request to add an attachment to a specific item
   addItemAttachment: (itemIndex, attachmentName, content, expectedFormSections = @_allOrderFormSections) =>
@@ -281,6 +295,27 @@
       url: @_getBundleItemAttachmentURL(itemIndex, bundleItemId, attachmentName)
       type: 'DELETE'
       data: JSON.stringify(dataRequest)
+
+  # Sends a request to add a discount coupon to the OrderForm.
+  addDiscountCoupon: (couponCode, expectedOrderFormSections = @_allOrderFormSections) =>
+    couponCodeRequest =
+      text: couponCode
+      expectedOrderFormSections: expectedOrderFormSections
+
+    @_updateOrderForm
+      url: @_getAddCouponURL()
+      data: JSON.stringify couponCodeRequest
+
+  # Sends a request to remove the discount coupon from the OrderForm.
+  removeDiscountCoupon: (expectedOrderFormSections) =>
+    @addDiscountCoupon('', expectedOrderFormSections)
+
+  # Sends a request to remove the gift registry for the current OrderForm.
+  removeGiftRegistry: (expectedFormSections = @_allOrderFormSections) =>
+    checkoutRequest = { expectedOrderFormSections: expectedFormSections }
+    @_updateOrderForm
+      url: @_getRemoveGiftRegistryURL()
+      data: JSON.stringify(checkoutRequest)

   # Sends a request to calculates shipping for the current OrderForm, given a COMPLETE address object.
   calculateShipping: (address) =>
@@ -360,41 +395,6 @@

   getLogoutURL: @::getChangeToAnonymousUserURL

-  # Sends a request to add an item in the OrderForm.
-  addToCart: (items, expectedOrderFormSections = @_allOrderFormSections, salesChannel) =>
-    addToCartRequest =
-      orderItems: items
-      expectedOrderFormSections: expectedOrderFormSections
-
-    salesChannelQueryString = ''
-    if salesChannel
-      salesChannelQueryString = '?sc=' + salesChannel
-
-    @_updateOrderForm
-      url: @_getAddToCartURL() + salesChannelQueryString
-      data: JSON.stringify addToCartRequest
-
-  # Sends a request to change the price of an item, updating manualPrice on the orderForm
-  # Only possible if allowManualPrice is true
-  setManualPrice: (itemIndex, manualPrice) =>
-    setManualPriceRequest =
-      price: manualPrice
-
-    @_updateOrderForm
-      url: @_manualPriceURL(itemIndex)
-      type: 'PUT'
-      contentType: 'application/json; charset=utf-8'
-      dataType: 'json'
-      data: JSON.stringify setManualPriceRequest
-
-  # Sends a request to remove the manualPrice of an item, updating manualPrice on the orderForm
-  removeManualPrice: (itemIndex) =>
-    @_updateOrderForm
-      url: @_manualPriceURL(itemIndex)
-      type: 'DELETE'
-      contentType: 'application/json; charset=utf-8'
-      dataType: 'json'
-
   # URL BUILDERS

   _getOrderFormId: =>


URL Parameters Processed Twice

URL Parameters Processed Twice

app.clientConnect has an incorrect test whether it is in the root browser window.
This always fails and results in CFM sending postMessage to itself.

--- src/code/app.coffee
+++ src/code/app.coffee
@@ -52,7 +52,7 @@
     @client.processUrlParams()

     # if iframed let the parent know about the connect
-    if window.parent
+    if window.parent isnt window
       window.parent.postMessage({type: "cfm::iframedClientConnected"}, "*")

   _createHiddenApp: ->


optimised imports

optimised imports

--- src/compiler.coffee
+++ src/compiler.coffee
@@ -2,7 +2,6 @@

 parser = require './parser'
 vocabulary = require './vocabulary'
-{Complex} = require './complex'
 {inherit, die, assert, all} = require './helpers'

 # # Stage 1: Assign parents


cleanup

cleanup

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -265,9 +265,9 @@
           throw new Error ErrorCodes.INVALID_SCHEDULE_TIME {time: form.when}

     props =
-      messageId: messageId
-      when: w
-      scheduled: now
+      messageId:  messageId
+      when:       w
+      scheduled:  now
       how:
         star:     String(form.star)     is "true"
         unread:   String(form.unread)   is "true"


EditPage:  recreate Freesheet environment when load new file

--- src/main/js/edit/editpage.coffee
+++ src/main/js/edit/editpage.coffee
@@ -6,28 +6,28 @@
 FileUtils = require 'file-utils'

 $ ->
+  freesheet = null
   sheetsEl = $('#sheets')
-  sheetCount = 0
+
+  newFreesheetEnvironment = ->
+    sheetsEl.empty()
+    freesheet?.destroy()
+    freesheet = new Freesheet()

   newSheet = (name, text) ->
-    sheetName = name or "Sheet#{++sheetCount}"
+    sheetName = name or "Sheet#{getWorksheets().length + 1}"
     sectionEl = $("""

#{sheetName}

""").appendTo sheetsEl

-    sheet = Freesheet.createSheet sheetName
+    sheet = freesheet.createSheet sheetName
     sheet.onValueChange (name, value) -> console.log("Change: " + name + " = " + value)
     sheet.addFunctions PageFunctions

     worksheet = new TableWorksheet sectionEl.find('.worksheet'), sheet
     if text then worksheet.loadText text
     sectionEl.data 'worksheet', worksheet
-
-
-  editor = CKEDITOR.replace 'editor', { extraPlugins: 'divarea'}
-  fileUtils = window.fileUtils = new FileUtils()
-  newSheet()

   getWorksheets = -> $('.worksheet-section').map( (i, el) -> $(el).data('worksheet')).get()
   sheetScript = (worksheet) ->
@@ -87,11 +87,21 @@
       worksheetScripts[scriptName] = scriptText
     {pageHtml, worksheetScripts}

+  # initialise page
+  editor = CKEDITOR.replace 'editor', { extraPlugins: 'divarea'}
+  fileUtils = window.fileUtils = new FileUtils()
+
+  newFreesheetEnvironment()
+  newSheet()
+
+  # handle events
+
   $('#save').on 'click', -> fileUtils.save getPageText(), $('#name').val()

   loadFile = $('#load')
   fileLoaded = (file, text) ->
     {pageHtml, worksheetScripts} = parsePage text
+    newFreesheetEnvironment()
     newSheet(name, script) for name, script of worksheetScripts
     editor.setData pageHtml
     $('#name').val(file.name)


keep originally selectedItem selected after refresh except query change

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -398,7 +398,7 @@
         {row} = @editor.getCursorBufferPosition()
         @editor.setCursorBufferPosition([row, column])

-  refresh: ({force}={}) ->
+  refresh: ({force, selectFirstItem}={}) ->
     @emitWillRefresh()

     if force
@@ -424,7 +424,10 @@
       @items = [@promptItem, items...]
       @renderItems(items)

-      @selectItemForRow(@findRowForNormalItem(0, 'next'))
+      if (not selectFirstItem) and item = @findItem(@selectedItem)
+        @selectItem(item)
+      else
+        @selectFirstNormalItem()

       @setModifiedState(false)
       unless @hasNormalItem()
@@ -471,7 +474,7 @@
             selection.destroy()
           @withIgnoreChange => @insertQuery(@lastQuery) # Recover query
         else
-          @refresh().then =>
+          @refresh(selectFirstItem: true).then =>
             if @autoPreviewOnQueryChange and @isActive()
               if @provider.boundToEditor
                 @preview()
@@ -525,12 +528,12 @@

   syncToEditor: (editor) ->
     return if @preventSyncToEditor
+    return if @isActive()
     if item = @findClosestItemForEditor(editor)
       @selectItem(item)
-      unless @isActive()
-        {row} = @editor.getCursorBufferPosition()
-        @moveToSelectedItem(scrollToColumnZero: true)
-        @emitDidMoveToItemArea() if @isPromptRow(row)
+      {row} = @editor.getCursorBufferPosition()
+      @moveToSelectedItem(scrollToColumnZero: true)
+      @emitDidMoveToItemArea() if @isPromptRow(row)

   moveToSelectedItem: ({scrollToColumnZero, ignoreCursorMove}={}) ->
     if (row = @getRowForSelectedItem()) >= 0
@@ -677,8 +680,17 @@
     @items.filter (item) ->
       (not item.skip) and (item.filePath is filePath)

+  # When filePath is undefined, it OK cause, `undefined` is `undefined`
+  findItem: ({point, filePath}={}) ->
+    for item in @getNormalItems()
+      if item.point.isEqual(point) and item.filePath is filePath
+        return item
+
   updateProviderPanel: (states) ->
     @providerPanel.updateStateElements(states)
+
+  selectFirstNormalItem: ->
+    @selectItemForRow(@findRowForNormalItem(0, 'next'))

   selectItemForRow: (row) ->
     item = @items[row]


set objective command

set objective command

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -127,6 +127,7 @@
       'narrow-ui:toggle-auto-preview': => @toggleAutoPreview()
       'narrow-ui:force-refresh': => @forceRefresh()
       'narrow-ui:move-to-query-or-current-item': => @moveToQueryOrCurrentItem()
+      'narrow-ui:update-real-file': => @updateRealFile()

   moveUpDown: (direction) ->
     if (row = @getRowForSelectedItem()) >= 0


feat: Resolve hookfiles glob patterns

feat: Resolve hookfiles glob patterns

...before passing them to hook handlers

Previously, Dredd provided hook file paths / globs to hook handlers
in the same form (untouched) as they were passed via the CLI. Since
every language has its own implementation of glob pattern resolution,
it could potentially lead to problems with wrong execution order of
hookfiles.

With the change introduced in this commit, Dredd now resolves glob
patterns, matched file paths resolves to their absolute form, sorts
them alphabetically and then passes to hook handlers.

Resolves https://github.com/apiaryio/dredd/issues/680
Resolves https://github.com/apiaryio/dredd/issues/742

--- src/add-hooks.coffee
+++ src/add-hooks.coffee
@@ -12,6 +12,8 @@
 mergeSandboxedHooks = require './merge-sandboxed-hooks'
 HooksWorkerClient = require './hooks-worker-client'

+# Ensure platform agnostic path.basename function
+basename = if process.platform is 'win32' then path.win32.basename else path.basename

 addHooks = (runner, transactions, callback) ->
   # Note: runner.configuration.options must be defined
@@ -31,10 +33,7 @@

           delete allHooks[hookType][transactionName]

-  loadHookFile = (filename, basePath) ->
-    basePath ?= customConfigCwd or process.cwd()
-    filePath = path.resolve(basePath, filename)
-
+  loadHookFile = (filePath) ->
     try
       proxyquire filePath, {
         'hooks': runner.hooks
@@ -55,15 +54,15 @@
     if typeof(runner.configuration.hooksData) != 'object' or Array.isArray(runner.configuration.hooksData) != false
       return callback(new Error("hooksData option must be an object e.g. {'filename.js':'console.log(\"Hey!\")'}"))

-    # run code in sandbox
+    # Run code in sandbox
     async.eachSeries Object.keys(runner.configuration.hooksData), (key, nextHook) ->
       data = runner.configuration.hooksData[key]

-      # run code in sandbox
+      # Run code in sandbox
       sandboxHooksCode data, (sandboxError, result) ->
         return nextHook(sandboxError) if sandboxError

-        # merge stringified hooks
+        # Merge stringified hooks
         runner.hooks = mergeSandboxedHooks(runner.hooks, result)

         # Fixing #168 issue
@@ -95,23 +94,43 @@
         callback(new Error(msg))
     else

-      # no data found, doing nothing
+      # No data found, doing nothing
       return callback()

   # Loading hookfiles from fs
   else
+    # Expand hookfiles - sort files alphabetically and resolve their paths
+    hookfiles = [].concat runner.configuration?.options?.hookfiles
+    files = hookfiles.reduce((result, item) ->
+      # glob.sync does not resolve paths, only glob patterns
+      input = if glob.hasMagic(item) then glob.sync(item) else [item]
+
+      # Gradually append sorted and resolved paths
+      result.concat input
+        # Create a filename / filepath map for easier sorting
+        # Example:
+        # [
+        #   { basename: 'filename1.coffee', path: './path/to/filename1.coffee' }
+        #   { basename: 'filename2.coffee', path: './path/to/filename2.coffee' }
+        # ]
+        .map((filepath) -> basename: basename(filepath), path: filepath)
+        # Sort 'em up
+        .sort((a, b) -> a.basename > b.basename)
+        # Resolve paths to absolute form. Take into account user defined current
+        # working directory, fallback to process.cwd() otherwise
+        .map((item) -> path.resolve(customConfigCwd or process.cwd(), item.path))
+    , [] # Start with empty result
+    )
+
+    logger.info('Found Hookfiles:', files)

     # Clone the configuration object to hooks.configuration to make it
     # accessible in the node.js hooks API
     runner.hooks.configuration = clone runner?.configuration

-    # Expand globs
-    files = []
-    globs = [].concat runner?.configuration?.options?.hookfiles
-    for globItem in globs
-      files = files.concat glob.sync(globItem)
-
-    logger.info('Found Hookfiles:', files)
+    # Override hookfiles option in configuration object with
+    # sorted and resolved files
+    runner.hooks.configuration.options.hookfiles = files

     # Loading files in non sandboxed nodejs
     if not runner.configuration.options.sandbox == true
@@ -121,7 +140,7 @@
       runner?.configuration?.options?.language == undefined or
       runner?.configuration?.options?.language == "nodejs"

-        # load regular files from fs
+        # Load regular files from fs
         for file in files
           loadHookFile file

@@ -130,21 +149,20 @@
       # If other language than nodejs, run hooks worker client
       # Worker client will start the worker server and pass the "hookfiles" options as CLI arguments to it
       else
-        # start hooks worker
+        # Start hooks worker
         hooksWorkerClient = new HooksWorkerClient(runner)
         hooksWorkerClient.start callback

     # Loading files in sandboxed mode
     else

-      # load sandbox files from fs
+      # Load sandbox files from fs
       logger.info('Loading hook files in sandboxed context:', files)
-      async.eachSeries files, (fileName, nextFile) ->
-        resolvedPath = path.resolve((customConfigCwd or process.cwd()), fileName)
-        # load hook file content
+      async.eachSeries files, (resolvedPath, nextFile) ->
+        # Load hook file content
         fs.readFile resolvedPath, 'utf8', (readingError, data) ->
           return nextFile(readingError) if readingError
-          # run code in sandbox
+          # Run code in sandbox
           sandboxHooksCode data, (sandboxError, result) ->
             return nextFile(sandboxError) if sandboxError
             runner.hooks = mergeSandboxedHooks(runner.hooks, result)


v0.9 final linux testing.

v0.9 final linux testing.

--- lib/local-theme-selector-view.coffee
+++ lib/local-theme-selector-view.coffee
@@ -131,7 +131,6 @@
     applyLocalTheme: (fn, themePath) ->
       baseCssPath = themePath || $( "#themeDropdown" ).val();
       sourcePath = baseCssPath + '/index.less'
-      console.log('applyLocalTheme: baseCssPath=' + baseCssPath)

       # Remember what theme is applied to what file.
       targetFile = fn || @utils.getActiveFile()


:gift: InnerPair pre-targeted rotate to rotate arguments of function

--- lib/operator-transform-string.coffee
+++ lib/operator-transform-string.coffee
@@ -714,16 +714,26 @@
 class Rotate extends ChangeOrder
   @extend()
   @registerToSelectList()
+  backwards: false
   getNewList: (rows) ->
-    rows.unshift(rows.pop())
+    if @backwards
+      rows.push(rows.shift())
+    else
+      rows.unshift(rows.pop())
     rows

 class RotateBackwards extends ChangeOrder
   @extend()
   @registerToSelectList()
-  getNewList: (rows) ->
-    rows.push(rows.shift())
-    rows
+  backwards: true
+
+class RotateArgumentsOfInnerPair extends Rotate
+  @extend()
+  target: "InnerAnyPair"
+
+class RotateArgumentsBackwardsOfInnerPair extends RotateArgumentsOfInnerPair
+  @extend()
+  backwards: true

 class Sort extends ChangeOrder
   @extend()


Resolve variable collision

Resolve variable collision

--- src/bootstrap.coffee
+++ src/bootstrap.coffee
@@ -11,7 +11,7 @@

 module.exports = ->
 	# Load config file
-	config = fs.readFileAsync('/boot/config.json', 'utf8').then(JSON.parse)
+	userConfig = fs.readFileAsync('/boot/config.json', 'utf8').then(JSON.parse)

 	version = utils.getSupervisorVersion()

@@ -37,23 +37,23 @@
 		division: ''
 	)

-	Promise.all([config, keys, version])
-	.then ([config, keys, version]) ->
+	Promise.all([userConfig, keys, version])
+	.then ([userConfig, keys, version]) ->
 		console.log('UUID:', uuid)
-		console.log('User ID:', config.userId)
-		console.log('User:', config.username)
+		console.log('User ID:', userConfig.userId)
+		console.log('User:', userConfig.username)
 		console.log('Supervisor Version:', version)
-		console.log('API key:', config.apiKey)
-		console.log('Application ID:', config.applicationId)
+		console.log('API key:', userConfig.apiKey)
+		console.log('Application ID:', userConfig.applicationId)
 		console.log('CSR :', keys.csr)
 		console.log('Posting to the API..')
-		config.csr = keys.csr
-		config.uuid = uuid
-		config.version = version
+		userConfig.csr = keys.csr
+		userConfig.uuid = uuid
+		userConfig.version = version
 		return request(
 			method: 'POST'
 			url: url.resolve(config.apiEndpoint, 'associate')
-			json: config
+			json: userConfig
 		)
 	.spread (response, body) ->
 		if response.statusCode >= 400
@@ -74,13 +74,13 @@
 		Promise.all([
 			knex('config').truncate()
 			.then ->
-				config
-				.then (config) ->
+				userConfig
+				.then (userConfig) ->
 					knex('config').insert([
 						{key: 'uuid', value: uuid}
-						{key: 'apiKey', value: config.apiKey}
-						{key: 'username', value: config.username}
-						{key: 'userId', value: config.userId}
+						{key: 'apiKey', value: userConfig.apiKey}
+						{key: 'username', value: userConfig.username}
+						{key: 'userId', value: userConfig.userId}
 						{key: 'version', value: version}
 					])
 			knex('app').truncate()


bulk-load: Use double-growth for colmetadata buffer.

--- src/bulk-load.coffee
+++ src/bulk-load.coffee
@@ -123,7 +123,7 @@
     return payload

   getColMetaData: () ->
-    tBuf = new WritableTrackingBuffer(100) # todo: take a good guess at a correct buffer size
+    tBuf = new WritableTrackingBuffer(100, null, true)
     # TokenType
     tBuf.writeUInt8(TOKEN_TYPE.COLMETADATA)
     # Count


fix title for paths that end in /

fix title for paths that end in /

--- HTML/osx.coffee
+++ HTML/osx.coffee
@@ -47,7 +47,7 @@

   openURL: (url) ->
     window.location = url
-    Chrome.title _.last url.split '/'
+    Chrome.title _.last url.replace(/\/$/,'').split '/'

   # name - Command name, like "Find in file"
   # shortcut - String command name, e.g.


Create Junction point

Create Junction point

--- src/cmd/setup.coffee
+++ src/cmd/setup.coffee
@@ -1,4 +1,5 @@
 fs = require '../sys/fs'
+sh = require '../sys/sh'
 echo = require '../sys/echo'
 mkpath = require '../tools/mkpath'
 junction = require '../tools/junction'
@@ -19,6 +20,9 @@
   echo "Installing to <#{dst = mkpath.dst()}>..."
   fs.CopyFile WScript.ScriptName,
     fs.BuildPath dst, PACKAGE.name + '.js'
-  fs.CopyFile junction.src(), junction.dst()
+  fs.CopyFile junction.src(), j = junction.dst()
   bat 'system'
+  sh.exec """
+    "#{j}" "#{fs.BuildPath dst, 'current'}"  "#{fs.BuildPath dst, 'system'}"
+"""
   path.install()


Upload debug build

Upload debug build

--- src/server/controllers/upload/post.iced
+++ src/server/controllers/upload/post.iced
@@ -28,16 +28,22 @@
 	"#{hash}#{extension}"

 module.exports = (req, res) ->
+	console.log "upload started"
+	console.log envi.azure
 	blobService = azure.createBlobService()
+	console.log "blob service created", blobService
 	form = new multiparty.Form()

 	form.on "part", (part) ->
 		if part.filename
+			console.log "part", part
 			filename = [ uploadPath, _randomizeFilename part.filename ].join "/"
 			size = part.byteCount
+			console.log filename, size

 			await blobService.createBlockBlobFromStream envi.azure.container, filename, part, size, defer err
 			if err
+				console.log "blob error", err
 				return res.status(400).send message: err.message

 			uploadModel = new db.uploadModel
@@ -47,15 +53,19 @@

 			await uploadModel.save defer err
 			if err
+				console.log "uploadModel save error"
 				return res.status(400).send message: err.message

 			return res.status(201).send uploadModel

 		else
+			console.log "filename is not set"
 			return res.status(400).send message: "No filename provided"

 	form.on "error", (err) ->
+		console.log "form.error", err
 		return res.status(400).send message: err.message

 	form.parse req
+	console.log "form.parse called"


Remove ability to force the ImpacWidgetsSvc.load method

--- src/services/widgets/widgets.svc.coffee
+++ src/services/widgets/widgets.svc.coffee
@@ -21,9 +21,9 @@
     # ====================================
     # Load and initialize
     # ====================================
-    @load = (force=false) ->
-      if !_self.getSsoSessionId()? || force
-        $q.all([ImpacMainSvc.loadUserData(force), ImpacDashboardsSvc.load(force)])
+    @load = ->
+      unless _self.getSsoSessionId()?
+        $q.all([ImpacMainSvc.loadUserData(), ImpacDashboardsSvc.load()])
       else
         $q.resolve()


chore(grunt): Cleaned up protractor task

--- misc/grunt/tasks.coffee
+++ misc/grunt/tasks.coffee
@@ -163,10 +163,13 @@
     browser          = grunt.option "browser"

     args = ["node_modules/protractor/bin/protractor", @data]
+
     args.push "--sauceUser=#{sauceUser}" if sauceUser
     args.push "--sauceKey=#{sauceKey}" if sauceKey
+
     if tunnelIdentifier
       args.push "--capabilities.tunnel-identifier=#{tunnelIdentifier}"
+
     if sauceBuild
       args.push "--capabilities.build=TRAVIS ##{sauceBuild}"


add newline at end of file

add newline at end of file

--- tests/mycha-test-helper.coffee
+++ tests/mycha-test-helper.coffee
@@ -8,4 +8,4 @@

 global.app_path = '../../app'

-process.env.NODE_ENV = 'test'+process.env.NODE_ENV = 'test'


remove highrise tool tests

remove highrise tool tests

--- test/integration/home.coffee
+++ test/integration/home.coffee
@@ -37,35 +37,3 @@
     xit 'shows the "Code your own Dataset" tool', ->
       tools = browser.queryAll '.dataset-tools .tool'
       $(tools).text().toLowerCase().should.include 'code your own dataset'
-
-    context 'when I click on the highrise tool', ->
-      before (done) ->
-        link = browser.query('a[href="/tool/highrise"].tool')
-        browser.fire 'click', link, done
-
-      it 'takes me to the highrise tool page', ->
-        result = browser.location.href
-        result.should.equal "#{url}/tool/highrise"
-
-      it 'displays the setup message of the tool', (done) ->
-        browser.wait ->
-          browser.text().should.include "Enter your username and password"
-          done()
-
-
-      context 'when I enter my details and click import', ->
-        before (done) ->
-          browser.fill '#username', process.env.HIGHRISE_USER
-          browser.fill '#password', process.env.HIGHRISE_PASSWORD
-          browser.fill '#domain', process.env.HIGHRISE_DOMAIN
-          browser.pressButton  '#import', ->
-            browser.wait done
-
-        it 'shows a lovely spreadsheet of our amazing data', ->
-          browser.location.href.should.include "#{url}/dataset/"
-
-        it 'has made a JSON cookie', ->
-          result = browser.evaluate "function(){return $.cookie('datasets')}"
-          parsed = JSON.parse result
-          should.exist parsed
-


more resilient view.coffee test

more resilient view.coffee test

--- test/integration/view.coffee
+++ test/integration/view.coffee
@@ -16,15 +16,15 @@

   context 'when I click on a Prune dataset then the graph of prunes view', ->
     before (done) ->
-      browser.elementByPartialLinkText 'Prune', (err, link) ->
+      wd40.elementByPartialLinkText 'Prune', (err, link) ->
         link.click done

     before (done) ->
-      wd40.click '#toolbar .tool[data-toolname="prune-graph"] .tool-icon', done
+      wd40.waitForVisibleByCss '#toolbar .tool[data-toolname="prune-graph"] .tool-icon', ->
+        wd40.click '#toolbar .tool[data-toolname="prune-graph"] .tool-icon', done

     it 'takes me to the Graph of Prunes page', (done) ->
-      wd40.trueURL (err, result) ->
-        result.should.match /\/view\/(\w+)/
+      wd40.waitForMatchingURL /\/view\/(\w+)/, (err, url) ->
         done()

     it 'there is a custom-named "Data Scientist\'s Report" tool', (done) ->
@@ -34,7 +34,7 @@

     context 'when I click the "hide" link on the "Code a prune" tool', ->
       before (done) ->
-          browser.elementByCss '#toolbar .tool[data-toolname="prune-graph"]', (err, el) ->
+          wd40.elementByCss '#toolbar .tool[data-toolname="prune-graph"]', (err, el) ->
             el.elementByCss '.dropdown-toggle', (err, optionsToggle) ->
               optionsToggle.click (err) ->
                  wd40.click '#tool-options-menu .hide-tool', done
@@ -43,6 +43,8 @@
         wd40.waitForMatchingURL /dataset[/]\w+([/]settings)?[/]?$/, done

       it 'And the "Code a prune" tool is no longer visible', (done) ->
+        # we use browser, and not wd40, here because wd40
+        # would timeout waiting for the element to appear
         browser.elementByCss '#toolbar .tool[data-toolname="prune-graph"]', (err, el) ->
           should.not.exist el
           done()


payment: update stripe form validators

payment: update stripe form validators

--- client/Main/payment/stripeformview.coffee
+++ client/Main/payment/stripeformview.coffee
@@ -12,6 +12,7 @@
       input.unsetTooltip()
       input.validationNotifications = {}
       input.clearValidationFeedback()
+      return no

     notificationOptions = { type: 'tooltip', placement: 'top' }

@@ -30,27 +31,25 @@
         rules        :
           clear      : clearValidationErrors

-          checkCC    : (input, event) ->
+          checkCC    : KD.utils.debounce 100, (input, event) =>
             val = $.trim input.getValue().replace(/-|\s/g,"")
-            result = Stripe.card.validateCardNumber val
-            result = if result
-            then no
-            else 'Card number is not valid'
+            returnResult = result = Stripe.card.validateCardNumber val
+            result = if result then no else 'Card number is not valid'
             input.setValidationResult 'checkCC', result
-
+            return returnResult
           cardType   : do =>
             cssClass = null
             return (input, event) =>
               @unsetClass cssClass  if cssClass
               val = $.trim input.getValue().replace(/-|\s/g,"")
-              cssClass = (Stripe.card.cardType val).toLowerCase()
+              cssClass = Stripe.card.cardType(val).toLowerCase()
               cssClass = KD.utils.slugify cssClass
               @setClass cssClass

         events       :
           cardType   : 'keyup'
-          checkCC    : 'blur'
-          clear      : 'focus'
+          checkCC    : ['keyup', 'blur']
+          clear      : ['keydown']
     }

     fields.cardCVC = {
@@ -60,16 +59,13 @@
         rules         :
           clear       : clearValidationErrors
           checkCVC    : (input, event) ->
-            val    = $.trim input.getValue().replace(/-|\s/g, '')
-            result = Stripe.card.validateCVC val
-            result = if result
-            then no
-            else 'CVC is not valid'
+            val = $.trim input.getValue().replace(/-|\s/g, '')
+            returnResult = result = Stripe.card.validateCVC val
+            result = if result then no else 'CVC is not valid'
             input.setValidationResult 'checkCVC', result
-
         events        :
-          clear       : 'focus'
-          checkCVC    : 'blur'
+          clear       : ['keydown']
+          checkCVC    : ['blur', 'keyup']
     }

     fields.cardName = {
@@ -82,8 +78,8 @@
           clear       : clearValidationErrors
           required    : yes
         events        :
-          required    : 'blur'
-          clear       : 'focus'
+          clear       : ['keydown']
+          required    : ['blur', 'keyup']
     }

     fields.cardMonth = {
@@ -93,21 +89,20 @@
         maxlength     : 2
       validate        :
         notifications : notificationOptions
-        event         : 'blur'
         rules         :
           clear       : clearValidationErrors
           checkMonth  : (input, event) ->
             val = $.trim input.getValue().replace(/-|\s/g, '')
             # just to check for month, we are setting
             # a happy value to the year.
-            result = Stripe.card.validateExpiry val, 2015
-            result = if result
-            then no
-            else 'Invalid month!'
+            happyYear = (new Date).getFullYear() + 1
+            returnResult = result = Stripe.card.validateExpiry val, happyYear
+            result = if result then no else 'Invalid month!'
             input.setValidationResult 'checkMonth', result
+            return returnResult
         events       :
-          checkMonth : 'blur'
-          clear      : 'focus'
+          clear      : ['keydown']
+          checkMonth : ['blur', 'keyup']
     }

     fields.cardYear = {
@@ -122,15 +117,14 @@
           checkYear      : (yearInput, event) =>
             yearVal    = $.trim yearInput.getValue().replace(/-|\s/g, '')
             validMonth = (new Date).getMonth() + 1
-            result = Stripe.card.validateExpiry validMonth, yearVal
-            result = if result
-            then no
-            else 'Invalid year!'
+            returnResult = result = Stripe.card.validateExpiry validMonth, yearVal
+            result = if result then no else 'Invalid year!'
             yearInput.setValidationResult 'checkYear', result
+            return returnResult

         events           :
-          checkYear      : 'blur'
-          clear          : 'focus'
+          checkYear      : ['keyup', 'blur']
+          clear          : ['keydown']
     }

     { planTitle, planInterval, currentPlan


use default https node agent

use default https node agent

--- src/prefixer.coffee
+++ src/prefixer.coffee
@@ -1,9 +1,13 @@
-Agent = require('agentkeepalive').HttpsAgent
-keepaliveAgent = new Agent
-	maxSockets: 500,
-	maxFreeSockets: 250,
-	timeout: 600000,
-	keepAliveTimeout: 600000
+#Agent = require('agentkeepalive').HttpsAgent
+#keepAliveAgent = new Agent
+#	maxSockets: 500,
+#	maxFreeSockets: 250,
+#	timeout: 600000,
+#	keepAliveTimeout: 600000
+
+https = require('https');
+keepAliveAgent = new https.Agent({ keepAlive: true, maxSockets: 500 });
+

 module.exports = (request)->
 	request.url = 'https://prod-api01.clanofthecloud.mobi'+request.url
@@ -13,4 +17,4 @@
 	#request.url = 'http://195.154.227.44:8000'+request.url

 	# not required in browsers
-	request.agent keepaliveAgent+	request.agent keepAliveAgent


Leverage esper for more friendly error messages.

--- src/problems.coffee
+++ src/problems.coffee
@@ -274,10 +274,19 @@
       when 'python' then 'self'
       when 'cofeescript' then '@'
       else 'this'
-    @thisValueAccess = switch languageID
+    @realThisValueAccess = switch languageID
       when 'python' then 'self.'
       when 'cofeescript' then '@'
       else 'this.'
+
+    # We use `hero` as `this` in CodeCombat now, so all `this` related hints
+    # we get in the problem context should really refrence `hero`
+    @thisValueAccess = switch languageID
+      when 'python' then 'hero.'
+      when 'cofeescript' then 'hero.'
+      when 'lua' then 'hero:'
+      else 'hero.'
+
     @newVariableTemplate = switch languageID
       when 'javascript' then _.template('var <%= name %> = ')
       else _.template('<%= name %> = ')
@@ -285,12 +294,32 @@
       when 'python' then new RegExp "self\\.(\\w+)\\s*\\("
       when 'cofeescript' then new RegExp "@(\\w+)\\s*\\("
       else new RegExp "this\\.(\\w+)\\("
+
     @context = context ? {}

   getHint: (code, {message, range, error, aether}) ->
     return unless @context?
     if error.code is 'UndefinedVariable' and error.when is 'write' and aether.language.id is 'javascript'
       return "Missing `var`. Use `var #{error.ident} =` to make a new variable."
+
+    if error.code is "CallNonFunction"
+      ast = error.targetAst
+      if ast.type is "MemberExpression" and not ast.computed
+        extra = ""
+        target = ast.property.name
+        if error.candidates?
+          candidatesLow = (s.toLowerCase() for s in error.candidates)
+          idx = candidatesLow.indexOf(target.toLowerCase())
+          if idx isnt -1
+            newName = error.targetName.replace target, error.candidates[idx]
+            return "Look out for capitalization: `#{error.targetName}` should be `#{newName}`."
+          sm = @getScoreMatch target, [{candidates: error.candidates, msgFormatFn: (match) -> match}]
+          if sm?
+            newName = error.targetName.replace target, sm
+            return "Look out for spelling issues: did you mean `#{newName}` instead of `#{error.targetName}`?"
+
+        return "`#{ast.object.srcName}` has no method `#{ast.property.name}`."
+
     if (missingMethodMatch = message.match(/has no method '(.*?)'/)) or message.match(/is not a function/) or message.match(/has no method/)
       # NOTE: We only get this for valid thisValue and parens: self.blahblah()
       # NOTE: We get different error messages for this based on javascript engine:


ACK packetsent

ACK packetsent

--- src/protocol.coffee
+++ src/protocol.coffee
@@ -70,7 +70,12 @@
   else if sub == 'runtime' and cmd == 'packet'
     proto.coordinator.sendToExportedPort payload.port, payload.payload, (err) ->
       return proto.transport.send 'runtime', 'error', serializeErr(err), ctx if err
-      # No ACK in this case apparently, as it is interpreted as output
+      proto.transport.send 'runtime', 'packetsent',
+        port: payload.port
+        event: payload.event
+        graph: payload.graph
+        payload: payload.payload
+      , ctx

   # Component
   else if sub == 'component' and cmd == 'list'


refactor

refactor

--- client/app/CommonViews/DNDUploader.coffee
+++ client/app/CommonViews/DNDUploader.coffee
@@ -9,6 +9,7 @@
     options.defaultPath or= "/home/#{KD.nick()}/Uploads"

     super options, data
+
     @reset()

     @setPath options.path  if options.path
@@ -20,12 +21,10 @@
       @on "drop",      => @unsetClass "hover"

     @on "uploadFile", (fsFile, percent)=>
-      finder = KD.getSingleton "finderController"
       filePath  = "[#{fsFile.vmName}]#{fsFile.path}"
-      finder.treeController.nodes[filePath]?.updateProgressView percent
+      @finder.treeController.nodes[filePath]?.updateProgressView percent

     @on "uploadStart", (fsFile)=>
-      finder = KD.getSingleton "finderController"
       filePath   = "[#{fsFile.vmName}]#{fsFile.path}"
       parentPath = "[#{fsFile.vmName}]#{fsFile.parentPath}"

@@ -34,18 +33,24 @@
         nodeView.once "abortProgress", => fsFile.abort yes

       fsFile._isBeingUploaded = yes
-      nodeView = finder.treeController.nodes[filePath]
+      nodeView = @finder.treeController.nodes[filePath]
       if nodeView
       then bindAbort()
       else
-        finder.treeController.on "NodeWasAdded", (nodeView)->
+        @finder.treeController.on "NodeWasAdded", (nodeView)->
           if nodeView.getData().path is filePath and fsFile._isBeingUploaded
             bindAbort()

-        fsFile.save "", -> finder.revealPath parentPath
+        fsFile.save "", -> @finder.revealPath parentPath

     @on "uploadEnd", (fsFile)->
       fsFile._isBeingUploaded = no
+
+  viewAppended: ->
+    super
+    @finder = KD.getSingleton "finderController"
+    tc      = @finder.treeController
+    @notify = tc.notify.bind tc

   reset: ->
     {uploadToVM, defaultPath, title} = @getOptions()
@@ -65,13 +70,10 @@
       for item in items
         itemEntry = item.webkitGetAsEntry?()
         if itemEntry.isDirectory
-          return new KDNotificationView
-            title: "Folder uploads not available for now."
+          return @notify "Folder uploads not available for now.", "error"

-    finder = KD.getSingleton "finderController"
-    {notify} = finder.treeController
     if files.length >= 20
-      notify """
+      @notify """
       Too many files to transfer!<br>
       Archive your files and try again.
       """, "error", """
@@ -84,7 +86,7 @@
       for file, index in files
         sizeInMb = file.size/1024/1024
         if sizeInMb > 100 && @getOptions().uploadToVM
-          notify "Too big file to upload.", "error", "Max 100MB allowed per file."
+          @notify "Too big file to upload.", "error", "Max 100MB allowed per file."
           continue
         reader = new FileReader
         reader.onloadend = do (file=files[index])=> (readEvent)=>
@@ -129,8 +131,7 @@
       </div>
     """
     if uploadToVM
-      finder = KD.getSingleton "finderController"
-      finder?.revealPath @path
+      @finder?.revealPath @path

     @addSubView new KDCustomHTMLView
       tagName   : "a"


ChangeTeam modal: coffee lint fix

ChangeTeam modal: coffee lint fix

--- client/app/lib/changeteam/itemview.coffee
+++ client/app/lib/changeteam/itemview.coffee
@@ -56,10 +56,10 @@

   pistachio: ->

-    """
+    '''
       <div class="team-title-wrapper">
         {{> @logoWrapper}}
         {span.team-title{#(title)}}
       </div>
       {{> @actionElement }}
-    """
+    '''


Collaboration: Added loading to settings pane for user list.

--- client/ide/views/chat/settingspane.coffee
+++ client/ide/views/chat/settingspane.coffee
@@ -61,7 +61,16 @@

     @everyone = new KDCustomHTMLView
       tagName  : 'ul'
-      cssClass : 'settings everyone'
+      cssClass : 'settings everyone loading'
+
+    @everyone.addSubView new KDLoaderView
+      showLoader : yes
+      size       :
+        width    : 24
+
+    @everyone.addSubView new KDCustomHTMLView
+      cssClass : 'label'
+      partial  : 'Fetching participants...'

   initiateSession: ->


login app : if login made via invitation , show notification on tray.

--- client/landing/Login/AppController.coffee
+++ client/landing/Login/AppController.coffee
@@ -41,7 +41,11 @@
             if err
               KD.notify_ err.message or err
               return window.location.href = "/"
-            KD.notify_ 'Success!'
+
+            new KDNotificationView
+              title : 'Success!'
+              type  : 'tray'
+
             KD.getSingleton('router').handleRoute "/#{group.slug}"
             KD.getSingleton('mainController').accountChanged KD.whoami()
       else


Convert lexer to functional style.

Convert lexer to functional style.

Uses CoffeeScript classes for convenience.

--- js/inequality.coffee
+++ js/inequality.coffee
@@ -6,52 +6,63 @@
 bad  = ["< 5", "horse", "x + y + z < 0", "x = 0"]

 # Normal form of the inequality is x + y + c < 0
-inequality = (x, y, c) -> {x: x, y: y, c: c}
+class Inequality
+  constructor: (@x, @y, @c) ->

-lex = (input) ->
-  tokens = []
-  while input.length > 0
-    # Extract previous token type (if any)
-    token = tokens.slice(-1)[0]
-    state = token[0] if token?
+  toString: -> "#{@x}x + #{@y}y + #{@c} < 0"

-    # Ignore whitespace
-    result = /^\s+/.exec input
+# Given a string and a RegExp.exec result, return the unmatched remainder
+# of the string
+consume = (input, result) -> input.slice(result.index + result[0].length)
+
+# Tokens have type and value
+class Token
+  constructor: (@type, @value) ->
+
+  toString: -> "[#{@type} '#{@value}']"
+
+lex = (input, tokens=[]) ->
+  if input.length == 0
+    return tokens
+
+  # Extract previous token type (if any)
+  previous = tokens.slice(-1)[0]
+  state = previous.type if previous?
+
+  # Ignore whitespace
+  result = /^\s+/.exec input
+  if result
+    return lex consume(input, result), tokens
+
+  # Operators must follow coefficients or variables
+  if state == "COEFFICIENT" or state == "VARIABLE"
+    result = /^[<>+-]/.exec input
     if result
-      input = input.slice(result.index + result[0].length)
-      continue
+      token = new Token "OPERATOR", result[0]
+      return lex consume(input, result), tokens.concat(token)

-    # Operators must follow coefficients or variables
-    if state == "COEFFICIENT" or state == "VARIABLE"
-      result = /^[<>+-]/.exec input
-      if result
-        input = input.slice(result.index + result[0].length)
-        tokens.push ["OPERATOR", result[0]]
-        continue
+  # Coefficients can exist at the start of an expression, or after an operator
+  if not state or state == "OPERATOR"
+    result = /^(-?[0-9]+(\.[0-9]+)?)|(-)/.exec input
+    if result
+      token = new Token "COEFFICIENT", result[0]
+      return lex consume(input, result), tokens.concat(token)

-    # Coefficients can exist at the start of an expression, or after an operator
-    if not state or state == "OPERATOR"
-      result = /^(-?[0-9]+(\.[0-9]+)?)|(-)/.exec input
-      if result
-        input = input.slice(result.index + result[0].length)
-        tokens.push ["COEFFICIENT", result[0]]
-        continue
+  # Variables can come at any point except after a variable
+  if state != "VARIABLE"
+    result = /^[xyXY]/.exec input
+    if result
+      token = new Token "VARIABLE", result[0]
+      return lex consume(input, result), tokens.concat(token)

-    # Variables can come at any point except after a variable
-    if state != "VARIABLE"
-      result = /^[xyXY]/.exec input
-      if result
-        input = input.slice(result.index + result[0].length)
-        tokens.push ["VARIABLE", result[0]]
-        continue
+  throw "Parse error at '#{input}' (tokens: #{tokens}, last token: #{previous})"

-    throw "Parse error at '#{input}' (tokens: #{tokens}, state: #{state})"
+console.log "Good inputs:\n"
+for i in good
+  console.log i
+  console.log lex(i).join(" -> ")

-  tokens
-
-for i in good
-  console.log i, lex(i)
-
+console.log "\nBad inputs:\n"
 for i in bad
   try
     tokens = lex(i)


css tweak

css tweak

--- src/renderer.coffee
+++ src/renderer.coffee
@@ -15,6 +15,7 @@
       'tab-size': '4'
       'white-space': 'pre-wrap'
     }
+    '.line': { 'min-height': '15px' }
     'body' : { 'margin': '0px', 'padding': '0px' }
     'a'    : { 'cursor': 'pointer', 'text-decoration': 'underline' }
     'b'    : { 'font-weight': 'bold' }
@@ -74,7 +75,7 @@
       'top': '-31px'
     }
     '.cursor-inner': { 'display': 'inline-block', 'width': '2px', 'position': 'relative', 'height': '15px', 'left': '-1px', 'top': '-31px' }
-
+    '.editor > .line:first-child .cursor-name': { 'border-top-left-radius': '0px', 'border-bottom-left-radius': '3px', 'top': '2px' }

   @objToCss: (obj) ->
     return _.map(obj, (value, key) ->


test: fix config unit tests

test: fix config unit tests

This should have been part of 8454898c5e2b56cb81f0c808153b5f82cfac62a4

--- test/unit/config.spec.coffee
+++ test/unit/config.spec.coffee
@@ -189,7 +189,7 @@

     it 'should set defaults with coffeescript', ->
       config = e.parseConfig '/conf/coffee.coffee', {}
-      expect(config.autoWatch).to.equal false
+      expect(config.autoWatch).to.equal true

     it 'should not read config file, when null', ->