• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sensedeep / dynamodb-onetable / #65

pending completion
#65

push

Michael O'Brien
DEV: temporarily disable stream unit tests

1107 of 1601 branches covered (69.14%)

Branch coverage included in aggregate %.

1780 of 2373 relevant lines covered (75.01%)

623.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.76
/src/Expression.js
1
/*
2
    Expression.js - DynamoDB API command builder
3

4
    This module converts API requests into DynamoDB commands.
5
*/
6
import {OneTableArgError, OneTableError} from './Error.js'
50✔
7

8
//  Operators used on sort keys for get/delete
9
const KeyOperators = ['<', '<=', '=', '>=', '>', 'begins', 'begins_with', 'between']
50✔
10

11
export class Expression {
50✔
12
    constructor(model, op, properties, params = {}) {
×
13
        this.init(model, op, properties, params)
959✔
14
        this.prepare()
959✔
15
    }
16

17
    init(model, op, properties, params) {
18
        this.model = model
959✔
19
        this.op = op
959✔
20
        this.properties = properties
959✔
21
        this.params = params
959✔
22

23
        this.table = model.table
959✔
24
        this.already = {} //  Fields already processed (index is property name).
959✔
25
        this.conditions = [] //  Condition expressions.
959✔
26
        this.filters = [] //  Filter expressions.
959✔
27
        this.key = {} //  Primary key attribute.
959✔
28
        this.keys = [] //  Key conditions.
959✔
29
        this.mapped = {} //  Mapped fields.
959✔
30
        this.names = {} //  Expression names. Keys are the indexes.
959✔
31
        this.namesMap = {} //  Expression names reverse map. Keys are the names.
959✔
32
        this.puts = {} //  Put values
959✔
33
        this.project = [] //  Projection expressions.
959✔
34
        this.values = {} //  Expression values. Keys are the value indexes.
959✔
35
        this.valuesMap = {} //  Expression values reverse map. Keys are the values.
959✔
36
        this.nindex = 0 //  Next index into names.
959✔
37
        this.vindex = 0 //  Next index into values.
959✔
38
        this.updates = {
959✔
39
            add: [],
40
            delete: [],
41
            remove: [],
42
            set: [],
43
        }
44
        this.execute = params.execute === false ? false : true
959✔
45
        this.tableName = model.tableName
959✔
46

47
        /*
48
            Find the index for this expression. Then store the attribute names for the index.
49
         */
50
        this.index = this.selectIndex(model.indexes)
959✔
51

52
        /*
53
            Get the request index hash/sort attributes
54
         */
55
        this.hash = this.index.hash
959✔
56
        this.sort = this.index.sort
959✔
57

58
        if (!this.table.client) {
959!
59
            throw new OneTableArgError('Table has not yet defined a "client" instance')
×
60
        }
61
    }
62

63
    prepare() {
64
        let {op, params, properties} = this
959✔
65
        let fields = this.model.block.fields
959✔
66
        if (op == 'find') {
959✔
67
            this.addWhereFilters()
70✔
68
        } else if (op == 'delete' || op == 'put' || op == 'update' || op == 'check') {
889✔
69
            this.addConditions(op)
742✔
70
        } else if (op == 'scan') {
147✔
71
            this.addWhereFilters()
71✔
72
            /*
73
                Setup scan filters for properties outside the model.
74
                Use the property name here as there can't be a mapping.
75
            */
76
            for (let [name, value] of Object.entries(this.properties)) {
71✔
77
                if (fields[name] == null && value != null) {
103✔
78
                    this.addGenericFilter(name, value)
1✔
79
                    this.already[name] = true
1✔
80
                }
81
            }
82
        }
83

84
        this.addProperties(op, null, fields, properties)
959✔
85

86
        /*
87
            Emit mapped attributes that don't correspond to schema fields.
88
        */
89
        if (this.mapped) {
959✔
90
            for (let [att, props] of Object.entries(this.mapped)) {
959✔
91
                if (Object.keys(props).length != this.model.mappings[att].length) {
2!
92
                    throw new OneTableArgError(
×
93
                        `Missing properties for mapped data field "${att}" in model "${this.model.name}"`
94
                    )
95
                }
96
            }
97
            for (let [k, v] of Object.entries(this.mapped)) {
959✔
98
                this.add(null, properties, {attribute: [k], name: k, filter: false}, v, properties)
2✔
99
            }
100
        }
101
        if (params.fields) {
959✔
102
            for (let name of params.fields) {
4✔
103
                if (op == 'batchGet') {
5✔
104
                    //  BatchGet params.project must provide attributes not properties
105
                    this.project.push(`#_${this.addName(name)}`)
2✔
106
                } else if (fields[name]) {
3✔
107
                    let att = fields[name].attribute[0]
3✔
108
                    this.project.push(`#_${this.addName(att)}`)
3✔
109
                }
110
            }
111
        }
112
    }
113

114
    addProperties(op, pathname, fields, properties) {
115
        for (let [name, value] of Object.entries(properties)) {
967✔
116
            if (this.already[name]) {
7,605✔
117
                continue
4✔
118
            }
119
            let field = fields[name]
7,601✔
120
            if (field) {
7,601✔
121
                let partial = this.model.getPartial(field, this.params)
7,593✔
122
                if (op != 'put' && partial) {
7,593✔
123
                    if (field.schema && value != null) {
61✔
124
                        let path = pathname ? `${pathname}.${field.attribute[0]}` : field.attribute[0]
9✔
125
                        if (field.isArray && Array.isArray(value)) {
9✔
126
                            let i = 0
1✔
127
                            for (let rvalue of value) {
1✔
128
                                let indexPath = path ? `${path}[${i}]` : `${path}[${i}]`
×
129
                                this.addProperties(op, indexPath, field.block.fields, rvalue)
×
130
                                i++
×
131
                            }
132
                        } else {
133
                            this.addProperties(op, path, field.block.fields, value)
8✔
134
                        }
135
                    } else {
136
                        this.add(pathname, properties, field, value)
52✔
137
                    }
138
                } else {
139
                    this.add(pathname, properties, field, value)
7,532✔
140
                }
141
            } else if (this.model.generic) {
8✔
142
                this.add(pathname, properties, {attribute: [name], name}, value)
8✔
143
            }
144
        }
145
    }
146

147
    /*
148
        Add a field to the command expression
149
     */
150
    add(pathname, properties, field, value) {
151
        let op = this.op
7,594✔
152
        let attribute = field.attribute
7,594✔
153

154
        /*
155
            Handle mapped and packed attributes.
156
            The attribute[0] contains the top level attribute name. Attribute[1] contains a nested mapping name.
157
        */
158
        if (attribute.length > 1) {
7,594✔
159
            let mapped = this.mapped
6✔
160
            let [k, v] = attribute
6✔
161
            mapped[k] = mapped[k] || {}
6✔
162
            mapped[k][v] = value
6✔
163
            properties[k] = value
6✔
164
            return
6✔
165
        }
166
        //  May contain a '.'
167
        let path = pathname ? `${pathname}.${attribute[0]}` : attribute[0]
7,588✔
168

169
        if (path == this.hash || path == this.sort) {
7,588✔
170
            if (op == 'find') {
1,799✔
171
                this.addKey(op, field, value)
131✔
172
            } else if (op == 'scan') {
1,668✔
173
                if (properties[field.name] !== undefined && field.filter !== false) {
46✔
174
                    this.addFilter(path, field, value)
46✔
175
                }
176
            } else if ((op == 'delete' || op == 'get' || op == 'update' || op == 'check') && field.isIndexed) {
1,622✔
177
                this.addKey(op, field, value)
369✔
178
            } else if (op == 'put' || (this.params.batch && op == 'update')) {
1,253!
179
                //  Batch does not use update expressions (Ugh!)
180
                this.puts[path] = value
1,253✔
181
            }
182
        } else {
183
            if (op == 'find' || op == 'scan') {
5,789✔
184
                //  schema.filter == false disables a field from being used in a filter
185
                if (properties[field.name] !== undefined && field.filter !== false) {
98✔
186
                    if (!this.params.batch) {
98✔
187
                        //  Batch does not support filter expressions
188
                        this.addFilter(path, field, value)
98✔
189
                    }
190
                }
191
            } else if (op == 'put' || (this.params.batch && op == 'update')) {
5,691!
192
                //  Batch does not use update expressions (Ugh!)
193
                this.puts[path] = value
5,364✔
194
            } else if (op == 'update') {
327✔
195
                this.addUpdate(path, field, value)
298✔
196
            }
197
        }
198
    }
199

200
    /*
201
        Conditions for create | delete | update
202
        May also be used by 'get' in fallback mode.
203
     */
204
    addConditions(op) {
205
        let {conditions, params} = this
742✔
206
        let {hash, sort} = this.index
742✔
207
        if (params.exists === true) {
742✔
208
            conditions.push(`attribute_exists(#_${this.addName(hash)})`)
64✔
209
            if (sort) {
64✔
210
                conditions.push(`attribute_exists(#_${this.addName(sort)})`)
63✔
211
            }
212
        } else if (params.exists === false) {
678✔
213
            conditions.push(`attribute_not_exists(#_${this.addName(hash)})`)
627✔
214
            if (sort) {
627✔
215
                conditions.push(`attribute_not_exists(#_${this.addName(sort)})`)
623✔
216
            }
217
        }
218
        if (params.type && sort) {
742!
219
            conditions.push(`attribute_type(#_${this.addName(sort)}, ${params.type})`)
×
220
        }
221
        if (op == 'update') {
742✔
222
            this.addUpdateConditions()
69✔
223
        }
224
        if (params.where) {
742✔
225
            conditions.push(this.expand(params.where))
4✔
226
        }
227
    }
228

229
    /*
230
        Expand a where/set expression. Replace: ${var} and {value} tokens.
231
     */
232
    expand(where) {
233
        const expr = where
19✔
234
        let fields = this.model.block.fields
19✔
235
        //  Expand attribute references and make attribute name
236
        where = where.toString().replace(/\${(.*?)}/g, (match, varName) => {
19✔
237
            return this.makeTarget(fields, varName)
17✔
238
        })
239

240
        //  Expand variable substitutions
241
        where = where.replace(/@{(.*?)}/g, (match, value) => {
19✔
242
            let index
243
            const {substitutions} = this.params
2✔
244
            let name = value.replace(/^\.\.\./, '')
2✔
245
            if (!substitutions || substitutions[name] === undefined) {
2!
246
                throw new OneTableError(`Missing substitutions for attribute value "${name}"`, {
×
247
                    expr,
248
                    substitutions,
249
                    properties: this.properties,
250
                })
251
            }
252
            //  Support @{...list} to support filter expressions "IN ${...list}"
253
            if (value != name && Array.isArray(substitutions[name])) {
2!
254
                let indicies = []
×
255
                for (let item of substitutions[name]) {
×
256
                    indicies.push(this.addValue(item))
×
257
                }
258
                return indicies.map((i) => `:_${i}`).join(', ')
×
259
            }
260
            index = this.addValue(substitutions[name])
2✔
261
            return `:_${index}`
2✔
262
        })
263

264
        //  Expand value references and make attribute values. Allow new-lines in values.
265
        where = where.replace(/{(.*?)}/gs, (match, value) => {
19✔
266
            let index
267
            if (value.match(/^[-+]?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/)) {
21✔
268
                index = this.addValue(+value)
6✔
269
            } else {
270
                let matched = value.match(/^"(.*)"$/)
15✔
271
                if (matched) {
15✔
272
                    index = this.addValue(matched[1])
1✔
273
                } else if (value instanceof Date) {
14!
274
                    value = this.table.transformWriteDate(value)
×
275
                    index = this.addValue(value)
×
276
                } else if (value == 'true' || value == 'false') {
14!
277
                    index = this.addValue(value == 'true' ? true : false)
×
278
                } else {
279
                    index = this.addValue(value)
14✔
280
                }
281
            }
282
            return `:_${index}`
21✔
283
        })
284
        return where
19✔
285
    }
286

287
    /*
288
        Add where filter expressions for find and scan
289
     */
290
    addWhereFilters() {
291
        if (this.params.where) {
141✔
292
            this.filters.push(this.expand(this.params.where))
5✔
293
        }
294
    }
295

296
    addFilter(pathname, field, value) {
297
        let {filters} = this
144✔
298
        /*
299
        let att = field.attribute[0]
300
        let pathname = field.pathname || att
301
        */
302
        if (pathname == this.hash || pathname == this.sort) {
144✔
303
            return
46✔
304
        }
305
        let [target, variable] = this.prepareKeyValue(pathname, value)
98✔
306
        filters.push(`${target} = ${variable}`)
98✔
307
    }
308

309
    /*
310
        Add filters when model not known
311
     */
312
    addGenericFilter(att, value) {
313
        this.filters.push(`#_${this.addName(att)} = :_${this.addValue(value)}`)
1✔
314
    }
315

316
    /*
317
        Add key for find, delete, get or update
318
     */
319
    addKey(op, field, value) {
320
        let att = field.attribute[0]
500✔
321
        if (op == 'find') {
500✔
322
            let keys = this.keys
131✔
323
            if (att == this.sort && typeof value == 'object' && Object.keys(value).length > 0) {
131✔
324
                let [action, vars] = Object.entries(value)[0]
42✔
325
                if (KeyOperators.indexOf(action) < 0) {
42!
326
                    throw new OneTableArgError(`Invalid KeyCondition operator "${action}"`)
×
327
                }
328
                if (action == 'begins_with' || action == 'begins') {
42!
329
                    keys.push(`begins_with(#_${this.addName(att)}, :_${this.addValue(vars)})`)
42✔
330
                } else if (action == 'between') {
×
331
                    keys.push(
×
332
                        `#_${this.addName(att)} BETWEEN :_${this.addValue(vars[0])} AND :_${this.addValue(vars[1])}`
333
                    )
334
                } else {
335
                    keys.push(`#_${this.addName(att)} ${action} :_${this.addValue(value[action])}`)
×
336
                }
337
            } else {
338
                keys.push(`#_${this.addName(att)} = :_${this.addValue(value)}`)
89✔
339
            }
340
        } else {
341
            this.key[att] = value
369✔
342
        }
343
    }
344

345
    /*
346
        Convert literal attribute names to symbolic ExpressionAttributeName indexes
347
     */
348
    prepareKey(key) {
349
        this.already[key] = true
389✔
350
        return this.makeTarget(this.model.block.fields, key)
389✔
351
    }
352

353
    /*
354
        Convert attribute values to symbolic ExpressionAttributeValue indexes
355
     */
356
    prepareKeyValue(key, value) {
357
        let target = this.prepareKey(key)
129✔
358
        let requiresExpansion = typeof value == 'string' && value.match(/\${.*?}|@{.*?}|{.*?}/)
129✔
359
        if (requiresExpansion) {
129✔
360
            return [target, this.expand(value)]
10✔
361
        } else {
362
            return [target, this.addValueExp(value)]
119✔
363
        }
364
    }
365

366
    addUpdate(pathname, field, value) {
367
        let {params, updates} = this
298✔
368
        /*
369
        let att = field.attribute[0]
370
        let pathname = field.pathname || att
371
        */
372
        if (pathname == this.hash || pathname == this.sort) {
298!
373
            return
×
374
        }
375
        if (field.name == this.model.typeField) {
298✔
376
            if (!(params.exists === null || params.exists == false)) {
66✔
377
                //  If not creating, then don't need to update the type as it must already exist
378
                return
62✔
379
            }
380
        }
381
        if (params.remove && params.remove.indexOf(field.name) >= 0) {
236!
382
            return
×
383
        }
384
        let target = this.prepareKey(pathname)
236✔
385
        let variable = this.addValueExp(value)
236✔
386
        updates.set.push(`${target} = ${variable}`)
236✔
387
    }
388

389
    addUpdateConditions() {
390
        let {params, updates} = this
69✔
391
        let fields = this.model.block.fields
69✔
392

393
        const assertIsNotPartition = (key, op) => {
69✔
394
            if (key == this.hash || key == this.sort) {
55!
395
                throw new OneTableArgError(`Cannot ${op} hash or sort`)
×
396
            }
397
        }
398

399
        if (params.add) {
69✔
400
            for (let [key, value] of Object.entries(params.add)) {
4✔
401
                assertIsNotPartition(key, 'add')
5✔
402
                const [target, variable] = this.prepareKeyValue(key, value)
5✔
403
                updates.add.push(`${target} ${variable}`)
5✔
404
            }
405
        }
406
        if (params.delete) {
69✔
407
            for (let [key, value] of Object.entries(params.delete)) {
1✔
408
                assertIsNotPartition(key, 'delete')
2✔
409
                const [target, variable] = this.prepareKeyValue(key, value)
2✔
410
                updates.delete.push(`${target} ${variable}`)
2✔
411
            }
412
        }
413
        if (params.remove) {
69✔
414
            params.remove = [].concat(params.remove) // enforce array
16✔
415
            for (let key of params.remove) {
16✔
416
                assertIsNotPartition(key, 'remove')
23✔
417
                if (fields.required) {
23!
418
                    throw new OneTableArgError('Cannot remove required field')
×
419
                }
420
                const target = this.prepareKey(key)
23✔
421
                updates.remove.push(`${target}`)
23✔
422
            }
423
        }
424
        if (params.set) {
69✔
425
            for (let [key, value] of Object.entries(params.set)) {
15✔
426
                assertIsNotPartition(key, 'set')
24✔
427
                const [target, variable] = this.prepareKeyValue(key, value)
24✔
428
                updates.set.push(`${target} = ${variable}`)
24✔
429
            }
430
        }
431
        if (params.push) {
69✔
432
            for (let [key, value] of Object.entries(params.push)) {
1✔
433
                assertIsNotPartition(key, 'push')
1✔
434
                let empty = this.addValueExp([])
1✔
435
                let items = this.addValueExp([].concat(value)) // enforce array on values
1✔
436
                const target = this.prepareKey(key)
1✔
437
                updates.set.push(`${target} = list_append(if_not_exists(${target}, ${empty}), ${items})`)
1✔
438
            }
439
        }
440
    }
441

442
    //  Translate an attribute reference to use name attributes. Works with "."
443
    makeTarget(fields, name) {
444
        let target = []
406✔
445
        for (let prop of name.split('.')) {
406✔
446
            let subscript = prop.match(/\[[^\]]+\]+/)
429✔
447
            if (subscript) {
429✔
448
                prop = prop.replace(/\[[^\]]+\]+/, '')
6✔
449
                subscript = subscript[0]
6✔
450
            } else {
451
                subscript = ''
423✔
452
            }
453
            let field = fields ? fields[prop] : null
429!
454
            if (field) {
429✔
455
                target.push(`#_${this.addName(field.attribute[0])}${subscript}`)
418✔
456
                //  If nested schema, advance to the next level
457
                fields = field.schema ? field.block.fields : null
418✔
458
            } else {
459
                //  No field, so just use the property name.
460
                target.push(`#_${this.addName(prop)}${subscript}`)
11✔
461
                fields = null
11✔
462
            }
463
        }
