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

sensedeep / dynamodb-onetable / #79

16 Jun 2024 11:29PM UTC coverage: 75.098% (-0.2%) from 75.252%
#79

push

Michael O'Brien
DEV: update deps

1174 of 1642 branches covered (71.5%)

Branch coverage included in aggregate %.

1884 of 2430 relevant lines covered (77.53%)

740.01 hits per line

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

79.23
/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'
57✔
7
import {Expression} from './Expression.js'
57✔
8
import {OneTableError, OneTableArgError} from './Error.js'
57✔
9

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

22
const TransformParseResponseAs = {
57✔
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}
57✔
32
const TransactOps = {delete: 'Delete', get: 'Get', put: 'Put', update: 'Update', check: 'ConditionCheck'}
57✔
33
const BatchOps = {delete: 'DeleteRequest', put: 'PutRequest', update: 'PutRequest'}
57✔
34
const ValidTypes = ['array', 'arraybuffer', 'binary', 'boolean', 'buffer', 'date', 'number', 'object', 'set', 'string']
57✔
35
const SanityPages = 1000
57✔
36
const FollowThreads = 10
57✔
37

38
export class Model {
57✔
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) {
313!
46
            throw new OneTableArgError('Missing table argument')
×
47
        }
48
        if (!table.typeField) {
313!
49
            throw new OneTableArgError('Invalid table instance')
×
50
        }
51
        if (!name) {
313!
52
            throw new OneTableArgError('Missing name of model')
×
53
        }
54
        this.table = table
313✔
55
        this.name = name
313✔
56
        this.options = options
313✔
57

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

62
        //  Cache table properties
63
        this.createdField = table.createdField
313✔
64
        this.generic = options.generic
313✔
65
        this.nested = false
313✔
66
        this.nulls = table.nulls
313✔
67
        this.tableName = table.name
313✔
68
        this.typeField = options.typeField || table.typeField
313✔
69
        this.generic = options.generic != null ? options.generic : table.generic
313✔
70
        this.timestamps = options.timestamps
313✔
71
        if (this.timestamps == null) {
313✔
72
            this.timestamps = table.timestamps
191✔
73
        }
74
        this.updatedField = table.updatedField
313✔
75
        this.block = {fields: {}, deps: []}
313✔
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 = {}
313✔
83

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

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

92
        let fields = options.fields || this.schema.definition.models[this.name]
313!
93
        if (fields) {
313✔
94
            this.prepModel(fields, this.block)
313✔
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
327✔
103

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

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

127
        for (let [name, field] of Object.entries(schemaFields)) {
327✔
128
            if (!field.type) {
2,364!
129
                field.type = 'string'
×
130
                this.table.log.error(`Missing type field for ${field.name}`, {field})
×
131
            }
132

133
            field.name = name
2,364✔
134
            fields[name] = field
2,364✔
135
            field.isoDates = field.isoDates != null ? field.isoDates : table.isoDates || false
2,364!
136

137
            if (field.partial == null) {
2,364✔
138
                field.partial = parent && parent.partial != null ? parent.partial : this.table.partial
2,363✔
139
            }
140

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

147
            field.type = this.checkType(field)
2,364✔
148

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

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

232
        /*
233
            Order the fields so value templates can depend on each other safely
234
        */
235
        for (let field of Object.values(fields)) {
327✔
236
            this.orderFields(block, field)
2,364✔
237
        }
238
    }
239

240
    checkType(field) {
241
        let type = field.type
2,364✔
242
        if (typeof type == 'function') {
2,364✔
243
            type = type.name
893✔
244
        }
245
        type = type.toLowerCase()
2,364✔
246
        if (ValidTypes.indexOf(type) < 0) {
2,364!
247
            throw new OneTableArgError(`Unknown type "${type}" for field "${field.name}" in model "${this.name}"`)
×
248
        }
249
        return type
2,364✔
250
    }
251

252
    orderFields(block, field) {
253
        let {deps, fields} = block
2,366✔
254
        if (deps.find((i) => i.name == field.name)) {
10,638✔
255
            return
1✔
256
        }
257
        if (field.value) {
2,365✔
258
            let vars = this.table.getVars(field.value)
510✔
259
            for (let path of vars) {
510✔
260
                let name = path.split(/[.[]/g).shift().trim(']')
586✔
261
                let ref = fields[name]
586✔
262
                if (ref && ref != field) {
586✔
263
                    if (ref.schema) {
585✔
264
                        this.orderFields(ref.block, ref)
2✔
265
                    } else if (ref.value) {
583!
266
                        this.orderFields(block, ref)
×
267
                    }
268
                }
269
            }
270
        }
271
        deps.push(field)
2,365✔
272
    }
273

274
    getPropValue(properties, path) {
275
        let v = properties
7,795✔
276
        for (let part of path.split('.')) {
7,795✔
277
            if (v == null) return v
7,799!
278
            v = v[part]
7,799✔
279
        }
280
        return v
7,795✔
281
    }
282

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

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

354
        /*
355
            Run command. Paginate if required.
356
         */
357
        let pages = 0,
1,148✔
358
            items = [],
1,148✔
359
            count = 0
1,148✔
360
        let maxPages = params.maxPages ? params.maxPages : SanityPages
1,148!
361
        let result
362
        do {
1,148✔
363
            result = await this.table.execute(this.name, op, cmd, properties, params)
1,153✔
364
            if (result.LastEvaluatedKey) {
1,152✔
365
                //  Continue next page
366
                cmd.ExclusiveStartKey = result.LastEvaluatedKey
31✔
367
            }
368
            if (result.Items) {
1,152✔
369
                items = items.concat(result.Items)
147✔
370
            } else if (result.Item) {
1,005✔
371
                items = [result.Item]
51✔
372
                break
51✔
373
            } else if (result.Attributes) {
954✔
374
                items = [result.Attributes]
94✔
375
                break
94✔
376
            } else if (params.count || params.select == 'COUNT') {
860✔
377
                count += result.Count
9✔
378
            }
379
            if (stats) {
1,007✔
380
                if (result.Count) {
1✔
381
                    stats.count += result.Count
1✔
382
                }
383
                if (result.ScannedCount) {
1✔
384
                    stats.scanned += result.ScannedCount
1✔
385
                }
386
                if (result.ConsumedCapacity) {
1✔
387
                    stats.capacity += result.ConsumedCapacity.CapacityUnits
1✔
388
                }
389
            }
390
            if (params.progress) {
1,007!
391
                params.progress({items, pages, stats, params, cmd})
×
392
            }
393
            if (items.length) {
1,007✔
394
                if (cmd.Limit) {
128✔
395
                    cmd.Limit -= result.Count
38✔
396
                    if (cmd.Limit <= 0) {
38✔
397
                        break
26✔
398
                    }
399
                }
400
            }
401
        } while (result.LastEvaluatedKey && (maxPages == null || ++pages < maxPages))
991✔
402

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

429
        /*
430
            Process the response
431
        */
432
        if (params.parse) {
1,147✔
433
            items = this.parseResponse(op, expression, items)
1,130✔
434
        }
435

436
        /*
437
            Handle pagination next/prev
438
        */
439
        if (op == 'find' || op == 'scan') {
1,147✔
440
            if (result.LastEvaluatedKey) {
151✔
441
                items.next = this.table.unmarshall(result.LastEvaluatedKey, params)
26✔
442
                Object.defineProperty(items, 'next', {enumerable: false})
26✔
443
            }
444
            if (params.count || params.select == 'COUNT') {
151✔
445
                items.count = count
8✔
446
                Object.defineProperty(items, 'count', {enumerable: false})
8✔
447
            }
448
            if (prev) {
151✔
449
                items.prev = this.table.unmarshall(prev, params)
19✔
450
                Object.defineProperty(items, 'prev', {enumerable: false})
19✔
451
            }
452
            if (params.prev && params.next == null && op != 'scan') {
151✔
453
                //  DynamoDB scan ignores ScanIndexForward
454
                items = items.reverse()
1✔
455
                let tmp = items.prev
1✔
456
                items.prev = items.next
1✔
457
                items.next = tmp
1✔
458
            }
459
        }
460

461
        /*
462
            Log unless the user provides params.log: false.
463
            The logger will typically filter data/trace.
464
        */
465
        if (params.log !== false) {
1,147✔
466
            this.table.log[params.log ? 'info' : 'data'](`OneTable result for "${op}" "${this.name}"`, {
1,146✔
467
                cmd,
468
                items,
469
                op,
470
                properties,
471
                params,
472
            })
473
        }
474

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

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

545
    /*
546
        Create/Put a new item. Will overwrite existing items if exists: null.
547
    */
548
    async create(properties, params = {}) {
844✔
549
        /* eslint-disable-next-line */
550
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true, exists: false}))
890✔
551
        let result
552
        if (this.hasUniqueFields) {
890✔
553
            result = await this.createUnique(properties, params)
7✔
554
        } else {
555
            result = await this.putItem(properties, params)
883✔
556
        }
557
        return result
885✔
558
    }
559

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

572
        fields = Object.values(fields).filter((f) => f.unique && f.attribute != hash && f.attribute != sort)
53✔
573

574
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
7✔
575

576
        if (params.timestamps !== false) {
7✔
577
            if (this.timestamps === true || this.timestamps == 'create') {
7✔
578
                properties[this.createdField] = timestamp
1✔
579
            }
580
            if (this.timestamps === true || this.timestamps == 'update') {
7✔
581
                properties[this.updatedField] = timestamp
1✔
582
            }
583
        }
584
        params.prepared = properties = this.prepareProperties('put', properties, params)
7✔
585

586
        for (let field of fields) {
7✔
587
            if (properties[field.name] !== undefined) {
13✔
588
                let scope = ''
11✔
589
                if (field.scope) {
11!
590
                    scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
591
                    if (scope == undefined) {
×
592
                        throw new OneTableError('Missing properties to resolve unique scope', {
×
593
                            properties,
594
                            field,
595
                            scope: field.scope,
596
                            code: 'UniqueError',
597
                        })
598
                    }
599
                }
600
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
11✔
601
                let sk = '_unique#'
11✔
602
                await this.schema.uniqueModel.create(
11✔
603
                    {[this.hash]: pk, [this.sort]: sk},
604
                    {transaction, exists: false, return: 'NONE'}
605
                )
606
            }
607
        }
608
        let item = await this.putItem(properties, params)
7✔
609

610
        if (!transactHere) {
7✔
611
            return item
1✔
612
        }
613
        let expression = params.expression
6✔
614
        try {
6✔
615
            await this.table.transact('write', params.transaction, params)
6✔
616
        } catch (err) {
617
            if (
1✔
618
                err instanceof OneTableError &&
3✔
619
                err.code === 'TransactionCanceledException' &&
620
                err.context.err.message.indexOf('ConditionalCheckFailed') !== -1
621
            ) {
622
                let names = fields.map((f) => f.name).join(', ')
3✔
623
                throw new OneTableError(
1✔
624
                    `Cannot create unique attributes "${names}" for "${this.name}". An item of the same name already exists.`,
625
                    {properties, transaction, code: 'UniqueError'}
626
                )
627
            }
628
            throw err
×
629
        }
630
        let items = this.parseResponse('put', expression)
5✔
631
        return items[0]
5✔
632
    }
633

634
    async check(properties, params) {
635
        /* eslint-disable-next-line */
636
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
3✔
637
        properties = this.prepareProperties('get', properties, params)
3✔
638
        const expression = new Expression(this, 'check', properties, params)
3✔
639
        this.run('check', expression)
3✔
640
    }
641

642
    async find(properties = {}, params = {}) {
10✔
643
        /* eslint-disable-next-line */
644
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
69✔
645
        return await this.queryItems(properties, params)
69✔
646
    }
647

648
    async get(properties = {}, params = {}) {
36!
649
        /* eslint-disable-next-line */
650
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
89✔
651
        properties = this.prepareProperties('get', properties, params)
89✔
652
        if (params.fallback) {
89✔
653
            if (params.batch) {
13!
654
                throw new OneTableError('Need complete keys for batched get operation', {
×
655
                    properties,
656
                    code: 'NonUniqueError',
657
                })
658
            }
659
            //  Fallback via find when using non-primary indexes
660
            params.limit = 2
13✔
661
            let items = await this.find(properties, params)
13✔
662
            if (items.length > 1) {
13✔
663
                throw new OneTableError('Get without sort key returns more than one result', {
2✔
664
                    properties,
665
                    code: 'NonUniqueError',
666
                })
667
            }
668
            return items[0]
11✔
669
        }
670
        //  FUTURE refactor to use getItem
671
        let expression = new Expression(this, 'get', properties, params)
76✔
672
        return await this.run('get', expression)
76✔
673
    }
674

675
    async load(properties = {}, params = {}) {
6!
676
        /* eslint-disable-next-line */
677
        ;({properties, params} = this.checkArgs(properties, params))
6✔
678
        properties = this.prepareProperties('get', properties, params)
6✔
679
        let expression = new Expression(this, 'get', properties, params)
6✔
680
        return await this.table.batchLoad(expression)
6✔
681
    }
682

683
    init(properties = {}, params = {}) {
×
684
        /* eslint-disable-next-line */
685
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
686
        return this.initItem(properties, params)
×
687
    }
688

689
    async remove(properties, params = {}) {
18✔
690
        /* eslint-disable-next-line */
691
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
42✔
692

693
        properties = this.prepareProperties('delete', properties, params)
42✔
694
        if (params.fallback || params.many) {
42✔
695
            return await this.removeByFind(properties, params)
3✔
696
        }
697
        let expression = new Expression(this, 'delete', properties, params)
39✔
698
        if (this.hasUniqueFields) {
39✔
699
            return await this.removeUnique(properties, params)
2✔
700
        } else {
701
            return await this.run('delete', expression)
37✔
702
        }
703
    }
704

705
    /*
706
        Remove multiple objects after doing a full find/query
707
     */
708
    async removeByFind(properties, params) {
709
        if (params.retry) {
3!
710
            throw new OneTableArgError('Remove cannot retry', {properties})
×
711
        }
712
        params.parse = true
3✔
713
        let findParams = Object.assign({}, params)
3✔
714
        delete findParams.transaction
3✔
715
        let items = await this.find(properties, findParams)
3✔
716
        if (items.length > 1 && !params.many) {
3!
717
            throw new OneTableError(`Removing multiple items from "${this.name}". Use many:true to enable.`, {
×
718
                properties,
719
                code: 'NonUniqueError',
720
            })
721
        }
722
        let response = []
3✔
723
        for (let item of items) {
3✔
724
            let removed
725
            if (this.hasUniqueFields) {
7!
726
                removed = await this.removeUnique(item, {retry: true, transaction: params.transaction})
×
727
            } else {
728
                removed = await this.remove(item, {retry: true, return: params.return, transaction: params.transaction})
7✔
729
            }
730
            response.push(removed)
7✔
731
        }
732
        return response
3✔
733
    }
734

735
    /*
736
        Remove an item with unique properties. Use transactions to remove unique items.
737
    */
738
    async removeUnique(properties, params) {
739
        let transactHere = params.transaction ? false : true
2!
740
        let transaction = (params.transaction = params.transaction || {})
2✔
741
        let {hash, sort} = this.indexes.primary
2✔
742
        let fields = Object.values(this.block.fields).filter(
2✔
743
            (f) => f.unique && f.attribute != hash && f.attribute != sort
16✔
744
        )
745

746
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
747

748
        let keys = {
2✔
749
            [hash]: properties[hash],
750
        }
751
        if (sort) {
2✔
752
            keys[sort] = properties[sort]
2✔
753
        }
754
        /*
755
            Get the prior item so we know the previous unique property values so they can be removed.
756
            This must be run here, even if part of a transaction.
757
        */
758
        let prior = await this.get(keys, {hidden: true})
2✔
759
        if (prior) {
2!
760
            prior = this.prepareProperties('update', prior)
2✔
761
        } else if (params.exists === undefined || params.exists == true) {
×
762
            throw new OneTableError('Cannot find existing item to remove', {properties, code: 'NotFoundError'})
×
763
        }
764

765
        for (let field of fields) {
2✔
766
            let sk = `_unique#`
6✔
767
            let scope = ''
6✔
768
            if (field.scope) {
6!
769
                scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
770
                if (scope == undefined) {
×
771
                    throw new OneTableError('Missing properties to resolve unique scope', {
×
772
                        properties,
773
                        field,
774
                        params,
775
                        scope: field.scope,
776
                        code: 'UniqueError',
777
                    })
778
                }
779
            }
780
            // If we had a prior record, remove unique values that existed
781
            if (prior && prior[field.name]) {
6✔
782
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${prior[field.name]}`
4✔
783
                await this.schema.uniqueModel.remove(
4✔
784
                    {[this.hash]: pk, [this.sort]: sk},
785
                    {transaction, exists: params.exists}
786
                )
787
            } else if (!prior && properties[field.name] !== undefined) {
2!
788
                // if we did not have a prior record and the field is defined, try to remove it
789
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
×
790
                await this.schema.uniqueModel.remove(
×
791
                    {[this.hash]: pk, [this.sort]: sk},
792
                    {
793
                        transaction,
794
                        exists: params.exists,
795
                    }
796
                )
797
            }
798
        }
799
        let removed = await this.deleteItem(properties, params)
2✔
800
        // Only execute transaction if we are not in a transaction
801
        if (transactHere) {
2✔
802
            removed = await this.table.transact('write', transaction, params)
2✔
803
        }
804
        return removed
2✔
805
    }
806

807
    async scan(properties = {}, params = {}) {
63✔
808
        /* eslint-disable-next-line */
809
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
52✔
810
        return await this.scanItems(properties, params)
52✔
811
    }
812

813
    async update(properties, params = {}) {
25✔
814
        /* eslint-disable-next-line */
815
        ;({properties, params} = this.checkArgs(properties, params, {exists: true, parse: true, high: true}))
83✔
816
        if (this.hasUniqueFields) {
83✔
817
            let hasUniqueProperties = Object.entries(properties).find((pair) => {
9✔
818
                return this.block.fields[pair[0]] && this.block.fields[pair[0]].unique
19✔
819
            })
820
            if (hasUniqueProperties) {
9✔
821
                return await this.updateUnique(properties, params)
7✔
822
            }
823
        }
824
        return await this.updateItem(properties, params)
76✔
825
    }
826

827
    async upsert(properties, params = {}) {
×
828
        params.exists = null
×
829
        return await this.update(properties, params)
×
830
    }
831

832
    /*
833
        Update an item with unique attributes and actually updating a unique property.
834
        Use a transaction to update a unique item for each unique attribute.
835
     */
836
    async updateUnique(properties, params) {
837
        if (params.batch) {
7!
838
            throw new OneTableArgError('Cannot use batch with unique properties which require transactions')
×
839
        }
840
        let transactHere = params.transaction ? false : true
7✔
841
        let transaction = (params.transaction = params.transaction || {})
7✔
842
        let index = this.indexes.primary
7✔
843
        let {hash, sort} = index
7✔
844

845
        params.prepared = properties = this.prepareProperties('update', properties, params)
7✔
846
        let keys = {
7✔
847
            [index.hash]: properties[index.hash],
848
        }
849
        if (index.sort) {
7✔
850
            keys[index.sort] = properties[index.sort]
7✔
851
        }
852
        /*
853
            Get the prior item so we know the previous unique property values so they can be removed.
854
            This must be run here, even if part of a transaction.
855
        */
856
        let prior = await this.get(keys, {hidden: true})
7✔
857
        if (prior) {
7✔
858
            prior = this.prepareProperties('update', prior)
6✔
859
        } else if (params.exists === undefined || params.exists == true) {
1!
860
            throw new OneTableError('Cannot find existing item to update', {properties, code: 'NotFoundError'})
×
861
        }
862
        /*
863
            Create all required unique properties. Remove prior unique properties if they have changed.
864
        */
865
        let fields = Object.values(this.block.fields).filter(
7✔
866
            (f) => f.unique && f.attribute != hash && f.attribute != sort
56✔
867
        )
868

869
        for (let field of fields) {
7✔
870
            let toBeRemoved = params.remove && params.remove.includes(field.name)
17✔
871
            let isUnchanged = prior && properties[field.name] === prior[field.name]
17✔
872
            if (isUnchanged) {
17✔
873
                continue
4✔
874
            }
875
            let scope = ''
13✔
876
            if (field.scope) {
13!
877
                scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
878
                if (scope == undefined) {
×
879
                    throw new OneTableError('Missing properties to resolve unique scope', {
×
880
                        properties,
881
                        field,
882
                        scope: field.scope,
883
                        code: 'UniqueError',
884
                    })
885
                }
886
            }
887
            let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
13✔
888
            let sk = `_unique#`
13✔
889
            // If we had a prior value AND value is changing or being removed, remove old value
890
            if (prior && prior[field.name] && (properties[field.name] !== undefined || toBeRemoved)) {
13✔
891
                /*
892
                    Remove prior unique properties if they have changed and create new unique property.
893
                */
894
                let priorPk = `_unique#${scope}${this.name}#${field.attribute}#${prior[field.name]}`
6✔
895
                if (pk == priorPk) {
6!
896
                    //  Hasn't changed
897
                    continue
×
898
                }
899
                await this.schema.uniqueModel.remove(
6✔
900
                    {[this.hash]: priorPk, [this.sort]: sk},
901
                    {
902
                        transaction,
903
                        exists: null,
904
                        execute: params.execute,
905
                        log: params.log,
906
                    }
907
                )
908
            }
909
            // If value is changing, add new unique value
910
            if (properties[field.name] !== undefined) {
13✔
911
                await this.schema.uniqueModel.create(
7✔
912
                    {[this.hash]: pk, [this.sort]: sk},
913
                    {
914
                        transaction,
915
                        exists: false,
916
                        return: 'NONE',
917
                        log: params.log,
918
                        execute: params.execute,
919
                    }
920
                )
921
            }
922
        }
923
        let item = await this.updateItem(properties, params)
7✔
924

925
        if (!transactHere) {
7✔
926
            return item
2✔
927
        }
928

929
        /*
930
            Perform all operations in a transaction so update will only be applied if the unique properties can be created.
931
        */
932
        try {
5✔
933
            await this.table.transact('write', params.transaction, params)
5✔
934
        } catch (err) {
935
            if (
1✔
936
                err instanceof OneTableError &&
3✔
937
                err.code === 'TransactionCanceledException' &&
938
                err.context.err.message.indexOf('ConditionalCheckFailed') !== -1
939
            ) {
940
                let names = fields.map((f) => f.name).join(', ')
3✔
941
                throw new OneTableError(
1✔
942
                    `Cannot update unique attributes "${names}" for "${this.name}". An item of the same name already exists.`,
943
                    {properties, transaction, code: 'UniqueError'}
944
                )
945
            }
946
            throw err
×
947
        }
948
        if (params.return == 'none' || params.return == 'NONE' || params.return === false) {
4!
949
            return
×
950
        }
951
        if (params.return == 'get') {
4✔
952
            return await this.get(keys, {
4✔
953
                hidden: params.hidden,
954
                log: params.log,
955
                parse: params.parse,
956
                execute: params.execute,
957
            })
958
        }
959
        if (this.table.warn !== false) {
×
960
            console.warn(
×
961
                `Update with unique items uses transactions and cannot return the updated item.` +
962
                    `Use params {return: 'none'} to squelch this warning. ` +
963
                    `Use {return: 'get'} to do a non-transactional get of the item after the update. `
964
            )
965
        }
966
    }
967

968
    //  Low level API
969

970
    /* private */
971
    async deleteItem(properties, params = {}) {
1✔
972
        /* eslint-disable-next-line */
973
        ;({properties, params} = this.checkArgs(properties, params))
3✔
974
        if (!params.prepared) {
3✔
975
            properties = this.prepareProperties('delete', properties, params)
1✔
976
        }
977
        let expression = new Expression(this, 'delete', properties, params)
3✔
978
        return await this.run('delete', expression)
3✔
979
    }
980

981
    /* private */
982
    async getItem(properties, params = {}) {
×
983
        /* eslint-disable-next-line */
984
        ;({properties, params} = this.checkArgs(properties, params))
2✔
985
        properties = this.prepareProperties('get', properties, params)
2✔
986
        let expression = new Expression(this, 'get', properties, params)
2✔
987
        return await this.run('get', expression)
2✔
988
    }
989

990
    /* private */
991
    initItem(properties, params = {}) {
×
992
        /* eslint-disable-next-line */
993
        ;({properties, params} = this.checkArgs(properties, params))
×
994
        let fields = this.block.fields
×
995
        this.setDefaults('init', fields, properties, params)
×
996
        //  Ensure all fields are present
997
        for (let key of Object.keys(fields)) {
×
998
            if (properties[key] === undefined) {
×
999
                properties[key] = null
×
1000
            }
1001
        }
1002
        this.runTemplates('put', '', this.indexes.primary, this.block.deps, properties, params)
×
1003
        return properties
×
1004
    }
1005

1006
    /* private */
1007
    async putItem(properties, params = {}) {
×
1008
        /* eslint-disable-next-line */
1009
        ;({properties, params} = this.checkArgs(properties, params))
892✔
1010
        if (!params.prepared) {
892✔
1011
            if (params.timestamps !== false) {
885✔
1012
                let timestamp = params.transaction
885✔
1013
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
36✔
1014
                    : new Date()
1015

1016
                if (this.timestamps === true || this.timestamps == 'create') {
885✔
1017
                    properties[this.createdField] = timestamp
225✔
1018
                }
1019
                if (this.timestamps === true || this.timestamps == 'update') {
885✔
1020
                    properties[this.updatedField] = timestamp
225✔
1021
                }
1022
            }
1023
            properties = this.prepareProperties('put', properties, params)
885✔
1024
        }
1025
        let expression = new Expression(this, 'put', properties, params)
888✔
1026
        return await this.run('put', expression)
888✔
1027
    }
1028

1029
    /* private */
1030
    async queryItems(properties = {}, params = {}) {
×
1031
        /* eslint-disable-next-line */
1032
        ;({properties, params} = this.checkArgs(properties, params))
81✔
1033
        properties = this.prepareProperties('find', properties, params)
81✔
1034
        let expression = new Expression(this, 'find', properties, params)
81✔
1035
        return await this.run('find', expression)
81✔
1036
    }
1037

1038
    //  Note: scanItems will return all model types
1039
    /* private */
1040
    async scanItems(properties = {}, params = {}) {
22✔
1041
        /* eslint-disable-next-line */
1042
        ;({properties, params} = this.checkArgs(properties, params))
72✔
1043
        properties = this.prepareProperties('scan', properties, params)
72✔
1044
        let expression = new Expression(this, 'scan', properties, params)
72✔
1045
        return await this.run('scan', expression)
72✔
1046
    }
1047

1048
    /* private */
1049
    async updateItem(properties, params = {}) {
×
1050
        /* eslint-disable-next-line */
1051
        ;({properties, params} = this.checkArgs(properties, params))
86✔
1052
        if (this.timestamps === true || this.timestamps == 'update') {
86✔
1053
            if (params.timestamps !== false) {
49✔
1054
                let timestamp = params.transaction
49✔
1055
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
7✔
1056
                    : new Date()
1057
                properties[this.updatedField] = timestamp
49✔
1058
                if (params.exists == null) {
49✔
1059
                    let field = this.block.fields[this.createdField] || this.table
2!
1060
                    let when = field.isoDates ? timestamp.toISOString() : timestamp.getTime()
2!
1061
                    params.set = params.set || {}
2✔
1062
                    params.set[this.createdField] = `if_not_exists(\${${this.createdField}}, {${when}})`
2✔
1063
                }
1064
            }
1065
        }
1066
        properties = this.prepareProperties('update', properties, params)
86✔
1067
        let expression = new Expression(this, 'update', properties, params)
85✔
1068
        return await this.run('update', expression)
85✔
1069
    }
1070

1071
    /* private */
1072
    async fetch(models, properties = {}, params = {}) {
2!
1073
        /* eslint-disable-next-line */
1074
        ;({properties, params} = this.checkArgs(properties, params))
2✔
1075
        if (models.length == 0) {
2!
1076
            return {}
×
1077
        }
1078
        let where = []
2✔
1079
        for (let model of models) {
2✔
1080
            where.push(`\${${this.typeField}} = {${model}}`)
4✔
1081
        }
1082
        if (params.where) {
2!
1083
            params.where = `(${params.where}) and (${where.join(' or ')})`
×
1084
        } else {
1085
            params.where = where.join(' or ')
2✔
1086
        }
1087
        params.parse = true
2✔
1088
        params.hidden = true
2✔
1089

1090
        let items = await this.queryItems(properties, params)
2✔
1091
        return this.table.groupByType(items)
2✔
1092
    }
1093

1094
    /*
1095
        Map Dynamo types to Javascript types after reading data
1096
     */
1097
    transformReadItem(op, raw, properties, params) {
1098
        if (!raw) {
3,343!
1099
            return raw
×
1100
        }
1101
        return this.transformReadBlock(op, raw, properties, params, this.block.fields)
3,343✔
1102
    }
1103

1104
    transformReadBlock(op, raw, properties, params, fields) {
1105
        let rec = {}
3,434✔
1106
        for (let [name, field] of Object.entries(fields)) {
3,434✔
1107
            //  Skip hidden params. Follow needs hidden params to do the follow.
1108
            if (field.hidden && params.follow !== true) {
33,305✔
1109
                if (params.hidden === false || (params.hidden == null && this.table.hidden === false)) {
18,440✔
1110
                    continue
18,182✔
1111
                }
1112
            }
1113
            let att, sub
1114
            if (op == 'put') {
15,123✔
1115
                att = field.name
4,321✔
1116
            } else {
1117
                /* eslint-disable-next-line */
1118
                ;[att, sub] = field.attribute
10,802✔
1119
            }
1120
            let value = raw[att]
15,123✔
1121
            if (value === undefined) {
15,123✔
1122
                if (field.encode) {
2,608✔
1123
                    let [att, sep, index] = field.encode
2✔
1124
                    value = (raw[att] || '').split(sep)[index]
2!
1125
                }
1126
                if (value === undefined) {
2,608✔
1127
                    continue
2,606✔
1128
                }
1129
            }
1130
            if (sub) {
12,517✔
1131
                value = value[sub]
45✔
1132
            }
1133
            if (field.crypt && params.decrypt !== false) {
12,517✔
1134
                value = this.decrypt(value)
2✔
1135
            }
1136
            if (field.default !== undefined && value === undefined) {
12,517!
1137
                value = field.default
×
1138
            } else if (value === undefined) {
12,517!
1139
                if (field.required) {
×
1140
                    this.table.log.error(`Required field "${name}" in model "${this.name}" not defined in table item`, {
×
1141
                        model: this.name,
1142
                        raw,
1143
                        params,
1144
                        field,
1145
                    })
1146
                }
1147
            } else if (field.schema && value !== null && typeof value == 'object') {
12,517✔
1148
                if (field.items && Array.isArray(value)) {
95✔
1149
                    rec[name] = []
16✔
1150
                    let i = 0
16✔
1151
                    for (let rvalue of raw[att]) {
16✔
1152
                        rec[name][i] = this.transformReadBlock(
12✔
1153
                            op,
1154
                            rvalue,
1155
                            properties[name] || [],
15✔
1156
                            params,
1157
                            field.block.fields
1158
                        )
1159
                        i++
12✔
1160
                    }
1161
                } else {
1162
                    rec[name] = this.transformReadBlock(
79✔
1163
                        op,
1164
                        raw[att],
1165
                        properties[name] || {},
130✔
1166
                        params,
1167
                        field.block.fields
1168
                    )
1169
                }
1170
            } else {
1171
                rec[name] = this.transformReadAttribute(field, name, value, params, properties)
12,422✔
1172
            }
1173
        }
1174
        if (this.generic) {
3,434✔
1175
            //  Generic must include attributes outside the schema.
1176
            for (let [name, value] of Object.entries(raw)) {
15✔
1177
                if (rec[name] === undefined) {
51✔
1178
                    rec[name] = value
31✔
1179
                }
1180
            }
1181
        }
1182
        if (
3,434✔
1183
            params.hidden == true &&
3,482✔
1184
            rec[this.typeField] === undefined &&
1185
            !this.generic &&
1186
            this.block.fields == fields
1187
        ) {
1188
            rec[this.typeField] = this.name
1✔
1189
        }
1190
        if (this.table.params.transform) {
3,434✔
1191
            let opForTransform = TransformParseResponseAs[op]
6✔
1192
            rec = this.table.params.transform(this, ReadWrite[opForTransform], rec, properties, params, raw)
6✔
1193
        }
1194
        return rec
3,434✔
1195
    }
1196

1197
    transformReadAttribute(field, name, value, params, properties) {
1198
        if (typeof params.transform == 'function') {
12,422!
1199
            //  Invoke custom data transform after reading
1200
            return params.transform(this, 'read', name, value, properties)
×
1201
        }
1202
        if (field.type == 'date' && value != undefined) {
12,422✔
1203
            if (field.ttl) {
1,230!
1204
                //  Parse incase stored as ISO string
1205
                return new Date(new Date(value).getTime() * 1000)
×
1206
            } else {
1207
                return new Date(value)
1,230✔
1208
            }
1209
        }
1210
        if (field.type == 'buffer' || field.type == 'arraybuffer' || field.type == 'binary') {
11,192✔
1211
            return Buffer.from(value, 'base64')
10✔
1212
        }
1213
        return value
11,182✔
1214
    }
1215

1216
    /*
1217
        Validate properties and map types if required.
1218
        Note: this does not map names to attributes or evaluate value templates, that happens in Expression.
1219
     */
1220
    prepareProperties(op, properties, params = {}) {
8✔
1221
        delete params.fallback
1,291✔
1222
        let index = this.selectIndex(op, params)
1,291✔
1223

1224
        if (this.needsFallback(op, index, params)) {
1,291✔
1225
            params.fallback = true
9✔
1226
            return properties
9✔
1227
        }
1228
        //  DEPRECATE
1229
        this.tunnelProperties(properties, params)
1,282✔
1230

1231
        if (params.filter) {
1,282!
1232
            this.convertFilter(properties, params, index)
×
1233
        }
1234
        let rec = this.collectProperties(op, '', this.block, index, properties, params)
1,282✔
1235
        if (params.fallback) {
1,277✔
1236
            return properties
7✔
1237
        }
1238
        if (op != 'scan' && this.getHash(rec, this.block.fields, index, params) == null) {
1,270!
1239
            this.table.log.error(`Empty hash key`, {properties, params, op, rec, index, model: this.name})
×
1240
            throw new OneTableError(`Empty hash key. Check hash key and any value template variable references.`, {
×
1241
                properties,
1242
                rec,
1243
                code: 'MissingError',
1244
            })
1245
        }
1246
        if (this.table.params.transform && ReadWrite[op] == 'write') {
1,270✔
1247
            rec = this.table.params.transform(this, ReadWrite[op], rec, properties, params)
5✔
1248
        }
1249
        return rec
1,270✔
1250
    }
1251

1252
    /*
1253
        Convert a full text params.filter into a smart params.where
1254
        NOTE: this is prototype code and definitely not perfect! Use at own risk.
1255
     */
1256
    convertFilter(properties, params, index) {
1257
        let filter = params.filter
×
1258
        let fields = this.block.fields
×
1259
        let where
1260
        //  TODO support > >= < <= ..., AND or ...
1261
        let [name, value] = filter.split('=')
×
1262
        if (value) {
×
1263
            name = name.trim()
×
1264
            value = value.trim()
×
1265
            let field = fields[name]
×
1266
            if (field) {
×
1267
                name = field.map ? field.map : name
×
1268
                if (field.encode) {
×
1269
                    properties[name] = value
×
1270
                } else {
1271
                    where = `\${${name}} = {${value}}`
×
1272
                }
1273
            } else {
1274
                //  TODO support > >= < <= ..., AND or ...
1275
                where = `\${${name}} = "{${value}}"`
×
1276
            }
1277
        } else {
1278
            value = name
×
1279
            where = []
×
1280
            for (let [name, field] of Object.entries(fields)) {
×
1281
                let primary = this.indexes.primary
×
1282
                if (primary.hash == name || primary.sort == name || index.hash == name || index.sort == name) {
×
1283
                    continue
×
1284
                }
1285
                if (field.encode) {
×
1286
                    continue
×
1287
                }
1288
                name = field.map ? field.map : name
×
1289
                //  Does not seem to work with a numeric filter
1290
                let term = `(contains(\${${name}}, {${filter}}))`
×
1291
                where.push(term)
×
1292
            }
1293
            if (where) {
×
1294
                where = where.join(' or ')
×
1295
            }
1296
        }
1297
        params.where = where
×
1298
        //  TODO SANITY
1299
        params.maxPages = 25
×
1300
    }
1301

1302
    //  Handle fallback for get/delete as GSIs only support find and scan
1303
    needsFallback(op, index, params) {
1304
        if (index != this.indexes.primary && op != 'find' && op != 'scan') {
1,291✔
1305
            if (params.low) {
9!
1306
                throw new OneTableArgError('Cannot use non-primary index for "${op}" operation')
×
1307
            }
1308
            return true
9✔
1309
        }
1310
        return false
1,282✔
1311
    }
1312

1313
    /*
1314
        Return the hash property name for the selected index.
1315
    */
1316
    getHash(rec, fields, index, params) {
1317
        let generic = params.generic != null ? params.generic : this.generic
1,198!
1318
        if (generic) {
1,198✔
1319
            return rec[index.hash]
20✔
1320
        }
1321
        let field = Object.values(fields).find((f) => f.attribute[0] == index.hash)
1,649✔
1322
        if (!field) {
1,178!
1323
            return null
×
1324
        }
1325
        return rec[field.name]
1,178✔
1326
    }
1327

1328
    /*
1329
        Get the index for the request
1330
    */
1331
    selectIndex(op, params) {
1332
        let index
1333
        if (params.index && params.index != 'primary') {
1,291✔
1334
            index = this.indexes[params.index]
43✔
1335
            if (!index) {
43!
1336
                throw new OneTableError(`Cannot find index ${params.index}`, {code: 'MissingError'})
×
1337
            }
1338
        } else {
1339
            index = this.indexes.primary
1,248✔
1340
        }
1341
        return index
1,291✔
1342
    }
1343

1344
    /*
1345
        Collect the required attribute from the properties and context.
1346
        This handles tunneled properties, blends context properties, resolves default values,
1347
        handles Nulls and empty strings, and invokes validations. Nested schemas are handled here.
1348

1349
        NOTE: pathname is only needed for DEPRECATED and undocumented callbacks.
1350
    */
1351
    collectProperties(op, pathname, block, index, properties, params, context, rec = {}) {
1,318✔
1352
        let fields = block.fields
1,318✔
1353
        if (!context) {
1,318✔
1354
            context = params.context || this.table.context
1,282✔
1355
        }
1356
        /*
1357
            First process nested schemas recursively
1358
        */
1359
        if (this.nested && !KeysOnly[op]) {
1,318✔
1360
            this.collectNested(op, pathname, fields, index, properties, params, context, rec)
96✔
1361
        }
1362
        /*
1363
            Then process the non-schema properties at this level (non-recursive)
1364
        */
1365
        this.addContext(op, fields, index, properties, params, context)
1,318✔
1366
        this.setDefaults(op, fields, properties, params)
1,318✔
1367
        this.runTemplates(op, pathname, index, block.deps, properties, params)
1,318✔
1368
        this.convertNulls(op, pathname, fields, properties, params)
1,318✔
1369
        this.validateProperties(op, fields, properties, params)
1,318✔
1370
        this.selectProperties(op, block, index, properties, params, rec)
1,313✔
1371
        this.transformProperties(op, fields, properties, params, rec)
1,313✔
1372
        return rec
1,313✔
1373
    }
1374

1375
    /*
1376
        Process nested schema recursively
1377
    */
1378
    collectNested(op, pathname, fields, index, properties, params, context, rec) {
1379
        for (let field of Object.values(fields)) {
96✔
1380
            let schema = field.schema || field?.items?.schema
827✔
1381
            if (schema) {
827✔
1382
                let name = field.name
83✔
1383
                let value = properties[name]
83✔
1384
                if (op == 'put' && value === undefined) {
83✔
1385
                    value = field.required ? (field.type == 'array' ? [] : {}) : field.default
2!
1386
                }
1387
                let ctx = context[name] || {}
83✔
1388
                let partial = this.getPartial(field, params)
83✔
1389

1390
                if (value === null && field.nulls === true) {
83!
1391
                    rec[name] = null
×
1392
                } else if (value !== undefined) {
83✔
1393
                    if (field.items && Array.isArray(value)) {
39✔
1394
                        rec[name] = []
11✔
1395
                        let i = 0
11✔
1396
                        for (let rvalue of value) {
11✔
1397
                            let path = pathname ? `${pathname}.${name}[${i}]` : `${name}[${i}]`
8!
1398
                            let obj = this.collectProperties(op, path, field.block, index, rvalue, params, ctx)
8✔
1399
                            //  Don't update properties if empty and partial and no default
1400
                            if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
8✔
1401
                                rec[name][i++] = obj
7✔
1402
                            }
1403
                        }
1404
                    } else {
1405
                        let path = pathname ? `${pathname}.${field.name}` : field.name
28✔
1406
                        let obj = this.collectProperties(op, path, field.block, index, value, params, ctx)
28✔
1407
                        if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
28✔
1408
                            rec[name] = obj
28✔
1409
                        }
1410
                    }
1411
                }
1412
            }
1413
        }
1414
    }
1415

1416
    /*
1417
        DEPRECATE - not needed anymore
1418
    */
1419
    tunnelProperties(properties, params) {
1420
        if (params.tunnel) {
1,282!
1421
            if (this.table.warn !== false) {
×
1422
                console.warn(
×
1423
                    'WARNING: tunnel properties should not be required for typescript and will be removed soon.'
1424
                )
1425
            }
1426
            for (let [kind, settings] of Object.entries(params.tunnel)) {
×
1427
                for (let [key, value] of Object.entries(settings)) {
×
1428
                    properties[key] = {[kind]: value}
×
1429
                }
1430
            }
1431
        }
1432
    }
1433

1434
    /*
1435
        Select the attributes to include in the request
1436
    */
1437
    selectProperties(op, block, index, properties, params, rec) {
1438
        let project = this.getProjection(index)
1,313✔
1439
        /*
1440
            NOTE: Value templates for unique items may need other properties when removing unique items
1441
        */
1442
        for (let [name, field] of Object.entries(block.fields)) {
1,313✔
1443
            if (field.schema) continue
13,794✔
1444
            let omit = false
13,692✔
1445

1446
            if (block == this.block) {
13,692✔
1447
                let attribute = field.attribute[0]
13,605✔
1448
                //  Missing sort key on a high-level API for get/delete
1449
                if (properties[name] == null && attribute == index.sort && params.high && KeysOnly[op]) {
13,605✔
1450
                    if (op == 'delete' && !params.many) {
7!
1451
                        throw new OneTableError('Missing sort key', {code: 'MissingError', properties, params})
×
1452
                    }
1453
                    /*
1454
                        Missing sort key for high level get, or delete without "any".
1455
                        Fallback to find to select the items of interest. Get will throw if more than one result is returned.
1456
                    */
1457
                    params.fallback = true
7✔
1458
                    return
7✔
1459
                }
1460
                if (KeysOnly[op] && attribute != index.hash && attribute != index.sort && !this.hasUniqueFields) {
13,598✔
1461
                    //  Keys only for get and delete. Must include unique properties and all properties if unique value templates.
1462
                    //  FUTURE: could have a "strict" mode where we warn for other properties instead of ignoring.
1463
                    omit = true
1,199✔
1464
                } else if (project && project.indexOf(attribute) < 0) {
12,399✔
1465
                    //  Attribute is not projected
1466
                    omit = true
16✔
1467
                } else if (name == this.typeField && name != index.hash && name != index.sort && op == 'find') {
12,383✔
1468
                    omit = true
77✔
1469
                } else if (field.encode) {
12,306✔
1470
                    omit = true
1✔
1471
                }
1472
            }
1473
            if (!omit && properties[name] !== undefined) {
13,685✔
1474
                rec[name] = properties[name]
9,905✔
1475
            }
1476
        }
1477
        if (block == this.block) {
1,306✔
1478
            //  Only do at top level
1479
            this.addProjectedProperties(op, properties, params, project, rec)
1,270✔
1480
        }
1481
    }
1482

1483
    getProjection(index) {
1484
        let project = index.project
1,313✔
1485
        if (project) {
1,313✔
1486
            if (project == 'all') {
50✔
1487
                project = null
46✔
1488
            } else if (project == 'keys') {
4!
1489
                let primary = this.indexes.primary
×
1490
                project = [primary.hash, primary.sort, index.hash, index.sort]
×
1491
                project = project.filter((v, i, a) => a.indexOf(v) === i)
×
1492
            } else if (Array.isArray(project)) {
4✔
1493
                let primary = this.indexes.primary
4✔
1494
                project = project.concat([primary.hash, primary.sort, index.hash, index.sort])
4✔
1495
                project = project.filter((v, i, a) => a.indexOf(v) === i)
28✔
1496
            }
1497
        }
1498
        return project
1,313✔
1499
    }
1500

1501
    //  For generic (table low level APIs), add all properties that are projected
1502
    addProjectedProperties(op, properties, params, project, rec) {
1503
        let generic = params.generic != null ? params.generic : this.generic
1,270!
1504
        if (generic && !KeysOnly[op]) {
1,270✔
1505
            for (let [name, value] of Object.entries(properties)) {
37✔
1506
                if (project && project.indexOf(name) < 0) {
31!
1507
                    continue
×
1508
                }
1509
                if (rec[name] === undefined) {
31✔
1510
                    //  Cannot do all type transformations - don't have enough info without fields
1511
                    if (value instanceof Date) {
9!
1512
                        if (this.isoDates) {
×
1513
                            rec[name] = value.toISOString()
×
1514
                        } else {
1515
                            rec[name] = value.getTime()
×
1516
                        }
1517
                    } else {
1518
                        rec[name] = value
9✔
1519
                    }
1520
                }
1521
            }
1522
        }
1523
        return rec
1,270✔
1524
    }
1525

1526
    /*
1527
        Add context to properties. If 'put', then for all fields, otherwise just key fields.
1528
        Context overrides properties.
1529
     */
1530
    addContext(op, fields, index, properties, params, context) {
1531
        for (let field of Object.values(fields)) {
1,318✔
1532
            if (field.schema) continue
13,900✔
1533
            if (op == 'put' || (field.attribute[0] != index.hash && field.attribute[0] != index.sort)) {
13,798✔
1534
                if (context[field.name] !== undefined) {
13,035✔
1535
                    properties[field.name] = context[field.name]
33✔
1536
                }
1537
            }
1538
        }
1539
        if (!this.generic && fields == this.block.fields) {
1,318✔
1540
            //  Set type field for the top level only
1541
            properties[this.typeField] = this.name
1,242✔
1542
        }
1543
    }
1544

1545
    /*
1546
        Set default property values on Put.
1547
    */
1548
    setDefaults(op, fields, properties, params) {
1549
        if (op != 'put' && op != 'init' && !(op == 'update' && params.exists == null)) {
1,318✔
1550
            return
392✔
1551
        }
1552
        for (let field of Object.values(fields)) {
926✔
1553
            if (field.schema) continue
9,603✔
1554
            let value = properties[field.name]
9,583✔
1555

1556
            //  Set defaults and uuid fields
1557
            if (value === undefined && !field.value) {
9,583✔
1558
                if (field.default !== undefined) {
1,619✔
1559
                    value = field.default
7✔
1560
                } else if (op == 'init') {
1,612!
1561
                    if (!field.generate) {
×
1562
                        //  Set non-default, non-uuid properties to null
1563
                        value = null
×
1564
                    }
1565
                } else if (field.generate) {
1,612✔
1566
                    let generate = field.generate
853✔
1567
                    if (generate === true) {
853!
1568
                        value = this.table.generate()
×
1569
                    } else if (generate == 'uuid') {
853✔
1570
                        value = this.table.uuid()
1✔
1571
                    } else if (generate == 'ulid') {
852!
1572
                        value = this.table.ulid()
852✔
1573
                    } else if (generate == 'uid') {
×
1574
                        value = this.table.uid(10)
×
1575
                    } else if (generate.indexOf('uid') == 0) {
×
1576
                        let [, size] = generate.split('(')
×
1577
                        value = this.table.uid(parseInt(size) || 10)
×
1578
                    }
1579
                }
1580
                if (value !== undefined) {
1,619✔
1581
                    properties[field.name] = value
860✔
1582
                }
1583
            }
1584
        }
1585
        return properties
926✔
1586
    }
1587

1588
    /*
1589
        Remove null properties from the table unless Table.nulls == true
1590
        TODO - null conversion would be better done in Expression then pathnames would not be needed.
1591
        NOTE: pathname is only needed for DEPRECATED callbacks.
1592
    */
1593
    convertNulls(op, pathname, fields, properties, params) {
1594
        for (let [name, value] of Object.entries(properties)) {
1,318✔
1595
            let field = fields[name]
10,353✔
1596
            if (!field || field.schema) continue
10,353✔
1597
            if (value === null && field.nulls !== true) {
10,297✔
1598
                //  create with null/undefined, or update with null property
1599
                if (
10✔
1600
                    field.required &&
13!
1601
                    ((op == 'put' && properties[field.name] == null) ||
1602
                        (op == 'update' && properties[field.name] === null))
1603
                ) {
1604
                    //  Validation will catch this
1605
                    continue
1✔
1606
                }
1607
                delete properties[name]
9✔
1608
                if (this.getPartial(field, params) === false && pathname.match(/[[.]/)) {
9!
1609
                    /*
1610
                        Partial disabled for a nested object 
1611
                        Don't create remove entry as the entire object is being created/updated
1612
                     */
1613
                    continue
×
1614
                }
1615
                if (params.remove && !Array.isArray(params.remove)) {
9!
1616
                    params.remove = [params.remove]
×
1617
                } else {
1618
                    params.remove = params.remove || []
9✔
1619
                }
1620
                let path = pathname ? `${pathname}.${field.name}` : field.name
9✔
1621
                params.remove.push(path)
9✔
1622
            } else if (typeof value == 'object' && (field.type == 'object' || field.type == 'array')) {
10,287✔
1623
                //  Remove nested empty strings because DynamoDB cannot handle these nested in objects or arrays
1624
                properties[name] = this.handleEmpties(field, value)
92✔
1625
            }
1626
        }
1627
    }
1628

1629
    /*
1630
        Process value templates and property values that are functions
1631
     */
1632
    runTemplates(op, pathname, index, deps, properties, params) {
1633
        for (let field of deps) {
1,318✔
1634
            if (field.schema) continue
13,901✔
1635
            let name = field.name
13,798✔
1636
            if (
13,798✔
1637
                field.isIndexed &&
23,411✔
1638
                op != 'put' &&
1639
                op != 'update' &&
1640
                field.attribute[0] != index.hash &&
1641
                field.attribute[0] != index.sort
1642
            ) {
1643
                //  Ignore indexes not being used for this call
1644
                continue
747✔
1645
            }
1646
            let path = pathname ? `${pathname}.${field.name}` : field.name
13,051✔
1647

1648
            if (field.value === true && typeof this.table.params.value == 'function') {
13,051✔
1649
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1650
            } else if (properties[name] === undefined) {
13,047✔
1651
                if (field.value) {
7,652✔
1652
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
5,009✔
1653
                    if (value != null) {
5,009✔
1654
                        properties[name] = value
4,892✔
1655
                    }
1656
                }
1657
            }
1658
        }
1659
    }
1660

1661
    /*
1662
        Expand a value template by substituting ${variable} values from context and properties.
1663
     */
1664
    runTemplate(op, index, field, properties, params, value) {
1665
        /*
1666
            Replace property references in ${var}
1667
            Support ${var:length:pad-character} which is useful for sorting.
1668
        */
1669
        value = value.replace(/\${(.*?)}/g, (match, varName) => {
5,009✔
1670
            let [name, len, pad] = varName.split(':')
7,795✔
1671
            let v = this.getPropValue(properties, name)
7,795✔
1672
            if (v != null) {
7,795✔
1673
                if (v instanceof Date) {
7,617!
1674
                    v = this.transformWriteDate(field, v)
×
1675
                }
1676
                if (len) {
7,617!
1677
                    //  Add leading padding for sorting numerics
1678
                    pad = pad || '0'
×
1679
                    let s = v + ''
×
1680
                    while (s.length < len) s = pad + s
×
1681
                    v = s
×
1682
                }
1683
            } else {
1684
                v = match
178✔
1685
            }
1686
            if (typeof v == 'object' && v.toString() == '[object Object]') {
7,795!
1687
                throw new OneTableError(`Value for "${field.name}" is not a primitive value`, {code: 'TypeError'})
×
1688
            }
1689
            return v
7,795✔
1690
        })
1691

1692
        /*
1693
            Consider unresolved template variables. If field is the sort key and doing find,
1694
            then use sort key prefix and begins_with, (provide no where clause).
1695
         */
1696
        if (value.indexOf('${') >= 0) {
5,009✔
1697
            if (index) {
163✔
1698
                if (field.attribute[0] == index.sort) {
163✔
1699
                    if (op == 'find') {
61✔
1700
                        //  Strip from first ${ onward and retain fixed prefix portion
1701
                        value = value.replace(/\${.*/g, '')
46✔
1702
                        if (value) {
46✔
1703
                            return {begins: value}
46✔
1704
                        }
1705
                    }
1706
                }
1707
            }
1708
            /*
1709
                Return undefined if any variables remain undefined. This is critical to stop updating
1710
                templates which do not have all the required properties to complete.
1711
            */
1712
            return undefined
117✔
1713
        }
1714
        return value
4,846✔
1715
    }
1716

1717
    //  Public routine to run templates
1718
    template(name, properties, params = {}) {
×
1719
        let fields = this.block.fields
×
1720
        let field = fields[name]
×
1721
        if (!field) {
×
1722
            throw new OneTableError('Cannot find field', {name})
×
1723
        }
1724
        return this.runTemplate('find', null, field, properties, params, field.value)
×
1725
    }
1726

1727
    validateProperties(op, fields, properties, params) {
1728
        if (op != 'put' && op != 'update') {
1,318✔
1729
            return
291✔
1730
        }
1731
        let validation = {}
1,027✔
1732
        if (typeof this.table.params.validate == 'function') {
1,027✔
1733
            validation = this.table.params.validate(this, properties, params) || {}
4!
1734
        }
1735
        for (let [name, value] of Object.entries(properties)) {
1,027✔
1736
            let field = fields[name]
9,388✔
1737
            if (!field || field.schema) continue
9,388✔
1738
            if (params.validate || field.validate || field.enum) {
9,340✔
1739
                value = this.validateProperty(field, value, validation, params)
20✔
1740
                properties[name] = value
20✔
1741
            }
1742
        }
1743
        for (let field of Object.values(fields)) {
1,027✔
1744
            //  If required and create, must be defined. If required and update, must not be null.
1745
            if (
10,662✔
1746
                field.required &&
27,039✔
1747
                !field.schema &&
1748
                ((op == 'put' && properties[field.name] == null) || (op == 'update' && properties[field.name] === null))
1749
            ) {
1750
                validation[field.name] = `Value not defined for required field "${field.name}"`
4✔
1751
            }
1752
        }
1753

1754
        if (Object.keys(validation).length > 0) {
1,027✔
1755
            throw new OneTableError(`Validation Error in "${this.name}" for "${Object.keys(validation).join(', ')}"`, {
5✔
1756
                validation,
1757
                code: 'ValidationError',
1758
                properties,
1759
            })
1760
        }
1761
    }
1762

1763
    validateProperty(field, value, details, params) {
1764
        let fieldName = field.name
20✔
1765

1766
        if (typeof params.validate == 'function') {
20!
1767
            let error
1768
            ;({error, value} = params.validate(this, field, value))
×
1769
            if (error) {
×
1770
                details[fieldName] = error
×
1771
            }
1772
        }
1773
        let validate = field.validate
20✔
1774
        if (validate) {
20✔
1775
            if (value === null) {
20✔
1776
                if (field.required && field.value == null) {
1✔
1777
                    details[fieldName] = `Value not defined for "${fieldName}"`
1✔
1778
                }
1779
            } else if (validate instanceof RegExp) {
19✔
1780
                if (!validate.exec(value)) {
13✔
1781
                    details[fieldName] = `Bad value "${value}" for "${fieldName}"`
3✔
1782
                }
1783
            } else {
1784
                let pattern = validate.toString()
6✔
1785
                if (pattern[0] == '/' && pattern.lastIndexOf('/') > 0) {
6✔
1786
                    let parts = pattern.split('/')
4✔
1787
                    let qualifiers = parts.pop()
4✔
1788
                    let pat = parts.slice(1).join('/')
4✔
1789
                    validate = new RegExp(pat, qualifiers)
4✔
1790
                    if (!validate.exec(value)) {
4✔
1791
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
2✔
1792
                    }
1793
                } else {
1794
                    if (!value.match(pattern)) {
2✔
1795
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
1✔
1796
                    }
1797
                }
1798
            }
1799
        }
1800
        if (field.enum) {
20!
1801
            if (field.enum.indexOf(value) < 0) {
×
1802
                details[fieldName] = `Bad value "${value}" for "${fieldName}"`
×
1803
            }
1804
        }
1805
        return value
20✔
1806
    }
1807

1808
    transformProperties(op, fields, properties, params, rec) {
1809
        for (let [name, field] of Object.entries(fields)) {
1,313✔
1810
            //  Nested schemas handled via collectProperties
1811
            if (field.schema) continue
13,850✔
1812
            let value = rec[name]
13,748✔
1813
            if (value !== undefined) {
13,748✔
1814
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
9,905✔
1815
            }
1816
        }
1817
        return rec
1,313✔
1818
    }
1819

1820
    /*
1821
        Transform an attribute before writing. This invokes transform callbacks and handles nested objects.
1822
     */
1823
    transformWriteAttribute(op, field, value, properties, params) {
1824
        let type = field.type
9,905✔
1825

1826
        if (typeof params.transform == 'function') {
9,905!
1827
            value = params.transform(this, 'write', field.name, value, properties, null)
×
1828
        } else if (value == null && field.nulls === true) {
9,905!
1829
            //  Keep the null
1830
        } else if (op == 'find' && value != null && typeof value == 'object') {
9,905✔
1831
            //  Find used {begins} for sort keys and other operators
1832
            value = this.transformNestedWriteFields(field, value)
48✔
1833
        } else if (type == 'date') {
9,857✔
1834
            value = this.transformWriteDate(field, value)
517✔
1835
        } else if (type == 'number') {
9,340✔
1836
            let num = Number(value)
124✔
1837
            if (isNaN(num)) {
124!
1838
                throw new OneTableError(`Invalid value "${value}" provided for field "${field.name}"`, {
×
1839
                    code: 'ValidationError',
1840
                })
1841
            }
1842
            value = num
124✔
1843
        } else if (type == 'boolean') {
9,216!
1844
            if (value == 'false' || value == 'null' || value == 'undefined') {
×
1845
                value = false
×
1846
            }
1847
            value = Boolean(value)
×
1848
        } else if (type == 'string') {
9,216✔
1849
            if (value != null) {
9,117✔
1850
                value = value.toString()
9,117✔
1851
            }
1852
        } else if (type == 'buffer' || type == 'arraybuffer' || type == 'binary') {
99✔
1853
            if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
5!
1854
                value = value.toString('base64')
5✔
1855
            }
1856
        } else if (type == 'array') {
94✔
1857
            if (value != null) {
10✔
1858
                if (Array.isArray(value)) {
10!
1859
                    value = this.transformNestedWriteFields(field, value)
10✔
1860
                } else {
1861
                    //  Heursistics to accept legacy string values for array types. Note: TS would catch this also.
1862
                    if (value == '') {
×
1863
                        value = []
×
1864
                    } else {
1865
                        //  FUTURE: should be moved to validations
1866
                        throw new OneTableArgError(
×
1867
                            `Invalid data type for Array field "${field.name}" in "${this.name}"`
1868
                        )
1869
                    }
1870
                }
1871
            }
1872
        } else if (type == 'set' && Array.isArray(value)) {
84!
1873
            value = this.transformWriteSet(type, value)
×
1874
        } else if (type == 'object' && value != null && typeof value == 'object') {
84✔
1875
            value = this.transformNestedWriteFields(field, value)
81✔
1876
        }
1877

1878
        if (field.crypt && value != null) {
9,905✔
1879
            value = this.encrypt(value)
1✔
1880
        }
1881
        return value
9,905✔
1882
    }
1883

1884
    transformNestedWriteFields(field, obj) {
1885
        for (let [key, value] of Object.entries(obj)) {
139✔
1886
            let type = field.type
2,158✔
1887
            if (value instanceof Date) {
2,158✔
1888
                obj[key] = this.transformWriteDate(field, value)
1✔
1889
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,157!
1890
                value = value.toString('base64')
×
1891
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,157!
1892
                value = this.transformWriteSet(type, value)
×
1893
            } else if (value == null && field.nulls !== true) {
2,157!
1894
                //  Skip nulls
1895
                continue
×
1896
            } else if (value != null && typeof value == 'object') {
2,157!
1897
                obj[key] = this.transformNestedWriteFields(field, value)
×
1898
            }
1899
        }
1900
        return obj
139✔
1901
    }
1902

1903
    transformWriteSet(type, value) {
1904
        if (!Array.isArray(value)) {
×
1905
            throw new OneTableError('Set values must be arrays', {code: 'TypeError'})
×
1906
        }
1907
        if (type == Set || type == 'Set' || type == 'set') {
×
1908
            let v = value.values().next().value
×
1909
            if (typeof v == 'string') {
×
1910
                value = value.map((v) => v.toString())
×
1911
            } else if (typeof v == 'number') {
×
1912
                value = value.map((v) => Number(v))
×
1913
            } else if (v instanceof Buffer || v instanceof ArrayBuffer || v instanceof DataView) {
×
1914
                value = value.map((v) => v.toString('base64'))
×
1915
            }
1916
        } else {
1917
            throw new OneTableError('Unknown type', {code: 'TypeError'})
×
1918
        }
1919
        return value
×
1920
    }
1921

1922
    /*
1923
        Handle dates. Supports epoch and ISO date transformations.
1924
    */
1925
    transformWriteDate(field, value) {
1926
        let isoDates = field.isoDates || this.table.isoDates
518✔
1927
        if (field.ttl) {
518!
1928
            //  Convert dates to DynamoDB TTL
1929
            if (value instanceof Date) {
×
1930
                value = value.getTime()
×
1931
            } else if (typeof value == 'string') {
×
1932
                value = new Date(Date.parse(value)).getTime()
×
1933
            }
1934
            value = Math.ceil(value / 1000)
×
1935
        } else if (isoDates) {
518✔
1936
            if (value instanceof Date) {
464!
1937
                value = value.toISOString()
464✔
1938
            } else if (typeof value == 'string') {
×
1939
                value = new Date(Date.parse(value)).toISOString()
×
1940
            } else if (typeof value == 'number') {
×
1941
                value = new Date(value).toISOString()
×
1942
            }
1943
        } else {
1944
            //  Convert dates to unix epoch in milliseconds
1945
            if (value instanceof Date) {
54!
1946
                value = value.getTime()
54✔
1947
            } else if (typeof value == 'string') {
×
1948
                value = new Date(Date.parse(value)).getTime()
×
1949
            }
1950
        }
1951
        return value
518✔
1952
    }
1953

1954
    /*
1955
        Get a hash of all the property names of the indexes. Keys are properties, values are index names.
1956
        Primary takes precedence if property used in multiple indexes (LSIs)
1957
     */
1958
    getIndexProperties(indexes) {
1959
        let properties = {}
313✔
1960
        for (let [indexName, index] of Object.entries(indexes)) {
313✔
1961
            for (let [type, pname] of Object.entries(index)) {
715✔
1962
                if (type == 'hash' || type == 'sort') {
1,792✔
1963
                    if (properties[pname] != 'primary') {
1,399✔
1964
                        //  Let primary take precedence
1965
                        properties[pname] = indexName
1,374✔
1966
                    }
1967
                }
1968
            }
1969
        }
1970
        return properties
313✔
1971
    }
1972

1973
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1974
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1975
    }
1976

1977
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1978
        return this.table.decrypt(text, inCode, outCode)
2✔
1979
    }
1980

1981
    /*
1982
        Clone properties and params to callers objects are not polluted
1983
    */
1984
    checkArgs(properties, params, overrides = {}) {
1,144✔
1985
        if (params.checked) {
2,372✔
1986
            //  Only need to clone once
1987
            return {properties, params}
1,117✔
1988
        }
1989
        if (!properties) {
1,255!
1990
            throw new OneTableArgError('Missing properties')
×
1991
        }
1992
        if (typeof params != 'object') {
1,255!
1993
            throw new OneTableError('Invalid type for params', {code: 'TypeError'})
×
1994
        }
1995
        //  Must not use merge as we need to modify the callers batch/transaction objects
1996
        params = Object.assign(overrides, params)
1,255✔
1997

1998
        params.checked = true
1,255✔
1999
        properties = this.table.assign({}, properties)
1,255✔
2000
        return {properties, params}
1,255✔
2001
    }
2002

2003
    /*
2004
        Handle nulls and empty strings properly according to nulls preference in plain objects and arrays.
2005
        NOTE: DynamoDB can handle empty strings as top level non-key string attributes, but not nested in lists or maps. Ugh!
2006
    */
2007
    handleEmpties(field, obj) {
2008
        let result
2009
        if (
93✔
2010
            obj !== null &&
291✔
2011
            typeof obj == 'object' &&
2012
            (obj.constructor.name == 'Object' || obj.constructor.name == 'Array')
2013
        ) {
2014
            result = Array.isArray(obj) ? [] : {}
92✔
2015
            for (let [key, value] of Object.entries(obj)) {
92✔
2016
                if (value === '') {
2,113!
2017
                    //  Convert to null and handle according to field.nulls
2018
                    value = null
×
2019
                }
2020
                if (value == null && field.nulls !== true) {
2,113!
2021
                    //  Match null and undefined
2022
                    continue
×
2023
                } else if (typeof value == 'object') {
2,113✔
2024
                    result[key] = this.handleEmpties(field, value)
1✔
2025
                } else {
2026
                    result[key] = value
2,112✔
2027
                }
2028
            }
2029
        } else {
2030
            result = obj
1✔
2031
        }
2032
        return result
93✔
2033
    }
2034

2035
    /*
2036
        Return if a field supports partial updates of its children.
2037
        Only relevant for fields with nested schema 
2038
     */
2039
    getPartial(field, params) {
2040
        let partial = params.partial
131✔
2041
        if (partial === undefined) {
131✔
2042
            partial = field.partial
107✔
2043
        }
2044
        return partial ? true : false
131✔
2045
    }
2046

2047
    /*  KEEP
2048
    captureStack() {
2049
        let limit = Error.stackTraceLimit
2050
        Error.stackTraceLimit = 1
2051

2052
        let obj = {}
2053
        let v8Handler = Error.prepareStackTrace
2054
        Error.prepareStackTrace = function(obj, stack) { return stack }
2055
        Error.captureStackTrace(obj, this.captureStack)
2056

2057
        let stack = obj.stack
2058
        Error.prepareStackTrace = v8Handler
2059
        Error.stackTraceLimit = limit
2060

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

© 2026 Coveralls, Inc