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

sensedeep / dynamodb-onetable / #66

pending completion
#66

push

Michael O'Brien
DEV: bump version

1128 of 1609 branches covered (70.11%)

Branch coverage included in aggregate %.

1808 of 2379 relevant lines covered (76.0%)

623.95 hits per line

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

79.06
/src/Model.js
1
/*
2
    Model.js - DynamoDB model class
3

4
    A model represents a DynamoDB single-table entity.
5
*/
6
import {Expression} from './Expression.js'
51✔
7
import {OneTableError, OneTableArgError} from './Error.js'
51✔
8

9
/*
10
    Ready / write tags for interceptions
11
 */
12
const ReadWrite = {
51✔
13
    delete: 'write',
14
    get: 'read',
15
    find: 'read',
16
    put: 'write',
17
    scan: 'read',
18
    update: 'write',
19
}
20

21
const TransformParseResponseAs = {
51✔
22
    delete: 'get',
23
    get: 'get',
24
    find: 'find',
25
    put: 'get',
26
    scan: 'scan',
27
    update: 'get',
28
}
29

30
const KeysOnly = {delete: true, get: true}
51✔
31
const TransactOps = {delete: 'Delete', get: 'Get', put: 'Put', update: 'Update', check: 'ConditionCheck'}
51✔
32
const BatchOps = {delete: 'DeleteRequest', put: 'PutRequest', update: 'PutRequest'}
51✔
33
const ValidTypes = ['array', 'arraybuffer', 'binary', 'boolean', 'buffer', 'date', 'number', 'object', 'set', 'string']
51✔
34
const SanityPages = 1000
51✔
35
const FollowThreads = 10
51✔
36

37
export class Model {
51✔
38
    /*
39
        @param table Instance of Table.
40
        @param name Name of the model.
41
        @param options Hash of options.
42
     */
43
    constructor(table, name, options = {}) {
×
44
        if (!table) {
283!
45
            throw new OneTableArgError('Missing table argument')
×
46
        }
47
        if (!table.typeField) {
283!
48
            throw new OneTableArgError('Invalid table instance')
×
49
        }
50
        if (!name) {
283!
51
            throw new OneTableArgError('Missing name of model')
×
52
        }
53
        this.table = table
283✔
54
        this.name = name
283✔
55
        this.options = options
283✔
56

57
        //  Primary hash and sort attributes and properties
58
        this.hash = null
283✔
59
        this.sort = null
283✔
60

61
        //  Cache table properties
62
        this.createdField = table.createdField
283✔
63
        this.generic = options.generic
283✔
64
        this.nested = false
283✔
65
        this.nulls = table.nulls
283✔
66
        this.tableName = table.name
283✔
67
        this.typeField = options.typeField || table.typeField
283✔
68
        this.generic = options.generic != null ? options.generic : table.generic
283✔
69
        this.timestamps = options.timestamps
283✔
70
        if (this.timestamps == null) {
283✔
71
            this.timestamps = table.timestamps
173✔
72
        }
73
        this.updatedField = table.updatedField
283✔
74
        this.block = {fields: {}, deps: []}
283✔
75

76
        /*
77
            Map Javascript API properties to DynamoDB attribute names. The schema fields
78
            map property may contain a '.' like 'obj.prop' to pack multiple properties into a single attribute.
79
            field.attribute = [attributeName, optional-sub-propertiy]
80
        */
81
        this.mappings = {}
283✔
82

83
        this.schema = table.schema
283✔
84
        this.indexes = this.schema.indexes
283✔
85

86
        if (!this.indexes) {
283!
87
            throw new OneTableArgError('Indexes must be defined on the Table before creating models')
×
88
        }
89
        this.indexProperties = this.getIndexProperties(this.indexes)
283✔
90

91
        let fields = options.fields || this.schema.definition.models[this.name]
283!
92
        if (fields) {
283✔
93
            this.prepModel(fields, this.block)
283✔
94
        }
95
    }
96

97
    /*
98
        Prepare a model based on the schema and compute the attribute mapping.
99
     */
100
    prepModel(schemaFields, block, parent) {
101
        let {fields} = block
295✔
102

103
        schemaFields = this.table.assign({}, schemaFields)
295✔
104
        if (!parent) {
295✔
105
            //  Top level only
106
            if (!schemaFields[this.typeField]) {
283✔
107
                schemaFields[this.typeField] = {type: String, hidden: true}
283✔
108
                if (!this.generic) {
283✔
109
                    schemaFields[this.typeField].required = true
228✔
110
                }
111
            }
112
            if (this.timestamps === true || this.timestamps == 'create') {
283✔
113
                schemaFields[this.createdField] = schemaFields[this.createdField] || {type: 'date'}
72✔
114
            }
115
            if (this.timestamps === true || this.timestamps == 'update') {
283✔
116
                schemaFields[this.updatedField] = schemaFields[this.updatedField] || {type: 'date'}
72✔
117
            }
118
        }
119
        let {indexes, table} = this
295✔
120
        let primary = indexes.primary
295✔
121

122
        //  Attributes that are mapped to a different attribute. Indexed by attribute name for this block.
123
        let mapTargets = {}
295✔
124
        let map = {}
295✔
125

126
        for (let [name, field] of Object.entries(schemaFields)) {
295✔
127
            if (!field.type) {
2,111!
128
                field.type = 'string'
×
129
                this.table.log.error(`Missing type field for ${field.name}`, {field})
×
130
            }
131
            //  Propagate parent schema partial overrides
132
            if (parent && field.partial === undefined && parent.partial !== undefined) {
2,111✔
133
                field.partial = parent.partial
2✔
134
            }
135
            field.name = name
2,111✔
136
            fields[name] = field
2,111✔
137
            field.isoDates = field.isoDates != null ? field.isoDates : table.isoDates || false
2,111!
138

139
            if (field.uuid) {
2,111!
140
                throw new OneTableArgError(
×
141
                    'The "uuid" schema property is deprecated. Please use "generate": "uuid or ulid" instead'
142
                )
143
            }
144

145
            field.type = this.checkType(field)
2,111✔
146

147
            /*
148
                Handle mapped attributes. May be packed also (obj.prop)
149
            */
150
            let to = field.map
2,111✔
151
            if (to) {
2,111✔
152
                let [att, sub] = to.split('.')
17✔
153
                mapTargets[att] = mapTargets[att] || []
17✔
154
                if (sub) {
17✔
155
                    if (map[name] && !Array.isArray(map[name])) {
9!
156
                        throw new OneTableArgError(`Map already defined as literal for ${this.name}.${name}`)
×
157
                    }
158
                    field.attribute = map[name] = [att, sub]
9✔
159
                    if (mapTargets[att].indexOf(sub) >= 0) {
9!
160
                        throw new OneTableArgError(`Multiple attributes in ${field.name} mapped to the target ${to}`)
×
161
                    }
162
                    mapTargets[att].push(sub)
9✔
163
                } else {
164
                    if (mapTargets[att].length > 1) {
8!
165
                        throw new OneTableArgError(`Multiple attributes in ${this.name} mapped to the target ${to}`)
×
166
                    }
167
                    field.attribute = map[name] = [att]
8✔
168
                    mapTargets[att].push(true)
8✔
169
                }
170
            } else {
171
                field.attribute = map[name] = [name]
2,094✔
172
            }
173
            if (field.nulls !== true && field.nulls !== false) {
2,111✔
174
                field.nulls = this.nulls
2,111✔
175
            }
176

177
            /*
178
                Handle index requirements
179
            */
180
            let index = this.indexProperties[field.attribute[0]]
2,111✔
181
            if (index && !parent) {
2,111✔
182
                field.isIndexed = true
698✔
183
                if (field.attribute.length > 1) {
698!
184
                    throw new OneTableArgError(`Cannot map property "${field.name}" to a compound attribute"`)
×
185
                }
186
                if (index == 'primary') {
698✔
187
                    field.required = true
549✔
188
                    let attribute = field.attribute[0]
549✔
189
                    if (attribute == primary.hash) {
549✔
190
                        this.hash = attribute
282✔
191
                    } else if (attribute == primary.sort) {
267✔
192
                        this.sort = attribute
267✔
193
                    }
194
                }
195
            }
196
            if (field.value) {
2,111✔
197
                //  Value template properties are hidden by default
198
                if (field.hidden == null) {
476✔
199
                    field.hidden = true
476✔
200
                }
201
            }
202
            /*
203
                Handle nested schema (recursive)
204
            */
205
            if (field.items && field.type == 'array') {
2,111✔
206
                field.schema = field.items.schema
1✔
207
                field.isArray = true
1✔
208
            }
209
            if (field.schema) {
2,111✔
210
                if (field.type == 'object' || field.type == 'array') {
12!
211
                    field.block = {deps: [], fields: {}}
12✔
212
                    this.prepModel(field.schema, field.block, field)
12✔
213
                    //  FUTURE - better to apply this to the field block
214
                    this.nested = true
12✔
215
                } else {
216
                    throw new OneTableArgError(
×
217
                        `Nested scheme does not supported "${field.type}" types for field "${field.name}" in model "${this.name}"`
218
                    )
219
                }
220
            }
221
        }
222
        if (Object.values(fields).find((f) => f.unique && f.attribute != this.hash && f.attribute != this.sort)) {
2,089✔
223
            this.hasUniqueFields = true
7✔
224
        }
225
        this.mappings = mapTargets
295✔
226

227
        /*
228
            Order the fields so value templates can depend on each other safely
229
        */
230
        for (let field of Object.values(fields)) {
295✔
231
            this.orderFields(block, field)
2,111✔
232
        }
233
    }
234

235
    checkType(field) {
236
        let type = field.type
2,111✔
237
        if (typeof type == 'function') {
2,111✔
238
            type = type.name
833✔
239
        }
240
        type = type.toLowerCase()
2,111✔
241
        if (ValidTypes.indexOf(type) < 0) {
2,111!
242
            throw new OneTableArgError(`Unknown type "${type}" for field "${field.name}" in model "${this.name}"`)
×
243
        }
244
        return type
2,111✔
245
    }
246

247
    orderFields(block, field) {
248
        let {deps, fields} = block
2,113✔
249
        if (deps.find((i) => i.name == field.name)) {
9,483✔
250
            return
1✔
251
        }
252
        if (field.value) {
2,112✔
253
            let vars = this.table.getVars(field.value)
476✔
254
            for (let path of vars) {
476✔
255
                let name = path.split(/[.[]/g).shift().trim(']')
501✔
256
                let ref = fields[name]
501✔
257
                if (ref && ref != field) {
501✔
258
                    if (ref.schema) {
500✔
259
                        this.orderFields(ref.block, ref)
2✔
260
                    } else if (ref.value) {
498!
261
                        this.orderFields(block, ref)
×
262
                    }
263
                }
264
            }
265
        }
266
        deps.push(field)
2,112✔
267
    }
268

269
    getPropValue(properties, path) {
270
        let v = properties
6,172✔
271
        for (let part of path.split('.')) {
6,172✔
272
            v = v[part]
6,176✔
273
        }
274
        return v
6,172✔
275
    }
276

277
    /*
278
        Run an operation on DynamodDB. The command has been parsed via Expression.
279
        Returns [] for find/scan, cmd if !execute, else returns item.
280
     */
281
    async run(op, expression) {
282
        let {index, properties, params} = expression
950✔
283

284
        /*
285
            Get a string representation of the API request
286
         */
287
        let cmd = expression.command()
950✔
288
        if (!expression.execute) {
948✔
289
            if (params.log !== false) {
1✔
290
                this.table.log[params.log ? 'info' : 'data'](
1!
291
                    `OneTable command for "${op}" "${this.name} (not executed)"`,
292
                    {
293
                        cmd,
294
                        op,
295
                        properties,
296
                        params,
297
                    }
298
                )
299
            }
300
            return cmd
1✔
301
        }
302
        /*
303
            Transactions save the command in params.transaction and wait for db.transaction() to be called.
304
         */
305
        let t = params.transaction
947✔
306
        if (t) {
947✔
307
            if (params.batch) {
65!
308
                throw new OneTableArgError('Cannot have batched transactions')
×
309
            }
310
            let top = TransactOps[op]
65✔
311
            if (top) {
65!
312
                params.expression = expression
65✔
313
                let items = (t.TransactItems = t.TransactItems || [])
65✔
314
                items.push({[top]: cmd})
65✔
315
                return this.transformReadItem(op, properties, properties, params)
65✔
316
            } else {
317
                throw new OneTableArgError(`Unknown transaction operation ${op}`)
×
318
            }
319
        }
320
        /*
321
            Batch operations save the command in params.transaction and wait for db.batchGet|batchWrite to be called.
322
         */
323
        let b = params.batch
882✔
324
        if (b) {
882✔
325
            params.expression = expression
24✔
326
            let ritems = (b.RequestItems = b.RequestItems || {})
24✔
327
            if (op == 'get') {
24✔
328
                let list = (ritems[this.tableName] = ritems[this.tableName] || {Keys: []})
9✔
329
                list.Keys.push(cmd.Keys)
9✔
330
                return this.transformReadItem(op, properties, properties, params)
9✔
331
            } else {
332
                let list = (ritems[this.tableName] = ritems[this.tableName] || [])
15✔
333
                let bop = BatchOps[op]
15✔
334
                list.push({[bop]: cmd})
15✔
335
                return this.transformReadItem(op, properties, properties, params)
15✔
336
            }
337
        }
338
        /*
339
            Prep the stats
340
        */
341
        let stats = params.stats
858✔
342
        if (stats && typeof params == 'object') {
858✔
343
            stats.count = stats.count || 0
1✔
344
            stats.scanned = stats.capacity || 0
1✔
345
            stats.capacity = stats.capacity || 0
1✔
346
        }
347

348
        /*
349
            Run command. Paginate if required.
350
         */
351
        let pages = 0,
858✔
352
            items = [],
858✔
353
            count = 0
858✔
354
        let maxPages = params.maxPages ? params.maxPages : SanityPages
858!
355
        let result
356
        do {
858✔
357
            result = await this.table.execute(this.name, op, cmd, properties, params)
863✔
358
            if (result.LastEvaluatedKey) {
862✔
359
                //  Continue next page
360
                cmd.ExclusiveStartKey = result.LastEvaluatedKey
26✔
361
            }
362
            if (result.Items) {
862✔
363
                items = items.concat(result.Items)
135✔
364
            } else if (result.Item) {
727✔
365
                items = [result.Item]
43✔
366
                break
43✔
367
            } else if (result.Attributes) {
684✔
368
                items = [result.Attributes]
80✔
369
                break
80✔
370
            } else if (params.count || params.select == 'COUNT') {
604✔
371
                count += result.Count
9✔
372
            }
373
            if (stats) {
739✔
374
                if (result.Count) {
1✔
375
                    stats.count += result.Count
1✔
376
                }
377
                if (result.ScannedCount) {
1✔
378
                    stats.scanned += result.ScannedCount
1✔
379
                }
380
                if (result.ConsumedCapacity) {
1✔
381
                    stats.capacity += result.ConsumedCapacity.CapacityUnits
1✔
382
                }
383
            }
384
            if (params.progress) {
739!
385
                params.progress({items, pages, stats, params, cmd})
×
386
            }
387
            if (items.length) {
739✔
388
                if (cmd.Limit) {
117✔
389
                    cmd.Limit -= result.Count
33✔
390
                    if (cmd.Limit <= 0) {
33✔
391
                        break
21✔
392
                    }
393
                }
394
            }
395
        } while (result.LastEvaluatedKey && (maxPages == null || ++pages < maxPages))
728✔
396

397
        let prev
398
        if ((op == 'find' || op == 'scan') && items.length) {
857✔
399
            if (items.length) {
114✔
400
                /*
401
                    Determine next / previous cursors. Note: data items not yet reversed if scanning backwards.
402
                    Can use LastEvaluatedKey for the direction of scanning. Calculate the other end from the returned items.
403
                    Next/prev will be swapped when the items are reversed below
404
                */
405
                let {hash, sort} = params.index && params.index != 'primary' ? index : this.indexes.primary
114✔
406
                let cursor = {[hash]: items[0][hash], [sort]: items[0][sort]}
114✔
407
                if (cursor[hash] == null || cursor[sort] == null) {
114✔
408
                    cursor = null
7✔
409
                }
410
                if (params.next || params.prev) {
114✔
411
                    prev = cursor
15✔
412
                    if (cursor && params.index != 'primary') {
15✔
413
                        let {hash, sort} = this.indexes.primary
15✔
414
                        prev[hash] = items[0][hash]
15✔
415
                        prev[sort] = items[0][sort]
15✔
416
                    }
417
                }
418
            }
419
        }
420

421
        /*
422
            Process the response
423
        */
424
        if (params.parse) {
857✔
425
            items = this.parseResponse(op, expression, items)
840✔
426
        }
427

428
        /*
429
            Handle pagination next/prev
430
        */
431
        if (op == 'find' || op == 'scan') {
857✔
432
            if (result.LastEvaluatedKey) {
139✔
433
                items.next = this.table.unmarshall(result.LastEvaluatedKey, params)
21✔
434
                Object.defineProperty(items, 'next', {enumerable: false})
21✔
435
            }
436
            if (params.count || params.select == 'COUNT') {
139✔
437
                items.count = count
8✔
438
                Object.defineProperty(items, 'count', {enumerable: false})
8✔
439
            }
440
            if (prev) {
139✔
441
                items.prev = this.table.unmarshall(prev, params)
15✔
442
                Object.defineProperty(items, 'prev', {enumerable: false})
15✔
443
            }
444
            if (params.prev && params.next == null && op != 'scan') {
139✔
445
                //  DynamoDB scan ignores ScanIndexForward
446
                items = items.reverse()
1✔
447
                let tmp = items.prev
1✔
448
                items.prev = items.next
1✔
449
                items.next = tmp
1✔
450
            }
451
        }
452

453
        /*
454
            Log unless the user provides params.log: false.
455
            The logger will typically filter data/trace.
456
        */
457
        if (params.log !== false) {
857✔
458
            this.table.log[params.log ? 'info' : 'data'](`OneTable result for "${op}" "${this.name}"`, {
856✔
459
                cmd,
460
                items,
461
                op,
462
                properties,
463
                params,
464
            })
465
        }
466

467
        /*
468
            Handle transparent follow. Get/Update/Find the actual item using the keys
469
            returned from the request on the GSI.
470
        */
471
        if (params.follow || (index.follow && params.follow !== false)) {
857!
472
            if (op == 'get') {
3!
473
                return await this.get(items[0])
×
474
            }
475
            if (op == 'update') {
3!
476
                properties = Object.assign({}, properties, items[0])
×
477
                return await this.update(properties)
×
478
            }
479
            if (op == 'find') {
3✔
480
                let results = [],
3✔
481
                    promises = []
3✔
482
                params = Object.assign({}, params)
3✔
483
                delete params.follow
3✔
484
                delete params.index
3✔
485
                delete params.fallback
3✔
486
                for (let item of items) {
3✔
487
                    promises.push(this.get(item, params))
3✔
488
                    if (promises.length > FollowThreads) {
3!
489
                        results = results.concat(await Promise.all(promises))
×
490
                        promises = []
×
491
                    }
492
                }
493
                if (promises.length) {
3✔
494
                    results = results.concat(await Promise.all(promises))
3✔
495
                }
496
                results.next = items.next
3✔
497
                results.prev = items.prev
3✔
498
                Object.defineProperty(results, 'next', {enumerable: false})
3✔
499
                Object.defineProperty(results, 'prev', {enumerable: false})
3✔
500
                return results
3✔
501
            }
502
        }
503
        return op == 'find' || op == 'scan' ? items : items[0]
854✔
504
    }
505

506
    /*
507
        Parse the response into Javascript objects and transform for the high level API.
508
     */
509
    parseResponse(op, expression, items) {
510
        let {properties, params} = expression
845✔
511
        let {schema, table} = this
845✔
512
        if (op == 'put') {
845✔
513
            //  Put requests do not return the item. So use the properties.
514
            items = [properties]
592✔
515
        } else {
516
            items = table.unmarshall(items, params)
253✔
517
        }
518
        for (let [index, item] of Object.entries(items)) {
845✔
519
            if (params.high && params.index == this.indexes.primary && item[this.typeField] != this.name) {
2,442!
520
                //  High level API on the primary index and item for a different model
521
                continue
×
522
            }
523
            let type = item[this.typeField] ? item[this.typeField] : this.name
2,442✔
524
            let model = schema.models[type] ? schema.models[type] : this
2,442✔
525
            if (model) {
2,442✔
526
                if (model == schema.uniqueModel) {
2,442!
527
                    //  Special "unique" model for unique fields. Don't return in result.
528
                    continue
×
529
                }
530
                items[index] = model.transformReadItem(op, item, properties, params)
2,442✔
531
            }
532
        }
533
        return items
845✔
534
    }
535

536
    /*
537
        Create/Put a new item. Will overwrite existing items if exists: null.
538
    */
539
    async create(properties, params = {}) {
588✔
540
        /* eslint-disable-next-line */
541
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true, exists: false}))
631✔
542
        let result
543
        if (this.hasUniqueFields) {
631✔
544
            result = await this.createUnique(properties, params)
6✔
545
        } else {
546
            result = await this.putItem(properties, params)
625✔
547
        }
548
        return result
626✔
549
    }
550

551
    /*
552
        Create an item with unique attributes. Use a transaction to create a unique item for each unique attribute.
553
     */
554
    async createUnique(properties, params) {
555
        if (params.batch) {
6!
556
            throw new OneTableArgError('Cannot use batch with unique properties which require transactions')
×
557
        }
558
        let transactHere = params.transaction ? false : true
6!
559
        let transaction = (params.transaction = params.transaction || {})
6✔
560
        let {hash, sort} = this.indexes.primary
6✔
561
        let fields = this.block.fields
6✔
562

563
        fields = Object.values(fields).filter((f) => f.unique && f.attribute != hash && f.attribute != sort)
45✔
564

565
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
6✔
566

567
        if (params.timestamps !== false) {
6✔
568
            if (this.timestamps === true || this.timestamps == 'create') {
6!
569
                properties[this.createdField] = timestamp
×
570
            }
571
            if (this.timestamps === true || this.timestamps == 'update') {
6!
572
                properties[this.updatedField] = timestamp
×
573
            }
574
        }
575
        params.prepared = properties = this.prepareProperties('put', properties, params)
6✔
576

577
        for (let field of fields) {
6✔
578
            if (properties[field.name] !== undefined) {
12✔
579
                let scope = ''
10✔
580
                if (field.scope) {
10!
581
                    scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
582
                }
583
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
10✔
584
                let sk = '_unique#'
10✔
585
                await this.schema.uniqueModel.create(
10✔
586
                    {[this.hash]: pk, [this.sort]: sk},
587
                    {transaction, exists: false, return: 'NONE'}
588
                )
589
            }
590
        }
591
        let item = await this.putItem(properties, params)
6✔
592

593
        if (!transactHere) {
6!
594
            return item
×
595
        }
596
        let expression = params.expression
6✔
597
        try {
6✔
598
            await this.table.transact('write', params.transaction, params)
6✔
599
        } catch (err) {
600
            if (
1✔
601
                err instanceof OneTableError &&
3✔
602
                err.code === 'TransactionCanceledException' &&
603
                err.context.err.message.indexOf('ConditionalCheckFailed') !== -1
604
            ) {
605
                let names = fields.map((f) => f.name).join(', ')
3✔
606
                throw new OneTableError(
1✔
607
                    `Cannot create unique attributes "${names}" for "${this.name}". An item of the same name already exists.`,
608
                    {properties, transaction, code: 'UniqueError'}
609
                )
610
            }
611
            throw err
×
612
        }
613
        let items = this.parseResponse('put', expression)
5✔
614
        return items[0]
5✔
615
    }
616

617
    async check(properties, params) {
618
        /* eslint-disable-next-line */
619
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
3✔
620
        properties = this.prepareProperties('get', properties, params)
3✔
621
        const expression = new Expression(this, 'check', properties, params)
3✔
622
        this.run('check', expression)
3✔
623
    }
624

625
    async find(properties = {}, params = {}) {
9✔
626
        /* eslint-disable-next-line */
627
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
60✔
628
        return await this.queryItems(properties, params)
60✔
629
    }
630

631
    async get(properties = {}, params = {}) {
33!
632
        /* eslint-disable-next-line */
633
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
81✔
634
        properties = this.prepareProperties('get', properties, params)
81✔
635
        if (params.fallback) {
81✔
636
            //  Fallback via find when using non-primary indexes
637
            params.limit = 2
13✔
638
            let items = await this.find(properties, params)
13✔
639
            if (items.length > 1) {
13✔
640
                throw new OneTableError('Get without sort key returns more than one result', {
2✔
641
                    properties,
642
                    code: 'NonUniqueError',
643
                })
644
            }
645
            return items[0]
11✔
646
        }
647
        //  FUTURE refactor to use getItem
648
        let expression = new Expression(this, 'get', properties, params)
68✔
649
        return await this.run('get', expression)
68✔
650
    }
651

652
    async load(properties = {}, params = {}) {
6!
653
        /* eslint-disable-next-line */
654
        ;({properties, params} = this.checkArgs(properties, params))
6✔
655
        properties = this.prepareProperties('get', properties, params)
6✔
656
        let expression = new Expression(this, 'get', properties, params)
6✔
657
        return await this.table.batchLoad(expression)
6✔
658
    }
659

660
    init(properties = {}, params = {}) {
×
661
        /* eslint-disable-next-line */
662
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
663
        return this.initItem(properties, params)
×
664
    }
665

666
    async remove(properties, params = {}) {
18✔
667
        /* eslint-disable-next-line */
668
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
41✔
669

670
        properties = this.prepareProperties('delete', properties, params)
41✔
671
        if (params.fallback || params.many) {
41✔
672
            return await this.removeByFind(properties, params)
3✔
673
        }
674
        let expression = new Expression(this, 'delete', properties, params)
38✔
675
        if (this.hasUniqueFields) {
38✔
676
            return await this.removeUnique(properties, params)
2✔
677
        } else {
678
            return await this.run('delete', expression)
36✔
679
        }
680
    }
681

682
    /*
683
        Remove multiple objects after doing a full find/query
684
     */
685
    async removeByFind(properties, params) {
686
        if (params.retry) {
3!
687
            throw new OneTableArgError('Remove cannot retry', {properties})
×
688
        }
689
        params.parse = true
3✔
690
        let findParams = Object.assign({}, params)
3✔
691
        delete findParams.transaction
3✔
692
        let items = await this.find(properties, findParams)
3✔
693
        if (items.length > 1 && !params.many) {
3!
694
            throw new OneTableError(`Removing multiple items from "${this.name}". Use many:true to enable.`, {
×
695
                properties,
696
                code: 'NonUniqueError',
697
            })
698
        }
699
        let response = []
3✔
700
        for (let item of items) {
3✔
701
            let removed
702
            if (this.hasUniqueFields) {
7!
703
                removed = await this.removeUnique(item, {retry: true, transaction: params.transaction})
×
704
            } else {
705
                removed = await this.remove(item, {retry: true, return: params.return, transaction: params.transaction})
7✔
706
            }
707
            response.push(removed)
7✔
708
        }
709
        return response
3✔
710
    }
711

712
    /*
713
        Remove an item with unique properties. Use transactions to remove unique items.
714
    */
715
    async removeUnique(properties, params) {
716
        let transactHere = params.transaction ? false : true
2!
717
        let transaction = (params.transaction = params.transaction || {})
2✔
718
        let {hash, sort} = this.indexes.primary
2✔
719
        let fields = Object.values(this.block.fields).filter(
2✔
720
            (f) => f.unique && f.attribute != hash && f.attribute != sort
16✔
721
        )
722

723
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
724

725
        let keys = {
2✔
726
            [hash]: properties[hash],
727
        }
728
        if (sort) {
2✔
729
            keys[sort] = properties[sort]
2✔
730
        }
731
        /*
732
            Get the prior item so we know the previous unique property values so they can be removed.
733
            This must be run here, even if part of a transaction.
734
        */
735
        let prior = await this.get(keys, {hidden: true})
2✔
736
        if (prior) {
2!
737
            prior = this.prepareProperties('update', prior)
2✔
738
        } else if (params.exists === undefined || params.exists == true) {
×
739
            throw new OneTableError('Cannot find existing item to remove', {properties, code: 'NotFoundError'})
×
740
        }
741

742
        for (let field of fields) {
2✔
743
            let sk = `_unique#`
6✔
744
            let scope = ''
6✔
745
            if (field.scope) {
6!
746
                scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
747
            }
748
            // If we had a prior record, remove unique values that existed
749
            if (prior && prior[field.name]) {
6✔
750
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${prior[field.name]}`
4✔
751
                await this.schema.uniqueModel.remove(
4✔
752
                    {[this.hash]: pk, [this.sort]: sk},
753
                    {transaction, exists: params.exists}
754
                )
755
            } else if (!prior && properties[field.name] !== undefined) {
2!
756
                // if we did not have a prior record and the field is defined, try to remove it
757
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
×
758
                await this.schema.uniqueModel.remove(
×
759
                    {[this.hash]: pk, [this.sort]: sk},
760
                    {
761
                        transaction,
762
                        exists: params.exists,
763
                    }
764
                )
765
            }
766
        }
767
        let removed = await this.deleteItem(properties, params)
2✔
768
        // Only execute transaction if we are not in a transaction
769
        if (transactHere) {
2✔
770
            removed = await this.table.transact('write', transaction, params)
2✔
771
        }
772
        return removed
2✔
773
    }
774

775
    async scan(properties = {}, params = {}) {
61✔
776
        /* eslint-disable-next-line */
777
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
51✔
778
        return await this.scanItems(properties, params)
51✔
779
    }
780

781
    async update(properties, params = {}) {
23✔
782
        /* eslint-disable-next-line */
783
        ;({properties, params} = this.checkArgs(properties, params, {exists: true, parse: true, high: true}))
67✔
784
        if (this.hasUniqueFields) {
67✔
785
            let hasUniqueProperties = Object.entries(properties).find((pair) => {
7✔
786
                return this.block.fields[pair[0]] && this.block.fields[pair[0]].unique
15✔
787
            })
788
            if (hasUniqueProperties) {
7✔
789
                return await this.updateUnique(properties, params)
5✔
790
            }
791
        }
792
        return await this.updateItem(properties, params)
62✔
793
    }
794

795
    async upsert(properties, params = {}) {
×
796
        params.exists = null
×
797
        return await this.update(properties, params)
×
798
    }
799

800
    /*
801
        Update an item with unique attributes and actually updating a unique property.
802
        Use a transaction to update a unique item for each unique attribute.
803
     */
804
    async updateUnique(properties, params) {
805
        if (params.batch) {
5!
806
            throw new OneTableArgError('Cannot use batch with unique properties which require transactions')
×
807
        }
808
        let transactHere = params.transaction ? false : true
5!
809
        let transaction = (params.transaction = params.transaction || {})
5✔
810
        let index = this.indexes.primary
5✔
811
        let {hash, sort} = index
5✔
812

813
        params.prepared = properties = this.prepareProperties('update', properties, params)
5✔
814
        let keys = {
5✔
815
            [index.hash]: properties[index.hash],
816
        }
817
        if (index.sort) {
5✔
818
            keys[index.sort] = properties[index.sort]
5✔
819
        }
820

821
        /*
822
            Get the prior item so we know the previous unique property values so they can be removed.
823
            This must be run here, even if part of a transaction.
824
        */
825
        let prior = await this.get(keys, {hidden: true})
5✔
826
        if (prior) {
5✔
827
            prior = this.prepareProperties('update', prior)
4✔
828
        } else if (params.exists === undefined || params.exists == true) {
1!
829
            throw new OneTableError('Cannot find existing item to update', {properties, code: 'NotFoundError'})
×
830
        }
831
        /*
832
            Create all required unique properties. Remove prior unique properties if they have changed.
833
        */
834
        let fields = Object.values(this.block.fields).filter(
5✔
835
            (f) => f.unique && f.attribute != hash && f.attribute != sort
40✔
836
        )
837

838
        for (let field of fields) {
5✔
839
            let toBeRemoved = params.remove && params.remove.includes(field.name)
15✔
840
            let isUnchanged = prior && properties[field.name] === prior[field.name]
15✔
841
            if (isUnchanged) {
15✔
842
                continue
3✔
843
            }
844

845
            let scope = ''
12✔
846
            if (field.scope) {
12!
847
                scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
848
            }
849
            let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
12✔
850
            let sk = `_unique#`
12✔
851
            // If we had a prior value AND value is changing or being removed, remove old value
852
            if (prior && prior[field.name] && (properties[field.name] !== undefined || toBeRemoved)) {
12✔
853
                /*
854
                    Remove prior unique properties if they have changed and create new unique property.
855
                */
856
                let priorPk = `_unique#${scope}${this.name}#${field.attribute}#${prior[field.name]}`
5✔
857
                if (pk == priorPk) {
5!
858
                    //  Hasn't changed
859
                    continue
×
860
                }
861
                await this.schema.uniqueModel.remove(
5✔
862
                    {[this.hash]: priorPk, [this.sort]: sk},
863
                    {
864
                        transaction,
865
                        exists: null,
866
                        execute: params.execute,
867
                        log: params.log,
868
                    }
869
                )
870
            }
871
            // If value is changing, add new unique value
872
            if (properties[field.name] !== undefined) {
12✔
873
                await this.schema.uniqueModel.create(
6✔
874
                    {[this.hash]: pk, [this.sort]: sk},
875
                    {
876
                        transaction,
877
                        exists: false,
878
                        return: 'NONE',
879
                        log: params.log,
880
                        execute: params.execute,
881
                    }
882
                )
883
            }
884
        }
885
        let item = await this.updateItem(properties, params)
5✔
886

887
        if (!transactHere) {
5!
888
            return item
×
889
        }
890

891
        /*
892
            Perform all operations in a transaction so update will only be applied if the unique properties can be created.
893
        */
894
        try {
5✔
895
            await this.table.transact('write', params.transaction, params)
5✔
896
        } catch (err) {
897
            if (
1✔
898
                err instanceof OneTableError &&
3✔
899
                err.code === 'TransactionCanceledException' &&
900
                err.context.err.message.indexOf('ConditionalCheckFailed') !== -1
901
            ) {
902
                let names = fields.map((f) => f.name).join(', ')
3✔
903
                throw new OneTableError(
1✔
904
                    `Cannot update unique attributes "${names}" for "${this.name}". An item of the same name already exists.`,
905
                    {properties, transaction, code: 'UniqueError'}
906
                )
907
            }
908
            throw err
×
909
        }
910
        if (params.return == 'none' || params.return == 'NONE' || params.return === false) {
4!
911
            return
×
912
        }
913
        if (params.return == 'get') {
4✔
914
            return await this.get(keys, {
4✔
915
                hidden: params.hidden,
916
                log: params.log,
917
                parse: params.parse,
918
                execute: params.execute,
919
            })
920
        }
921
        if (this.table.warn !== false) {
×
922
            console.warn(
×
923
                `Update with unique items uses transactions and cannot return the updated item.` +
924
                    `Use params {return: 'none'} to squelch this warning. ` +
925
                    `Use {return: 'get'} to do a non-transactional get of the item after the update. `
926
            )
927
        }
928
    }
929

930
    //  Low level API
931

932
    /* private */
933
    async deleteItem(properties, params = {}) {
1✔
934
        /* eslint-disable-next-line */
935
        ;({properties, params} = this.checkArgs(properties, params))
3✔
936
        if (!params.prepared) {
3✔
937
            properties = this.prepareProperties('delete', properties, params)
1✔
938
        }
939
        let expression = new Expression(this, 'delete', properties, params)
3✔
940
        return await this.run('delete', expression)
3✔
941
    }
942

943
    /* private */
944
    async getItem(properties, params = {}) {
×
945
        /* eslint-disable-next-line */
946
        ;({properties, params} = this.checkArgs(properties, params))
1✔
947
        properties = this.prepareProperties('get', properties, params)
1✔
948
        let expression = new Expression(this, 'get', properties, params)
1✔
949
        return await this.run('get', expression)
1✔
950
    }
951

952
    /* private */
953
    initItem(properties, params = {}) {
×
954
        /* eslint-disable-next-line */
955
        ;({properties, params} = this.checkArgs(properties, params))
×
956
        let fields = this.block.fields
×
957
        this.setDefaults('init', fields, properties, params)
×
958
        //  Ensure all fields are present
959
        for (let key of Object.keys(fields)) {
×
960
            if (properties[key] === undefined) {
×
961
                properties[key] = null
×
962
            }
963
        }
964
        this.runTemplates('put', '', this.indexes.primary, this.block.deps, properties, params)
×
965
        return properties
×
966
    }
967

968
    /* private */
969
    async putItem(properties, params = {}) {
×
970
        /* eslint-disable-next-line */
971
        ;({properties, params} = this.checkArgs(properties, params))
633✔
972
        if (!params.prepared) {
633✔
973
            if (params.timestamps !== false) {
627✔
974
                let timestamp = params.transaction
627✔
975
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
33✔
976
                    : new Date()
977

978
                if (this.timestamps === true || this.timestamps == 'create') {
627✔
979
                    properties[this.createdField] = timestamp
223✔
980
                }
981
                if (this.timestamps === true || this.timestamps == 'update') {
627✔
982
                    properties[this.updatedField] = timestamp
223✔
983
                }
984
            }
985
            properties = this.prepareProperties('put', properties, params)
627✔
986
        }
987
        let expression = new Expression(this, 'put', properties, params)
629✔
988
        return await this.run('put', expression)
629✔
989
    }
990

991
    /* private */
992
    async queryItems(properties = {}, params = {}) {
×
993
        /* eslint-disable-next-line */
994
        ;({properties, params} = this.checkArgs(properties, params))
70✔
995
        properties = this.prepareProperties('find', properties, params)
70✔
996
        let expression = new Expression(this, 'find', properties, params)
70✔
997
        return await this.run('find', expression)
70✔
998
    }
999

1000
    //  Note: scanItems will return all model types
1001
    /* private */
1002
    async scanItems(properties = {}, params = {}) {
22✔
1003
        /* eslint-disable-next-line */
1004
        ;({properties, params} = this.checkArgs(properties, params))
71✔
1005
        properties = this.prepareProperties('scan', properties, params)
71✔
1006
        let expression = new Expression(this, 'scan', properties, params)
71✔
1007
        return await this.run('scan', expression)
71✔
1008
    }
1009

1010
    /* private */
1011
    async updateItem(properties, params = {}) {
×
1012
        /* eslint-disable-next-line */
1013
        ;({properties, params} = this.checkArgs(properties, params))
70✔
1014
        if (this.timestamps === true || this.timestamps == 'update') {
70✔
1015
            if (params.timestamps !== false) {
45✔
1016
                let timestamp = params.transaction
45✔
1017
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
4✔
1018
                    : new Date()
1019
                properties[this.updatedField] = timestamp
45✔
1020
                if (params.exists == null) {
45✔
1021
                    let field = this.block.fields[this.createdField] || this.table
2!
1022
                    let when = field.isoDates ? timestamp.toISOString() : timestamp.getTime()
2!
1023
                    params.set = params.set || {}
2✔
1024
                    params.set[this.createdField] = `if_not_exists(\${${this.createdField}}, {${when}})`
2✔
1025
                }
1026
            }
1027
        }
1028
        properties = this.prepareProperties('update', properties, params)
70✔
1029
        let expression = new Expression(this, 'update', properties, params)
69✔
1030
        return await this.run('update', expression)
69✔
1031
    }
1032

1033
    /* private */
1034
    async fetch(models, properties = {}, params = {}) {
2!
1035
        /* eslint-disable-next-line */
1036
        ;({properties, params} = this.checkArgs(properties, params))
2✔
1037
        if (models.length == 0) {
2!
1038
            return {}
×
1039
        }
1040
        let where = []
2✔
1041
        for (let model of models) {
2✔
1042
            where.push(`\${${this.typeField}} = {${model}}`)
4✔
1043
        }
1044
        if (params.where) {
2!
1045
            params.where = `(${params.where}) and (${where.join(' or ')})`
×
1046
        } else {
1047
            params.where = where.join(' or ')
2✔
1048
        }
1049
        params.parse = true
2✔
1050
        params.hidden = true
2✔
1051

1052
        let items = await this.queryItems(properties, params)
2✔
1053
        return this.table.groupByType(items)
2✔
1054
    }
1055

1056
    /*
1057
        Map Dynamo types to Javascript types after reading data
1058
     */
1059
    transformReadItem(op, raw, properties, params) {
1060
        if (!raw) {
2,558!
1061
            return raw
×
1062
        }
1063
        return this.transformReadBlock(op, raw, properties, params, this.block.fields)
2,558✔
1064
    }
1065

1066
    transformReadBlock(op, raw, properties, params, fields) {
1067
        let rec = {}
2,643✔
1068
        for (let [name, field] of Object.entries(fields)) {
2,643✔
1069
            //  Skip hidden params. Follow needs hidden params to do the follow.
1070
            if (field.hidden && params.follow !== true) {
27,071✔
1071
                if (params.hidden === false || (params.hidden == null && this.table.hidden === false)) {
14,585✔
1072
                    continue
14,336✔
1073
                }
1074
            }
1075
            let att, sub
1076
            if (op == 'put') {
12,735✔
1077
                att = field.name
3,534✔
1078
            } else {
1079
                /* eslint-disable-next-line */
1080
                ;[att, sub] = field.attribute
9,201✔
1081
            }
1082
            let value = raw[att]
12,735✔
1083
            if (value === undefined) {
12,735✔
1084
                if (field.encode) {
2,588✔
1085
                    let [att, sep, index] = field.encode
2✔
1086
                    value = (raw[att] || '').split(sep)[index]
2!
1087
                }
1088
                if (value === undefined) {
2,588✔
1089
                    continue
2,586✔
1090
                }
1091
            }
1092
            if (sub) {
10,149✔
1093
                value = value[sub]
45✔
1094
            }
1095
            if (field.crypt && params.decrypt !== false) {
10,149✔
1096
                value = this.decrypt(value)
2✔
1097
            }
1098
            if (field.default !== undefined && value === undefined) {
10,149!
1099
                value = field.default
×
1100
            } else if (value === undefined) {
10,149!
1101
                if (field.required) {
×
1102
                    this.table.log.error(`Required field "${name}" in model "${this.name}" not defined in table item`, {
×
1103
                        model: this.name,
1104
                        raw,
1105
                        params,
1106
                        field,
1107
                    })
1108
                }
1109
            } else if (field.schema && value !== null && typeof value == 'object') {
10,149✔
1110
                if (field.items && Array.isArray(value)) {
85✔
1111
                    rec[name] = []
3✔
1112
                    let i = 0
3✔
1113
                    for (let rvalue of raw[att]) {
3✔
1114
                        rec[name][i] = this.transformReadBlock(
3✔
1115
                            op,
1116
                            rvalue,
1117
                            properties[name] || [],
4✔
1118
                            params,
1119
                            field.block.fields
1120
                        )
1121
                        i++
3✔
1122
                    }
1123
                } else {
1124
                    rec[name] = this.transformReadBlock(
82✔
1125
                        op,
1126
                        raw[att],
1127
                        properties[name] || {},
139✔
1128
                        params,
1129
                        field.block.fields
1130
                    )
1131
                }
1132
            } else {
1133
                rec[name] = this.transformReadAttribute(field, name, value, params, properties)
10,064✔
1134
            }
1135
        }
1136
        if (this.generic) {
2,643✔
1137
            //  Generic must include attributes outside the schema.
1138
            for (let [name, value] of Object.entries(raw)) {
12✔
1139
                if (rec[name] === undefined) {
48✔
1140
                    rec[name] = value
28✔
1141
                }
1142
            }
1143
        }
1144
        if (
2,643✔
1145
            params.hidden == true &&
2,688✔
1146
            rec[this.typeField] === undefined &&
1147
            !this.generic &&
1148
            this.block.fields == fields
1149
        ) {
1150
            rec[this.typeField] = this.name
1✔
1151
        }
1152
        if (this.table.params.transform) {
2,643✔
1153
            let opForTransform = TransformParseResponseAs[op]
6✔
1154
            rec = this.table.params.transform(this, ReadWrite[opForTransform], rec, properties, params, raw)
6✔
1155
        }
1156
        return rec
2,643✔
1157
    }
1158

1159
    transformReadAttribute(field, name, value, params, properties) {
1160
        if (typeof params.transform == 'function') {
10,064!
1161
            //  Invoke custom data transform after reading
1162
            return params.transform(this, 'read', name, value, properties)
×
1163
        }
1164
        if (field.type == 'date' && value != undefined) {
10,064✔
1165
            if (field.ttl) {
1,216!
1166
                //  Parse incase stored as ISO string
1167
                return new Date(new Date(value).getTime() * 1000)
×
1168
            } else {
1169
                return new Date(value)
1,216✔
1170
            }
1171
        }
1172
        if (field.type == 'buffer' || field.type == 'arraybuffer' || field.type == 'binary') {
8,848✔
1173
            return Buffer.from(value, 'base64')
10✔
1174
        }
1175
        return value
8,838✔
1176
    }
1177

1178
    /*
1179
        Validate properties and map types if required.
1180
        Note: this does not map names to attributes or evaluate value templates, that happens in Expression.
1181
     */
1182
    prepareProperties(op, properties, params = {}) {
6✔
1183
        delete params.fallback
990✔
1184
        let index = this.selectIndex(op, params)
990✔
1185

1186
        if (this.needsFallback(op, index, params)) {
990✔
1187
            params.fallback = true
9✔
1188
            return properties
9✔
1189
        }
1190
        //  DEPRECATE
1191
        this.tunnelProperties(properties, params)
981✔
1192

1193
        if (params.filter) {
981!
1194
            this.convertFilter(properties, params, index)
×
1195
        }
1196
        let rec = this.collectProperties(op, '', this.block, index, properties, params)
981✔
1197
        if (params.fallback) {
976✔
1198
            return properties
7✔
1199
        }
1200
        if (op != 'scan' && this.getHash(rec, this.block.fields, index, params) == null) {
969!
1201
            this.table.log.error(`Empty hash key`, {properties, params, op, rec, index, model: this.name})
×
1202
            throw new OneTableError(`Empty hash key. Check hash key and any value template variable references.`, {
×
1203
                properties,
1204
                rec,
1205
                code: 'MissingError',
1206
            })
1207
        }
1208
        if (this.table.params.transform && ReadWrite[op] == 'write') {
969✔
1209
            rec = this.table.params.transform(this, ReadWrite[op], rec, properties, params)
5✔
1210
        }
1211
        return rec
969✔
1212
    }
1213

1214
    /*
1215
        Convert a full text params.filter into a smart params.where
1216
        NOTE: this is prototype code and definitely not perfect! Use at own risk.
1217
     */
1218
    convertFilter(properties, params, index) {
1219
        let filter = params.filter
×
1220
        let fields = this.block.fields
×
1221
        let where
1222
        //  TODO support > >= < <= ..., AND or ...
1223
        let [name, value] = filter.split('=')
×
1224
        if (value) {
×
1225
            name = name.trim()
×
1226
            value = value.trim()
×
1227
            let field = fields[name]
×
1228
            if (field) {
×
1229
                name = field.map ? field.map : name
×
1230
                if (field.encode) {
×
1231
                    properties[name] = value
×
1232
                } else {
1233
                    where = `\${${name}} = {${value}}`
×
1234
                }
1235
            } else {
1236
                //  TODO support > >= < <= ..., AND or ...
1237
                where = `\${${name}} = {${value}}`
×
1238
            }
1239
        } else {
1240
            value = name
×
1241
            where = []
×
1242
            for (let [name, field] of Object.entries(fields)) {
×
1243
                let primary = this.indexes.primary
×
1244
                if (primary.hash == name || primary.sort == name || index.hash == name || index.sort == name) {
×
1245
                    continue
×
1246
                }
1247
                if (field.encode) {
×
1248
                    continue
×
1249
                }
1250
                name = field.map ? field.map : name
×
1251
                let term = `(contains(\${${name}}, {${filter}}))`
×
1252
                where.push(term)
×
1253
            }
1254
            if (where) {
×
1255
                where = where.join(' or ')
×
1256
            }
1257
        }
1258
        params.where = where
×
1259
        //  TODO SANITY
1260
        params.maxPages = 25
×
1261
    }
1262

1263
    //  Handle fallback for get/delete as GSIs only support find and scan
1264
    needsFallback(op, index, params) {
1265
        if (index != this.indexes.primary && op != 'find' && op != 'scan') {
990✔
1266
            if (params.low) {
9!
1267
                throw new OneTableArgError('Cannot use non-primary index for "${op}" operation')
×
1268
            }
1269
            return true
9✔
1270
        }
1271
        return false
981✔
1272
    }
1273

1274
    /*
1275
        Return the hash property name for the selected index.
1276
    */
1277
    getHash(rec, fields, index, params) {
1278
        let generic = params.generic != null ? params.generic : this.generic
898!
1279
        if (generic) {
898✔
1280
            return rec[index.hash]
17✔
1281
        }
1282
        let field = Object.values(fields).find((f) => f.attribute[0] == index.hash)
1,072✔
1283
        if (!field) {
881!
1284
            return null
×
1285
        }
1286
        return rec[field.name]
881✔
1287
    }
1288

1289
    /*
1290
        Get the index for the request
1291
    */
1292
    selectIndex(op, params) {
1293
        let index
1294
        if (params.index && params.index != 'primary') {
990✔
1295
            index = this.indexes[params.index]
37✔
1296
            if (!index) {
37!
1297
                throw new OneTableError(`Cannot find index ${params.index}`, {code: 'MissingError'})
×
1298
            }
1299
        } else {
1300
            index = this.indexes.primary
953✔
1301
        }
1302
        return index
990✔
1303
    }
1304

1305
    /*
1306
        Collect the required attribute from the properties and context.
1307
        This handles tunneled properties, blends context properties, resolves default values,
1308
        handles Nulls and empty strings, and invokes validations. Nested schemas are handled here.
1309

1310
        NOTE: pathname is only needed for DEPRECATED and undocumented callbacks.
1311
    */
1312
    collectProperties(op, pathname, block, index, properties, params, context, rec = {}) {
1,008✔
1313
        let fields = block.fields
1,008✔
1314
        if (!context) {
1,008✔
1315
            context = params.context || this.table.context
981✔
1316
        }
1317
        /*
1318
            First process nested schemas recursively
1319
        */
1320
        if (this.nested && !KeysOnly[op]) {
1,008✔
1321
            this.collectNested(op, pathname, fields, index, properties, params, context, rec)
76✔
1322
        }
1323
        /*
1324
            Then process the non-schema properties at this level (non-recursive)
1325
        */
1326
        this.addContext(op, fields, index, properties, params, context)
1,008✔
1327
        this.setDefaults(op, fields, properties, params)
1,008✔
1328
        this.runTemplates(op, pathname, index, block.deps, properties, params)
1,008✔
1329
        this.convertNulls(op, pathname, fields, properties, params)
1,008✔
1330
        this.validateProperties(op, fields, properties, params)
1,008✔
1331
        this.selectProperties(op, block, index, properties, params, rec)
1,003✔
1332
        this.transformProperties(op, fields, properties, params, rec)
1,003✔
1333
        return rec
1,003✔
1334
    }
1335

1336
    /*
1337
        Process nested schema recursively
1338
    */
1339
    collectNested(op, pathname, fields, index, properties, params, context, rec) {
1340
        for (let field of Object.values(fields)) {
76✔
1341
            let schema = field.schema || field?.items?.schema
741✔
1342
            if (schema) {
741✔
1343
                let name = field.name
64✔
1344
                let value = properties[name]
64✔
1345
                if (op == 'put' && value === undefined) {
64!
1346
                    value = field.required ? (field.type == 'array' ? [] : {}) : field.default
×
1347
                }
1348
                let ctx = context[name] || {}
64✔
1349
                let partial = this.getPartial(field, params)
64✔
1350

1351
                if (value === null && field.nulls === true) {
64!
1352
                    rec[name] = null
×
1353
                } else if (value !== undefined) {
64✔
1354
                    if (field.items && Array.isArray(value)) {
27✔
1355
                        rec[name] = []
2✔
1356
                        let i = 0
2✔
1357
                        for (let rvalue of value) {
2✔
1358
                            let path = pathname ? `${pathname}.${name}[${i}]` : `${name}[${i}]`
2!
1359
                            let obj = this.collectProperties(op, path, field.block, index, rvalue, params, ctx)
2✔
1360
                            //  Don't update properties if empty and partial and no default
1361
                            if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
2✔
1362
                                rec[name][i++] = obj
1✔
1363
                            }
1364
                        }
1365
                    } else {
1366
                        let path = pathname ? `${pathname}.${field.name}` : field.name
25✔
1367
                        let obj = this.collectProperties(op, path, field.block, index, value, params, ctx)
25✔
1368
                        if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
25✔
1369
                            rec[name] = obj
25✔
1370
                        }
1371
                    }
1372
                }
1373
            }
1374
        }