464
        return target.join('.')
406✔
465
    }
466

467
    selectIndex(indexes) {
468
        let index = indexes.primary
959✔
469
        if (this.params.index) {
959✔
470
            if (this.params.index != 'primary') {
28✔
471
                index = indexes[this.params.index]
28✔
472
            }
473
        }
474
        return index
959✔
475
    }
476

477
    /*
478
        Create the Dynamo command parameters. Called from Model.run
479
     */
480
    command() {
481
        let {conditions, filters, key, keys, hash, model, names, op, params, project, puts, values} = this
957✔
482

483
        if (key == null && values[hash] == null && op != 'scan') {
957!
484
            throw new OneTableError(`Cannot find hash key for "${op}"`, {values})
×
485
        }
486
        if (op == 'get' || op == 'delete' || op == 'update') {
957✔
487
            if (key == null) {
183!
488
                return null
×
489
            }
490
        }
491
        let namesLen = Object.keys(names).length
957✔
492
        let valuesLen = Object.keys(values).length
957✔
493

494
        if (op == 'put') {
957✔
495
            puts = this.table.marshall(puts, params)
629✔
496
        }
497
        values = this.table.marshall(values, params)
957✔
498
        key = this.table.marshall(key, params)
957✔
499

500
        let args
501
        if (params.batch) {
957✔
502
            if (op == 'get') {
24✔
503
                args = {Keys: key}
9✔
504
            } else if (op == 'delete') {
15✔
505
                args = {Key: key}
4✔
506
            } else if (op == 'put') {
11!
507
                args = {Item: puts}
11✔
508
            } else {
509
                throw new OneTableArgError(`Unsupported batch operation "${op}"`)
×
510
            }
511
            if (filters.length) {
24!
512
                throw new OneTableArgError('Invalid filters with batch operation')
×
513
            }
514
        } else {
515
            args = {
933✔
516
                ConditionExpression: conditions.length ? this.and(conditions) : undefined,
933✔
517
                ExpressionAttributeNames: namesLen > 0 ? names : undefined,
933✔
518
                ExpressionAttributeValues: namesLen > 0 && valuesLen > 0 ? values : undefined,
2,677✔
519
                FilterExpression: filters.length ? this.and(filters) : undefined,
933✔
520
                KeyConditionExpression: keys.length ? keys.join(' and ') : undefined,
933✔
521
                ProjectionExpression: project.length ? project.join(', ') : undefined,
933✔
522
                TableName: this.tableName,
523
            }
524
            if (params.select) {
933✔
525
                //  Select: ALL_ATTRIBUTES | ALL_PROJECTED_ATTRIBUTES | SPECIFIC_ATTRIBUTES | COUNT
526
                if (project.length && params.select != 'SPECIFIC_ATTRIBUTES') {
2✔
527
                    throw new OneTableArgError('Select must be SPECIFIC_ATTRIBUTES with projection expressions')
1✔
528
                }
529
                args.Select = params.select
1✔
530
            } else if (params.count) {
931✔
531
                if (project.length) {
8✔
532
                    throw new OneTableArgError('Cannot use select and count together')
1✔
533
                }
534
                args.Select = 'COUNT'
7✔
535
            }
536
            if (params.stats || this.table.metrics) {
931✔
537
                args.ReturnConsumedCapacity = params.capacity || 'TOTAL' // INDEXES | TOTAL | NONE
1✔
538
                args.ReturnItemCollectionMetrics = 'SIZE' // SIZE | NONE
1✔
539
            }
540
            let returnValues
541
            if (params.return !== undefined) {
931✔
542
                if (params.return === true) {
23!
543
                    returnValues = op === 'delete' ? 'ALL_OLD' : 'ALL_NEW'
×
544
                } else if (params.return === false || params.return == 'none') {
23✔
545
                    returnValues = 'NONE'
1✔
546
                } else if (params.return != 'get') {
22✔
547
                    returnValues = params.return
16✔
548
                }
549
            }
550
            if (op == 'put') {
931✔
551
                args.Item = puts
618✔
552
                args.ReturnValues = returnValues || 'NONE'
618✔
553
            } else if (op == 'update') {
313✔
554
                args.ReturnValues = returnValues || 'ALL_NEW'
69✔
555
                let updates = []
69✔
556
                for (let op of ['add', 'delete', 'remove', 'set']) {
69✔
557
                    if (this.updates[op].length) {
276✔
558
                        updates.push(`${op} ${this.updates[op].join(', ')}`)
90✔
559
                    }
560
                }
561
                args.UpdateExpression = updates.join(' ')
69✔
562
            } else if (op == 'delete') {
244✔
563
                args.ReturnValues = returnValues || 'ALL_OLD'
35✔
564
            }
565

566
            if (op == 'delete' || op == 'get' || op == 'update' || op == 'check') {
931✔
567
                args.Key = key
173✔
568
            }
569
            if (op == 'find' || op == 'get' || op == 'scan') {
931✔
570
                args.ConsistentRead = params.consistent ? true : false
205!
571
                args.IndexName = params.index ? params.index : null
205✔
572
            }
573
            if (op == 'find' || op == 'scan') {
931✔
574
                args.Limit = params.limit ? params.limit : undefined
139✔
575
                /*
576
                    Scan reverse if either reverse or prev is true but not both. (XOR)
577
                    If both are true, then requesting the previous page of a reverse scan which is actually forwards.
578
                */
579
                args.ScanIndexForward =
139✔
580
                    (params.reverse == true) ^ (params.prev != null && params.next == null) ? false : true
279✔
581

582
                /*
583
                    Cherry pick the required properties from the next/prev param
584
                 */
585
                let cursor = params.next || params.prev
139✔
586
                if (cursor) {
139✔
587
                    let {hash, sort} = this.index
18✔
588
                    let start = {[hash]: cursor[hash], [sort]: cursor[sort]}
18✔
589
                    if (this.params.index != 'primary') {
18✔
590
                        let {hash, sort} = this.model.indexes.primary
18✔
591
                        start[hash] = cursor[hash]
18✔
592
                        start[sort] = cursor[sort]
18✔
593
                    }
594
                    args.ExclusiveStartKey = this.table.marshall(start, params)
18✔
595
                }
596
            }
597
            if (op == 'scan') {
931✔
598
                if (params.segments != null) {
69✔
599
                    args.TotalSegments = params.segments
4✔
600
                }
601
                if (params.segment != null) {
69✔
602
                    args.Segment = params.segment
4✔
603
                }
604
            }
605
        }
606
        //  Remove null entries
607
        if (args) {
955✔
608
            args = Object.fromEntries(Object.entries(args).filter(([, v]) => v != null))
8,847✔
609
        }
610

611
        if (typeof params.postFormat == 'function') {
955✔
612
            args = params.postFormat(model, args)
1✔
613
        }
614
        return args
955✔
615
    }
