1 | var Big = require('big.js'),
|
2 | db = require('../db')
|
3 |
|
4 | module.exports = function updateItem(store, data, cb) {
|
5 |
|
6 | store.getTable(data.TableName, function(err, table) {
|
7 | if (err) return cb(err)
|
8 |
|
9 | if ((err = db.validateKey(data.Key, table)) != null) return cb(err)
|
10 |
|
11 | if ((err = db.validateUpdates(data.AttributeUpdates, data._updates, table)) != null) return cb(err)
|
12 |
|
13 | var itemDb = store.getItemDb(data.TableName), key = db.createKey(data.Key, table)
|
14 |
|
15 | itemDb.lock(key, function(release) {
|
16 | cb = release(cb)
|
17 |
|
18 | itemDb.get(key, function(err, oldItem) {
|
19 | if (err && err.name != 'NotFoundError') return cb(err)
|
20 |
|
21 | if ((err = db.checkConditional(data, oldItem)) != null) return cb(err)
|
22 |
|
23 | var returnObj = {}, item = data.Key,
|
24 | paths = data._updates ? data._updates.paths : Object.keys(data.AttributeUpdates || {})
|
25 |
|
26 | if (oldItem) {
|
27 | item = deepClone(oldItem)
|
28 | if (data.ReturnValues == 'ALL_OLD') {
|
29 | returnObj.Attributes = oldItem
|
30 | } else if (data.ReturnValues == 'UPDATED_OLD') {
|
31 | returnObj.Attributes = db.mapPaths(paths, oldItem)
|
32 | }
|
33 | }
|
34 |
|
35 | err = data._updates ? applyUpdateExpression(data._updates.sections, table, item) :
|
36 | applyAttributeUpdates(data.AttributeUpdates, table, item)
|
37 | if (err) return cb(err)
|
38 |
|
39 | if (db.itemSize(item) > store.options.maxItemSize)
|
40 | return cb(db.validationError('Item size to update has exceeded the maximum allowed size'))
|
41 |
|
42 | if (data.ReturnValues == 'ALL_NEW') {
|
43 | returnObj.Attributes = item
|
44 | } else if (data.ReturnValues == 'UPDATED_NEW') {
|
45 | returnObj.Attributes = db.mapPaths(paths, item)
|
46 | }
|
47 |
|
48 | returnObj.ConsumedCapacity = db.addConsumedCapacity(data, false, oldItem, item)
|
49 |
|
50 | db.updateIndexes(store, table, oldItem, item, function(err) {
|
51 | if (err) return cb(err)
|
52 |
|
53 | itemDb.put(key, item, function(err) {
|
54 | if (err) return cb(err)
|
55 | cb(null, returnObj)
|
56 | })
|
57 | })
|
58 | })
|
59 | })
|
60 | })
|
61 | }
|
62 |
|
63 |
|
64 | function deepClone(obj) {
|
65 | if (typeof obj != 'object' || obj == null) return obj
|
66 | var result
|
67 | if (Array.isArray(obj)) {
|
68 | result = new Array(obj.length)
|
69 | for (var i = 0; i < obj.length; i++) {
|
70 | result[i] = deepClone(obj[i])
|
71 | }
|
72 | } else {
|
73 | result = Object.create(null)
|
74 | for (var attr in obj) {
|
75 | result[attr] = deepClone(obj[attr])
|
76 | }
|
77 | }
|
78 | return result
|
79 | }
|
80 |
|
81 | function applyAttributeUpdates(updates, table, item) {
|
82 | for (var attr in updates) {
|
83 | var update = updates[attr]
|
84 | if (update.Action == 'PUT' || update.Action == null) {
|
85 | item[attr] = update.Value
|
86 | } else if (update.Action == 'ADD') {
|
87 | if (update.Value.N) {
|
88 | if (item[attr] && !item[attr].N)
|
89 | return db.validationError('Type mismatch for attribute to update')
|
90 | if (!item[attr]) item[attr] = {N: '0'}
|
91 | item[attr].N = new Big(item[attr].N).plus(update.Value.N).toFixed()
|
92 | } else {
|
93 | var type = Object.keys(update.Value)[0]
|
94 | if (item[attr] && !item[attr][type])
|
95 | return db.validationError('Type mismatch for attribute to update')
|
96 | if (!item[attr]) item[attr] = {}
|
97 | if (!item[attr][type]) item[attr][type] = []
|
98 | var val = type == 'L' ? update.Value[type] : update.Value[type].filter(function(a) {
|
99 | return !~item[attr][type].indexOf(a)
|
100 | })
|
101 | item[attr][type] = item[attr][type].concat(val)
|
102 | }
|
103 | } else if (update.Action == 'DELETE') {
|
104 | if (update.Value) {
|
105 | type = Object.keys(update.Value)[0]
|
106 | if (item[attr] && !item[attr][type])
|
107 | return db.validationError('Type mismatch for attribute to update')
|
108 | if (item[attr] && item[attr][type]) {
|
109 | item[attr][type] = item[attr][type].filter(function(val) {
|
110 | return !~update.Value[type].indexOf(val)
|
111 | })
|
112 | if (!item[attr][type].length) delete item[attr]
|
113 | }
|
114 | } else {
|
115 | delete item[attr]
|
116 | }
|
117 | }
|
118 | }
|
119 | }
|
120 |
|
121 | function applyUpdateExpression(sections, table, item) {
|
122 | var toSquash = []
|
123 | for (var i = 0; i < sections.length; i++) {
|
124 | var section = sections[i]
|
125 | if (section.type == 'set') {
|
126 | section.val = resolveValue(section.val, item)
|
127 | if (typeof section.val == 'string') {
|
128 | return db.validationError(section.val)
|
129 | }
|
130 | }
|
131 | }
|
132 | for (i = 0; i < sections.length; i++) {
|
133 | section = sections[i]
|
134 | var parent = db.mapPath(section.path.slice(0, -1), item)
|
135 | var attr = section.path[section.path.length - 1]
|
136 | if (parent == null || (typeof attr == 'number' ? parent.L : parent.M) == null) {
|
137 | return db.validationError('The document path provided in the update expression is invalid for update')
|
138 | }
|
139 | var existing = parent.M ? parent.M[attr] : parent.L[attr]
|
140 | var alreadyExists = existing != null
|
141 | if (section.type == 'remove') {
|
142 | deleteFromParent(parent, attr)
|
143 | } else if (section.type == 'delete') {
|
144 | if (alreadyExists && Object.keys(existing)[0] != section.attrType) {
|
145 | return db.validationError('An operand in the update expression has an incorrect data type')
|
146 | }
|
147 | if (alreadyExists) {
|
148 | existing[section.attrType] = existing[section.attrType].filter(function(val) {
|
149 | return !~section.val[section.attrType].indexOf(val)
|
150 | })
|
151 | if (!existing[section.attrType].length) {
|
152 | deleteFromParent(parent, attr)
|
153 | }
|
154 | }
|
155 | } else if (section.type == 'add') {
|
156 | if (alreadyExists && Object.keys(existing)[0] != section.attrType) {
|
157 | return db.validationError('An operand in the update expression has an incorrect data type')
|
158 | }
|
159 | if (section.attrType == 'N') {
|
160 | if (!existing) existing = {N: '0'}
|
161 | existing.N = new Big(existing.N).plus(section.val.N).toFixed()
|
162 | } else {
|
163 | if (!existing) existing = {}
|
164 | if (!existing[section.attrType]) existing[section.attrType] = []
|
165 | existing[section.attrType] = existing[section.attrType].concat(section.val[section.attrType].filter(function(a) {
|
166 | return !~existing[section.attrType].indexOf(a)
|
167 | }))
|
168 | }
|
169 | if (!alreadyExists) {
|
170 | addToParent(parent, attr, existing, toSquash)
|
171 | }
|
172 | } else if (section.type == 'set') {
|
173 | if (section.path.length == 1) {
|
174 | var err = db.traverseIndexes(table, function(attr, type) {
|
175 | if (section.path[0] == attr && section.val[type] == null) {
|
176 | return db.validationError('The update expression attempted to update the secondary index key to unsupported type')
|
177 | }
|
178 | })
|
179 | if (err) return err
|
180 | }
|
181 | addToParent(parent, attr, section.val, toSquash)
|
182 | }
|
183 | }
|
184 | toSquash.forEach(function(obj) { obj.L = obj.L.filter(Boolean) })
|
185 | }
|
186 |
|
187 | function resolveValue(val, item) {
|
188 | if (Array.isArray(val)) {
|
189 | val = db.mapPath(val, item)
|
190 | } else if (val.type == 'add' || val.type == 'subtract') {
|
191 | var val1 = resolveValue(val.args[0], item)
|
192 | if (typeof val1 == 'string') return val1
|
193 | if (val1.N == null) {
|
194 | return 'An operand in the update expression has an incorrect data type'
|
195 | }
|
196 | var val2 = resolveValue(val.args[1], item)
|
197 | if (typeof val2 == 'string') return val2
|
198 | if (val2.N == null) {
|
199 | return 'An operand in the update expression has an incorrect data type'
|
200 | }
|
201 | val = {N: new Big(val1.N)[val.type == 'add' ? 'plus' : 'minus'](val2.N).toFixed()}
|
202 | } else if (val.type == 'function' && val.name == 'if_not_exists') {
|
203 | val = db.mapPath(val.args[0], item) || resolveValue(val.args[1], item)
|
204 | } else if (val.type == 'function' && val.name == 'list_append') {
|
205 | val1 = resolveValue(val.args[0], item)
|
206 | if (typeof val1 == 'string') return val1
|
207 | if (val1.L == null) {
|
208 | return 'An operand in the update expression has an incorrect data type'
|
209 | }
|
210 | val2 = resolveValue(val.args[1], item)
|
211 | if (typeof val2 == 'string') return val2
|
212 | if (val2.L == null) {
|
213 | return 'An operand in the update expression has an incorrect data type'
|
214 | }
|
215 | return {L: val1.L.concat(val2.L)}
|
216 | }
|
217 | return val || 'The provided expression refers to an attribute that does not exist in the item'
|
218 | }
|
219 |
|
220 | function deleteFromParent(parent, attr) {
|
221 | if (parent.M) {
|
222 | delete parent.M[attr]
|
223 | } else if (parent.L) {
|
224 | parent.L.splice(attr, 1)
|
225 | }
|
226 | }
|
227 |
|
228 | function addToParent(parent, attr, val, toSquash) {
|
229 | if (parent.M) {
|
230 | parent.M[attr] = val
|
231 | } else if (parent.L) {
|
232 | if (attr > parent.L.length && !~toSquash.indexOf(parent)) {
|
233 | toSquash.push(parent)
|
234 | }
|
235 | parent.L[attr] = val
|
236 | }
|
237 | }
|