1375
    }
1376

1377
    /*
1378
        DEPRECATE - not needed anymore
1379
    */
1380
    tunnelProperties(properties, params) {
1381
        if (params.tunnel) {
981!
1382
            if (this.table.warn !== false) {
×
1383
                console.warn(
×
1384
                    'WARNING: tunnel properties should not be required for typescript and will be removed soon.'
1385
                )
1386
            }
1387
            for (let [kind, settings] of Object.entries(params.tunnel)) {
×
1388
                for (let [key, value] of Object.entries(settings)) {
×
1389
                    properties[key] = {[kind]: value}
×
1390
                }
1391
            }
1392
        }
1393
    }
1394

1395
    /*
1396
        Select the attributes to include in the request
1397
    */
1398
    selectProperties(op, block, index, properties, params, rec) {
1399
        let project = this.getProjection(index)
1,003✔
1400
        /*
1401
            NOTE: Value templates for unique items may need other properties when removing unique items
1402
        */
1403
        for (let [name, field] of Object.entries(block.fields)) {
1,003✔
1404
            if (field.schema) continue
11,424✔
1405
            let omit = false
11,344✔
1406

1407
            if (block == this.block) {
11,344✔
1408
                let attribute = field.attribute[0]
11,272✔
1409
                //  Missing sort key on a high-level API for get/delete
1410
                if (properties[name] == null && attribute == index.sort && params.high && KeysOnly[op]) {
11,272✔
1411
                    if (op == 'delete' && !params.many) {
7!
1412
                        throw new OneTableError('Missing sort key', {code: 'MissingError', properties, params})
×
1413
                    }
1414
                    /*
1415
                        Missing sort key for high level get, or delete without "any".
1416
                        Fallback to find to select the items of interest. Get will throw if more than one result is returned.
1417
                    */
1418
                    params.fallback = true
7✔
1419
                    return
7✔
1420
                }
1421
                if (KeysOnly[op] && attribute != index.hash && attribute != index.sort && !this.hasUniqueFields) {
11,265✔
1422
                    //  Keys only for get and delete. Must include unique properties and all properties if unique value templates.
1423
                    //  FUTURE: could have a "strict" mode where we warn for other properties instead of ignoring.
1424
                    omit = true
1,176✔
1425
                } else if (project && project.indexOf(attribute) < 0) {
10,089✔
1426
                    //  Attribute is not projected
1427
                    omit = true
16✔
1428
                } else if (name == this.typeField && name != index.hash && name != index.sort && op == 'find') {
10,073✔
1429
                    omit = true
66✔
1430
                } else if (field.encode) {
10,007✔
1431
                    omit = true
1✔
1432
                }
1433
            }
1434
            if (!omit && properties[name] !== undefined) {
11,337✔
1435
                rec[name] = properties[name]
7,687✔
1436
            }
1437
        }
1438
        if (block == this.block) {
996✔
1439
            //  Only do at top level
1440
            this.addProjectedProperties(op, properties, params, project, rec)
969✔
1441
        }
1442
    }
1443

1444
    getProjection(index) {
1445
        let project = index.project
1,003✔
1446
        if (project) {
1,003✔
1447
            if (project == 'all') {
50✔
1448
                project = null
46✔
1449
            } else if (project == 'keys') {
4!
1450
                let primary = this.indexes.primary
×
1451
                project = [primary.hash, primary.sort, index.hash, index.sort]
×
1452
                project = project.filter((v, i, a) => a.indexOf(v) === i)
×
1453
            } else if (Array.isArray(project)) {
4✔
1454
                let primary = this.indexes.primary
4✔
1455
                project = project.concat([primary.hash, primary.sort, index.hash, index.sort])
4✔
1456
                project = project.filter((v, i, a) => a.indexOf(v) === i)
28✔
1457
            }
1458
        }
1459
        return project
1,003✔
1460
    }
1461

1462
    //  For generic (table low level APIs), add all properties that are projected
1463
    addProjectedProperties(op, properties, params, project, rec) {
1464
        let generic = params.generic != null ? params.generic : this.generic
969!
1465
        if (generic && !KeysOnly[op]) {
969✔
1466
            for (let [name, value] of Object.entries(properties)) {
35✔
1467
                if (project && project.indexOf(name) < 0) {
28!
1468
                    continue
×
1469
                }
1470
                if (rec[name] === undefined) {
28✔
1471
                    //  Cannot do all type transformations - don't have enough info without fields
1472
                    if (value instanceof Date) {
9!
1473
                        if (this.isoDates) {
×
1474
                            rec[name] = value.toISOString()
×
1475
                        } else {
1476
                            rec[name] = value.getTime()
×
1477
                        }
1478
                    } else {
1479
                        rec[name] = value
9✔
1480
                    }
1481
                }
1482
            }
1483
        }
1484
        return rec
969✔
1485
    }
1486

1487
    /*
1488
        Add context to properties. If 'put', then for all fields, otherwise just key fields.
1489
        Context overrides properties.
1490
     */
1491
    addContext(op, fields, index, properties, params, context) {
1492
        for (let field of Object.values(fields)) {
1,008✔
1493
            if (field.schema) continue
11,530✔
1494
            if (op == 'put' || (field.attribute[0] != index.hash && field.attribute[0] != index.sort)) {
11,450✔
1495
                if (context[field.name] !== undefined) {
10,769✔
1496
                    properties[field.name] = context[field.name]
33✔
1497
                }
1498
            }
1499
        }
1500
        if (!this.generic && fields == this.block.fields) {
1,008✔
1501
            //  Set type field for the top level only
1502
            properties[this.typeField] = this.name
944✔
1503
        }
1504
    }
1505

1506
    /*
1507
        Set default property values on Put.
1508
    */
1509
    setDefaults(op, fields, properties, params) {
1510
        if (op != 'put' && op != 'init' && !(op == 'update' && params.exists == null)) {
1,008✔
1511
            return
346✔
1512
        }
1513
        for (let field of Object.values(fields)) {
662✔
1514
            if (field.schema) continue
7,530✔
1515
            let value = properties[field.name]
7,515✔
1516

1517
            //  Set defaults and uuid fields
1518
            if (value === undefined && !field.value) {
7,515✔
1519
                if (field.default !== undefined) {
1,365✔
1520
                    value = field.default
4✔
1521
                } else if (op == 'init') {
1,361!
1522
                    if (!field.generate) {
×
1523
                        //  Set non-default, non-uuid properties to null
1524
                        value = null
×
1525
                    }
1526
                } else if (field.generate) {
1,361✔
1527
                    let generate = field.generate
602✔
1528
                    if (generate === true) {
602!
1529
                        value = this.table.generate()
×
1530
                    } else if (generate == 'uuid') {
602✔
1531
                        value = this.table.uuid()
1✔
1532
                    } else if (generate == 'ulid') {
601!
1533
                        value = this.table.ulid()
601✔
1534
                    } else if (generate == 'uid') {
×
1535
                        value = this.table.uid(10)
×
1536
                    } else if (generate.indexOf('uid') == 0) {
×
1537
                        let [, size] = generate.split('(')
×
1538
                        value = this.table.uid(parseInt(size) || 10)
×
1539
                    }
1540
                }
1541
                if (value !== undefined) {
1,365✔
1542
                    properties[field.name] = value
606✔
1543
                }
1544
            }
1545
        }
1546
        return properties
662✔
1547
    }
1548

1549
    /*
1550
        Remove null properties from the table unless Table.nulls == true
1551
        TODO - null conversion would be better done in Expression then pathnames would not be needed.
1552
        NOTE: pathname is only needed for DEPRECATED callbacks.
1553
    */
1554
    convertNulls(op, pathname, fields, properties, params) {
1555
        for (let [name, value] of Object.entries(properties)) {
1,008✔
1556
            let field = fields[name]
8,108✔
1557
            if (!field || field.schema) continue
8,108✔
1558
            if (value === null && field.nulls !== true) {
8,063✔
1559
                //  create with null/undefined, or update with null property
1560
                if (
10✔
1561
                    field.required &&
13!
1562
                    ((op == 'put' && properties[field.name] == null) ||
1563
                        (op == 'update' && properties[field.name] === null))
1564
                ) {
1565
                    //  Validation will catch this
1566
                    continue
1✔
1567
                }
1568
                delete properties[name]
9✔
1569
                if (this.getPartial(field, params) === false && pathname.match(/[[.]/)) {
9!
1570
                    /*
1571
                        Partial disabled for a nested object 
1572
                        Don't create remove entry as the entire object is being created/updated
1573
                     */
1574
                    continue
×
1575
                }
1576
                if (params.remove && !Array.isArray(params.remove)) {
9!
1577
                    params.remove = [params.remove]
×
1578
                } else {
1579
                    params.remove = params.remove || []
9✔
1580
                }
1581
                let path = pathname ? `${pathname}.${field.name}` : field.name
9✔
1582
                params.remove.push(path)
9✔
1583
            } else if (typeof value == 'object' && (field.type == 'object' || field.type == 'array')) {
8,053✔
1584
                //  Remove nested empty strings because DynamoDB cannot handle these nested in objects or arrays
1585
                properties[name] = this.handleEmpties(field, value)
91✔
1586
            }
1587
        }
1588
    }
1589

1590
    /*
1591
        Process value templates and property values that are functions
1592
     */
1593
    runTemplates(op, pathname, index, deps, properties, params) {
1594
        for (let field of deps) {
1,008✔
1595
            if (field.schema) continue
11,531✔
1596
            let name = field.name
11,450✔
1597
            if (
11,450✔
1598
                field.isIndexed &&
19,989✔
1599
                op != 'put' &&
1600
                op != 'update' &&
1601
                field.attribute[0] != index.hash &&
1602
                field.attribute[0] != index.sort
1603
            ) {
1604
                //  Ignore indexes not being used for this call
1605
                continue
727✔
1606
            }
1607
            let path = pathname ? `${pathname}.${field.name}` : field.name
10,723✔
1608

1609
            if (field.value === true && typeof this.table.params.value == 'function') {
10,723✔
1610
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1611
            } else if (properties[name] === undefined) {
10,719✔
1612
                if (field.value) {
6,475✔
1613
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
3,925✔
1614
                    if (value != null) {
3,925✔
1615
                        properties[name] = value
3,809✔
1616
                    }
1617
                }
1618
            }
1619
        }
1620
    }
1621

1622
    /*
1623
        Expand a value template by substituting ${variable} values from context and properties.
1624
     */
1625
    runTemplate(op, index, field, properties, params, value) {
1626
        /*
1627
            Replace property references in ${var}
1628
            Support ${var:length:pad-character} which is useful for sorting.
1629
        */
1630
        value = value.replace(/\${(.*?)}/g, (match, varName) => {
3,925✔
1631
            let [name, len, pad] = varName.split(':')
6,172✔
1632
            let v = this.getPropValue(properties, name)
6,172✔
1633
            if (v != null) {
6,172✔
1634
                if (v instanceof Date) {
6,001!
1635
                    v = this.transformWriteDate(field, v)
×
1636
                }
1637
                if (len) {
6,001!
1638
                    //  Add leading padding for sorting numerics
1639
                    pad = pad || '0'
×
1640
                    let s = v + ''
×
1641
                    while (s.length < len) s = pad + s
×
1642
                    v = s
×
1643
                }
1644
            } else {
1645
                v = match
171✔
1646
            }
1647
            if (typeof v == 'object' && v.toString() == '[object Object]') {
6,172!
1648
                throw new OneTableError(`Value for "${field.name}" is not a primitive value`, {code: 'TypeError'})
×
1649
            }
1650
            return v
6,172✔
1651
        })
1652

1653
        /*
1654
            Consider unresolved template variables. If field is the sort key and doing find,
1655
            then use sort key prefix and begins_with, (provide no where clause).
1656
         */
1657
        if (value.indexOf('${') >= 0 && index) {
3,925✔
1658
            if (field.attribute[0] == index.sort) {
156✔
1659
                if (op == 'find') {
55✔
1660
                    //  Strip from first ${ onward and retain fixed prefix portion
1661
                    value = value.replace(/\${.*/g, '')
40✔
1662
                    if (value) {
40✔
1663
                        return {begins: value}
40✔
1664
                    }
1665
                }
1666
            }
1667
            /*
1668
                Return undefined if any variables remain undefined. This is critical to stop updating
1669
                templates which do not have all the required properties to complete.
1670
            */
1671
            return undefined
116✔
1672
        }
1673
        return value
3,769✔
1674
    }
1675

1676
    //  Public routine to run templates
1677
    template(name, properties, params = {}) {
×
1678
        let fields = this.block.fields
×
1679
        let field = fields[name]
×
1680
        if (!field) {
×
1681
            throw new OneTableError('Cannot find field', {name})
×
1682
        }
1683
        return this.runTemplate('find', null, field, properties, params, field.value)
×
1684
    }
1685

1686
    validateProperties(op, fields, properties, params) {
1687
        if (op != 'put' && op != 'update') {
1,008✔
1688
            return
269✔
1689
        }
1690
        let validation = {}
739✔
1691
        if (typeof this.table.params.validate == 'function') {
739✔
1692
            validation = this.table.params.validate(this, properties, params) || {}
4!
1693
        }
1694
        for (let [name, value] of Object.entries(properties)) {
739✔
1695
            let field = fields[name]
7,219✔
1696
            if (!field || field.schema) continue
7,219✔
1697
            if (params.validate || field.validate || field.enum) {
7,182✔
1698
                value = this.validateProperty(field, value, validation, params)
20✔
1699
                properties[name] = value
20✔
1700
            }
1701
        }
1702
        for (let field of Object.values(fields)) {
739✔
1703
            //  If required and create, must be defined. If required and update, must not be null.
1704
            if (
8,461✔
1705
                field.required &&
20,202✔
1706
                !field.schema &&
1707
                ((op == 'put' && properties[field.name] == null) || (op == 'update' && properties[field.name] === null))
1708
            ) {
1709
                validation[field.name] = `Value not defined for required field "${field.name}"`
4✔
1710
            }
1711
        }
1712

1713
        if (Object.keys(validation).length > 0) {
739✔
1714
            throw new OneTableError(`Validation Error in "${this.name}" for "${Object.keys(validation).join(', ')}"`, {
5✔
1715
                validation,
1716
                code: 'ValidationError',
1717
                properties,
1718
            })
1719
        }
1720
    }
1721

1722
    validateProperty(field, value, details, params) {
1723
        let fieldName = field.name
20✔
1724

1725
        if (typeof params.validate == 'function') {
20!
1726
            let error
1727
            ;({error, value} = params.validate(this, field, value))
×
1728
            if (error) {
×
1729
                details[fieldName] = error
×
1730
            }
1731
        }
1732
        let validate = field.validate
20✔
1733
        if (validate) {
20✔
1734
            if (value === null) {
20✔
1735
                if (field.required && field.value == null) {
1✔
1736
                    details[fieldName] = `Value not defined for "${fieldName}"`
1✔
1737
                }
1738
            } else if (validate instanceof RegExp) {
19✔
1739
                if (!validate.exec(value)) {
13✔
1740
                    details[fieldName] = `Bad value "${value}" for "${fieldName}"`
3✔
1741
                }
1742
            } else {
1743
                let pattern = validate.toString()
6✔
1744
                if (pattern[0] == '/' && pattern.lastIndexOf('/') > 0) {
6✔
1745
                    let parts = pattern.split('/')
4✔
1746
                    let qualifiers = parts.pop()
4✔
1747
                    let pat = parts.slice(1).join('/')
4✔
1748
                    validate = new RegExp(pat, qualifiers)
4✔
1749
                    if (!validate.exec(value)) {
4✔
1750
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
2✔
1751
                    }
1752
                } else {
1753
                    if (!value.match(pattern)) {
2✔
1754
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
1✔
1755
                    }
1756
                }
1757
            }
1758
        }
1759
        if (field.enum) {
20!
1760
            if (field.enum.indexOf(value) < 0) {
×
1761
                details[fieldName] = `Bad value "${value}" for "${fieldName}"`
×
1762
            }
1763
        }
1764
        return value
20✔
1765
    }
1766

1767
    transformProperties(op, fields, properties, params, rec) {
1768
        for (let [name, field] of Object.entries(fields)) {
1,003✔
1769
            //  Nested schemas handled via collectProperties
1770
            if (field.schema) continue
11,480✔
1771
            let value = rec[name]
11,400✔
1772
            if (value !== undefined) {
11,400✔
1773
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
7,687✔
1774
            }
1775
        }
1776
        return rec
1,003✔
1777
    }
1778

1779
    /*
1780
        Transform an attribute before writing. This invokes transform callbacks and handles nested objects.
1781
     */
1782
    transformWriteAttribute(op, field, value, properties, params) {
1783
        let type = field.type
7,687✔
1784

1785
        if (typeof params.transform == 'function') {
7,687!
1786
            value = params.transform(this, 'write', field.name, value, properties, null)
×
1787
        } else if (value == null && field.nulls === true) {
7,687!
1788
            //  Keep the null
1789
        } else if (op == 'find' && value != null && typeof value == 'object') {
7,687✔
1790
            //  Find used {begins} for sort keys and other operators
1791
            value = this.transformNestedWriteFields(field, value)
42✔
1792
        } else if (type == 'date') {
7,645✔
1793
            value = this.transformWriteDate(field, value)
503✔
1794
        } else if (type == 'number') {
7,142✔
1795
            let num = Number(value)
111✔
1796
            if (isNaN(num)) {
111!
1797
                throw new OneTableError(`Invalid value "${value}" provided for field "${field.name}"`, {
×
1798
                    code: 'ValidationError',
1799
                })
1800
            }
1801
            value = num
111✔
1802
        } else if (type == 'boolean') {
7,031!
1803
            if (value == 'false' || value == 'null' || value == 'undefined') {
×
1804
                value = false
×
1805
            }
1806
            value = Boolean(value)
×
1807
        } else if (type == 'string') {
7,031✔
1808
            if (value != null) {
6,933✔
1809
                value = value.toString()
6,933✔
1810
            }
1811
        } else if (type == 'buffer' || type == 'arraybuffer' || type == 'binary') {
98✔
1812
            if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
5!
1813
                value = value.toString('base64')
5✔
1814
            }
1815
        } else if (type == 'array') {
93✔
1816
            if (value != null) {
9✔
1817
                if (Array.isArray(value)) {
9!
1818
                    value = this.transformNestedWriteFields(field, value)
9✔
1819
                } else {
1820
                    //  Heursistics to accept legacy string values for array types. Note: TS would catch this also.
1821
                    if (value == '') {
×
1822
                        value = []
×
1823
                    } else {
1824
                        //  FUTURE: should be moved to validations
1825
                        throw new OneTableArgError(
×
1826
                            `Invalid data type for Array field "${field.name}" in "${this.name}"`
1827
                        )
1828
                    }
1829
                }
1830
            }
1831
        } else if (type == 'set' && Array.isArray(value)) {
84!
1832
            value = this.transformWriteSet(type, value)
×
1833
        } else if (type == 'object' && value != null && typeof value == 'object') {
84✔
1834
            value = this.transformNestedWriteFields(field, value)
81✔
1835
        }
1836

1837
        if (field.crypt && value != null) {
7,687✔
1838
            value = this.encrypt(value)
1✔
1839
        }
1840
        return value
7,687✔
1841
    }
1842

1843
    transformNestedWriteFields(field, obj) {
1844
        for (let [key, value] of Object.entries(obj)) {
132✔
1845
            let type = field.type
2,151✔
1846
            if (value instanceof Date) {
2,151✔
1847
                obj[key] = this.transformWriteDate(field, value)
1✔
1848
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,150!
1849
                value = value.toString('base64')
×
1850
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,150!
1851
                value = this.transformWriteSet(type, value)
×
1852
            } else if (value == null && field.nulls !== true) {
2,150!
1853
                //  Skip nulls
1854
                continue
×
1855
            } else if (value != null && typeof value == 'object') {
2,150!
1856
                obj[key] = this.transformNestedWriteFields(field, value)
×
1857
            }
1858
        }
1859
        return obj
132✔
1860
    }
1861

1862
    transformWriteSet(type, value) {
1863
        if (!Array.isArray(value)) {
×
1864
            throw new OneTableError('Set values must be arrays', {code: 'TypeError'})
×
1865
        }
1866
        if (type == Set || type == 'Set' || type == 'set') {
×
1867
            let v = value.values().next().value
×
1868
            if (typeof v == 'string') {
×
1869
                value = value.map((v) => v.toString())
×
1870
            } else if (typeof v == 'number') {
×
1871
                value = value.map((v) => Number(v))
×
1872
            } else if (v instanceof Buffer || v instanceof ArrayBuffer || v instanceof DataView) {
×
1873
                value = value.map((v) => v.toString('base64'))
×
1874
            }
1875
        } else {
1876
            throw new OneTableError('Unknown type', {code: 'TypeError'})
×
1877
        }
1878
        return value
×
1879
    }
1880

1881
    /*
1882
        Handle dates. Supports epoch and ISO date transformations.
1883
    */
1884
    transformWriteDate(field, value) {
1885
        let isoDates = field.isoDates || this.table.isoDates
504✔
1886
        if (field.ttl) {
504!
1887
            //  Convert dates to DynamoDB TTL
1888
            if (value instanceof Date) {
×
1889
                value = value.getTime()
×
1890
            } else if (typeof value == 'string') {
×
1891
                value = new Date(Date.parse(value)).getTime()
×
1892
            }
1893
            value = Math.ceil(value / 1000)
×
1894
        } else if (isoDates) {
504✔
1895
            if (value instanceof Date) {
450!
1896
                value = value.toISOString()
450✔
1897
            } else if (typeof value == 'string') {
×
1898
                value = new Date(Date.parse(value)).toISOString()
×
1899
            } else if (typeof value == 'number') {
×
1900
                value = new Date(value).toISOString()
×
1901
            }
1902
        } else {
1903
            //  Convert dates to unix epoch in milliseconds
1904
            if (value instanceof Date) {
54!
1905
                value = value.getTime()
54✔
1906
            } else if (typeof value == 'string') {
×
1907
                value = new Date(Date.parse(value)).getTime()
×
1908
            }
1909
        }
1910
        return value
504✔
1911
    }
1912

1913
    /*
1914
        Get a hash of all the property names of the indexes. Keys are properties, values are index names.
1915
        Primary takes precedence if property used in multiple indexes (LSIs)
1916
     */
1917
    getIndexProperties(indexes) {
1918
        let properties = {}
283✔
1919
        for (let [indexName, index] of Object.entries(indexes)) {
283✔
1920
            for (let [type, pname] of Object.entries(index)) {
660✔
1921
                if (type == 'hash' || type == 'sort') {
1,667✔
1922
                    if (properties[pname] != 'primary') {
1,294✔
1923
                        //  Let primary take precedence
1924
                        properties[pname] = indexName
1,269✔
1925
                    }
1926
                }
1927
            }
1928
        }
1929
        return properties
283✔
1930
    }
1931

1932
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1933
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1934
    }
1935

1936
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1937
        return this.table.decrypt(text, inCode, outCode)
2✔
1938
    }
1939

1940
    /*
1941
        Clone properties and params to callers objects are not polluted
1942
    */
1943
    checkArgs(properties, params, overrides = {}) {
856✔
1944
        if (params.checked) {
1,790✔
1945
            //  Only need to clone once
1946
            return {properties, params}
832✔
1947
        }
1948
        if (!properties) {
958!
1949
            throw new OneTableArgError('Missing properties')
×
1950
        }
1951
        if (typeof params != 'object') {
958!
1952
            throw new OneTableError('Invalid type for params', {code: 'TypeError'})
×
1953
        }
1954
        //  Must not use merge as we need to modify the callers batch/transaction objects
1955
        params = Object.assign(overrides, params)
958✔
1956

1957
        params.checked = true
958✔
1958
        properties = this.table.assign({}, properties)
958✔
1959
        return {properties, params}
958✔
1960
    }
1961

1962
    /*
1963
        Handle nulls and empty strings properly according to nulls preference in plain objects and arrays.
1964
        NOTE: DynamoDB can handle empty strings as top level non-key string attributes, but not nested in lists or maps. Ugh!
1965
    */
1966
    handleEmpties(field, obj) {
1967
        let result
1968
        if (
92✔
1969
            obj !== null &&
287✔
1970
            typeof obj == 'object' &&
1971
            (obj.constructor.name == 'Object' || obj.constructor.name == 'Array')
1972
        ) {
1973
            result = Array.isArray(obj) ? [] : {}
91✔
1974
            for (let [key, value] of Object.entries(obj)) {
91✔
1975
                if (value === '') {
2,112!
1976
                    //  Convert to null and handle according to field.nulls
1977
                    value = null
×
1978
                }
1979
                if (value == null && field.nulls !== true) {
2,112!
1980
                    //  Match null and undefined
1981
                    continue
×
1982
                } else if (typeof value == 'object') {
2,112✔
1983
                    result[key] = this.handleEmpties(field, value)
1✔
1984
                } else {
1985
                    result[key] = value
2,111✔
1986
                }
1987
            }
1988
        } else {
1989
            result = obj
1✔
1990
        }
1991
        return result
92✔
1992
    }
1993

1994
    getPartial(field, params) {
1995
        let partial = params.partial
7,666✔
1996
        if (partial === undefined) {
7,666✔
1997
            partial = field.partial
7,650✔
1998
            if (partial == undefined) {
7,650✔
1999
                partial = this.table.partial
7,645✔
2000
            }
2001
        }
2002
        return partial
7,666✔
2003
    }
2004

2005
    /*  KEEP
2006
    captureStack() {
2007
        let limit = Error.stackTraceLimit
2008
        Error.stackTraceLimit = 1
2009

2010
        let obj = {}
2011
        let v8Handler = Error.prepareStackTrace
2012
        Error.prepareStackTrace = function(obj, stack) { return stack }
2013
        Error.captureStackTrace(obj, this.captureStack)
2014

2015
        let stack = obj.stack
2016
        Error.prepareStackTrace = v8Handler
2017
        Error.stackTraceLimit = limit
2018

2019
        let frame = stack[0]
2020
        return `${frame.getFunctionName()}:${frame.getFileName()}:${frame.getLineNumber()}`
2021
    } */
2022
}
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