Skip to content

Commit

Permalink
Merge pull request #96 from aloof-ruf/Issue-85-Create-Required-Attrib…
Browse files Browse the repository at this point in the history
…utes-On-Update

Issue 85 create required attributes on update
  • Loading branch information
brandongoode committed Jan 17, 2017
2 parents 72c2c67 + 1b30fea commit b14e418
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 73 deletions.
21 changes: 14 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,6 @@ Scans a table. If callback is not provided, then a Scan object is returned. See
Updates and existing item in the table. Three types of updates: $PUT, $ADD, and $DELETE. Refer to DynamoDB's updateItem documentation for details on how PUT, ADD, and DELETE work.
##### Options
**allowEmptyArray**: boolean
If true, the attribute can be updated to an empty array. If falsey, empty arrays will remove the attribute. Defaults to false.
**$PUT**
Put is the default behavior. The two example below are identical.
Expand Down Expand Up @@ -527,6 +520,20 @@ Dog.update({ownerId: 4, name: 'Odie'}, {$DELETE: {age: null}}, function (err) {
})
```
##### Options
**allowEmptyArray**: boolean
If true, the attribute can be updated to an empty array. If falsey, empty arrays will remove the attribute. Defaults to false.
**createRequired**: boolean
If true, required attributes will be filled with their default values on update (regardless of you specifying them for the update). Defaults to false.
**updateTimestamps**: boolean
If true, the `timestamps` attributes will be updated. Will not do anything if timestamps attribute were not specified. Defaults to true.
### Query
#### Model.query(query, options, callback)
Expand Down
2 changes: 1 addition & 1 deletion lib/Attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function Attribute(schema, name, value) {

this.attributes = {};

if ( this.type.name === 'map'){
if (this.type.name === 'map'){

if(value.type) {
value = value.map;
Expand Down
272 changes: 210 additions & 62 deletions lib/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ Model.get = function(NewModel, key, options, next) {
debug('Error returned by getItem', err);
return deferred.reject(err);
}
// console.log('RESP',JSON.stringify(data, null, 4));

debug('getItem response', data);

if(!Object.keys(data).length) {
Expand Down Expand Up @@ -400,17 +400,50 @@ Model.get = function(NewModel, key, options, next) {
Model.update = function(NewModel, key, update, options, next) {
debug('Update %j', key);
var deferred = Q.defer();
if(key === null || key === undefined) {
deferred.reject(new errors.ModelError('Key required to get item'));
return deferred.promise.nodeify(next);
}
var schema = NewModel.$__.schema;

options = options || {};
if(typeof options === 'function') {
next = options;
options = {};
}

var schema = NewModel.$__.schema;
// default createRequired to false
if (typeof options.createRequired === 'undefined') {
options.createRequired = false;
}

// default updateTimestamps to true
if (typeof options.updateTimestamps === 'undefined') {
options.updateTimestamps = true;
}

// if the key part was emtpy, try the key defaults before giving up...
if (key === null || key === undefined) {
key = {};

// first figure out the primary/hash key
var hashKeyDefault = schema.attributes[schema.hashKey.name].options.default;

if (typeof hashKeyDefault === 'undefined') {
deferred.reject(new errors.ModelError('Key required to get item'));
return deferred.promise.nodeify(next);
}

key[schema.hashKey.name] = typeof hashKeyDefault === 'function' ? hashKeyDefault() : hashKeyDefault;

// now see if you have to figure out a range key
if (schema.rangeKey) {
var rangeKeyDefault = schema.attributes[schema.rangeKey.name].options.default;

if (typeof rangeKeyDefault === 'undefined') {
deferred.reject(new errors.ModelError('Range key required: ' + schema.rangeKey.name));
return deferred.promise.nodeify(next);
}

key[schema.rangeKey.name] = typeof rangeKeyDefault === 'function' ? rangeKeyDefault() : rangeKeyDefault;
}
}

var hashKeyName = schema.hashKey.name;
if(!key[hashKeyName]) {
Expand All @@ -419,12 +452,6 @@ Model.update = function(NewModel, key, update, options, next) {
key[hashKeyName] = keyVal;
}

if(schema.rangeKey && !key[schema.rangeKey.name]) {
deferred.reject(new errors.ModelError('Range key required: ' + schema.rangeKey.name));
return deferred.promise.nodeify(next);
}


var updateReq = {
TableName: NewModel.$__.name,
Key: {},
Expand All @@ -441,25 +468,116 @@ Model.update = function(NewModel, key, update, options, next) {
updateReq.Key[rangeKeyName] = schema.rangeKey.toDynamo(key[rangeKeyName]);
}

// update the 'updatedAt' timestamp if requested
if (schema.timestamps) {
if (!update.$PUT) {
update.$PUT = {};
}
update.$PUT[schema.timestamps.updatedAt] = update.$PUT[schema.timestamps.updatedAt] || Date.now();
// determine the set of operations to be executed
function Operations() {
this.ifNotExistsSet = {};
this.SET = {};
this.ADD = {};
this.REMOVE = {};

this.addIfNotExistsSet = function(name, item) {
this.ifNotExistsSet[name] = item;
};

this.addSet = function(name, item) {
this.SET[name] = item;
};

this.addAdd = function(name, item) {
this.ADD[name] = item;
};

this.addRemove = function(name, item) {
this.REMOVE[name] = item;
};

this.getUpdateExpression = function(updateReq) {
var attrCount = 0;
var updateExpression = '';

var attrName;
var valName;
var name;
var item;

var setExpressions = [];
for (name in this.ifNotExistsSet) {
item = this.ifNotExistsSet[name];

attrName = '#_n' + attrCount;
valName = ':_p' + attrCount;

updateReq.ExpressionAttributeNames[attrName] = name;
updateReq.ExpressionAttributeValues[valName] = item;

setExpressions.push(attrName + ' = if_not_exists(' + attrName + ', ' + valName + ')');

attrCount += 1;
}

for (name in this.SET) {
item = this.SET[name];

attrName = '#_n' + attrCount;
valName = ':_p' + attrCount;

updateReq.ExpressionAttributeNames[attrName] = name;
updateReq.ExpressionAttributeValues[valName] = item;

setExpressions.push(attrName + ' = ' + valName);

attrCount += 1;
}
if (setExpressions.length > 0) {
updateExpression += 'SET ' + setExpressions.join(',') + ' ';
}

var addExpressions = [];
for (name in this.ADD) {
item = this.ADD[name];

attrName = '#_n' + attrCount;
valName = ':_p' + attrCount;

updateReq.ExpressionAttributeNames[attrName] = name;
updateReq.ExpressionAttributeValues[valName] = item;

addExpressions.push(attrName + ' ' + valName);

attrCount += 1;
}
if (addExpressions.length > 0) {
updateExpression += 'ADD ' + addExpressions.join(',') + ' ';
}

var removeExpressions = [];
for (name in this.REMOVE) {
item = this.REMOVE[name];

attrName = '#_n' + attrCount;

updateReq.ExpressionAttributeNames[attrName] = name;

removeExpressions.push(attrName);

attrCount += 1;
}
if (removeExpressions.length > 0) {
updateExpression += 'REMOVE ' + removeExpressions.join(',');
}

updateReq.UpdateExpression = updateExpression;
};
}

// determine the set of operations to be executed
var operations = {
SET: {},
ADD: {},
REMOVE: {}
};
if(update.$PUT || (!update.$PUT && !update.$DELETE && !update.$ADD)) {
var operations = new Operations();

if (update.$PUT || (!update.$PUT && !update.$DELETE && !update.$ADD)) {
var updatePUT = update.$PUT || update;
for(var putItem in updatePUT) {

for (var putItem in updatePUT) {
var putAttr = schema.attributes[putItem];
if(putAttr) {
if (putAttr) {
var val = updatePUT[putItem];

var removeParams = val === null || val === undefined || val === '';
Expand All @@ -468,10 +586,15 @@ Model.update = function(NewModel, key, update, options, next) {
removeParams = removeParams || (Array.isArray(val) && val.length === 0);
}

if(removeParams) {
operations.REMOVE[putItem] = null;
if (removeParams) {
operations.addRemove(putItem, null);
} else {
operations.SET[putItem] = putAttr.toDynamo(val);
try {
operations.addSet(putItem, putAttr.toDynamo(val));
} catch (err) {
deferred.reject(err);
return deferred.promise.nodeify(next);
}
}
}
}
Expand All @@ -483,9 +606,14 @@ Model.update = function(NewModel, key, update, options, next) {
if(deleteAttr) {
var delVal = update.$DELETE[deleteItem];
if(delVal !== null && delVal !== undefined) {
operations.REMOVE[deleteItem] = deleteAttr.toDynamo(delVal);
try {
operations.addRemove(deleteItem, deleteAttr.toDynamo(delVal));
} catch (err) {
deferred.reject(err);
return deferred.promise.nodeify(next);
}
} else {
operations.REMOVE[deleteItem] = null;
operations.addRemove(deleteItem, null);
}
}
}
Expand All @@ -495,45 +623,65 @@ Model.update = function(NewModel, key, update, options, next) {
for(var addItem in update.$ADD) {
var addAttr = schema.attributes[addItem];
if(addAttr) {
operations.ADD[addItem] = addAttr.toDynamo(update.$ADD[addItem]);
try {
operations.addAdd(addItem, addAttr.toDynamo(update.$ADD[addItem]));
} catch (err) {
deferred.reject(err);
return deferred.promise.nodeify(next);
}
}
}
}

// construct the update expression
//
// we have to use update expressions because we are supporting
// condition expressions, and you can't mix expressions with
// non-expressions
var attrCount = 0;
updateReq.UpdateExpression = '';
var first, k;
for(var op in operations) {
if(Object.keys(operations[op]).length) {
if (updateReq.UpdateExpression) {
updateReq.UpdateExpression += ' ';
}
updateReq.UpdateExpression += op + ' ';
first = true;
for(k in operations[op]) {
if(first) {
first = false;
} else {
updateReq.UpdateExpression += ',';
// update schema timestamps
if (options.updateTimestamps && schema.timestamps) {
var createdAtLabel = schema.timestamps.createdAt;
var updatedAtLabel = schema.timestamps.updatedAt;

var createdAtAttribute = schema.attributes[createdAtLabel];
var updatedAtAttribute = schema.attributes[updatedAtLabel];

var createdAtDefaultValue = createdAtAttribute.options.default();
var updatedAtDefaultValue = updatedAtAttribute.options.default();

operations.addIfNotExistsSet(createdAtLabel, createdAtAttribute.toDynamo(createdAtDefaultValue));
operations.addSet(updatedAtLabel, updatedAtAttribute.toDynamo(updatedAtDefaultValue));
}

// do the required items check. Throw an error if you have an item that is required and
// doesn't have a default.
if (options.createRequired) {
for (var attributeName in schema.attributes) {
var attribute = schema.attributes[attributeName];
if (attribute.required && // if the attribute is required...
attributeName !== schema.hashKey.name && // ...and it isn't the hash key...
(!schema.rangeKey || attributeName !== schema.rangeKey.name) && // ...and it isn't the range key...
(!schema.timestamps || attributeName !== schema.timestamps.createdAt) && // ...and it isn't the createdAt attribute...
(!schema.timestamps || attributeName !== schema.timestamps.updatedAt) && // ...and it isn't the updatedAt attribute...
!operations.SET[attributeName] &&
!operations.ADD[attributeName] &&
!operations.REMOVE[attributeName]) {

var defaultValueOrFunction = attribute.options.default;

// throw an error if you have required attribute without a default (and you didn't supply
// anything to update with)
if (typeof defaultValueOrFunction === 'undefined') {
var err = 'Required attribute "' + attributeName + '" does not have a default.';
debug('Error returned by updateItem', err);
deferred.reject(err);
return deferred.promise.nodeify(next);
}
var attrName = '#_n' + attrCount;
var valName = ':_p' + attrCount;
updateReq.UpdateExpression += attrName;
updateReq.ExpressionAttributeNames[attrName] = k;
if(operations[op][k]) {
updateReq.UpdateExpression += ' ' + (op === 'SET' ? '= ' : '') + valName;
updateReq.ExpressionAttributeValues[valName] = operations[op][k];
}
attrCount += 1;

var defaultValue = typeof defaultValueOrFunction === 'function' ? defaultValueOrFunction() : defaultValueOrFunction;

operations.addIfNotExistsSet(attributeName, attribute.toDynamo(defaultValue));
}
}
}

operations.getUpdateExpression(updateReq);

// AWS doesn't allow empty expressions or attribute collections
if(!updateReq.UpdateExpression) {
delete updateReq.UpdateExpression;
Expand Down
1 change: 0 additions & 1 deletion lib/Schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,4 @@ Schema.prototype.virtualpath = function (name) {
return this.virtuals[name];
};


module.exports = Schema;

0 comments on commit b14e418

Please sign in to comment.