Skip to content

Commit

Permalink
implemented Model.upsertWithWhere
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbatista committed Apr 21, 2017
1 parent d76810a commit de50ae6
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 1 deletion.
129 changes: 128 additions & 1 deletion src/arangodb.coffee
Expand Up @@ -595,7 +595,7 @@ class ArangoDBConnector extends Connector
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 operator for : ', condOp
console.warn 'No matching operator for: ', condOp
else
aqlArray.push qb.eq "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}"
return {
Expand All @@ -604,6 +604,68 @@ class ArangoDBConnector extends Connector
geoExpr: geoExpr
}

###
###
_buildWhereUpsert: (model, where, index) ->
# arangoDB support only literal object on upsert
literalObject = {}
bindVars = {}
# index for condition parameter binding
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++)
bindVars[partName] = value
return '@' + partName

idName = @idName model
fullIdName = @_fullIdName model
fromName = @_fromName model
toName = @_toName 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 where
do() =>
# support only 'and' operator
if condProp in ['and']
# 'and' has multiple conditions so we run buildWhere recursively on their array to
if Array.isArray condValue
# condValue is an array of conditions so get the conditions from it via a recursive buildWhere call
for c, a of condValue
cond = @_buildWhereUpsert model, a, ++index
literalObject = merge true, literalObject, cond.literalObject
bindVars = merge true, bindVars, cond.bindVars
return

# correct if the conditionProperty falsely references to 'id'
if condProp is idName
condProp = '_key'
if typeof condValue is 'number' then condValue = String(condValue)
if condProp is fullIdName
condProp = '_id'
if condProp is fromName
condProp = '_from'
if condProp is toName
condProp = '_to'

# 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]
if condOp in ['lte', 'lt', 'gte', 'gt', 'eq', 'neq', 'between', 'like', 'nlike', 'nin', 'inq', 'near']
console.warn 'upsert support only `and` operator skip: ', condOp
else
literalObject[condProp] = "#{assignNewQueryVariable(condValue)}"
return {
literalObject: literalObject
bindVars: bindVars
}

###
Find matching model instances by the filter
@param [String] model The model name
Expand Down Expand Up @@ -852,6 +914,71 @@ class ArangoDBConnector extends Connector
###
updateAll: @::update

###
Upsert all instances that matching where criteria
###
upsertWithWhere: (model, where, data, options, callback) ->
debug "upsertWithWhere for #{model} with where #{JSON.stringify where} and data #{JSON.stringify data}" if @debug

idValue = @getIdValue(model, data)
idName = @idName(model)
idValue = @getDefaultIdType() idValue if typeof idValue is 'number'
delete data[idName]

fullIdName = @_fullIdName model
if fullIdName then delete data[fullIdName]

isEdge = @_isEdge model
fromName = null
toName = null

if isEdge
fromName = @_fromName model
if fromName isnt '_from'
data._from = data[fromName]
delete data[fromName]
toName = @_toName model
if toName isnt '_to'
data._to = data[toName]
delete data[toName]

dataI = _.clone(data)

if idValue
dataI._key = idValue

bindVars =
'@collection': @getCollectionName model
data: data
dataI: dataI

where = @_buildWhereUpsert(model, where)
bindVars = merge true, bindVars, where.bindVars
where = where.literalObject

aql = qb.upsert(where).insert('@dataI').update('@data').in('@@collection').let('isNewInstance',
qb.ref('OLD').then(false).else(true)).return({doc: 'NEW', isNewInstance: 'isNewInstance'});

@execute model, 'query', aql, bindVars, (err, result) =>
if result and result._result[0]
newDoc = result._result[0].doc
# Delete revision
delete newDoc._rev
if fullIdName
data[fullIdName] = newDoc._id
if fullIdName isnt '_id' then delete newDoc._id
else
delete newDoc._id
if isEdge
if fromName isnt '_from' then data[fromName] = data._from
if toName isnt '_to' then data[toName] = data._to

isNewInstance = { isNewInstance: result._result[0].isNewInstance }
@setIdValue(model, data, newDoc._key)
@setIdValue(model, newDoc, newDoc._key)
if idName isnt '_key' then delete newDoc._key
callback err, newDoc, isNewInstance

###
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
Expand Down
73 changes: 73 additions & 0 deletions test/crud/document.test.coffee
Expand Up @@ -825,6 +825,79 @@ describe 'document', () ->
# should.not.exist(err)
# console.warn.calledOnce.should.be.ok;
# done()

describe 'upsertWithWhere', () ->
it 'simple where - new instance', (done) ->
Post.upsertWithWhere {title: 'My Post'}, {title: 'New Post'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('New Post')
done()

it 'simple where - update instance', (done) ->
Post.create {title: 'My Post'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('My Post')
Post.upsertWithWhere {title: 'My Post'}, {title: 'New Post'}, (err, p) ->
return done(err) if err
p.should.exist
p.id.exist
p.title.should.equal('New Post')
p.id.should.equal(post.id)
done()

it 'complex where - new instance', (done) ->
Post.upsertWithWhere {title: 'My Post', content: 'Hello'}, {title: 'New Post'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('New Post')
should.not.exist(post.content)
done()

it 'complex where - update instance', (done) ->
Post.create {title: 'My Post Created', content: 'Hello'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('My Post Created')
Post.upsertWithWhere {title: 'My Post Created', content: 'Hello'}, {title: 'New Post'}, (err, p) ->
return done(err) if err
p.should.exist
p.id.exist
p.title.should.equal('New Post')
p.content.should.equal('Hello')
p.id.should.equal(post.id)
done()


it 'complex where with `and` operator - new instance', (done) ->
Post.upsertWithWhere {and: [{title: 'My Post'}, {content: 'Hello'}]}, {title: 'New Post'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('New Post')
should.not.exist(post.content)
done()

it 'complex where with `and` operator - update instance', (done) ->
Post.create {title: 'My Post Created', content: 'Hello'}, (err, post) ->
return done(err) if err
post.should.exist
post.id.exist
post.title.should.equal('My Post Created')
post.content.should.equal('Hello')
Post.upsertWithWhere {and: [{title: 'My Post Created'}, {content: 'Hello'}]}, {title: 'New Post'}, (err, p) ->
return done(err) if err
p.should.exist
p.id.exist
p.title.should.equal('New Post')
p.content.should.equal('Hello')
p.id.should.equal(post.id)
done()

after (done) ->
User.destroyAll ->
Expand Down

0 comments on commit de50ae6

Please sign in to comment.