1 | var once = require('once'),
|
2 | db = require('../db')
|
3 |
|
4 | module.exports = function query(store, data, cb) {
|
5 | cb = once(cb)
|
6 |
|
7 | store.getTable(data.TableName, function(err, table) {
|
8 | if (err) return cb(err)
|
9 |
|
10 | var keySchema = table.KeySchema, startKeyNames = keySchema.map(function(key) { return key.AttributeName }),
|
11 | hashKey = startKeyNames[0], rangeKey = startKeyNames[1], fetchFromItemDb = false, isLocal
|
12 |
|
13 | if (data.IndexName) {
|
14 | var index = db.traverseIndexes(table, function(attr, type, index, isGlobal) {
|
15 | if (index.IndexName == data.IndexName) {
|
16 | isLocal = !isGlobal
|
17 | return index
|
18 | }
|
19 | })
|
20 | if (index == null) {
|
21 | return cb(db.validationError('The table does not have the specified index: ' + data.IndexName))
|
22 | }
|
23 | if (!isLocal && data.ConsistentRead) {
|
24 | return cb(db.validationError('Consistent reads are not supported on global secondary indexes'))
|
25 | }
|
26 | keySchema = index.KeySchema
|
27 | fetchFromItemDb = data.Select == 'ALL_ATTRIBUTES' && index.Projection.ProjectionType != 'ALL'
|
28 | keySchema.forEach(function(key) { if (!~startKeyNames.indexOf(key.AttributeName)) startKeyNames.push(key.AttributeName) })
|
29 | hashKey = keySchema[0].AttributeName
|
30 | rangeKey = keySchema[1] && keySchema[1].AttributeName
|
31 | }
|
32 |
|
33 | if (data.ExclusiveStartKey && Object.keys(data.ExclusiveStartKey).length != startKeyNames.length) {
|
34 | return cb(db.validationError('The provided starting key is invalid'))
|
35 | }
|
36 |
|
37 | err = db.traverseKey(table, keySchema, function(attr, type, isHash) {
|
38 | if (data.ExclusiveStartKey) {
|
39 | if (!data.ExclusiveStartKey[attr]) {
|
40 | return db.validationError('The provided starting key is invalid')
|
41 | }
|
42 | err = db.validateKeyPiece(data.ExclusiveStartKey, attr, type, isHash)
|
43 | if (err) return err
|
44 | }
|
45 |
|
46 | if (isHash && keySchema.length == 1 && Object.keys(data.KeyConditions).length > 1) {
|
47 | return db.validationError('Query key condition not supported')
|
48 | }
|
49 |
|
50 | if (!data.KeyConditions[attr]) {
|
51 | if (isHash || Object.keys(data.KeyConditions).length > 1) {
|
52 | return db.validationError('Query condition missed key schema element: ' + attr)
|
53 | }
|
54 | return
|
55 | }
|
56 |
|
57 | var comparisonOperator = data.KeyConditions[attr].ComparisonOperator
|
58 |
|
59 | if (~['NULL', 'NOT_NULL', 'NE', 'CONTAINS', 'NOT_CONTAINS', 'IN'].indexOf(comparisonOperator)) {
|
60 | return db.validationError('Attempted conditional constraint is not an indexable operation')
|
61 | }
|
62 |
|
63 | if (data.KeyConditions[attr].AttributeValueList.some(function(attrVal) { return attrVal[type] == null })) {
|
64 | return db.validationError('One or more parameter values were invalid: Condition parameter type does not match schema type')
|
65 | }
|
66 |
|
67 | if (isHash && comparisonOperator != 'EQ') {
|
68 | return db.validationError('Query key condition not supported')
|
69 | }
|
70 | })
|
71 | if (err) return cb(err)
|
72 |
|
73 | var hashType = Object.keys(data.KeyConditions[hashKey].AttributeValueList[0])[0]
|
74 | var hashVal = data.KeyConditions[hashKey].AttributeValueList[0][hashType]
|
75 |
|
76 | if (data.ExclusiveStartKey) {
|
77 | var tableStartKey = table.KeySchema.reduce(function(obj, attr) {
|
78 | obj[attr.AttributeName] = data.ExclusiveStartKey[attr.AttributeName]
|
79 | return obj
|
80 | }, {})
|
81 | if ((err = db.validateKey(tableStartKey, table)) != null) {
|
82 | return cb(db.validationError('The provided starting key is invalid: ' + err.message))
|
83 | }
|
84 |
|
85 | if (!rangeKey || !data.KeyConditions[rangeKey]) {
|
86 | if (data.ExclusiveStartKey[hashKey][hashType] != hashVal) {
|
87 | return cb(db.validationError('The provided starting key is outside query boundaries based on provided conditions'))
|
88 | }
|
89 | } else {
|
90 | var matchesRange = db.compare(data.KeyConditions[rangeKey].ComparisonOperator,
|
91 | data.ExclusiveStartKey[rangeKey], data.KeyConditions[rangeKey].AttributeValueList)
|
92 | if (!matchesRange) {
|
93 | return cb(db.validationError('The provided starting key does not match the range key predicate'))
|
94 | }
|
95 | if (data.ExclusiveStartKey[hashKey][hashType] != hashVal) {
|
96 | return cb(db.validationError('The query can return at most one row and cannot be restarted'))
|
97 | }
|
98 | }
|
99 | }
|
100 |
|
101 | if ((err = db.validateKeyPaths((data._projection || {}).nestedPaths, table)) != null) return cb(err)
|
102 |
|
103 | if (data.QueryFilter || data._filter) {
|
104 | var pathHeads = data.QueryFilter ? data.QueryFilter : data._filter.pathHeads
|
105 | var propertyName = data.QueryFilter ? 'QueryFilter' : 'Filter Expression'
|
106 | err = db.traverseKey(table, keySchema, function(attr) {
|
107 | if (pathHeads[attr]) {
|
108 | return db.validationError(propertyName + ' can only contain non-primary key attributes: ' +
|
109 | 'Primary key attribute: ' + attr)
|
110 | }
|
111 | })
|
112 | if (err) return cb(err)
|
113 | }
|
114 |
|
115 | if (fetchFromItemDb && !isLocal) {
|
116 | return cb(db.validationError('One or more parameter values were invalid: ' +
|
117 | 'Select type ALL_ATTRIBUTES is not supported for global secondary index ' +
|
118 | data.IndexName + ' because its projection type is not ALL'))
|
119 | }
|
120 |
|
121 | if ((err = db.validateKeyPaths((data._filter || {}).nestedPaths, table)) != null) return cb(err)
|
122 |
|
123 | var opts = {reverse: data.ScanIndexForward === false, limit: data.Limit ? data.Limit + 1 : -1}
|
124 |
|
125 | opts.gte = db.hashPrefix(hashVal, hashType) + '/' + db.toRangeStr(hashVal, hashType) + '/'
|
126 | opts.lt = opts.gte + '~'
|
127 |
|
128 | if (data.KeyConditions[rangeKey]) {
|
129 | var rangeStrPrefix = db.toRangeStr(data.KeyConditions[rangeKey].AttributeValueList[0])
|
130 | var rangeStr = rangeStrPrefix + '/'
|
131 | var comp = data.KeyConditions[rangeKey].ComparisonOperator
|
132 | if (comp == 'EQ') {
|
133 | opts.gte += rangeStr
|
134 | opts.lte = opts.gte + '~'
|
135 | delete opts.lt
|
136 | } else if (comp == 'LT') {
|
137 | opts.lt = opts.gte + rangeStr
|
138 | } else if (comp == 'LE') {
|
139 | opts.lte = opts.gte + rangeStr + '~'
|
140 | delete opts.lt
|
141 | } else if (comp == 'GT') {
|
142 | opts.gt = opts.gte + rangeStr + '~'
|
143 | delete opts.gte
|
144 | } else if (comp == 'GE') {
|
145 | opts.gte += rangeStr
|
146 | } else if (comp == 'BEGINS_WITH') {
|
147 | opts.lt = opts.gte + rangeStrPrefix + '~'
|
148 | opts.gte += rangeStr
|
149 | } else if (comp == 'BETWEEN') {
|
150 | opts.lte = opts.gte + db.toRangeStr(data.KeyConditions[rangeKey].AttributeValueList[1]) + '/~'
|
151 | opts.gte += rangeStr
|
152 | delete opts.lt
|
153 | }
|
154 | }
|
155 |
|
156 | if (data.ExclusiveStartKey) {
|
157 | var createKey = data.IndexName ? db.createIndexKey : db.createKey
|
158 | var startKey = createKey(data.ExclusiveStartKey, table, keySchema)
|
159 |
|
160 | if (data.ScanIndexForward === false) {
|
161 | opts.lt = startKey
|
162 | delete opts.lte
|
163 | } else {
|
164 | opts.gt = startKey
|
165 | delete opts.gte
|
166 | }
|
167 | }
|
168 |
|
169 | db.queryTable(store, table, data, opts, isLocal, fetchFromItemDb, startKeyNames, cb)
|
170 | })
|
171 | }
|