UNPKG

40.2 kBJavaScriptView Raw
1var Big = require('big.js'),
2 db = require('../db'),
3 conditionParser = require('../db/conditionParser'),
4 projectionParser = require('../db/projectionParser'),
5 updateParser = require('../db/updateParser')
6
7exports.checkTypes = checkTypes
8exports.checkValidations = checkValidations
9exports.toLowerFirst = toLowerFirst
10exports.findDuplicate = findDuplicate
11exports.validateAttributeValue = validateAttributeValue
12exports.validateConditions = validateConditions
13exports.validateAttributeConditions = validateAttributeConditions
14exports.validateExpressionParams = validateExpressionParams
15exports.validateExpressions = validateExpressions
16exports.convertKeyCondition = convertKeyCondition
17
18function checkTypes(data, types) {
19 var key
20 for (key in data) {
21 // TODO: deal with nulls
22 if (!types[key] || data[key] == null)
23 delete data[key]
24 }
25
26 return Object.keys(types).reduce(function(newData, key) {
27 var val = checkType(data[key], types[key])
28 if (val != null) newData[key] = val
29 return newData
30 }, {})
31
32 function typeError(msg) {
33 var err = new Error(msg)
34 err.statusCode = 400
35 err.body = {
36 __type: 'com.amazon.coral.service#SerializationException',
37 Message: msg,
38 }
39 return err
40 }
41
42 function checkType(val, type) {
43 if (val == null) return null
44 var children = type.children
45 if (typeof children == 'string') children = {type: children}
46 if (type.type) type = type.type
47 var subtypeMatch = type.match(/(.+?)<(.+)>$/), subtype
48 if (subtypeMatch != null) {
49 type = subtypeMatch[1]
50 subtype = subtypeMatch[2]
51 }
52
53 if (type == 'AttrStruct') {
54 return checkType(val, {
55 type: subtype + '<AttributeValue>',
56 children: {
57 S: 'String',
58 B: 'Blob',
59 N: 'String',
60 BOOL: 'Boolean',
61 NULL: 'Boolean',
62 BS: {
63 type: 'List',
64 children: 'Blob',
65 },
66 NS: {
67 type: 'List',
68 children: 'String',
69 },
70 SS: {
71 type: 'List',
72 children: 'String',
73 },
74 L: {
75 type: 'List',
76 children: 'AttrStruct<ValueStruct>',
77 },
78 M: {
79 type: 'Map<AttributeValue>',
80 children: 'AttrStruct<ValueStruct>',
81 },
82 },
83 })
84 }
85
86 switch (type) {
87 case 'Boolean':
88 switch (typeof val) {
89 case 'number':
90 throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type)
91 case 'string':
92 // 'true'/'false'/'1'/'0'/'no'/'yes' seem to convert fine
93 val = val.toUpperCase()
94 if (~['TRUE', '1', 'YES'].indexOf(val)) {
95 val = true
96 } else if (~['FALSE', '0', 'NO'].indexOf(val)) {
97 val = false
98 } else {
99 throw typeError('Unexpected token received from parser')
100 }
101 break
102 case 'object':
103 if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
104 throw typeError('Start of structure or map found where not expected')
105 }
106 return val
107 case 'Short':
108 case 'Integer':
109 case 'Long':
110 case 'Double':
111 switch (typeof val) {
112 case 'boolean':
113 throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type)
114 case 'number':
115 if (type != 'Double') val = Math.floor(val)
116 break
117 case 'string':
118 throw typeError('STRING_VALUE cannot be converted to ' + type)
119 case 'object':
120 if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
121 throw typeError('Start of structure or map found where not expected')
122 }
123 return val
124 case 'String':
125 switch (typeof val) {
126 case 'boolean':
127 throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type)
128 case 'number':
129 throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type)
130 case 'object':
131 if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
132 throw typeError('Start of structure or map found where not expected')
133 }
134 return val
135 case 'Blob':
136 switch (typeof val) {
137 case 'boolean':
138 case 'number':
139 throw typeError('only base-64-encoded strings are convertible to bytes')
140 case 'object':
141 if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.nio.ByteBuffer')
142 throw typeError('Start of structure or map found where not expected')
143 }
144 if (val.length % 4)
145 throw typeError('Base64 encoded length is expected a multiple of 4 bytes but found: ' + val.length)
146 // TODO: need a better check than this...
147 if (Buffer.from(val, 'base64').toString('base64') != val)
148 throw typeError('Invalid last non-pad Base64 character dectected')
149 return val
150 case 'List':
151 switch (typeof val) {
152 case 'boolean':
153 case 'number':
154 case 'string':
155 throw typeError('Unexpected field type')
156 case 'object':
157 if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected')
158 }
159 return val.map(function(child) { return checkType(child, children) })
160 case 'ParameterizedList':
161 switch (typeof val) {
162 case 'boolean':
163 case 'number':
164 case 'string':
165 throw typeError('sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to java.lang.Class')
166 case 'object':
167 if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected')
168 }
169 return val.map(function(child) { return checkType(child, children) })
170 case 'ParameterizedMap':
171 switch (typeof val) {
172 case 'boolean':
173 case 'number':
174 case 'string':
175 throw typeError('sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to java.lang.Class')
176 case 'object':
177 if (Array.isArray(val)) throw typeError('Unrecognized collection type java.util.Map<java.lang.String, com.amazonaws.dynamodb.v20120810.AttributeValue>')
178 }
179 return Object.keys(val).reduce(function(newVal, key) {
180 newVal[key] = checkType(val[key], children)
181 return newVal
182 }, {})
183 case 'Map':
184 switch (typeof val) {
185 case 'boolean':
186 case 'number':
187 case 'string':
188 throw typeError('Unexpected field type')
189 case 'object':
190 if (Array.isArray(val)) {
191 throw typeError('Unrecognized collection type java.util.Map<java.lang.String, ' +
192 (~subtype.indexOf('.') ? subtype : 'com.amazonaws.dynamodb.v20120810.' + subtype) + '>')
193 }
194 }
195 return Object.keys(val).reduce(function(newVal, key) {
196 newVal[key] = checkType(val[key], children)
197 return newVal
198 }, {})
199 case 'ValueStruct':
200 switch (typeof val) {
201 case 'boolean':
202 case 'number':
203 case 'string':
204 throw typeError('Unexpected value type in payload')
205 case 'object':
206 if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype)
207 }
208 return checkTypes(val, children)
209 case 'FieldStruct':
210 switch (typeof val) {
211 case 'boolean':
212 case 'number':
213 case 'string':
214 throw typeError('Unexpected field type')
215 case 'object':
216 if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype)
217 }
218 return checkTypes(val, children)
219 default:
220 throw new Error('Unknown type: ' + type)
221 }
222 }
223}
224
225var validateFns = {}
226
227function checkValidations(data, validations, custom, store) {
228 var attr, msg, errors = []
229
230 for (attr in validations) {
231 if (validations[attr].required && data[attr] == null) {
232 throw db.validationError('The parameter \'' + attr + '\' is required but was not present in the request')
233 }
234 if (validations[attr].tableName) {
235 msg = validateTableName(attr, data[attr])
236 if (msg) throw db.validationError(msg)
237 }
238 }
239
240 function checkNonRequireds(data, types, parent) {
241 for (attr in types) {
242 checkNonRequired(attr, data[attr], types[attr], parent)
243 }
244 }
245
246 checkNonRequireds(data, validations)
247
248 function checkNonRequired(attr, data, validations, parent) {
249 if (validations == null || typeof validations != 'object') return
250 for (var validation in validations) {
251 if (errors.length >= 10) return
252 if (~['type', 'required', 'tableName'].indexOf(validation)) continue
253 if (validation != 'notNull' && data == null) continue
254 if (validation == 'children') {
255 if (/List$/.test(validations.type)) {
256 for (var i = 0; i < data.length; i++) {
257 checkNonRequired('member', data[i], validations.children,
258 (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + (i + 1))
259 }
260 continue
261 } else if (/Map/.test(validations.type)) {
262 Object.keys(data).forEach(function(key) { // eslint-disable-line no-loop-func
263 checkNonRequired('member', data[key], validations.children,
264 (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + key)
265 })
266 continue
267 }
268 checkNonRequireds(data, validations.children, (parent ? parent + '.' : '') + toLowerFirst(attr))
269 continue
270 }
271 validateFns[validation](parent, attr, validations[validation], data, errors)
272 }
273 }
274
275 if (errors.length)
276 throw db.validationError(errors.length + ' validation error' + (errors.length > 1 ? 's' : '') + ' detected: ' + errors.join('; '))
277
278 if (custom) {
279 msg = custom(data, store)
280 if (msg) throw db.validationError(msg)
281 }
282}
283
284validateFns.notNull = function(parent, key, val, data, errors) {
285 validate(data != null, 'Member must not be null', data, parent, key, errors)
286}
287validateFns.greaterThanOrEqual = function(parent, key, val, data, errors) {
288 validate(data >= val, 'Member must have value greater than or equal to ' + val, data, parent, key, errors)
289}
290validateFns.lessThanOrEqual = function(parent, key, val, data, errors) {
291 validate(data <= val, 'Member must have value less than or equal to ' + val, data, parent, key, errors)
292}
293validateFns.regex = function(parent, key, pattern, data, errors) {
294 validate(RegExp('^' + pattern + '$').test(data), 'Member must satisfy regular expression pattern: ' + pattern, data, parent, key, errors)
295}
296validateFns.lengthGreaterThanOrEqual = function(parent, key, val, data, errors) {
297 var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length
298 validate(length >= val, 'Member must have length greater than or equal to ' + val, data, parent, key, errors)
299}
300validateFns.lengthLessThanOrEqual = function(parent, key, val, data, errors) {
301 var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length
302 validate(length <= val, 'Member must have length less than or equal to ' + val, data, parent, key, errors)
303}
304validateFns.enum = function(parent, key, val, data, errors) {
305 validate(~val.indexOf(data), 'Member must satisfy enum value set: [' + val.join(', ') + ']', data, parent, key, errors)
306}
307validateFns.keys = function(parent, key, val, data, errors) {
308 Object.keys(data).forEach(function(mapKey) {
309 try {
310 Object.keys(val).forEach(function(validation) {
311 validateFns[validation]('', '', val[validation], mapKey, [])
312 })
313 } catch (e) {
314 var msgs = Object.keys(val).map(function(validation) {
315 if (validation == 'lengthGreaterThanOrEqual')
316 return 'Member must have length greater than or equal to ' + val[validation]
317 if (validation == 'lengthLessThanOrEqual')
318 return 'Member must have length less than or equal to ' + val[validation]
319 if (validation == 'regex')
320 return 'Member must satisfy regular expression pattern: ' + val[validation]
321 })
322 validate(false, 'Map keys must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors)
323 }
324 })
325}
326validateFns.values = function(parent, key, val, data, errors) {
327 Object.keys(data).forEach(function(mapKey) {
328 try {
329 Object.keys(val).forEach(function(validation) {
330 validateFns[validation]('', '', val[validation], data[mapKey], [])
331 })
332 } catch (e) {
333 var msgs = Object.keys(val).map(function(validation) {
334 if (validation == 'lengthGreaterThanOrEqual')
335 return 'Member must have length greater than or equal to ' + val[validation]
336 if (validation == 'lengthLessThanOrEqual')
337 return 'Member must have length less than or equal to ' + val[validation]
338 })
339 validate(false, 'Map value must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors)
340 }
341 })
342}
343
344function validate(predicate, msg, data, parent, key, errors) {
345 if (predicate) return
346 var value = valueStr(data)
347 if (value != 'null') value = '\'' + value + '\''
348 parent = parent ? parent + '.' : ''
349 errors.push('Value ' + value + ' at \'' + parent + toLowerFirst(key) + '\' failed to satisfy constraint: ' + msg)
350}
351
352function validateTableName(key, val) {
353 if (val == null) return null
354 if (val.length < 3 || val.length > 255)
355 return key + ' must be at least 3 characters long and at most 255 characters long'
356}
357
358function toLowerFirst(str) {
359 return str[0].toLowerCase() + str.slice(1)
360}
361
362function validateAttributeValue(value) {
363 var types = Object.keys(value), msg, i, attr
364 if (!types.length)
365 return 'Supplied AttributeValue is empty, must contain exactly one of the supported datatypes'
366
367 for (var type in value) {
368 if (type == 'N') {
369 msg = checkNum(type, value)
370 if (msg) return msg
371 }
372
373 if (type == 'B' && !value[type])
374 return 'One or more parameter values were invalid: An AttributeValue may not contain a null or empty binary type.'
375
376 if (type == 'S' && !value[type])
377 return 'One or more parameter values were invalid: An AttributeValue may not contain an empty string'
378
379 if (type == 'NULL' && !value[type])
380 return 'One or more parameter values were invalid: Null attribute value types must have the value of true'
381
382 if (type == 'SS' && !value[type].length)
383 return 'One or more parameter values were invalid: An string set may not be empty'
384
385 if (type == 'NS' && !value[type].length)
386 return 'One or more parameter values were invalid: An number set may not be empty'
387
388 if (type == 'BS' && !value[type].length)
389 return 'One or more parameter values were invalid: Binary sets should not be empty'
390
391 if (type == 'SS' && value[type].some(function(x) { return !x })) // eslint-disable-line no-loop-func
392 return 'One or more parameter values were invalid: An string set may not have a empty string as a member'
393
394 if (type == 'BS' && value[type].some(function(x) { return !x })) // eslint-disable-line no-loop-func
395 return 'One or more parameter values were invalid: Binary sets may not contain null or empty values'
396
397 if (type == 'NS') {
398 for (i = 0; i < value[type].length; i++) {
399 msg = checkNum(i, value[type])
400 if (msg) return msg
401 }
402 }
403
404 if (type == 'SS' && findDuplicate(value[type]))
405 return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + ' contains duplicates.'
406
407 if (type == 'NS' && findDuplicate(value[type]))
408 return 'Input collection contains duplicates'
409
410 if (type == 'BS' && findDuplicate(value[type]))
411 return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + 'of type BS contains duplicates.'
412
413 if (type == 'M') {
414 for (attr in value[type]) {
415 msg = validateAttributeValue(value[type][attr])
416 if (msg) return msg
417 }
418 }
419
420 if (type == 'L') {
421 for (i = 0; i < value[type].length; i++) {
422 msg = validateAttributeValue(value[type][i])
423 if (msg) return msg
424 }
425 }
426 }
427
428 if (types.length > 1)
429 return 'Supplied AttributeValue has more than one datatypes set, must contain exactly one of the supported datatypes'
430}
431
432function checkNum(attr, obj) {
433 if (!obj[attr])
434 return 'The parameter cannot be converted to a numeric value'
435
436 var bigNum
437 try {
438 bigNum = new Big(obj[attr])
439 } catch (e) {
440 return 'The parameter cannot be converted to a numeric value: ' + obj[attr]
441 }
442 if (bigNum.e > 125)
443 return 'Number overflow. Attempting to store a number with magnitude larger than supported range'
444 else if (bigNum.e < -130)
445 return 'Number underflow. Attempting to store a number with magnitude smaller than supported range'
446 else if (bigNum.c.length > 38)
447 return 'Attempting to store more than 38 significant digits in a Number'
448
449 obj[attr] = bigNum.toFixed()
450}
451
452function valueStr(data) {
453 return data == null ? 'null' : Array.isArray(data) ? '[' + data.map(valueStr).join(', ') + ']' :
454 typeof data == 'object' ? JSON.stringify(data) : data
455}
456
457function findDuplicate(arr) {
458 if (!arr) return null
459 var vals = Object.create(null)
460 for (var i = 0; i < arr.length; i++) {
461 if (vals[arr[i]]) return arr[i]
462 vals[arr[i]] = true
463 }
464}
465
466function validateAttributeConditions(data) {
467 for (var key in data.Expected) {
468 var condition = data.Expected[key]
469
470 if ('AttributeValueList' in condition && 'Value' in condition)
471 return 'One or more parameter values were invalid: ' +
472 'Value and AttributeValueList cannot be used together for Attribute: ' + key
473
474 if ('ComparisonOperator' in condition) {
475 if ('Exists' in condition)
476 return 'One or more parameter values were invalid: ' +
477 'Exists and ComparisonOperator cannot be used together for Attribute: ' + key
478
479 if (condition.ComparisonOperator != 'NULL' && condition.ComparisonOperator != 'NOT_NULL' &&
480 !('AttributeValueList' in condition) && !('Value' in condition))
481 return 'One or more parameter values were invalid: ' +
482 'Value or AttributeValueList must be used with ComparisonOperator: ' + condition.ComparisonOperator +
483 ' for Attribute: ' + key
484
485 var values = condition.AttributeValueList ?
486 condition.AttributeValueList.length : condition.Value ? 1 : 0
487 var validAttrCount = false
488
489 switch (condition.ComparisonOperator) {
490 case 'EQ':
491 case 'NE':
492 case 'LE':
493 case 'LT':
494 case 'GE':
495 case 'GT':
496 case 'CONTAINS':
497 case 'NOT_CONTAINS':
498 case 'BEGINS_WITH':
499 if (values === 1) validAttrCount = true
500 break
501 case 'NOT_NULL':
502 case 'NULL':
503 if (values === 0) validAttrCount = true
504 break
505 case 'IN':
506 if (values > 0) validAttrCount = true
507 break
508 case 'BETWEEN':
509 if (values === 2) validAttrCount = true
510 break
511 }
512 if (!validAttrCount)
513 return 'One or more parameter values were invalid: ' +
514 'Invalid number of argument(s) for the ' + condition.ComparisonOperator + ' ComparisonOperator'
515
516 if (condition.AttributeValueList && condition.AttributeValueList.length) {
517 var type = Object.keys(condition.AttributeValueList[0])[0]
518 if (condition.AttributeValueList.some(function(attr) { return Object.keys(attr)[0] != type })) {
519 return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type'
520 }
521 if (condition.ComparisonOperator == 'BETWEEN' && db.compare('GT', condition.AttributeValueList[0], condition.AttributeValueList[1])) {
522 return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound'
523 }
524 }
525 } else if ('AttributeValueList' in condition) {
526 return 'One or more parameter values were invalid: ' +
527 'AttributeValueList can only be used with a ComparisonOperator for Attribute: ' + key
528 } else {
529 var exists = condition.Exists == null || condition.Exists
530 if (exists && condition.Value == null)
531 return 'One or more parameter values were invalid: ' +
532 'Value must be provided when Exists is ' +
533 (condition.Exists == null ? 'null' : condition.Exists) +
534 ' for Attribute: ' + key
535 else if (!exists && condition.Value != null)
536 return 'One or more parameter values were invalid: ' +
537 'Value cannot be used when Exists is false for Attribute: ' + key
538 if (condition.Value != null) {
539 var msg = validateAttributeValue(condition.Value)
540 if (msg) return msg
541 }
542 }
543 }
544}
545
546function validateConditions(conditions) {
547 var lengths = {
548 NULL: 0,
549 NOT_NULL: 0,
550 EQ: 1,
551 NE: 1,
552 LE: 1,
553 LT: 1,
554 GE: 1,
555 GT: 1,
556 CONTAINS: 1,
557 NOT_CONTAINS: 1,
558 BEGINS_WITH: 1,
559 IN: [1],
560 BETWEEN: 2,
561 }
562 var types = {
563 EQ: ['S', 'N', 'B', 'SS', 'NS', 'BS'],
564 NE: ['S', 'N', 'B', 'SS', 'NS', 'BS'],
565 LE: ['S', 'N', 'B'],
566 LT: ['S', 'N', 'B'],
567 GE: ['S', 'N', 'B'],
568 GT: ['S', 'N', 'B'],
569 CONTAINS: ['S', 'N', 'B'],
570 NOT_CONTAINS: ['S', 'N', 'B'],
571 BEGINS_WITH: ['S', 'B'],
572 IN: ['S', 'N', 'B'],
573 BETWEEN: ['S', 'N', 'B'],
574 }
575 for (var key in conditions) {
576 var comparisonOperator = conditions[key].ComparisonOperator
577 var attrValList = conditions[key].AttributeValueList || []
578 for (var i = 0; i < attrValList.length; i++) {
579 var msg = validateAttributeValue(attrValList[i])
580 if (msg) return msg
581 }
582
583 if ((typeof lengths[comparisonOperator] == 'number' && attrValList.length != lengths[comparisonOperator]) ||
584 (attrValList.length < lengths[comparisonOperator][0] || attrValList.length > lengths[comparisonOperator][1]))
585 return 'One or more parameter values were invalid: Invalid number of argument(s) for the ' +
586 comparisonOperator + ' ComparisonOperator'
587
588 if (attrValList.length) {
589 var type = Object.keys(attrValList[0])[0]
590 if (attrValList.some(function(attr) { return Object.keys(attr)[0] != type })) {
591 return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type'
592 }
593 }
594
595 if (types[comparisonOperator]) {
596 for (i = 0; i < attrValList.length; i++) {
597 if (!~types[comparisonOperator].indexOf(Object.keys(attrValList[i])[0]))
598 return 'One or more parameter values were invalid: ComparisonOperator ' + comparisonOperator +
599 ' is not valid for ' + Object.keys(attrValList[i])[0] + ' AttributeValue type'
600 }
601 }
602
603 if (comparisonOperator == 'BETWEEN' && db.compare('GT', attrValList[0], attrValList[1])) {
604 return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound'
605 }
606 }
607}
608
609function validateExpressionParams(data, expressions, nonExpressions) {
610 var exprParams = expressions.filter(function(expr) { return data[expr] != null })
611
612 if (exprParams.length) {
613 // Special case for KeyConditions and KeyConditionExpression
614 if (data.KeyConditions != null && data.KeyConditionExpression == null) {
615 nonExpressions.splice(nonExpressions.indexOf('KeyConditions'), 1)
616 }
617 var nonExprParams = nonExpressions.filter(function(expr) { return data[expr] != null })
618 if (nonExprParams.length) {
619 return 'Can not use both expression and non-expression parameters in the same request: ' +
620 'Non-expression parameters: {' + nonExprParams.join(', ') + '} ' +
621 'Expression parameters: {' + exprParams.join(', ') + '}'
622 }
623 }
624
625 if (data.ExpressionAttributeNames != null && !exprParams.length) {
626 return 'ExpressionAttributeNames can only be specified when using expressions'
627 }
628
629 var valExprs = expressions.filter(function(expr) { return expr != 'ProjectionExpression' })
630 if (valExprs.length && data.ExpressionAttributeValues != null &&
631 valExprs.every(function(expr) { return data[expr] == null })) {
632 return 'ExpressionAttributeValues can only be specified when using expressions: ' +
633 valExprs.join(' and ') + ' ' + (valExprs.length > 1 ? 'are' : 'is') + ' null'
634 }
635}
636
637function validateExpressions(data) {
638 var key, msg, result, context = {
639 attrNames: data.ExpressionAttributeNames,
640 attrVals: data.ExpressionAttributeValues,
641 unusedAttrNames: {},
642 unusedAttrVals: {},
643 }
644
645 if (data.ExpressionAttributeNames != null) {
646 if (!Object.keys(data.ExpressionAttributeNames).length)
647 return 'ExpressionAttributeNames must not be empty'
648 for (key in data.ExpressionAttributeNames) {
649 if (!/^#[0-9a-zA-Z_]+$/.test(key)) {
650 return 'ExpressionAttributeNames contains invalid key: Syntax error; key: "' + key + '"'
651 }
652 context.unusedAttrNames[key] = true
653 }
654 }
655
656 if (data.ExpressionAttributeValues != null) {
657 if (!Object.keys(data.ExpressionAttributeValues).length)
658 return 'ExpressionAttributeValues must not be empty'
659 for (key in data.ExpressionAttributeValues) {
660 if (!/^:[0-9a-zA-Z_]+$/.test(key)) {
661 return 'ExpressionAttributeValues contains invalid key: Syntax error; key: "' + key + '"'
662 }
663 context.unusedAttrVals[key] = true
664 }
665 for (key in data.ExpressionAttributeValues) {
666 msg = validateAttributeValue(data.ExpressionAttributeValues[key])
667 if (msg) {
668 msg = 'ExpressionAttributeValues contains invalid value: ' + msg + ' for key ' + key
669 return msg
670 }
671 }
672 }
673
674 if (data.UpdateExpression != null) {
675 result = parse(data.UpdateExpression, updateParser, context)
676 if (typeof result == 'string') {
677 return 'Invalid UpdateExpression: ' + result
678 }
679 data._updates = result
680 }
681
682 if (data.ConditionExpression != null) {
683 result = parse(data.ConditionExpression, conditionParser, context)
684 if (typeof result == 'string') {
685 return 'Invalid ConditionExpression: ' + result
686 }
687 data._condition = result
688 }
689
690 if (data.KeyConditionExpression != null) {
691 context.isKeyCondition = true
692 result = parse(data.KeyConditionExpression, conditionParser, context)
693 if (typeof result == 'string') {
694 return 'Invalid KeyConditionExpression: ' + result
695 }
696 data._keyCondition = result
697 }
698
699 if (data.FilterExpression != null) {
700 result = parse(data.FilterExpression, conditionParser, context)
701 if (typeof result == 'string') {
702 return 'Invalid FilterExpression: ' + result
703 }
704 data._filter = result
705 }
706
707 if (data.ProjectionExpression != null) {
708 result = parse(data.ProjectionExpression, projectionParser, context)
709 if (typeof result == 'string') {
710 return 'Invalid ProjectionExpression: ' + result
711 }
712 data._projection = result
713 }
714
715 if (Object.keys(context.unusedAttrNames).length) {
716 return 'Value provided in ExpressionAttributeNames unused in expressions: ' +
717 'keys: {' + Object.keys(context.unusedAttrNames).join(', ') + '}'
718 }
719
720 if (Object.keys(context.unusedAttrVals).length) {
721 return 'Value provided in ExpressionAttributeValues unused in expressions: ' +
722 'keys: {' + Object.keys(context.unusedAttrVals).join(', ') + '}'
723 }
724}
725
726function parse(str, parser, context) {
727 if (str == '') return 'The expression can not be empty;'
728 context.isReserved = isReserved
729 context.compare = db.compare
730 try {
731 return parser.parse(str, {context: context})
732 } catch (e) {
733 return e.name == 'SyntaxError' ? 'Syntax error; ' + e.message : e.message
734 }
735}
736
737function convertKeyCondition(expression) {
738 var keyConds = Object.create(null)
739 return checkExpr(expression, keyConds) || keyConds
740}
741
742function checkExpr(expr, keyConds) {
743 if (!expr || !expr.type) return
744 if (~['or', 'not', 'in', '<>'].indexOf(expr.type)) {
745 return 'Invalid operator used in KeyConditionExpression: ' + expr.type.toUpperCase()
746 }
747 if (expr.type == 'function' && ~['attribute_exists', 'attribute_not_exists', 'attribute_type', 'contains'].indexOf(expr.name)) {
748 return 'Invalid operator used in KeyConditionExpression: ' + expr.name
749 }
750 if (expr.type == 'function' && expr.name == 'size') {
751 return 'KeyConditionExpressions cannot contain nested operations'
752 }
753 if (expr.type == 'between' && !Array.isArray(expr.args[0])) {
754 return 'Invalid condition in KeyConditionExpression: ' + expr.type.toUpperCase() + ' operator must have the key attribute as its first operand'
755 }
756 if (expr.type == 'function' && expr.name == 'begins_with' && !Array.isArray(expr.args[0])) {
757 return 'Invalid condition in KeyConditionExpression: ' + expr.name + ' operator must have the key attribute as its first operand'
758 }
759 if (expr.args) {
760 var attrName = '', attrIx = 0
761 for (var i = 0; i < expr.args.length; i++) {
762 if (Array.isArray(expr.args[i])) {
763 if (attrName) {
764 return 'Invalid condition in KeyConditionExpression: Multiple attribute names used in one condition'
765 }
766 if (expr.args[i].length > 1) {
767 return 'KeyConditionExpressions cannot have conditions on nested attributes'
768 }
769 attrName = expr.args[i][0]
770 attrIx = i
771 } else if (expr.args[i].type) {
772 var result = checkExpr(expr.args[i], keyConds)
773 if (result) return result
774 }
775 }
776 if (expr.type != 'and') {
777 if (!attrName) {
778 return 'Invalid condition in KeyConditionExpression: No key attribute specified'
779 }
780 if (keyConds[attrName]) {
781 return 'KeyConditionExpressions must only contain one condition per key'
782 }
783 if (attrIx != 0) {
784 expr.type = {
785 '=': '=',
786 '<': '>',
787 '<=': '>=',
788 '>': '<',
789 '>=': '<=',
790 }[expr.type]
791 expr.args[1] = expr.args[0]
792 }
793 keyConds[attrName] = {
794 ComparisonOperator: {
795 '=': 'EQ',
796 '<': 'LT',
797 '<=': 'LE',
798 '>': 'GT',
799 '>=': 'GE',
800 'between': 'BETWEEN',
801 'function': 'BEGINS_WITH',
802 }[expr.type],
803 AttributeValueList: expr.args.slice(1),
804 }
805 }
806 }
807}
808
809// http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
810var RESERVED_WORDS = {
811 ABORT: true,
812 ABSOLUTE: true,
813 ACTION: true,
814 ADD: true,
815 AFTER: true,
816 AGENT: true,
817 AGGREGATE: true,
818 ALL: true,
819 ALLOCATE: true,
820 ALTER: true,
821 ANALYZE: true,
822 AND: true,
823 ANY: true,
824 ARCHIVE: true,
825 ARE: true,
826 ARRAY: true,
827 AS: true,
828 ASC: true,
829 ASCII: true,
830 ASENSITIVE: true,
831 ASSERTION: true,
832 ASYMMETRIC: true,
833 AT: true,
834 ATOMIC: true,
835 ATTACH: true,
836 ATTRIBUTE: true,
837 AUTH: true,
838 AUTHORIZATION: true,
839 AUTHORIZE: true,
840 AUTO: true,
841 AVG: true,
842 BACK: true,
843 BACKUP: true,
844 BASE: true,
845 BATCH: true,
846 BEFORE: true,
847 BEGIN: true,
848 BETWEEN: true,
849 BIGINT: true,
850 BINARY: true,
851 BIT: true,
852 BLOB: true,
853 BLOCK: true,
854 BOOLEAN: true,
855 BOTH: true,
856 BREADTH: true,
857 BUCKET: true,
858 BULK: true,
859 BY: true,
860 BYTE: true,
861 CALL: true,
862 CALLED: true,
863 CALLING: true,
864 CAPACITY: true,
865 CASCADE: true,
866 CASCADED: true,
867 CASE: true,
868 CAST: true,
869 CATALOG: true,
870 CHAR: true,
871 CHARACTER: true,
872 CHECK: true,
873 CLASS: true,
874 CLOB: true,
875 CLOSE: true,
876 CLUSTER: true,
877 CLUSTERED: true,
878 CLUSTERING: true,
879 CLUSTERS: true,
880 COALESCE: true,
881 COLLATE: true,
882 COLLATION: true,
883 COLLECTION: true,
884 COLUMN: true,
885 COLUMNS: true,
886 COMBINE: true,
887 COMMENT: true,
888 COMMIT: true,
889 COMPACT: true,
890 COMPILE: true,
891 COMPRESS: true,
892 CONDITION: true,
893 CONFLICT: true,
894 CONNECT: true,
895 CONNECTION: true,
896 CONSISTENCY: true,
897 CONSISTENT: true,
898 CONSTRAINT: true,
899 CONSTRAINTS: true,
900 CONSTRUCTOR: true,
901 CONSUMED: true,
902 CONTINUE: true,
903 CONVERT: true,
904 COPY: true,
905 CORRESPONDING: true,
906 COUNT: true,
907 COUNTER: true,
908 CREATE: true,
909 CROSS: true,
910 CUBE: true,
911 CURRENT: true,
912 CURSOR: true,
913 CYCLE: true,
914 DATA: true,
915 DATABASE: true,
916 DATE: true,
917 DATETIME: true,
918 DAY: true,
919 DEALLOCATE: true,
920 DEC: true,
921 DECIMAL: true,
922 DECLARE: true,
923 DEFAULT: true,
924 DEFERRABLE: true,
925 DEFERRED: true,
926 DEFINE: true,
927 DEFINED: true,
928 DEFINITION: true,
929 DELETE: true,
930 DELIMITED: true,
931 DEPTH: true,
932 DEREF: true,
933 DESC: true,
934 DESCRIBE: true,
935 DESCRIPTOR: true,
936 DETACH: true,
937 DETERMINISTIC: true,
938 DIAGNOSTICS: true,
939 DIRECTORIES: true,
940 DISABLE: true,
941 DISCONNECT: true,
942 DISTINCT: true,
943 DISTRIBUTE: true,
944 DO: true,
945 DOMAIN: true,
946 DOUBLE: true,
947 DROP: true,
948 DUMP: true,
949 DURATION: true,
950 DYNAMIC: true,
951 EACH: true,
952 ELEMENT: true,
953 ELSE: true,
954 ELSEIF: true,
955 EMPTY: true,
956 ENABLE: true,
957 END: true,
958 EQUAL: true,
959 EQUALS: true,
960 ERROR: true,
961 ESCAPE: true,
962 ESCAPED: true,
963 EVAL: true,
964 EVALUATE: true,
965 EXCEEDED: true,
966 EXCEPT: true,
967 EXCEPTION: true,
968 EXCEPTIONS: true,
969 EXCLUSIVE: true,
970 EXEC: true,
971 EXECUTE: true,
972 EXISTS: true,
973 EXIT: true,
974 EXPLAIN: true,
975 EXPLODE: true,
976 EXPORT: true,
977 EXPRESSION: true,
978 EXTENDED: true,
979 EXTERNAL: true,
980 EXTRACT: true,
981 FAIL: true,
982 FALSE: true,
983 FAMILY: true,
984 FETCH: true,
985 FIELDS: true,
986 FILE: true,
987 FILTER: true,
988 FILTERING: true,
989 FINAL: true,
990 FINISH: true,
991 FIRST: true,
992 FIXED: true,
993 FLATTERN: true,
994 FLOAT: true,
995 FOR: true,
996 FORCE: true,
997 FOREIGN: true,
998 FORMAT: true,
999 FORWARD: true,
1000 FOUND: true,
1001 FREE: true,
1002 FROM: true,
1003 FULL: true,
1004 FUNCTION: true,
1005 FUNCTIONS: true,
1006 GENERAL: true,
1007 GENERATE: true,
1008 GET: true,
1009 GLOB: true,
1010 GLOBAL: true,
1011 GO: true,
1012 GOTO: true,
1013 GRANT: true,
1014 GREATER: true,
1015 GROUP: true,
1016 GROUPING: true,
1017 HANDLER: true,
1018 HASH: true,
1019 HAVE: true,
1020 HAVING: true,
1021 HEAP: true,
1022 HIDDEN: true,
1023 HOLD: true,
1024 HOUR: true,
1025 IDENTIFIED: true,
1026 IDENTITY: true,
1027 IF: true,
1028 IGNORE: true,
1029 IMMEDIATE: true,
1030 IMPORT: true,
1031 IN: true,
1032 INCLUDING: true,
1033 INCLUSIVE: true,
1034 INCREMENT: true,
1035 INCREMENTAL: true,
1036 INDEX: true,
1037 INDEXED: true,
1038 INDEXES: true,
1039 INDICATOR: true,
1040 INFINITE: true,
1041 INITIALLY: true,
1042 INLINE: true,
1043 INNER: true,
1044 INNTER: true,
1045 INOUT: true,
1046 INPUT: true,
1047 INSENSITIVE: true,
1048 INSERT: true,
1049 INSTEAD: true,
1050 INT: true,
1051 INTEGER: true,
1052 INTERSECT: true,
1053 INTERVAL: true,
1054 INTO: true,
1055 INVALIDATE: true,
1056 IS: true,
1057 ISOLATION: true,
1058 ITEM: true,
1059 ITEMS: true,
1060 ITERATE: true,
1061 JOIN: true,
1062 KEY: true,
1063 KEYS: true,
1064 LAG: true,
1065 LANGUAGE: true,
1066 LARGE: true,
1067 LAST: true,
1068 LATERAL: true,
1069 LEAD: true,
1070 LEADING: true,
1071 LEAVE: true,
1072 LEFT: true,
1073 LENGTH: true,
1074 LESS: true,
1075 LEVEL: true,
1076 LIKE: true,
1077 LIMIT: true,
1078 LIMITED: true,
1079 LINES: true,
1080 LIST: true,
1081 LOAD: true,
1082 LOCAL: true,
1083 LOCALTIME: true,
1084 LOCALTIMESTAMP: true,
1085 LOCATION: true,
1086 LOCATOR: true,
1087 LOCK: true,
1088 LOCKS: true,
1089 LOG: true,
1090 LOGED: true,
1091 LONG: true,
1092 LOOP: true,
1093 LOWER: true,
1094 MAP: true,
1095 MATCH: true,
1096 MATERIALIZED: true,
1097 MAX: true,
1098 MAXLEN: true,
1099 MEMBER: true,
1100 MERGE: true,
1101 METHOD: true,
1102 METRICS: true,
1103 MIN: true,
1104 MINUS: true,
1105 MINUTE: true,
1106 MISSING: true,
1107 MOD: true,
1108 MODE: true,
1109 MODIFIES: true,
1110 MODIFY: true,
1111 MODULE: true,
1112 MONTH: true,
1113 MULTI: true,
1114 MULTISET: true,
1115 NAME: true,
1116 NAMES: true,
1117 NATIONAL: true,
1118 NATURAL: true,
1119 NCHAR: true,
1120 NCLOB: true,
1121 NEW: true,
1122 NEXT: true,
1123 NO: true,
1124 NONE: true,
1125 NOT: true,
1126 NULL: true,
1127 NULLIF: true,
1128 NUMBER: true,
1129 NUMERIC: true,
1130 OBJECT: true,
1131 OF: true,
1132 OFFLINE: true,
1133 OFFSET: true,
1134 OLD: true,
1135 ON: true,
1136 ONLINE: true,
1137 ONLY: true,
1138 OPAQUE: true,
1139 OPEN: true,
1140 OPERATOR: true,
1141 OPTION: true,
1142 OR: true,
1143 ORDER: true,
1144 ORDINALITY: true,
1145 OTHER: true,
1146 OTHERS: true,
1147 OUT: true,
1148 OUTER: true,
1149 OUTPUT: true,
1150 OVER: true,
1151 OVERLAPS: true,
1152 OVERRIDE: true,
1153 OWNER: true,
1154 PAD: true,
1155 PARALLEL: true,
1156 PARAMETER: true,
1157 PARAMETERS: true,
1158 PARTIAL: true,
1159 PARTITION: true,
1160 PARTITIONED: true,
1161 PARTITIONS: true,
1162 PATH: true,
1163 PERCENT: true,
1164 PERCENTILE: true,
1165 PERMISSION: true,
1166 PERMISSIONS: true,
1167 PIPE: true,
1168 PIPELINED: true,
1169 PLAN: true,
1170 POOL: true,
1171 POSITION: true,
1172 PRECISION: true,
1173 PREPARE: true,
1174 PRESERVE: true,
1175 PRIMARY: true,
1176 PRIOR: true,
1177 PRIVATE: true,
1178 PRIVILEGES: true,
1179 PROCEDURE: true,
1180 PROCESSED: true,
1181 PROJECT: true,
1182 PROJECTION: true,
1183 PROPERTY: true,
1184 PROVISIONING: true,
1185 PUBLIC: true,
1186 PUT: true,
1187 QUERY: true,
1188 QUIT: true,
1189 QUORUM: true,
1190 RAISE: true,
1191 RANDOM: true,
1192 RANGE: true,
1193 RANK: true,
1194 RAW: true,
1195 READ: true,
1196 READS: true,
1197 REAL: true,
1198 REBUILD: true,
1199 RECORD: true,
1200 RECURSIVE: true,
1201 REDUCE: true,
1202 REF: true,
1203 REFERENCE: true,
1204 REFERENCES: true,
1205 REFERENCING: true,
1206 REGEXP: true,
1207 REGION: true,
1208 REINDEX: true,
1209 RELATIVE: true,
1210 RELEASE: true,
1211 REMAINDER: true,
1212 RENAME: true,
1213 REPEAT: true,
1214 REPLACE: true,
1215 REQUEST: true,
1216 RESET: true,
1217 RESIGNAL: true,
1218 RESOURCE: true,
1219 RESPONSE: true,
1220 RESTORE: true,
1221 RESTRICT: true,
1222 RESULT: true,
1223 RETURN: true,
1224 RETURNING: true,
1225 RETURNS: true,
1226 REVERSE: true,
1227 REVOKE: true,
1228 RIGHT: true,
1229 ROLE: true,
1230 ROLES: true,
1231 ROLLBACK: true,
1232 ROLLUP: true,
1233 ROUTINE: true,
1234 ROW: true,
1235 ROWS: true,
1236 RULE: true,
1237 RULES: true,
1238 SAMPLE: true,
1239 SATISFIES: true,
1240 SAVE: true,
1241 SAVEPOINT: true,
1242 SCAN: true,
1243 SCHEMA: true,
1244 SCOPE: true,
1245 SCROLL: true,
1246 SEARCH: true,
1247 SECOND: true,
1248 SECTION: true,
1249 SEGMENT: true,
1250 SEGMENTS: true,
1251 SELECT: true,
1252 SELF: true,
1253 SEMI: true,
1254 SENSITIVE: true,
1255 SEPARATE: true,
1256 SEQUENCE: true,
1257 SERIALIZABLE: true,
1258 SESSION: true,
1259 SET: true,
1260 SETS: true,
1261 SHARD: true,
1262 SHARE: true,
1263 SHARED: true,
1264 SHORT: true,
1265 SHOW: true,
1266 SIGNAL: true,
1267 SIMILAR: true,
1268 SIZE: true,
1269 SKEWED: true,
1270 SMALLINT: true,
1271 SNAPSHOT: true,
1272 SOME: true,
1273 SOURCE: true,
1274 SPACE: true,
1275 SPACES: true,
1276 SPARSE: true,
1277 SPECIFIC: true,
1278 SPECIFICTYPE: true,
1279 SPLIT: true,
1280 SQL: true,
1281 SQLCODE: true,
1282 SQLERROR: true,
1283 SQLEXCEPTION: true,
1284 SQLSTATE: true,
1285 SQLWARNING: true,
1286 START: true,
1287 STATE: true,
1288 STATIC: true,
1289 STATUS: true,
1290 STORAGE: true,
1291 STORE: true,
1292 STORED: true,
1293 STREAM: true,
1294 STRING: true,
1295 STRUCT: true,
1296 STYLE: true,
1297 SUB: true,
1298 SUBMULTISET: true,
1299 SUBPARTITION: true,
1300 SUBSTRING: true,
1301 SUBTYPE: true,
1302 SUM: true,
1303 SUPER: true,
1304 SYMMETRIC: true,
1305 SYNONYM: true,
1306 SYSTEM: true,
1307 TABLE: true,
1308 TABLESAMPLE: true,
1309 TEMP: true,
1310 TEMPORARY: true,
1311 TERMINATED: true,
1312 TEXT: true,
1313 THAN: true,
1314 THEN: true,
1315 THROUGHPUT: true,
1316 TIME: true,
1317 TIMESTAMP: true,
1318 TIMEZONE: true,
1319 TINYINT: true,
1320 TO: true,
1321 TOKEN: true,
1322 TOTAL: true,
1323 TOUCH: true,
1324 TRAILING: true,
1325 TRANSACTION: true,
1326 TRANSFORM: true,
1327 TRANSLATE: true,
1328 TRANSLATION: true,
1329 TREAT: true,
1330 TRIGGER: true,
1331 TRIM: true,
1332 TRUE: true,
1333 TRUNCATE: true,
1334 TTL: true,
1335 TUPLE: true,
1336 TYPE: true,
1337 UNDER: true,
1338 UNDO: true,
1339 UNION: true,
1340 UNIQUE: true,
1341 UNIT: true,
1342 UNKNOWN: true,
1343 UNLOGGED: true,
1344 UNNEST: true,
1345 UNPROCESSED: true,
1346 UNSIGNED: true,
1347 UNTIL: true,
1348 UPDATE: true,
1349 UPPER: true,
1350 URL: true,
1351 USAGE: true,
1352 USE: true,
1353 USER: true,
1354 USERS: true,
1355 USING: true,
1356 UUID: true,
1357 VACUUM: true,
1358 VALUE: true,
1359 VALUED: true,
1360 VALUES: true,
1361 VARCHAR: true,
1362 VARIABLE: true,
1363 VARIANCE: true,
1364 VARINT: true,
1365 VARYING: true,
1366 VIEW: true,
1367 VIEWS: true,
1368 VIRTUAL: true,
1369 VOID: true,
1370 WAIT: true,
1371 WHEN: true,
1372 WHENEVER: true,
1373 WHERE: true,
1374 WHILE: true,
1375 WINDOW: true,
1376 WITH: true,
1377 WITHIN: true,
1378 WITHOUT: true,
1379 WORK: true,
1380 WRAPPED: true,
1381 WRITE: true,
1382 YEAR: true,
1383 ZONE: true,
1384}
1385
1386function isReserved(name) {
1387 return RESERVED_WORDS[name.toUpperCase()] != null
1388}