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

sensedeep / dynamodb-onetable / #82

27 Dec 2024 04:58AM UTC coverage: 74.885% (-0.5%) from 75.377%
#82

push

web-flow
Merge pull request #542 from bozzaj/custom-generate

Add ability to use a field-specific custom generate function

1198 of 1682 branches covered (71.22%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 3 files covered. (100.0%)

309 existing lines in 4 files now uncovered.

1903 of 2459 relevant lines covered (77.39%)

742.14 hits per line

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

91.55
/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'
59✔
7

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

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

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

23
        this.table = model.table
1,265✔
24
        this.already = {} //  Fields already processed (index is property name).
1,265✔
25
        this.conditions = [] //  Condition expressions.
1,265✔
26
        this.filters = [] //  Filter expressions.
1,265✔
27
        this.key = {} //  Primary key attribute.
1,265✔
28
        this.keys = [] //  Key conditions.
1,265✔
29
        this.mapped = {} //  Mapped fields.
1,265✔
30
        this.names = {} //  Expression names. Keys are the indexes.
1,265✔
31
        this.namesMap = {} //  Expression names reverse map. Keys are the names.
1,265✔
32

33
        this.puts = {} //  Put values
1,265✔
34
        this.project = [] //  Projection expressions.
1,265✔
35
        this.values = {} //  Expression values. Keys are the value indexes.
1,265✔
36
        this.valuesMap = {} //  Expression values reverse map. Keys are the values.
1,265✔
37
        this.nindex = 0 //  Next index into names.
1,265✔
38
        this.vindex = 0 //  Next index into values.
1,265✔
39
        this.updates = {
1,265✔
40
            add: [],
41
            delete: [],
42
            remove: [],
43
            set: [],
44
        }
45
        this.execute = params.execute === false ? false : true
1,265✔
46
        this.tableName = model.tableName
1,265✔
47

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

53
        /*
54
            Get the request index hash/sort attributes
55
         */
56
        this.hash = this.index.hash
1,265✔
57
        this.sort = this.index.sort
1,265✔
58

59
        if (!this.table.client) {
1,265✔
60
            throw new OneTableArgError('Table has not yet defined a "client" instance')
1✔
61
        }
62
    }
63

64
    prepare() {
65
        let {op, params, properties} = this
1,264✔
66
        let fields = this.model.block.fields
1,264✔
67
        this.canPut = op == 'put' || (this.params.batch && op == 'update')
1,264✔
68
        if (op == 'find') {
1,264✔
69
            this.addWhereFilters()
83✔
70
        } else if (op == 'delete' || op == 'put' || op == 'update' || op == 'check') {
1,181✔
71
            this.addConditions(op)
1,021✔
72
        } else if (op == 'scan') {
160✔
73
            this.addWhereFilters()
72✔
74
            /*
75
                Setup scan filters for properties outside the model.
76
                Use the property name here as there can't be a mapping.
77
            */
78
            for (let [name, value] of Object.entries(this.properties)) {
72✔
79
                if (fields[name] == null && value != null) {
105✔
80
                    this.addGenericFilter(name, value)
1✔
81
                    this.already[name] = true
1✔
82
                }
83
            }
84
        }
85
        //  Batch does not use update expressions (Ugh!)
86
        this.puts = this.addProperties(op, this.model.block, properties)
1,264✔
87

88
        /*
89
            Emit mapped attributes that don't correspond to schema fields.
90
        */
91
        if (this.mapped) {
1,264✔
92
            for (let [att, props] of Object.entries(this.mapped)) {
1,264✔
93
                if (Object.keys(props).length != this.model.mappings[att].length) {
2!
UNCOV
94
                    throw new OneTableArgError(
×
95
                        `Missing properties for mapped data field "${att}" in model "${this.model.name}"`
96
                    )
97
                }
98
            }
99
            for (let [k, v] of Object.entries(this.mapped)) {
1,264✔
100
                let field = {attribute: [k], name: k, filter: false, path: k}
2✔
101
                this.add(op, properties, field, k, v)
2✔
102
                this.puts[k] = v
2✔
103
            }
104
        }
105
        if (params.fields) {
1,264✔
106
            for (let name of params.fields) {
10✔
107
                if (op == 'batchGet') {
19✔
108
                    //  BatchGet params.project must provide attributes not properties
109
                    this.project.push(`#_${this.addName(name)}`)
2✔
110
                } else if (this.model.generic){
17✔
111
                    // Generic models don't know which attributes exist, so we allow requesting all
112
                    this.project.push(`#_${this.addName(name)}`)
6✔
113
                } else if (fields[name]) {
11✔
114
                    let att = fields[name].attribute[0]
9✔
115
                    this.project.push(`#_${this.addName(att)}`)
9✔
116
                }
117
            }
118
        }
119
    }