616

617
    /*
618
        Join the terms with 'and'
619
    */
620
    and(terms) {
621
        if (terms.length == 1) {
766✔
622
            return terms.join('')
76✔
623
        }
624
        return terms.map((t) => `(${t})`).join(' and ')
1,388✔
625
    }
626

627
    /*
628
        Add a name to the ExpressionAttribute names. Optimize duplicates and only store unique names once.
629
    */
630
    addName(name) {
631
        let index = this.namesMap[name]
1,943✔
632
        if (index == null) {
1,943✔
633
            index = this.nindex++
1,926✔
634
            this.names[`#_${index}`] = name
1,926✔
635
            this.namesMap[name] = index
1,926✔
636
        }
637
        return index
1,943✔
638
    }
639

640
    /*
641
        Add a value to the ExpressionAttribute values. Optimize duplicates and only store unique names once.
642
        Except for numbers because we don't want to confuse valuesMap indexes. i.e. 7 vs "7"
643
    */
644
    addValue(value) {
645
        let index
646
        if (value && typeof value != 'object' && typeof value != 'number') {
512✔
647
            index = this.valuesMap[value]
444✔
648
        }
649
        if (index == null) {
512✔
650
            index = this.vindex++
487✔
651
            this.values[`:_${index}`] = value
487✔
652
            if (value && typeof value != 'object' && typeof value != 'number') {
487✔
653
                this.valuesMap[value] = index
419✔
654
            }
655
        }
656
        return index
512✔
657
    }
658

659
    addValueExp(value) {
660
        return `:_${this.addValue(value)}`
357✔
661
    }
662
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc