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

sensedeep / dynamodb-onetable / #67

pending completion
#67

push

Michael O'Brien
CLEAN

1116 of 1616 branches covered (69.06%)

Branch coverage included in aggregate %.

1790 of 2386 relevant lines covered (75.02%)

623.67 hits per line

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

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

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

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

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

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

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

58
        //  Primary hash and sort attributes and properties
59
        this.hash = null
278✔
60
        this.sort = null
278✔
61

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

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

84
        this.schema = table.schema
278✔
85
        this.indexes = this.schema.indexes
278✔
86

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

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

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

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

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

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

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

146
            field.type = this.checkType(field)
2,073✔
147

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

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

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

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

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

270
    getPropValue(properties, path) {
271
        let v = properties
6,175✔
272
        for (let part of path.split('.')) {
6,175✔
273
            if (v == null) return v
6,179!
274
            v = v[part]
6,179✔
275
        }
276
        return v
6,175✔
277
    }
278

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

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

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

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

423
        /*
424
            Process the response
425
        */
426
        if (params.parse) {
858✔
427
            items = this.parseResponse(op, expression, items)
841✔
428
        }
429

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

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

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

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

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

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

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

567
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
6✔
568

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

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

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

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

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

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

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

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

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

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

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

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

725
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
726

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

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

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

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

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

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

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

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

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

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

889
        if (!transactHere) {
5!
890
            return item
×
891
        }
892

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

932
    //  Low level API
933

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1188
        if (this.needsFallback(op, index, params)) {
991✔
1189
            params.fallback = true
9✔
1190
            return properties
9✔
1191
        }
1192
        //  DEPRECATE
1193
        this.tunnelProperties(properties, params)
982✔
1194

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1612
            if (field.value === true && typeof this.table.params.value == 'function') {
10,729✔
1613
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1614
            } else if (properties[name] === undefined) {
10,725✔
1615
                if (field.value) {
6,480✔
1616
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
3,927✔
1617
                    if (value != null) {
3,927✔
1618
                        properties[name] = value
3,811✔
1619
                    }
1620
                }
1621
            }
1622
        }
1623
    }
1624

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

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

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

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

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

1725
    validateProperty(field, value, details, params) {
1726
        let fieldName = field.name
20✔
1727

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

1770
    transformProperties(op, fields, properties, params, rec) {
1771
        for (let [name, field] of Object.entries(fields)) {
1,004✔
1772
            //  Nested schemas handled via collectProperties
1773
            if (field.schema) continue
11,488✔
1774
            let value = rec[name]
11,408✔
1775
            if (value !== undefined) {
11,408✔
1776
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
7,689✔
1777
            }
1778
        }
1779
        return rec
1,004✔
1780
    }
1781

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

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

1840
        if (field.crypt && value != null) {
7,689✔
1841
            value = this.encrypt(value)
1✔
1842
        }
1843
        return value
7,689✔
1844
    }
1845

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

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

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

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

1935
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1936
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1937
    }
1938

1939
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1940
        return this.table.decrypt(text, inCode, outCode)
2✔
1941
    }
1942

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

1960
        params.checked = true
959✔
1961
        properties = this.table.assign({}, properties)
959✔
1962
        return {properties, params}
959✔
1963
    }
1964

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

1997
    getPartial(field, params) {
1998
        let partial = params.partial
7,668✔
1999
        if (partial === undefined) {
7,668✔
2000
            partial = field.partial
7,652✔
2001
            if (partial == undefined) {
7,652✔
2002
                partial = this.table.partial
7,647✔
2003
            }
2004
        }
2005
        return partial
7,668✔
2006
    }
2007

2008
    /*  KEEP
2009
    captureStack() {
2010
        let limit = Error.stackTraceLimit
2011
        Error.stackTraceLimit = 1
2012

2013
        let obj = {}
2014
        let v8Handler = Error.prepareStackTrace
2015
        Error.prepareStackTrace = function(obj, stack) { return stack }
2016
        Error.captureStackTrace(obj, this.captureStack)
2017

2018
        let stack = obj.stack
2019
        Error.prepareStackTrace = v8Handler
2020
        Error.stackTraceLimit = limit
2021

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