120

121
    /*
122
        Add properties to the command. This calls itself recursively for each schema nest level.
123
        Emit update/filter expressions if emit is true
124
     */
125
    addProperties(op, block, properties, ppath = '', emit = true) {
2,528✔
126
        let rec = {}
1,299✔
127
        let fields = block.fields
1,299✔
128

129
        if (!properties || typeof properties != 'object') {
1,299!
UNCOV
130
            return properties
×
131
        }
132
        for (let [name, value] of Object.entries(properties)) {
1,299✔
133
            let field = fields[name]
9,878✔
134
            if (!field) {
9,878✔
135
                field = {attribute: [name], name, path: name}
9✔
136
                if (this.model.generic) {
9✔
137
                    this.add(op, properties, field, name, value)
9✔
138
                }
139
            } else {
140
                let attribute = field.attribute[0]
9,869✔
141
                let path = ppath ? `${ppath}.${attribute}` : attribute
9,869✔
142
                if (!field.schema) {
9,869✔
143
                    this.add(op, properties, field, path, value, emit)
9,830✔
144
                } else {
145
                    let partial = this.model.getPartial(field, this.params)
39✔
146
                    if (field.isArray && Array.isArray(value)) {
39✔
147
                        value = value.slice(0)
11✔
148
                        for (let [key, v] of Object.entries(value)) {
11✔
149
                            let ipath = `${path}[${key}]`
7✔
150
                            value[key] = this.addProperties(op, field.block, v, ipath, emit && partial)
7✔
151
                        }
152
                        if (emit && !partial) {
11✔
153
                            this.add(op, properties, field, path, value, emit)
5✔
154
                        }
155
                    } else {
156
                        value = this.addProperties(op, field.block, value, path, emit && partial)
28✔
157
                        if (emit && !partial) {
28✔
158
                            this.add(op, properties, field, path, value, true)
14✔
159
                        }
160
                    }
161
                }
162
            }
163
            rec[field.attribute[0]] = value
9,878✔
164
        }
165
        return rec
1,299✔
166
    }
167

168
    /*
169
        Add a field to the command expression
170
        If emit is true, then emit update/filter expressions for this property
171
     */
172
    add(op, properties, field, path, value, emit = true) {
11✔
173
        if (this.already[path]) {
9,860✔
174
            return
4✔
175
        }
176
        /*
177
            Handle mapped and packed attributes.
178
            The attribute[0] contains the top level attribute name and 
179
            Attribute[1] contains a nested mapping name.
180
        */
181
        let attribute = field.attribute
9,856✔
182
        if (attribute.length > 1) {
9,856✔
183
            /*
184
                Save in mapped[] the mapped attributes which will be processed soon
185
             */
186
            let mapped = this.mapped
6✔
187
            let [k, v] = attribute
6✔
188
            mapped[k] = mapped[k] || {}
6✔
189
            mapped[k][v] = value
6✔
190
            if (op == 'put') {
6✔
191
                properties[k] = value
6✔
192
            }
193
            return
6✔
194
        }
195
        if (path == this.hash || path == this.sort) {
9,850✔
196
            if (op == 'find') {
2,156✔
197
                this.addKey(op, field, value)
156✔
198
            } else if (op == 'scan') {
2,000✔
199
                if (properties[field.name] !== undefined && field.filter !== false) {
46✔
200
                    this.addFilter(field, path, value)
46✔
201
                }
202
            } else if ((op == 'delete' || op == 'get' || op == 'update' || op == 'check') && field.isIndexed) {
1,954✔
203
                this.addKey(op, field, value)
427✔
204
            }
205
        } else if (emit) {
7,694✔
206
            if (op == 'find' || op == 'scan') {
7,659✔
207
                //  schema.filter == false disables a field from being used in a filter
208
                if (properties[field.name] !== undefined && field.filter !== false) {
110✔
209
                    if (!this.params.batch) {
110✔
210
                        //  Batch does not support filter expressions
211
                        this.addFilter(field, path, value)
110✔
212
                    }
213
                }
214
            } else if (op == 'update') {
7,549✔
215
                this.addUpdate(field, path, value)
345✔
216
            }
217
        }
218
    }
219

220
    /*
221
        Conditions for create | delete | update
222
        May also be used by 'get' in fallback mode.
223
     */
224
    addConditions(op) {
225
        let {conditions, params} = this
1,021✔
226
        let {hash, sort} = this.index
1,021✔
227
        if (params.exists === true) {
1,021✔
228
            conditions.push(`attribute_exists(#_${this.addName(hash)})`)
80✔
229
            if (sort) {
80✔
230
                conditions.push(`attribute_exists(#_${this.addName(sort)})`)
79✔
231
            }
232
        } else if (params.exists === false) {
941✔
233
            conditions.push(`attribute_not_exists(#_${this.addName(hash)})`)
889✔
234
            if (sort) {
889✔
235
                conditions.push(`attribute_not_exists(#_${this.addName(sort)})`)
635✔
236
            }
237
        }
238
        if (params.type && sort) {
1,021!
UNCOV
239
            conditions.push(`attribute_type(#_${this.addName(sort)}, ${params.type})`)
×
240
        }
241
        if (op == 'update') {
1,021✔
242
            this.addUpdateConditions()
85✔
243
        }
244
        if (params.where) {
1,021✔
245
            conditions.push(this.expand(params.where))
4✔
246
        }
247
    }
248

249
    /*
250
        Expand a where/set expression. Replace: ${var} and {value} tokens.
251
     */
252
    expand(where) {
253
        const expr = where
23✔
254
        let fields = this.model.block.fields
23✔
255
        //  Expand attribute references and make attribute name
256
        where = where.toString().replace(/\${(.*?)}/g, (match, varName) => {
23✔
257
            return this.makeTarget(fields, varName)
19✔
258
        })
259

260
        //  Expand variable substitutions
261
        where = where.replace(/@{(.*?)}/g, (match, value) => {
23✔
262
            let index
263
            const {substitutions} = this.params
4✔
264
            let name = value.replace(/^\.\.\./, '')
4✔
265
            if (!substitutions || substitutions[name] === undefined) {
4!
UNCOV
266
                throw new OneTableError(`Missing substitutions for attribute value "${name}"`, {
×
267
                    expr,
268
                    substitutions,
269
                    properties: this.properties,
270
                })
271
            }
272
            //  Support @{...list} to support filter expressions "IN ${...list}"
273
            if (value != name && Array.isArray(substitutions[name])) {
4!
UNCOV
274
                let indicies = []
×
275
                for (let item of substitutions[name]) {
×
276
                    indicies.push(this.addValue(item))
×
277
                }
UNCOV
278
                return indicies.map((i) => `:_${i}`).join(', ')
×
279
            }
280
            index = this.addValue(substitutions[name])
4✔
281
            return `:_${index}`
4✔
282
        })
283

284
        //  Expand value references and make attribute values. Allow new-lines in values.
285
        where = where.replace(/{(.*?)}/gs, (match, value) => {
23✔
286
            let index
287
            if (value.match(/^[-+]?([0-9]+(\.[0-9]*)?|\.[0-9]+)$/)) {
23✔
288
                index = this.addValue(+value)
8✔
289
            } else {
290
                let matched = value.match(/^"(.*)"$/)
15✔
291
                if (matched) {
15✔
292
                    index = this.addValue(matched[1])
1✔
293
                } else if (value instanceof Date) {
14!
UNCOV
294
                    value = this.table.transformWriteDate(value)
×
295
                    index = this.addValue(value)
×
296
                } else if (value == 'true' || value == 'false') {
14!
UNCOV
297
                    index = this.addValue(value == 'true' ? true : false)
×
298
                } else {
299
                    index = this.addValue(value)
14✔
300
                }
301
            }
302
            return `:_${index}`
23✔
303
        })
304
        return where
23✔
305
    }
306

307
    /*
308
        Add where filter expressions for find and scan
309
     */
310
    addWhereFilters() {
311
        if (this.params.where) {
155✔
312
            this.filters.push(this.expand(this.params.where))
5✔
313
        }
314
    }
315

316
    addFilter(field, path, value) {
317
        let {filters} = this
156✔
318
        if (path == this.hash || path == this.sort) {
156✔
319
            return
46✔
320
        }
321
        let [target, variable] = this.prepareKeyValue(path, value)
110✔
322
        filters.push(`${target} = ${variable}`)
110✔
323
    }
324

325
    /*
326
        Add filters when model not known
327
     */
328
    addGenericFilter(att, value) {
329
        this.filters.push(`#_${this.addName(att)} = :_${this.addValue(value)}`)
1✔
330
    }
331

332
    /*
333
        Add key for find, delete, get or update
334
     */
335
    addKey(op, field, value) {
336
        let att = field.attribute[0]
583✔
337
        if (op == 'find') {
583✔
338
            let keys = this.keys
156✔
339
            if (att == this.sort && typeof value == 'object' && Object.keys(value).length > 0) {
156✔
340
                let [action, vars] = Object.entries(value)[0]
49✔
341
                if (KeyOperators.indexOf(action) < 0) {
49!
UNCOV
342
                    throw new OneTableArgError(`Invalid KeyCondition operator "${action}"`)
×
343
                }
344
                if (action == 'begins_with' || action == 'begins') {
49!
345
                    keys.push(`begins_with(#_${this.addName(att)}, :_${this.addValue(vars)})`)
49✔
UNCOV
346
                } else if (action == 'between') {
×
347
                    keys.push(
×
348
                        `#_${this.addName(att)} BETWEEN :_${this.addValue(vars[0])} AND :_${this.addValue(vars[1])}`
349
                    )
350
                } else {
UNCOV
351
                    keys.push(`#_${this.addName(att)} ${action} :_${this.addValue(value[action])}`)
×
352
                }
353
            } else {
354
                keys.push(`#_${this.addName(att)} = :_${this.addValue(value)}`)
107✔
355
            }
356
        } else {
357
            this.key[att] = value
427✔
358
        }
359
    }
360

361
    /*
362
        Convert literal attribute names to symbolic ExpressionAttributeName indexes
363
     */
364
    prepareKey(key) {
365
        this.already[key] = true
438✔
366
        return this.makeTarget(this.model.block.fields, key)
438✔
367
    }
368

369
    /*
370
        Convert attribute values to symbolic ExpressionAttributeValue indexes
371
     */
372
    prepareKeyValue(key, value) {
373
        let target = this.prepareKey(key)
147✔
374
        let requiresExpansion = typeof value == 'string' && value.match(/\${.*?}|@{.*?}|{.*?}/)
147✔
375
        if (requiresExpansion) {
147✔
376
            return [target, this.expand(value)]
14✔
377
        } else {
378
            return [target, this.addValueExp(value)]
133✔
379
        }
380
    }
381

382
    addUpdate(field, path, value) {
383
        let {params, updates} = this
345✔
384
        if (path == this.hash || path == this.sort) {
345!
UNCOV
385
            return
×
386
        }
387
        if (field.name == this.model.typeField) {
345✔
388
            if (!(params.exists === null || params.exists == false)) {
82✔
389
                //  If not creating, then don't need to update the type as it must already exist
390
                return
78✔
391
            }
392
        }
393
        if (params.remove && params.remove.indexOf(field.name) >= 0) {
267!
UNCOV
394
            return
×
395
        }
396
        let target = this.prepareKey(path)
267✔
397
        let variable = this.addValueExp(value)
267✔
398
        updates.set.push(`${target} = ${variable}`)
267✔
399
    }
400

401
    addUpdateConditions() {
402
        let {params, updates} = this
85✔
403
        let fields = this.model.block.fields
85✔
404

405
        const assertIsNotPartition = (key, op) => {
85✔
406
            if (key == this.hash || key == this.sort) {
61!
UNCOV
407
                throw new OneTableArgError(`Cannot ${op} hash or sort`)
×
408
            }
409
        }
410

411
        if (params.add) {
85✔
412
            for (let [key, value] of Object.entries(params.add)) {
4✔
413
                assertIsNotPartition(key, 'add')
5✔
414
                const [target, variable] = this.prepareKeyValue(key, value)
5✔
415
                updates.add.push(`${target} ${variable}`)
5✔
416
            }
417
        }
418
        if (params.delete) {
85✔
419
            for (let [key, value] of Object.entries(params.delete)) {
1✔
420
                assertIsNotPartition(key, 'delete')
2✔
421
                const [target, variable] = this.prepareKeyValue(key, value)
2✔
422
                updates.delete.push(`${target} ${variable}`)
2✔
423
            }
424
        }
425
        if (params.remove) {
85✔
426
            params.remove = [].concat(params.remove) // enforce array
16✔
427
            for (let key of params.remove) {
16✔
428
                assertIsNotPartition(key, 'remove')
23✔
429
                if (fields.required) {
23!
UNCOV
430
                    throw new OneTableArgError('Cannot remove required field')
×
431
                }
432
                const target = this.prepareKey(key)
23✔
433
                updates.remove.push(`${target}`)
23✔
434
            }
435
        }
436
        if (params.set) {
85✔
437
            for (let [key, value] of Object.entries(params.set)) {
21✔
438
                assertIsNotPartition(key, 'set')
30✔
439
                const [target, variable] = this.prepareKeyValue(key, value)
30✔
440
                updates.set.push(`${target} = ${variable}`)
30✔
441
            }
442
        }
443
        if (params.push) {
85✔
444
            for (let [key, value] of Object.entries(params.push)) {
1✔
445
                assertIsNotPartition(key, 'push')
1✔
446
                let empty = this.addValueExp([])
1✔
447
                let items = this.addValueExp([].concat(value)) // enforce array on values
1✔
448
                const target = this.prepareKey(key)
1✔
449
                updates.set.push(`${target} = list_append(if_not_exists(${target}, ${empty}), ${items})`)
1✔
450
            }
451
        }
452
    }
453

454
    //  Translate an attribute reference to use name attributes. Works with "."
455
    makeTarget(fields, name) {
456
        let target = []
457✔
457
        for (let prop of name.split('.')) {
457✔
458
            let subscript = prop.match(/\[[^\]]+\]+/)
483✔
459
            if (subscript) {
483✔
460
                prop = prop.replace(/\[[^\]]+\]+/, '')
11✔
461
                subscript = subscript[0]
11✔
462
            } else {
463
                subscript = ''
472✔
464
            }
465
            let field = fields ? fields[prop] : null
483✔
466
            if (field) {
483✔
467
                target.push(`#_${this.addName(field.attribute[0])}${subscript}`)
459✔
468
                //  If nested schema, advance to the next level
469
                fields = field.schema ? field.block.fields : null
459✔
470
            } else {
471
                //  No field, so just use the property name.
472
                target.push(`#_${this.addName(prop)}${subscript}`)
24✔
473
                fields = null
24✔
474
            }
475
        }
476
        return target.join('.')
457✔
477
    }
478

479
    selectIndex(indexes) {
480
        let index = indexes.primary
1,265✔
481
        if (this.params.index) {
1,265✔
482
            if (this.params.index != 'primary') {
34✔
483
                index = indexes[this.params.index]
34✔
484
            }
485
        }
486
        return index
1,265✔
487
    }
488

489
    /*
490
        Create the Dynamo command parameters. Called from Model.run
491
     */
492
    command() {
493
        let {conditions, filters, key, keys, hash, model, names, op, params, project, puts, values} = this
1,262✔
494

495
        if (key == null && values[hash] == null && op != 'scan') {
1,262!
UNCOV
496
            throw new OneTableError(`Cannot find hash key for "${op}"`, {values})
×
497
        }
498
        if (op == 'get' || op == 'delete' || op == 'update') {
1,262✔
499
            if (key == null) {
212!
UNCOV
500
                return null
×
501
            }
502
        }
503
        let namesLen = Object.keys(names).length
1,262✔
504
        let valuesLen = Object.keys(values).length
1,262✔
505

506
        if (op == 'put') {
1,262✔
507
            puts = this.table.marshall(puts, params)
891✔
508
        }
509
        values = this.table.marshall(values, params)
1,262✔
510
        key = this.table.marshall(key, params)
1,262✔
511

512
        let args
513
        if (params.batch) {
1,262✔
514
            if (op == 'get') {
24✔
515
                args = {Keys: key}
9✔
516
            } else if (op == 'delete') {
15✔
517
                args = {Key: key}
4✔
518
            } else if (op == 'put') {
11!
519
                args = {Item: puts}
11✔
520
            } else {
UNCOV
521
                throw new OneTableArgError(`Unsupported batch operation "${op}"`)
×
522
            }
523
            if (filters.length) {
24!
UNCOV
524
                throw new OneTableArgError('Invalid filters with batch operation')
×
525
            }
526
        } else {
527
            args = {
1,238✔
528
                ConditionExpression: conditions.length ? this.and(conditions) : undefined,
1,238✔
529
                ExpressionAttributeNames: namesLen > 0 ? names : undefined,
1,238✔
530
                ExpressionAttributeValues: namesLen > 0 && valuesLen > 0 ? values : undefined,
3,581✔
531
                FilterExpression: filters.length ? this.and(filters) : undefined,
1,238✔
532
                KeyConditionExpression: keys.length ? keys.join(' and ') : undefined,
1,238✔
533
                ProjectionExpression: project.length ? project.join(', ') : undefined,
1,238✔
534
                TableName: this.tableName,
535
            }
536
            if (params.select) {
1,238✔
537
                //  Select: ALL_ATTRIBUTES | ALL_PROJECTED_ATTRIBUTES | SPECIFIC_ATTRIBUTES | COUNT
538
                if (project.length && params.select != 'SPECIFIC_ATTRIBUTES') {
2✔
539
                    throw new OneTableArgError('Select must be SPECIFIC_ATTRIBUTES with projection expressions')
1✔
540
                }
541
                args.Select = params.select
1✔
542
            } else if (params.count) {
1,236✔
543
                if (project.length) {
8✔
544
                    throw new OneTableArgError('Cannot use select and count together')
1✔
545
                }
546
                args.Select = 'COUNT'
7✔
547
            }
548
            if (params.stats || this.table.metrics) {
1,236✔
549
                args.ReturnConsumedCapacity = params.capacity || 'TOTAL' // INDEXES | TOTAL | NONE
4✔
550
                args.ReturnItemCollectionMetrics = 'SIZE' // SIZE | NONE
4✔
551
            }
552
            let returnValues
553
            if (params.return !== undefined) {
1,236✔
554
                if (params.return === true) {
25!
UNCOV
555
                    returnValues = op === 'delete' ? 'ALL_OLD' : 'ALL_NEW'
×
556
                } else if (params.return === false || params.return == 'none') {
25✔
557
                    returnValues = 'NONE'
1✔
558
                } else if (params.return != 'get') {
24✔
559
                    returnValues = params.return
18✔
560
                }
561
            }
562
            if (op == 'put') {
1,236✔
563
                args.Item = puts
880✔
564
                args.ReturnValues = returnValues || 'NONE'
880✔
565
            } else if (op == 'update') {
356✔
566
                args.ReturnValues = returnValues || 'ALL_NEW'
85✔
567
                let updates = []
85✔
568
                for (let op of ['add', 'delete', 'remove', 'set']) {
85✔
569
                    if (this.updates[op].length) {
340✔
570
                        updates.push(`${op} ${this.updates[op].join(', ')}`)
106✔
571
                    }
572
                }
573
                args.UpdateExpression = updates.join(' ')
85✔
574
            } else if (op == 'delete') {
271✔
575
                args.ReturnValues = returnValues || 'ALL_OLD'
36✔
576
            }
577

578
            if (op == 'delete' || op == 'get' || op == 'update' || op == 'check') {
1,236✔
579
                args.Key = key
202✔
580
            }
581
            if (op == 'find' || op == 'get' || op == 'scan') {
1,236✔
582
                args.ConsistentRead = params.consistent ? true : false
231!
583
                args.IndexName = params.index ? params.index : null
231✔
584
            }
585
            if (op == 'find' || op == 'scan') {
1,236✔
586
                args.Limit = params.limit ? params.limit : undefined
153✔
587
                /*
588
                    Scan reverse if either reverse or prev is true but not both. (XOR)
589
                    If both are true, then requesting the previous page of a reverse scan which is actually forwards.
590
                */
591
                args.ScanIndexForward =
153✔
592
                    (params.reverse == true) ^ (params.prev != null && params.next == null) ? false : true
307✔
593

594
                /*
595
                    Cherry pick the required properties from the next/prev param
596
                 */
597
                let cursor = params.next || params.prev
153✔
598
                if (cursor) {
153✔
599
                    let {hash, sort} = this.index
23✔
600
                    let start = {[hash]: cursor[hash]}
23✔
601
                    if (sort && cursor[sort]) {
23✔
602
                        start[sort] = cursor[sort]
23✔
603
                    }
604
                    if (this.params.index != 'primary') {
23✔
605
                        let {hash, sort} = this.model.indexes.primary
23✔
606
                        start[hash] = cursor[hash]
23✔
607
                        if (sort && cursor[sort] != null) {
23✔
608
                            start[sort] = cursor[sort]
18✔
609
                        }
610
                    }
611
                    if (start[hash]) {
23✔
612
                        args.ExclusiveStartKey = this.table.marshall(start, params)
23✔
613
                    }
614
                }
615
            }
616
            if (op == 'scan') {
1,236✔
617
                if (params.segments != null) {
70✔
618
                    args.TotalSegments = params.segments
4✔
619
                }
620
                if (params.segment != null) {
70✔
621
                    args.Segment = params.segment
4✔
622
                }
623
            }
624
        }
625
        //  Remove null entries
626
        if (args) {
1,260✔
627
            args = Object.fromEntries(Object.entries(args).filter(([, v]) => v != null))
11,659✔
628
        }
629

630
        if (typeof params.postFormat == 'function') {
1,260✔
631
            args = params.postFormat(model, args)
1✔
632
        }
633
        return args
1,260✔
634
    }
635

636
    /*
637
        Join the terms with 'and'
638
    */
639
    and(terms) {
640
        if (terms.length == 1) {
1,055✔
641
            return terms.join('')
336✔
642
        }
643
        return terms.map((t) => `(${t})`).join(' and ')
1,446✔
644
    }
645

646
    /*
647
        Add a name to the ExpressionAttribute names. Optimize duplicates and only store unique names once.
648
    */
649
    addName(name) {
650
        let index = this.namesMap[name]
2,340✔
651
        if (index == null) {
2,340✔
652
            index = this.nindex++
2,321✔
653
            this.names[`#_${index}`] = name
2,321✔
654
            this.namesMap[name] = index
2,321✔
655
        }
656
        return index
2,340✔
657
    }
658

659
    /*
660
        Add a value to the ExpressionAttribute values. Optimize duplicates and only store unique names once.
661
        Except for numbers because we don't want to confuse valuesMap indexes. i.e. 7 vs "7"
662
    */
663
    addValue(value) {
664
        let index
665
        if (value && typeof value != 'object' && typeof value != 'number') {
586✔
666
            index = this.valuesMap[value]
508✔
667
        }
668
        if (index == null) {
586✔
669
            index = this.vindex++
549✔
670
            this.values[`:_${index}`] = value
549✔
671
            if (value && typeof value != 'object' && typeof value != 'number') {
549✔
672
                this.valuesMap[value] = index
471✔
673
            }
674
        }
675
        return index
586✔
676
    }
677

678
    addValueExp(value) {
679
        return `:_${this.addValue(value)}`
402✔
680
    }
681
}
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