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

sensedeep / dynamodb-onetable / #68

18 Aug 2023 03:12AM UTC coverage: 72.741% (+0.1%) from 72.614%
#68

push

Michael O'Brien
Merge branch 'main' of github.com:sensedeep/dynamodb-onetable

1121 of 1620 branches covered (69.2%)

Branch coverage included in aggregate %.

1801 of 2397 relevant lines covered (75.14%)

625.06 hits per line

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

90.11
/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
        this.addProperties(op, this.model.block, null, properties, this.puts)
959✔
84

85
        /*
86
            Emit mapped attributes that don't correspond to schema fields.
87
        */
88
        if (this.mapped) {
959✔
89
            for (let [att, props] of Object.entries(this.mapped)) {
959✔
90
                if (Object.keys(props).length != this.model.mappings[att].length) {
2!
91
                    throw new OneTableArgError(
×
92
                        `Missing properties for mapped data field "${att}" in model "${this.model.name}"`
93
                    )
94
                }
95
            }
96
            for (let [k, v] of Object.entries(this.mapped)) {
959✔
97
                let field = {attribute: [k], name: k, filter: false}
2✔
98
                this.add(op, field, k, properties, v, this.puts)
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
    /*
115
        Add properties to the command. This calls itself recursively for each schema nest level.
116
        At each nest level, the pathname describes the property name at that level.
117
        Arrays are handled linearly here. The "rec" is only used for put requests.
118
     */
119
    addProperties(op, block, pathname, properties, rec) {
120
        if (!properties || typeof properties != 'object') {
973!
121
            return
×
122
        }
123
        let fields = block.fields
973✔
124
        for (let [name, value] of Object.entries(properties)) {
973✔
125
            let field = fields[name]
7,617✔
126
            let path
127
            if ((op == 'put')) {
7,617✔
128
                path = field ? field.attribute[0] : name
6,633✔
129
            } else if (field) {
984✔
130
                path = pathname ? `${pathname}.${field.attribute[0]}` : field.attribute[0]
979✔
131
            } else {
132
                path = name
5✔
133
            }
134
            if (!path) {
7,617!
135
                this.table.log.info(`Path is null`, {op, pathname, fields, field, name, value})
×
136
            }
137
            if (!field) {
7,617✔
138
                if (this.model.generic) {
9✔
139
                    this.add(op, {attribute: [name], name}, path, properties, value, rec)
9✔
140
                }
141
                continue
9✔
142
            }
143
            let partial = this.model.getPartial(field, this.params)
7,608✔
144

145
            if (field.schema && value != null && partial) {
7,608✔
146
                if (field.isArray && Array.isArray(value)) {
15✔
147
                    rec[path] = []
2✔
148
                    for (let i = 0; i < value.length; i++) {
2✔
149
                        rec[path][i] = {}
1✔
150
                        let ipath = `${path}[${i}]`
1✔
151
                        this.addProperties(op, field.block, ipath, value[i], rec[path][i])
1✔
152
                    }
153
                } else {
154
                    rec[path] = {}
13✔
155
                    this.addProperties(op, field.block, path, value, rec[path])
13✔
156
                }
157
            } else {
158
                this.add(op, field, path, properties, value, rec)
7,593✔
159
            }
160
        }
161
    }
162

163
    /*
164
        Add a field to the command expression
165
     */
166
    add(op, field, path, properties, value, rec) {
167
        if (this.already[path]) {
7,604✔
168
            return
4✔
169
        }
170
        /*
171
            Handle mapped and packed attributes.
172
            The attribute[0] contains the top level attribute name. Attribute[1] contains a nested mapping name.
173
        */
174
        let attribute = field.attribute
7,600✔
175
        if (attribute.length > 1) {
7,600✔
176
            /*
177
                Save in mapped[] the mapped attributes which will be processed soon
178
             */
179
            let mapped = this.mapped
6✔
180
            let [k, v] = attribute
6✔
181
            mapped[k] = mapped[k] || {}
6✔
182
            mapped[k][v] = value
6✔
183
            if (op == 'put') {
6✔
184
                properties[k] = value
6✔
185
            }
186
            return
6✔
187
        }
188
        if (path == this.hash || path == this.sort) {
7,594✔
189
            if (op == 'find') {
1,799✔
190
                this.addKey(op, field, value)
131✔
191
            } else if (op == 'scan') {
1,668✔
192
                if (properties[field.name] !== undefined && field.filter !== false) {
46✔
193
                    this.addFilter(path, field, value)
46✔
194
                }
195
            } else if ((op == 'delete' || op == 'get' || op == 'update' || op == 'check') && field.isIndexed) {
1,622✔
196
                this.addKey(op, field, value)
369✔
197
            } else if (op == 'put' || (this.params.batch && op == 'update')) {
1,253!
198
                //  Batch does not use update expressions (Ugh!)
199
                rec[path] = value
1,253✔
200
            }
201
        } else if (op == 'find' || op == 'scan') {
5,795✔
202
            //  schema.filter == false disables a field from being used in a filter
203
            if (properties[field.name] !== undefined && field.filter !== false) {
98✔
204
                if (!this.params.batch) {
98✔
205
                    //  Batch does not support filter expressions
206
                    this.addFilter(path, field, value)
98✔
207
                }
208
            }
209
        } else if (op == 'put' || (this.params.batch && op == 'update')) {
5,697!
210
            //  Batch does not use update expressions (Ugh!)
211
            rec[path] = value
5,370✔
212
        } else if (op == 'update') {
327✔
213
            this.addUpdate(path, field, value)
298✔
214
        }
215
    }
216

217
    /*
218
        Conditions for create | delete | update
219
        May also be used by 'get' in fallback mode.
220
     */
221
    addConditions(op) {
222
        let {conditions, params} = this
742✔
223
        let {hash, sort} = this.index
742✔
224
        if (params.exists === true) {
742✔
225
            conditions.push(`attribute_exists(#_${this.addName(hash)})`)
64✔
226
            if (sort) {
64✔
227
                conditions.push(`attribute_exists(#_${this.addName(sort)})`)
63✔
228
            }
229
        } else if (params.exists === false) {
678✔
230
            conditions.push(`attribute_not_exists(#_${this.addName(hash)})`)
627✔
231
            if (sort) {
627✔
232
                conditions.push(`attribute_not_exists(#_${this.addName(sort)})`)
623✔
233
            }
234
        }
235
        if (params.type && sort) {
742!
236
            conditions.push(`attribute_type(#_${this.addName(sort)}, ${params.type})`)
×
237
        }
238
        if (op == 'update') {
742✔
239
            this.addUpdateConditions()
69✔
240
        }
241
        if (params.where) {
742✔
242
            conditions.push(this.expand(params.where))
4✔
243
        }
244
    }
245

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

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

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

304
    /*
305
        Add where filter expressions for find and scan
306
     */
307
    addWhereFilters() {
308
        if (this.params.where) {
141✔
309
            this.filters.push(this.expand(this.params.where))
5✔
310
        }
311
    }
312

313
    addFilter(pathname, field, value) {
314
        let {filters} = this
144✔
315
        /*
316
        let att = field.attribute[0]
317
        let pathname = field.pathname || att
318
        */
319
        if (pathname == this.hash || pathname == this.sort) {
144✔
320
            return
46✔
321
        }
322
        let [target, variable] = this.prepareKeyValue(pathname, value)
98✔
323
        filters.push(`${target} = ${variable}`)
98✔
324
    }
325

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

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

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

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

383
    addUpdate(pathname, field, value) {
384
        let {params, updates} = this
298✔
385
        /*
386
        let att = field.attribute[0]
387
        let pathname = field.pathname || att
388
        */
389
        if (pathname == this.hash || pathname == this.sort) {
298!
390
            return
×
391
        }
392
        if (field.name == this.model.typeField) {
298✔
393
            if (!(params.exists === null || params.exists == false)) {
66✔
394
                //  If not creating, then don't need to update the type as it must already exist
395
                return
62✔
396
            }
397
        }
398
        if (params.remove && params.remove.indexOf(field.name) >= 0) {
236!
399
            return
×
400
        }
401
        let target = this.prepareKey(pathname)
236✔
402
        let variable = this.addValueExp(value)
236✔
403
        updates.set.push(`${target} = ${variable}`)
236✔
404
    }
405

406
    addUpdateConditions() {
407
        let {params, updates} = this
69✔
408
        let fields = this.model.block.fields
69✔
409

410
        const assertIsNotPartition = (key, op) => {
69✔
411
            if (key == this.hash || key == this.sort) {
55!
412
                throw new OneTableArgError(`Cannot ${op} hash or sort`)
×
413
            }
414
        }
415

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

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

484
    selectIndex(indexes) {
485
        let index = indexes.primary
959✔
486
        if (this.params.index) {
959✔
487
            if (this.params.index != 'primary') {
28✔
488
                index = indexes[this.params.index]
28✔
489
            }
490
        }
491
        return index
959✔
492
    }
493

494
    /*
495
        Create the Dynamo command parameters. Called from Model.run
496
     */
497
    command() {
498
        let {conditions, filters, key, keys, hash, model, names, op, params, project, puts, values} = this
957✔
499

500
        if (key == null && values[hash] == null && op != 'scan') {
957!
501
            throw new OneTableError(`Cannot find hash key for "${op}"`, {values})
×
502
        }
503
        if (op == 'get' || op == 'delete' || op == 'update') {
957✔
504
            if (key == null) {
183!
505
                return null
×
506
            }
507
        }
508
        let namesLen = Object.keys(names).length
957✔
509
        let valuesLen = Object.keys(values).length
957✔
510

511
        if (op == 'put') {
957✔
512
            puts = this.table.marshall(puts, params)
629✔
513
        }
514
        values = this.table.marshall(values, params)
957✔
515
        key = this.table.marshall(key, params)
957✔
516

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

583
            if (op == 'delete' || op == 'get' || op == 'update' || op == 'check') {
931✔
584
                args.Key = key
173✔
585
            }
586
            if (op == 'find' || op == 'get' || op == 'scan') {
931✔
587
                args.ConsistentRead = params.consistent ? true : false
205!
588
                args.IndexName = params.index ? params.index : null
205✔
589
            }
590
            if (op == 'find' || op == 'scan') {
931✔
591
                args.Limit = params.limit ? params.limit : undefined
139✔
592
                /*
593
                    Scan reverse if either reverse or prev is true but not both. (XOR)
594
                    If both are true, then requesting the previous page of a reverse scan which is actually forwards.
595
                */
596
                args.ScanIndexForward =
139✔
597
                    (params.reverse == true) ^ (params.prev != null && params.next == null) ? false : true
279✔
598

599
                /*
600
                    Cherry pick the required properties from the next/prev param
601
                 */
602
                let cursor = params.next || params.prev
139✔
603
                if (cursor) {
139✔
604
                    let {hash, sort} = this.index
18✔
605
                    let start = {[hash]: cursor[hash], [sort]: cursor[sort]}
18✔
606
                    if (this.params.index != 'primary') {
18✔
607
                        let {hash, sort} = this.model.indexes.primary
18✔
608
                        start[hash] = cursor[hash]
18✔
609
                        start[sort] = cursor[sort]
18✔
610
                    }
611
                    args.ExclusiveStartKey = this.table.marshall(start, params)
18✔
612
                }
613
            }
614
            if (op == 'scan') {
931✔
615
                if (params.segments != null) {
69✔
616
                    args.TotalSegments = params.segments
4✔
617
                }
618
                if (params.segment != null) {
69✔
619
                    args.Segment = params.segment
4✔
620
                }
621
            }
622
        }
623
        //  Remove null entries
624
        if (args) {
955✔
625
            args = Object.fromEntries(Object.entries(args).filter(([, v]) => v != null))
8,847✔
626
        }
627

628
        if (typeof params.postFormat == 'function') {
955✔
629
            args = params.postFormat(model, args)
1✔
630
        }
631
        return args
955✔
632
    }
633

634
    /*
635
        Join the terms with 'and'
636
    */
637
    and(terms) {
638
        if (terms.length == 1) {
766✔
639
            return terms.join('')
76✔
640
        }
641
        return terms.map((t) => `(${t})`).join(' and ')
1,388✔
642
    }
643

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

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

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

© 2025 Coveralls, Inc