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

sensedeep / dynamodb-onetable / #68

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

push

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

1121 of 1620 branches covered (69.2%)

Branch coverage included in aggregate %.

1801 of 2397 relevant lines covered (75.14%)

625.06 hits per line

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

78.98
/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'}
72✔
115
            }
116
            if (this.timestamps === true || this.timestamps == 'update') {
278✔
117
                schemaFields[this.updatedField] = schemaFields[this.updatedField] || {type: 'date'}
72✔
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,075!
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,075✔
134
                field.partial = parent.partial
2✔
135
            }
136
            field.name = name
2,075✔
137
            fields[name] = field
2,075✔
138
            field.isoDates = field.isoDates != null ? field.isoDates : table.isoDates || false
2,075!
139

140
            if (field.uuid) {
2,075!
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,075✔
147

148
            /*
149
                Handle mapped attributes. May be packed also (obj.prop)
150
            */
151
            let to = field.map
2,075✔
152
            if (to) {
2,075✔
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,058✔
173
            }
174
            if (field.nulls !== true && field.nulls !== false) {
2,075✔
175
                field.nulls = this.nulls
2,075✔
176
            }
177

178
            /*
179
                Handle index requirements
180
            */
181
            let index = this.indexProperties[field.attribute[0]]
2,075✔
182
            if (index && !parent) {
2,075✔
183
                field.isIndexed = true
686✔
184
                if (field.attribute.length > 1) {
686!
185
                    throw new OneTableArgError(`Cannot map property "${field.name}" to a compound attribute"`)
×
186
                }
187
                if (index == 'primary') {
686✔
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,075✔
198
                //  Value template properties are hidden by default
199
                if (field.hidden == null) {
468✔
200
                    field.hidden = true
468✔
201
                }
202
            }
203
            /*
204
                Handle nested schema (recursive)
205
            */
206
            if (field.items && field.type == 'array') {
2,075✔
207
                field.schema = field.items.schema
1✔
208
                field.isArray = true
1✔
209
            }
210
            if (field.schema) {
2,075✔
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,053✔
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,075✔
233
        }
234
    }
235

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

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

270
    getPropValue(properties, path) {
271
        let v = properties
6,172✔
272
        for (let part of path.split('.')) {
6,172✔
273
            if (v == null) return v
6,176!
274
            v = v[part]
6,176✔
275
        }
276
        return v
6,172✔
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
950✔
285

286
        /*
287
            Get a string representation of the API request
288
         */
289
        let cmd = expression.command()
950✔
290
        if (!expression.execute) {
948✔
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
947✔
308
        if (t) {
947✔
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
882✔
326
        if (b) {
882✔
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
858✔
344
        if (stats && typeof params == 'object') {
858✔
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,
858✔
354
            items = [],
858✔
355
            count = 0
858✔
356
        let maxPages = params.maxPages ? params.maxPages : SanityPages
858!
357
        let result
358
        do {
858✔
359
            result = await this.table.execute(this.name, op, cmd, properties, params)
863✔
360
            if (result.LastEvaluatedKey) {
862✔
361
                //  Continue next page
362
                cmd.ExclusiveStartKey = result.LastEvaluatedKey
26✔
363
            }
364
            if (result.Items) {
862✔
365
                items = items.concat(result.Items)
135✔
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) {
739✔
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) {
739!
387
                params.progress({items, pages, stats, params, cmd})
×
388
            }
389
            if (items.length) {
739✔
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))
728✔
398

399
        let prev
400
        if ((op == 'find' || op == 'scan') && items.length) {
857✔
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) {
857✔
427
            items = this.parseResponse(op, expression, items)
840✔
428
        }
429

430
        /*
431
            Handle pagination next/prev
432
        */
433
        if (op == 'find' || op == 'scan') {
857✔
434
            if (result.LastEvaluatedKey) {
139✔
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') {
139✔
439
                items.count = count
8✔
440
                Object.defineProperty(items, 'count', {enumerable: false})
8✔
441
            }
442
            if (prev) {
139✔
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') {
139✔
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) {
857✔
460
            this.table.log[params.log ? 'info' : 'data'](`OneTable result for "${op}" "${this.name}"`, {
856✔
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)) {
857!
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
                results.count = items.count
3✔
501
                Object.defineProperty(results, 'next', {enumerable: false})
3✔
502
                Object.defineProperty(results, 'prev', {enumerable: false})
3✔
503
                return results
3✔
504
            }
505
        }
506
        return op == 'find' || op == 'scan' ? items : items[0]
854✔
507
    }
508

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

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

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

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

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

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

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

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

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

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

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

661
    async load(properties = {}, params = {}) {
6!
662
        /* eslint-disable-next-line */
663
        ;({properties, params} = this.checkArgs(properties, params))
6✔
664
        properties = this.prepareProperties('get', properties, params)
6✔
665
        let expression = new Expression(this, 'get', properties, params)
6✔
666
        return await this.table.batchLoad(expression)
6✔
667
    }
668

669
    init(properties = {}, params = {}) {
×
670
        /* eslint-disable-next-line */
671
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
672
        return this.initItem(properties, params)
×
673
    }
674

675
    async remove(properties, params = {}) {
18✔
676
        /* eslint-disable-next-line */
677
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
41✔
678

679
        properties = this.prepareProperties('delete', properties, params)
41✔
680
        if (params.fallback || params.many) {
41✔
681
            return await this.removeByFind(properties, params)
3✔
682
        }
683
        let expression = new Expression(this, 'delete', properties, params)
38✔
684
        if (this.hasUniqueFields) {
38✔
685
            return await this.removeUnique(properties, params)
2✔
686
        } else {
687
            return await this.run('delete', expression)
36✔
688
        }
689
    }
690

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

721
    /*
722
        Remove an item with unique properties. Use transactions to remove unique items.
723
    */
724
    async removeUnique(properties, params) {
725
        let transactHere = params.transaction ? false : true
2!
726
        let transaction = (params.transaction = params.transaction || {})
2✔
727
        let {hash, sort} = this.indexes.primary
2✔
728
        let fields = Object.values(this.block.fields).filter(
2✔
729
            (f) => f.unique && f.attribute != hash && f.attribute != sort
16✔
730
        )
731

732
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
733

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

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

784
    async scan(properties = {}, params = {}) {
61✔
785
        /* eslint-disable-next-line */
786
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
51✔
787
        return await this.scanItems(properties, params)
51✔
788
    }
789

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

804
    async upsert(properties, params = {}) {
×
805
        params.exists = null
×
806
        return await this.update(properties, params)
×
807
    }
808

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

822
        params.prepared = properties = this.prepareProperties('update', properties, params)
5✔
823
        let keys = {
5✔
824
            [index.hash]: properties[index.hash],
825
        }
826
        if (index.sort) {
5✔
827
            keys[index.sort] = properties[index.sort]
5✔
828
        }
829

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

847
        for (let field of fields) {
5✔
848
            let toBeRemoved = params.remove && params.remove.includes(field.name)
15✔
849
            let isUnchanged = prior && properties[field.name] === prior[field.name]
15✔
850
            if (isUnchanged) {
15✔
851
                continue
3✔
852
            }
853

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

896
        if (!transactHere) {
5!
897
            return item
×
898
        }
899

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

939
    //  Low level API
940

941
    /* private */
942
    async deleteItem(properties, params = {}) {
1✔
943
        /* eslint-disable-next-line */
944
        ;({properties, params} = this.checkArgs(properties, params))
3✔
945
        if (!params.prepared) {
3✔
946
            properties = this.prepareProperties('delete', properties, params)
1✔
947
        }
948
        let expression = new Expression(this, 'delete', properties, params)
3✔
949
        return await this.run('delete', expression)
3✔
950
    }
951

952
    /* private */
953
    async getItem(properties, params = {}) {
×
954
        /* eslint-disable-next-line */
955
        ;({properties, params} = this.checkArgs(properties, params))
1✔
956
        properties = this.prepareProperties('get', properties, params)
1✔
957
        let expression = new Expression(this, 'get', properties, params)
1✔
958
        return await this.run('get', expression)
1✔
959
    }
960

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

977
    /* private */
978
    async putItem(properties, params = {}) {
×
979
        /* eslint-disable-next-line */
980
        ;({properties, params} = this.checkArgs(properties, params))
633✔
981
        if (!params.prepared) {
633✔
982
            if (params.timestamps !== false) {
627✔
983
                let timestamp = params.transaction
627✔
984
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
33✔
985
                    : new Date()
986

987
                if (this.timestamps === true || this.timestamps == 'create') {
627✔
988
                    properties[this.createdField] = timestamp
223✔
989
                }
990
                if (this.timestamps === true || this.timestamps == 'update') {
627✔
991
                    properties[this.updatedField] = timestamp
223✔
992
                }
993
            }
994
            properties = this.prepareProperties('put', properties, params)
627✔
995
        }
996
        let expression = new Expression(this, 'put', properties, params)
629✔
997
        return await this.run('put', expression)
629✔
998
    }
999

1000
    /* private */
1001
    async queryItems(properties = {}, params = {}) {
×
1002
        /* eslint-disable-next-line */
1003
        ;({properties, params} = this.checkArgs(properties, params))
70✔
1004
        properties = this.prepareProperties('find', properties, params)
70✔
1005
        let expression = new Expression(this, 'find', properties, params)
70✔
1006
        return await this.run('find', expression)
70✔
1007
    }
1008

1009
    //  Note: scanItems will return all model types
1010
    /* private */
1011
    async scanItems(properties = {}, params = {}) {
22✔
1012
        /* eslint-disable-next-line */
1013
        ;({properties, params} = this.checkArgs(properties, params))
71✔
1014
        properties = this.prepareProperties('scan', properties, params)
71✔
1015
        let expression = new Expression(this, 'scan', properties, params)
71✔
1016
        return await this.run('scan', expression)
71✔
1017
    }
1018

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

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

1061
        let items = await this.queryItems(properties, params)
2✔
1062
        return this.table.groupByType(items)
2✔
1063
    }
1064

1065
    /*
1066
        Map Dynamo types to Javascript types after reading data
1067
     */
1068
    transformReadItem(op, raw, properties, params) {
1069
        if (!raw) {
2,552!
1070
            return raw
×
1071
        }
1072
        return this.transformReadBlock(op, raw, properties, params, this.block.fields)
2,552✔
1073
    }
1074

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

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

1187
    /*
1188
        Validate properties and map types if required.
1189
        Note: this does not map names to attributes or evaluate value templates, that happens in Expression.
1190
     */
1191
    prepareProperties(op, properties, params = {}) {
6✔
1192
        delete params.fallback
990✔
1193
        let index = this.selectIndex(op, params)
990✔
1194

1195
        if (this.needsFallback(op, index, params)) {
990✔
1196
            params.fallback = true
9✔
1197
            return properties
9✔
1198
        }
1199
        //  DEPRECATE
1200
        this.tunnelProperties(properties, params)
981✔
1201

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

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

1273
    //  Handle fallback for get/delete as GSIs only support find and scan
1274
    needsFallback(op, index, params) {
1275
        if (index != this.indexes.primary && op != 'find' && op != 'scan') {
990✔
1276
            if (params.low) {
9!
1277
                throw new OneTableArgError('Cannot use non-primary index for "${op}" operation')
×
1278
            }
1279
            return true
9✔
1280
        }
1281
        return false
981✔
1282
    }
1283

1284
    /*
1285
        Return the hash property name for the selected index.
1286
    */
1287
    getHash(rec, fields, index, params) {
1288
        let generic = params.generic != null ? params.generic : this.generic
898!
1289
        if (generic) {
898✔
1290
            return rec[index.hash]
17✔
1291
        }
1292
        let field = Object.values(fields).find((f) => f.attribute[0] == index.hash)
1,072✔
1293
        if (!field) {
881!
1294
            return null
×
1295
        }
1296
        return rec[field.name]
881✔
1297
    }
1298

1299
    /*
1300
        Get the index for the request
1301
    */
1302
    selectIndex(op, params) {
1303
        let index
1304
        if (params.index && params.index != 'primary') {
990✔
1305
            index = this.indexes[params.index]
37✔
1306
            if (!index) {
37!
1307
                throw new OneTableError(`Cannot find index ${params.index}`, {code: 'MissingError'})
×
1308
            }
1309
        } else {
1310
            index = this.indexes.primary
953✔
1311
        }
1312
        return index
990✔
1313
    }
1314

1315
    /*
1316
        Collect the required attribute from the properties and context.
1317
        This handles tunneled properties, blends context properties, resolves default values,
1318
        handles Nulls and empty strings, and invokes validations. Nested schemas are handled here.
1319

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

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

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

1387
    /*
1388
        DEPRECATE - not needed anymore
1389
    */
1390
    tunnelProperties(properties, params) {
1391
        if (params.tunnel) {
981!
1392
            if (this.table.warn !== false) {
×
1393
                console.warn(
×
1394
                    'WARNING: tunnel properties should not be required for typescript and will be removed soon.'
1395
                )
1396
            }
1397
            for (let [kind, settings] of Object.entries(params.tunnel)) {
×
1398
                for (let [key, value] of Object.entries(settings)) {
×
1399
                    properties[key] = {[kind]: value}
×
1400
                }
1401
            }
1402
        }
1403
    }
1404

1405
    /*
1406
        Select the attributes to include in the request
1407
    */
1408
    selectProperties(op, block, index, properties, params, rec) {
1409
        let project = this.getProjection(index)
1,003✔
1410
        /*
1411
            NOTE: Value templates for unique items may need other properties when removing unique items
1412
        */
1413
        for (let [name, field] of Object.entries(block.fields)) {
1,003✔
1414
            if (field.schema) continue
11,424✔
1415
            let omit = false
11,344✔
1416

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

1454
    getProjection(index) {
1455
        let project = index.project
1,003✔
1456
        if (project) {
1,003✔
1457
            if (project == 'all') {
50✔
1458
                project = null
46✔
1459
            } else if (project == 'keys') {
4!
1460
                let primary = this.indexes.primary
×
1461
                project = [primary.hash, primary.sort, index.hash, index.sort]
×
1462
                project = project.filter((v, i, a) => a.indexOf(v) === i)
×
1463
            } else if (Array.isArray(project)) {
4✔
1464
                let primary = this.indexes.primary
4✔
1465
                project = project.concat([primary.hash, primary.sort, index.hash, index.sort])
4✔
1466
                project = project.filter((v, i, a) => a.indexOf(v) === i)
28✔
1467
            }
1468
        }
1469
        return project
1,003✔
1470
    }
1471

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

1497
    /*
1498
        Add context to properties. If 'put', then for all fields, otherwise just key fields.
1499
        Context overrides properties.
1500
     */
1501
    addContext(op, fields, index, properties, params, context) {
1502
        for (let field of Object.values(fields)) {
1,008✔
1503
            if (field.schema) continue
11,530✔
1504
            if (op == 'put' || (field.attribute[0] != index.hash && field.attribute[0] != index.sort)) {
11,450✔
1505
                if (context[field.name] !== undefined) {
10,769✔
1506
                    properties[field.name] = context[field.name]
33✔
1507
                }
1508
            }
1509
        }
1510
        if (!this.generic && fields == this.block.fields) {
1,008✔
1511
            //  Set type field for the top level only
1512
            properties[this.typeField] = this.name
944✔
1513
        }
1514
    }
1515

1516
    /*
1517
        Set default property values on Put.
1518
    */
1519
    setDefaults(op, fields, properties, params) {
1520
        if (op != 'put' && op != 'init' && !(op == 'update' && params.exists == null)) {
1,008✔
1521
            return
346✔
1522
        }
1523
        for (let field of Object.values(fields)) {
662✔
1524
            if (field.schema) continue
7,530✔
1525
            let value = properties[field.name]
7,515✔
1526

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

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

1600
    /*
1601
        Process value templates and property values that are functions
1602
     */
1603
    runTemplates(op, pathname, index, deps, properties, params) {
1604
        for (let field of deps) {
1,008✔
1605
            if (field.schema) continue
11,531✔
1606
            let name = field.name
11,450✔
1607
            if (
11,450✔
1608
                field.isIndexed &&
19,989✔
1609
                op != 'put' &&
1610
                op != 'update' &&
1611
                field.attribute[0] != index.hash &&
1612
                field.attribute[0] != index.sort
1613
            ) {
1614
                //  Ignore indexes not being used for this call
1615
                continue
727✔
1616
            }
1617
            let path = pathname ? `${pathname}.${field.name}` : field.name
10,723✔
1618

1619
            if (field.value === true && typeof this.table.params.value == 'function') {
10,723✔
1620
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1621
            } else if (properties[name] === undefined) {
10,719✔
1622
                if (field.value) {
6,475✔
1623
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
3,925✔
1624
                    if (value != null) {
3,925✔
1625
                        properties[name] = value
3,809✔
1626
                    }
1627
                }
1628
            }
1629
        }
1630
    }
1631

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

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

1686
    //  Public routine to run templates
1687
    template(name, properties, params = {}) {
×
1688
        let fields = this.block.fields
×
1689
        let field = fields[name]
×
1690
        if (!field) {
×
1691
            throw new OneTableError('Cannot find field', {name})
×
1692
        }
1693
        return this.runTemplate('find', null, field, properties, params, field.value)
×
1694
    }
1695

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

1723
        if (Object.keys(validation).length > 0) {
739✔
1724
            throw new OneTableError(`Validation Error in "${this.name}" for "${Object.keys(validation).join(', ')}"`, {
5✔
1725
                validation,
1726
                code: 'ValidationError',
1727
                properties,
1728
            })
1729
        }
1730
    }
1731

1732
    validateProperty(field, value, details, params) {
1733
        let fieldName = field.name
20✔
1734

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

1777
    transformProperties(op, fields, properties, params, rec) {
1778
        for (let [name, field] of Object.entries(fields)) {
1,003✔
1779
            //  Nested schemas handled via collectProperties
1780
            if (field.schema) continue
11,480✔
1781
            let value = rec[name]
11,400✔
1782
            if (value !== undefined) {
11,400✔
1783
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
7,687✔
1784
            }
1785
        }
1786
        return rec
1,003✔
1787
    }
1788

1789
    /*
1790
        Transform an attribute before writing. This invokes transform callbacks and handles nested objects.
1791
     */
1792
    transformWriteAttribute(op, field, value, properties, params) {
1793
        let type = field.type
7,687✔
1794

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

1847
        if (field.crypt && value != null) {
7,687✔
1848
            value = this.encrypt(value)
1✔
1849
        }
1850
        return value
7,687✔
1851
    }
1852

1853
    transformNestedWriteFields(field, obj) {
1854
        for (let [key, value] of Object.entries(obj)) {
132✔
1855
            let type = field.type
2,151✔
1856
            if (value instanceof Date) {
2,151✔
1857
                obj[key] = this.transformWriteDate(field, value)
1✔
1858
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,150!
1859
                value = value.toString('base64')
×
1860
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,150!
1861
                value = this.transformWriteSet(type, value)
×
1862
            } else if (value == null && field.nulls !== true) {
2,150!
1863
                //  Skip nulls
1864
                continue
×
1865
            } else if (value != null && typeof value == 'object') {
2,150!
1866
                obj[key] = this.transformNestedWriteFields(field, value)
×
1867
            }
1868
        }
1869
        return obj
132✔
1870
    }
1871

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

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

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

1942
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1943
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1944
    }
1945

1946
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1947
        return this.table.decrypt(text, inCode, outCode)
2✔
1948
    }
1949

1950
    /*
1951
        Clone properties and params to callers objects are not polluted
1952
    */
1953
    checkArgs(properties, params, overrides = {}) {
856✔
1954
        if (params.checked) {
1,790✔
1955
            //  Only need to clone once
1956
            return {properties, params}
832✔
1957
        }
1958
        if (!properties) {
958!
1959
            throw new OneTableArgError('Missing properties')
×
1960
        }
1961
        if (typeof params != 'object') {
958!
1962
            throw new OneTableError('Invalid type for params', {code: 'TypeError'})
×
1963
        }
1964
        //  Must not use merge as we need to modify the callers batch/transaction objects
1965
        params = Object.assign(overrides, params)
958✔
1966

1967
        params.checked = true
958✔
1968
        properties = this.table.assign({}, properties)
958✔
1969
        return {properties, params}
958✔
1970
    }
1971

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

2004
    getPartial(field, params) {
2005
        let partial = params.partial
7,681✔
2006
        if (partial === undefined) {
7,681✔
2007
            partial = field.partial
7,665✔
2008
            if (partial == undefined) {
7,665✔
2009
                partial = this.table.partial
7,659✔
2010
            }
2011
        }
2012
        return partial
7,681✔
2013
    }
2014

2015
    /*  KEEP
2016
    captureStack() {
2017
        let limit = Error.stackTraceLimit
2018
        Error.stackTraceLimit = 1
2019

2020
        let obj = {}
2021
        let v8Handler = Error.prepareStackTrace
2022
        Error.prepareStackTrace = function(obj, stack) { return stack }
2023
        Error.captureStackTrace(obj, this.captureStack)
2024

2025
        let stack = obj.stack
2026
        Error.prepareStackTrace = v8Handler
2027
        Error.stackTraceLimit = limit
2028

2029
        let frame = stack[0]
2030
        return `${frame.getFunctionName()}:${frame.getFileName()}:${frame.getLineNumber()}`
2031
    } */
2032
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc