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

sensedeep / dynamodb-onetable / #81

06 Jul 2024 11:46PM UTC coverage: 75.377% (+0.1%) from 75.239%
#81

push

Michael O'Brien
CLEAN: eslint disable

1197 of 1665 branches covered (71.89%)

Branch coverage included in aggregate %.

1901 of 2445 relevant lines covered (77.75%)

747.71 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

147
            if (field.encode) {
2,547✔
148
                let schema = this.schema.definition
4✔
149
                if (typeof field.encode == 'string' && this.table.separator) {
4✔
150
                    let def = schema.models[this.name][field.encode]
1✔
151
                    if (def?.value) {
1✔
152
                        let parts = def.value.match(/\${(.*?)}/g)
1✔
153
                        let index = parts.indexOf('${' + field.name + '}')
1✔
154
                        if (index >= 0) {
1✔
155
                            field.encode = [field.encode, this.table.separator, index]
1✔
156
                        }
157
                    }
158
                    if (typeof field.encode == 'string') {
1!
159
                        throw new OneTableArgError(`Cannot resolve encoded reference for ${this.name}.${field.name}`)
×
160
                    }
161
                }
162
            }
163

164
            field.type = this.checkType(field)
2,547✔
165

166
            /*
167
                Handle mapped attributes. May be packed also (obj.prop)
168
            */
169
            let to = field.map
2,547✔
170
            if (to) {
2,547✔
171
                let [att, sub] = to.split('.')
24✔
172
                mapTargets[att] = mapTargets[att] || []
24✔
173
                if (sub) {
24✔
174
                    if (map[name] && !Array.isArray(map[name])) {
9!
175
                        throw new OneTableArgError(`Map already defined as literal for ${this.name}.${name}`)
×
176
                    }
177
                    field.attribute = map[name] = [att, sub]
9✔
178
                    if (mapTargets[att].indexOf(sub) >= 0) {
9!
179
                        throw new OneTableArgError(`Multiple attributes in ${field.name} mapped to the target ${to}`)
×
180
                    }
181
                    mapTargets[att].push(sub)
9✔
182
                } else {
183
                    if (mapTargets[att].length > 1) {
15!
184
                        throw new OneTableArgError(`Multiple attributes in ${this.name} mapped to the target ${to}`)
×
185
                    }
186
                    field.attribute = map[name] = [att]
15✔
187
                    mapTargets[att].push(true)
15✔
188
                }
189
            } else {
190
                field.attribute = map[name] = [name]
2,523✔
191
            }
192
            if (field.nulls !== true && field.nulls !== false) {
2,547✔
193
                field.nulls = this.nulls
2,547✔
194
            }
195

196
            /*
197
                Handle index requirements
198
            */
199
            let index = this.indexProperties[field.attribute[0]]
2,547✔
200
            if (index && !parent) {
2,547✔
201
                field.isIndexed = true
797✔
202
                if (field.attribute.length > 1) {
797!
203
                    throw new OneTableArgError(`Cannot map property "${field.name}" to a compound attribute"`)
×
204
                }
205
                if (index == 'primary') {
797✔
206
                    field.required = true
648✔
207
                    let attribute = field.attribute[0]
648✔
208
                    if (attribute == primary.hash) {
648✔
209
                        this.hash = attribute
334✔
210
                    } else if (attribute == primary.sort) {
314✔
211
                        this.sort = attribute
314✔
212
                    }
213
                }
214
            }
215
            if (parent && field.partial === undefined && parent.partial !== undefined) {
2,547!
216
                field.partial = parent.partial
×
217
            }
218
            if (field.value) {
2,547✔
219
                //  Value template properties are hidden by default
220
                if (field.hidden == null) {
550✔
221
                    field.hidden = true
516✔
222
                }
223
            }
224
            /*
225
                Handle nested schema (recursive)
226
            */
227
            if (field.items && field.type == 'array') {
2,547✔
228
                field.schema = field.items.schema
3✔
229
                field.isArray = true
3✔
230
            }
231
            if (field.schema) {
2,547✔
232
                if (field.type == 'object' || field.type == 'array') {
14!
233
                    field.block = {deps: [], fields: {}}
14✔
234
                    this.prepModel(field.schema, field.block, field)
14✔
235
                    //  FUTURE - better to apply this to the field block
236
                    this.nested = true
14✔
237
                } else {
238
                    throw new OneTableArgError(
×
239
                        `Nested scheme does not supported "${field.type}" types for field "${field.name}" in model "${this.name}"`
240
                    )
241
                }
242
            }
243
        }
244
        if (Object.values(fields).find((f) => f.unique && f.attribute != this.hash && f.attribute != this.sort)) {
2,521✔
245
            this.hasUniqueFields = true
8✔
246
        }
247
        this.mappings = mapTargets
349✔
248

249
        /*
250
            Order the fields so value templates can depend on each other safely
251
        */
252
        for (let field of Object.values(fields)) {
349✔
253
            this.orderFields(block, field)
2,547✔
254
        }
255
    }
256

257
    checkType(field) {
258
        let type = field.type
2,547✔
259
        if (typeof type == 'function') {
2,547✔
260
            type = type.name
1,018✔
261
        }
262
        type = type.toLowerCase()
2,547✔
263
        if (ValidTypes.indexOf(type) < 0) {
2,547!
264
            throw new OneTableArgError(`Unknown type "${type}" for field "${field.name}" in model "${this.name}"`)
×
265
        }
266
        return type
2,547✔
267
    }
268

269
    orderFields(block, field) {
270
        let {deps, fields} = block
2,549✔
271
        if (deps.find((i) => i.name == field.name)) {
11,349✔
272
            return
1✔
273
        }
274
        if (field.value) {
2,548✔
275
            let vars = this.table.getVars(field.value)
550✔
276
            for (let path of vars) {
550✔
277
                let name = path.split(/[.[]/g).shift().trim(']')
644✔
278
                let ref = fields[name]
644✔
279
                if (ref && ref != field) {
644✔
280
                    if (ref.schema) {
643✔
281
                        this.orderFields(ref.block, ref)
2✔
282
                    } else if (ref.value) {
641!
283
                        this.orderFields(block, ref)
×
284
                    }
285
                }
286
            }
287
        }
288
        deps.push(field)
2,548✔
289
    }
290

291
    getPropValue(properties, path) {
292
        let v = properties
7,813✔
293
        for (let part of path.split('.')) {
7,813✔
294
            if (v == null) return v
7,817!
295
            v = v[part]
7,817✔
296
        }
297
        return v
7,813✔
298
    }
299

300
    /*
301
        Run an operation on DynamodDB. The command has been parsed via Expression.
302
        Returns [] for find/scan, cmd if !execute, else returns item.
303
     */
304
    async run(op, expression) {
305
        let {index, properties, params} = expression
1,252✔
306

307
        /*
308
            Get a string representation of the API request
309
         */
310
        let cmd = expression.command()
1,252✔
311
        if (!expression.execute) {
1,250✔
312
            if (params.log !== false) {
1✔
313
                this.table.log[params.log ? 'info' : 'data'](
1!
314
                    `OneTable command for "${op}" "${this.name} (not executed)"`,
315
                    {
316
                        cmd,
317
                        op,
318
                        properties,
319
                        params,
320
                    }
321
                )
322
            }
323
            return cmd
1✔
324
        }
325
        /*
326
            Transactions save the command in params.transaction and wait for db.transaction() to be called.
327
         */
328
        let t = params.transaction
1,249✔
329
        if (t) {
1,249✔
330
            if (params.batch) {
72!
331
                throw new OneTableArgError('Cannot have batched transactions')
×
332
            }
333
            let top = TransactOps[op]
72✔
334
            if (top) {
72!
335
                params.expression = expression
72✔
336
                let items = (t.TransactItems = t.TransactItems || [])
72✔
337
                items.push({[top]: cmd})
72✔
338
                return this.transformReadItem(op, properties, properties, params)
72✔
339
            } else {
340
                throw new OneTableArgError(`Unknown transaction operation ${op}`)
×
341
            }
342
        }
343
        /*
344
            Batch operations save the command in params.transaction and wait for db.batchGet|batchWrite to be called.
345
         */
346
        let b = params.batch
1,177✔
347
        if (b) {
1,177✔
348
            params.expression = expression
24✔
349
            let ritems = (b.RequestItems = b.RequestItems || {})
24✔
350
            if (op == 'get') {
24✔
351
                let list = (ritems[this.tableName] = ritems[this.tableName] || {Keys: []})
9✔
352
                list.Keys.push(cmd.Keys)
9✔
353
                return this.transformReadItem(op, properties, properties, params)
9✔
354
            } else {
355
                let list = (ritems[this.tableName] = ritems[this.tableName] || [])
15✔
356
                let bop = BatchOps[op]
15✔
357
                list.push({[bop]: cmd})
15✔
358
                return this.transformReadItem(op, properties, properties, params)
15✔
359
            }
360
        }
361
        /*
362
            Prep the stats
363
        */
364
        let stats = params.stats
1,153✔
365
        if (stats && typeof params == 'object') {
1,153✔
366
            stats.count = stats.count || 0
1✔
367
            stats.scanned = stats.capacity || 0
1✔
368
            stats.capacity = stats.capacity || 0
1✔
369
        }
370

371
        /*
372
            Run command. Paginate if required.
373
         */
374
        let pages = 0,
1,153✔
375
            items = [],
1,153✔
376
            count = 0
1,153✔
377
        let maxPages = params.maxPages ? params.maxPages : SanityPages
1,153!
378
        let result
379
        do {
1,153✔
380
            result = await this.table.execute(this.name, op, cmd, properties, params)
1,158✔
381
            if (result.LastEvaluatedKey) {
1,157✔
382
                //  Continue next page
383
                cmd.ExclusiveStartKey = result.LastEvaluatedKey
31✔
384
            }
385
            if (result.Items) {
1,157✔
386
                items = items.concat(result.Items)
149✔
387
            } else if (result.Item) {
1,008✔
388
                items = [result.Item]
52✔
389
                break
52✔
390
            } else if (result.Attributes) {
956✔
391
                items = [result.Attributes]
94✔
392
                break
94✔
393
            } else if (params.count || params.select == 'COUNT') {
862✔
394
                count += result.Count
9✔
395
            }
396
            if (stats) {
1,011✔
397
                if (result.Count) {
1✔
398
                    stats.count += result.Count
1✔
399
                }
400
                if (result.ScannedCount) {
1✔
401
                    stats.scanned += result.ScannedCount
1✔
402
                }
403
                if (result.ConsumedCapacity) {
1✔
404
                    stats.capacity += result.ConsumedCapacity.CapacityUnits
1✔
405
                }
406
            }
407
            if (params.progress) {
1,011!
408
                params.progress({items, pages, stats, params, cmd})
×
409
            }
410
            if (items.length) {
1,011✔
411
                if (cmd.Limit) {
130✔
412
                    cmd.Limit -= result.Count
38✔
413
                    if (cmd.Limit <= 0) {
38✔
414
                        break
26✔
415
                    }
416
                }
417
            }
418
        } while (result.LastEvaluatedKey && (maxPages == null || ++pages < maxPages))
995✔
419

420
        let prev
421
        if ((op == 'find' || op == 'scan') && items.length) {
1,152✔
422
            if (items.length) {
127✔
423
                /*
424
                    Determine next / previous cursors. Note: data items not yet reversed if scanning backwards.
425
                    Can use LastEvaluatedKey for the direction of scanning. Calculate the other end from the returned items.
426
                    Next/prev will be swapped when the items are reversed below
427
                */
428
                let {hash, sort} = params.index && params.index != 'primary' ? index : this.indexes.primary
127✔
429
                let cursor = {[hash]: items[0][hash], [sort]: items[0][sort]}
127✔
430
                if (cursor[hash] == null || cursor[sort] == null) {
127✔
431
                    cursor = null
12✔
432
                }
433
                if (params.next || params.prev) {
127✔
434
                    prev = cursor
19✔
435
                    if (cursor && params.index != 'primary') {
19✔
436
                        let {hash, sort} = this.indexes.primary
19✔
437
                        prev[hash] = items[0][hash]
19✔
438
                        if (sort != null) {
19✔
439
                            prev[sort] = items[0][sort]
15✔
440
                        }
441
                    }
442
                }
443
            }
444
        }
445

446
        /*
447
            Process the response
448
        */
449
        if (params.parse) {
1,152✔
450
            items = this.parseResponse(op, expression, items)
1,140✔
451
        }
452

453
        /*
454
            Handle pagination next/prev
455
        */
456
        if (op == 'find' || op == 'scan') {
1,152✔
457
            if (result.LastEvaluatedKey) {
153✔
458
                items.next = this.table.unmarshall(result.LastEvaluatedKey, params)
26✔
459
                Object.defineProperty(items, 'next', {enumerable: false})
26✔
460
            }
461
            if (params.count || params.select == 'COUNT') {
153✔
462
                items.count = count
8✔
463
                Object.defineProperty(items, 'count', {enumerable: false})
8✔
464
            }
465
            if (prev) {
153✔
466
                items.prev = this.table.unmarshall(prev, params)
19✔
467
                Object.defineProperty(items, 'prev', {enumerable: false})
19✔
468
            }
469
            if (params.prev && params.next == null && op != 'scan') {
153✔
470
                //  DynamoDB scan ignores ScanIndexForward
471
                items = items.reverse()
1✔
472
                let tmp = items.prev
1✔
473
                items.prev = items.next
1✔
474
                items.next = tmp
1✔
475
            }
476
        }
477

478
        /*
479
            Log unless the user provides params.log: false.
480
            The logger will typically filter data/trace.
481
        */
482
        if (params.log !== false) {
1,152✔
483
            this.table.log[params.log ? 'info' : 'data'](`OneTable result for "${op}" "${this.name}"`, {
1,152✔
484
                cmd,
485
                items,
486
                op,
487
                properties,
488
                params,
489
            })
490
        }
491

492
        /*
493
            Handle transparent follow. Get/Update/Find the actual item using the keys
494
            returned from the request on the GSI.
495
        */
496
        if (params.follow || (index.follow && params.follow !== false)) {
1,152!
497
            if (op == 'get') {
3!
498
                return await this.get(items[0])
×
499
            }
500
            if (op == 'update') {
3!
501
                properties = Object.assign({}, properties, items[0])
×
502
                return await this.update(properties)
×
503
            }
504
            if (op == 'find') {
3✔
505
                let results = [],
3✔
506
                    promises = []
3✔
507
                params = Object.assign({}, params)
3✔
508
                delete params.follow
3✔
509
                delete params.index
3✔
510
                delete params.fallback
3✔
511
                for (let item of items) {
3✔
512
                    promises.push(this.get(item, params))
3✔
513
                    if (promises.length > FollowThreads) {
3!
514
                        results = results.concat(await Promise.all(promises))
×
515
                        promises = []
×
516
                    }
517
                }
518
                if (promises.length) {
3✔
519
                    results = results.concat(await Promise.all(promises))
3✔
520
                }
521
                results.next = items.next
3✔
522
                results.prev = items.prev
3✔
523
                results.count = items.count
3✔
524
                Object.defineProperty(results, 'next', {enumerable: false})
3✔
525
                Object.defineProperty(results, 'prev', {enumerable: false})
3✔
526
                return results
3✔
527
            }
528
        }
529
        return op == 'find' || op == 'scan' ? items : items[0]
1,149✔
530
    }
531

532
    /*
533
        Parse the response into Javascript objects and transform for the high level API.
534
     */
535
    parseResponse(op, expression, items) {
536
        let {properties, params} = expression
1,145✔
537
        let {schema, table} = this
1,145✔
538
        if (op == 'put') {
1,145✔
539
            //  Put requests do not return the item. So use the properties.
540
            items = [properties]
850✔
541
        } else {
542
            items = table.unmarshall(items, params)
295✔
543
        }
544
        for (let [index, item] of Object.entries(items)) {
1,145✔
545
            if (params.high && params.index == this.indexes.primary && item[this.typeField] != this.name) {
3,261!
546
                //  High level API on the primary index and item for a different model
547
                continue
×
548
            }
549
            let type = item[this.typeField] ? item[this.typeField] : this.name
3,261✔
550
            let model = schema.models[type] ? schema.models[type] : this
3,261✔
551
            if (model) {
3,261✔
552
                if (model == schema.uniqueModel) {
3,261!
553
                    //  Special "unique" model for unique fields. Don't return in result.
554
                    continue
×
555
                }
556
                items[index] = model.transformReadItem(op, item, properties, params)
3,261✔
557
            }
558
        }
559
        return items
1,145✔
560
    }
561

562
    /*
563
        Create/Put a new item. Will overwrite existing items if exists: null.
564
    */
565
    async create(properties, params = {}) {
846✔
566
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true, exists: false}))
892✔
567
        let result
568
        if (this.hasUniqueFields) {
892✔
569
            result = await this.createUnique(properties, params)
7✔
570
        } else {
571
            result = await this.putItem(properties, params)
885✔
572
        }
573
        return result
887✔
574
    }
575

576
    /*
577
        Create an item with unique attributes. Use a transaction to create a unique item for each unique attribute.
578
     */
579
    async createUnique(properties, params) {
580
        if (params.batch) {
7!
581
            throw new OneTableArgError('Cannot use batch with unique properties which require transactions')
×
582
        }
583
        let transactHere = params.transaction ? false : true
7✔
584
        let transaction = (params.transaction = params.transaction || {})
7✔
585
        let {hash, sort} = this.indexes.primary
7✔
586
        let fields = this.block.fields
7✔
587

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

590
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
7✔
591

592
        if (params.timestamps !== false) {
7✔
593
            if (this.timestamps === true || this.timestamps == 'create') {
7✔
594
                properties[this.createdField] = timestamp
1✔
595
            }
596
            if (this.timestamps === true || this.timestamps == 'update') {
7✔
597
                properties[this.updatedField] = timestamp
1✔
598
            }
599
        }
600
        params.prepared = properties = this.prepareProperties('put', properties, params)
7✔
601

602
        for (let field of fields) {
7✔
603
            if (properties[field.name] !== undefined) {
13✔
604
                let scope = ''
11✔
605
                if (field.scope) {
11!
606
                    scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
607
                    if (scope == undefined) {
×
608
                        throw new OneTableError('Missing properties to resolve unique scope', {
×
609
                            properties,
610
                            field,
611
                            scope: field.scope,
612
                            code: 'UniqueError',
613
                        })
614
                    }
615
                }
616
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
11✔
617
                let sk = '_unique#'
11✔
618
                await this.schema.uniqueModel.create(
11✔
619
                    {[this.hash]: pk, [this.sort]: sk},
620
                    {transaction, exists: false, return: 'NONE'}
621
                )
622
            }
623
        }
624
        let item = await this.putItem(properties, params)
7✔
625

626
        if (!transactHere) {
7✔
627
            return item
1✔
628
        }
629
        let expression = params.expression
6✔
630
        try {
6✔
631
            await this.table.transact('write', params.transaction, params)
6✔
632
        } catch (err) {
633
            if (
1✔
634
                err instanceof OneTableError &&
3✔
635
                err.code === 'TransactionCanceledException' &&
636
                err.context.err.message.indexOf('ConditionalCheckFailed') !== -1
637
            ) {
638
                let names = fields.map((f) => f.name).join(', ')
3✔
639
                throw new OneTableError(
1✔
640
                    `Cannot create unique attributes "${names}" for "${this.name}". An item of the same name already exists.`,
641
                    {properties, transaction, code: 'UniqueError'}
642
                )
643
            }
644
            throw err
×
645
        }
646
        let items = this.parseResponse('put', expression)
5✔
647
        return items[0]
5✔
648
    }
649

650
    async check(properties, params) {
651
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
3✔
652
        properties = this.prepareProperties('get', properties, params)
3✔
653
        const expression = new Expression(this, 'check', properties, params)
3✔
654
        this.run('check', expression)
3✔
655
    }
656

657
    async find(properties = {}, params = {}) {
11✔
658
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
71✔
659
        return await this.queryItems(properties, params)
71✔
660
    }
661

662
    async get(properties = {}, params = {}) {
37!
663
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
90✔
664
        properties = this.prepareProperties('get', properties, params)
90✔
665
        if (params.fallback) {
90✔
666
            if (params.batch) {
13!
667
                throw new OneTableError('Need complete keys for batched get operation', {
×
668
                    properties,
669
                    code: 'NonUniqueError',
670
                })
671
            }
672
            //  Fallback via find when using non-primary indexes
673
            params.limit = 2
13✔
674
            let items = await this.find(properties, params)
13✔
675
            if (items.length > 1) {
13✔
676
                throw new OneTableError('Get without sort key returns more than one result', {
2✔
677
                    properties,
678
                    code: 'NonUniqueError',
679
                })
680
            }
681
            return items[0]
11✔
682
        }
683
        //  FUTURE refactor to use getItem
684
        let expression = new Expression(this, 'get', properties, params)
77✔
685
        return await this.run('get', expression)
77✔
686
    }
687

688
    async load(properties = {}, params = {}) {
6!
689
        ;({properties, params} = this.checkArgs(properties, params))
6✔
690
        properties = this.prepareProperties('get', properties, params)
6✔
691
        let expression = new Expression(this, 'get', properties, params)
6✔
692
        return await this.table.batchLoad(expression)
6✔
693
    }
694

695
    init(properties = {}, params = {}) {
×
696
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
697
        return this.initItem(properties, params)
×
698
    }
699

700
    async remove(properties, params = {}) {
18✔
701
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
42✔
702

703
        properties = this.prepareProperties('delete', properties, params)
42✔
704
        if (params.fallback || params.many) {
42✔
705
            return await this.removeByFind(properties, params)
3✔
706
        }
707
        let expression = new Expression(this, 'delete', properties, params)
39✔
708
        if (this.hasUniqueFields) {
39✔
709
            return await this.removeUnique(properties, params)
2✔
710
        } else {
711
            return await this.run('delete', expression)
37✔
712
        }
713
    }
714

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

745
    /*
746
        Remove an item with unique properties. Use transactions to remove unique items.
747
    */
748
    async removeUnique(properties, params) {
749
        let transactHere = params.transaction ? false : true
2!
750
        let transaction = (params.transaction = params.transaction || {})
2✔
751
        let {hash, sort} = this.indexes.primary
2✔
752
        let fields = Object.values(this.block.fields).filter(
2✔
753
            (f) => f.unique && f.attribute != hash && f.attribute != sort
16✔
754
        )
755

756
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
757

758
        let keys = {
2✔
759
            [hash]: properties[hash],
760
        }
761
        if (sort) {
2✔
762
            keys[sort] = properties[sort]
2✔
763
        }
764
        /*
765
            Get the prior item so we know the previous unique property values so they can be removed.
766
            This must be run here, even if part of a transaction.
767
        */
768
        let prior = await this.get(keys, {hidden: true})
2✔
769
        if (prior) {
2!
770
            prior = this.prepareProperties('update', prior)
2✔
771
        } else if (params.exists === undefined || params.exists == true) {
×
772
            throw new OneTableError('Cannot find existing item to remove', {properties, code: 'NotFoundError'})
×
773
        }
774

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

817
    async scan(properties = {}, params = {}) {
63✔
818
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
52✔
819
        return await this.scanItems(properties, params)
52✔
820
    }
821

822
    async update(properties, params = {}) {
25✔
823
        ;({properties, params} = this.checkArgs(properties, params, {exists: true, parse: true, high: true}))
83✔
824
        if (this.hasUniqueFields) {
83✔
825
            let hasUniqueProperties = Object.entries(properties).find((pair) => {
9✔
826
                return this.block.fields[pair[0]] && this.block.fields[pair[0]].unique
19✔
827
            })
828
            if (hasUniqueProperties) {
9✔
829
                return await this.updateUnique(properties, params)
7✔
830
            }
831
        }
832
        return await this.updateItem(properties, params)
76✔
833
    }
834

835
    async upsert(properties, params = {}) {
×
836
        params.exists = null
×
837
        return await this.update(properties, params)
×
838
    }
839

840
    /*
841
        Update an item with unique attributes.
842
        Use a transaction to update a unique item for each unique attribute.
843
     */
844
    async updateUnique(properties, params) {
845
        if (params.batch) {
7!
846
            throw new OneTableArgError('Cannot use batch with unique properties which require transactions')
×
847
        }
848
        let transactHere = params.transaction ? false : true
7✔
849
        let transaction = (params.transaction = params.transaction || {})
7✔
850
        let index = this.indexes.primary
7✔
851
        let {hash, sort} = index
7✔
852

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

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

933
        if (!transactHere) {
7✔
934
            return item
2✔
935
        }
936

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

976
    //  Low level API
977

978
    /* private */
979
    async deleteItem(properties, params = {}) {
1✔
980
        ;({properties, params} = this.checkArgs(properties, params))
3✔
981
        if (!params.prepared) {
3✔
982
            properties = this.prepareProperties('delete', properties, params)
1✔
983
        }
984
        let expression = new Expression(this, 'delete', properties, params)
3✔
985
        return await this.run('delete', expression)
3✔
986
    }
987

988
    /* private */
989
    async getItem(properties, params = {}) {
×
990
        ;({properties, params} = this.checkArgs(properties, params))
2✔
991
        properties = this.prepareProperties('get', properties, params)
2✔
992
        let expression = new Expression(this, 'get', properties, params)
2✔
993
        return await this.run('get', expression)
2✔
994
    }
995

996
    /* private */
997
    initItem(properties, params = {}) {
×
998
        ;({properties, params} = this.checkArgs(properties, params))
×
999
        let fields = this.block.fields
×
1000
        this.setDefaults('init', fields, properties, params)
×
1001
        //  Ensure all fields are present
1002
        for (let key of Object.keys(fields)) {
×
1003
            if (properties[key] === undefined) {
×
1004
                properties[key] = null
×
1005
            }
1006
        }
1007
        this.runTemplates('put', '', this.indexes.primary, this.block.deps, properties, params)
×
1008
        return properties
×
1009
    }
1010

1011
    /* private */
1012
    async putItem(properties, params = {}) {
×
1013
        ;({properties, params} = this.checkArgs(properties, params))
894✔
1014
        if (!params.prepared) {
894✔
1015
            if (params.timestamps !== false) {
887✔
1016
                let timestamp = params.transaction
887✔
1017
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
36✔
1018
                    : new Date()
1019

1020
                if (this.timestamps === true || this.timestamps == 'create') {
887✔
1021
                    properties[this.createdField] = timestamp
226✔
1022
                }
1023
                if (this.timestamps === true || this.timestamps == 'update') {
887✔
1024
                    properties[this.updatedField] = timestamp
226✔
1025
                }
1026
            }
1027
            properties = this.prepareProperties('put', properties, params)
887✔
1028
        }
1029
        let expression = new Expression(this, 'put', properties, params)
890✔
1030
        return await this.run('put', expression)
890✔
1031
    }
1032

1033
    /* private */
1034
    async queryItems(properties = {}, params = {}) {
×
1035
        ;({properties, params} = this.checkArgs(properties, params))
83✔
1036
        properties = this.prepareProperties('find', properties, params)
83✔
1037
        let expression = new Expression(this, 'find', properties, params)
83✔
1038
        return await this.run('find', expression)
83✔
1039
    }
1040

1041
    //  Note: scanItems will return all model types
1042
    /* private */
1043
    async scanItems(properties = {}, params = {}) {
12✔
1044
        ;({properties, params} = this.checkArgs(properties, params))
72✔
1045
        properties = this.prepareProperties('scan', properties, params)
72✔
1046
        let expression = new Expression(this, 'scan', properties, params)
72✔
1047
        return await this.run('scan', expression)
72✔
1048
    }
1049

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

1072
    /* private */
1073
    async fetch(models, properties = {}, params = {}) {
2!
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,379!
1099
            return raw
×
1100
        }
1101
        return this.transformReadBlock(op, raw, properties, params, this.block.fields)
3,379✔
1102
    }
1103

1104
    transformReadBlock(op, raw, properties, params, fields) {
1105
        let rec = {}
3,470✔
1106
        for (let [name, field] of Object.entries(fields)) {
3,470✔
1107
            //  Skip hidden params. Follow needs hidden params to do the follow.
1108
            if (field.hidden && params.follow !== true) {
33,476✔
1109
                if (params.hidden === false || (params.hidden == null && this.table.hidden === false)) {
18,513✔
1110
                    continue
18,255✔
1111
                }
1112
            }
1113
            let att, sub
1114
            if (op == 'put') {
15,221✔
1115
                att = field.name
4,329✔
1116
            } else {
1117
                ;[att, sub] = field.attribute
10,892✔
1118
            }
1119
            let value = raw[att]
15,221✔
1120
            if (value === undefined) {
15,221✔
1121
                if (field.encode) {
2,629✔
1122
                    let [att, sep, index] = field.encode
5✔
1123
                    value = (raw[att] || '').split(sep)[index]
5!
1124
                }
1125
            }
1126
            if (sub && value) {
15,221✔
1127
                value = value[sub]
45✔
1128
            }
1129
            if (field.crypt && params.decrypt !== false) {
15,221✔
1130
                value = this.decrypt(value)
2✔
1131
            }
1132
            if (field.default !== undefined && value === undefined) {
15,221✔
1133
                if (!params.fields || params.fields.indexOf(name) >= 0) {
34✔
1134
                    rec[name] = field.default
32✔
1135
                }
1136
            } else if (value === undefined) {
15,187✔
1137
                if (field.required) {
2,590✔
1138
                    /*
1139
                        Transactions transform the properties to return something, but
1140
                        does not have all the properties and required fields may be missing).
1141
                        Also find operation with fields selections may not include required fields.
1142
                     */
1143
                    if (!params.transaction && !params.batch && !params.fields && !field.encode && !params.noerror) {
16✔
1144
                        this.table.log.error(`Required field "${name}" in model "${this.name}" not defined in table item`, {
4✔
1145
                            model: this.name, raw, params, field,
1146
                        })
1147
                    }
1148
                }
1149
            } else if (field.schema && value !== null && typeof value == 'object') {
12,597✔
1150
                if (field.items && Array.isArray(value)) {
95✔
1151
                    rec[name] = []
16✔
1152
                    let i = 0
16✔
1153
                    for (let rvalue of raw[att]) {
16✔
1154
                        rec[name][i] = this.transformReadBlock(
12✔
1155
                            op,
1156
                            rvalue,
1157
                            properties[name] || [],
15✔
1158
                            params,
1159
                            field.block.fields
1160
                        )
1161
                        i++
12✔
1162
                    }
1163
                } else {
1164
                    rec[name] = this.transformReadBlock(
79✔
1165
                        op,
1166
                        raw[att],
1167
                        properties[name] || {},
130✔
1168
                        params,
1169
                        field.block.fields
1170
                    )
1171
                }
1172
            } else {
1173
                rec[name] = this.transformReadAttribute(field, name, value, params, properties)
12,502✔
1174
            }
1175
        }
1176
        if (this.generic) {
3,470✔
1177
            //  Generic must include attributes outside the schema.
1178
            for (let [name, value] of Object.entries(raw)) {
37✔
1179
                if (rec[name] === undefined) {
117✔
1180
                    rec[name] = value
53✔
1181
                }
1182
            }
1183
        }
1184
        if (
3,470✔
1185
            params.hidden == true &&
3,518✔
1186
            rec[this.typeField] === undefined &&
1187
            !this.generic &&
1188
            this.block.fields == fields
1189
        ) {
1190
            rec[this.typeField] = this.name
1✔
1191
        }
1192
        if (this.table.params.transform) {
3,470✔
1193
            let opForTransform = TransformParseResponseAs[op]
6✔
1194
            rec = this.table.params.transform(this, ReadWrite[opForTransform], rec, properties, params, raw)
6✔
1195
        }
1196
        return rec
3,470✔
1197
    }
1198

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

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

1226
        if (this.needsFallback(op, index, params)) {
1,296✔
1227
            params.fallback = true
9✔
1228
            return properties
9✔
1229
        }
1230
        //  DEPRECATE
1231
        this.tunnelProperties(properties, params)
1,287✔
1232

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

1254
    /*
1255
        Convert a full text params.filter into a smart params.where
1256
        NOTE: this is prototype code and definitely not perfect! Use at own risk.
1257
     */
1258
    convertFilter(properties, params, index) {
1259
        let filter = params.filter.toString()
×
1260
        let fields = this.block.fields
×
1261
        let where
1262
        //  TODO support > >= < <= ..., AND or ...
1263
        let [name, value] = filter.split('=')
×
1264
        if (value) {
×
1265
            name = name.trim()
×
1266
            value = value.trim()
×
1267
            let field = fields[name]
×
1268
            if (field) {
×
1269
                name = field.map ? field.map : name
×
1270
                if (field.encode) {
×
1271
                    properties[name] = value
×
1272
                } else {
1273
                    where = `\${${name}} = {"${value}"}`
×
1274
                }
1275
            } else {
1276
                where = `\${${name}} = {"${value}"}`
×
1277
            }
1278
        } else {
1279
            value = name
×
1280
            where = []
×
1281
            for (let [name, field] of Object.entries(fields)) {
×
1282
                let primary = this.indexes.primary
×
1283
                if (primary.hash == name || primary.sort == name || index.hash == name || index.sort == name) {
×
1284
                    continue
×
1285
                }
1286
                if (field.encode) {
×
1287
                    continue
×
1288
                }
1289
                name = field.map ? field.map : name
×
1290
                where.push(`(contains(\${${name}}, {"${filter}"}))`)
×
1291
            }
1292
            if (where) {
×
1293
                where = where.join(' or ')
×
1294
            }
1295
        }
1296
        params.where = where
×
1297
        params.maxPages = params.maxPages || 25
×
1298
        //  Remove limit otherwise the search will only search "limit" items at a time
1299
        delete params.limit
×
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,296✔
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,287✔
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,203!
1318
        if (generic) {
1,203✔
1319
            return rec[index.hash]
20✔
1320
        }
1321
        let field = Object.values(fields).find((f) => f.attribute[0] == index.hash)
1,654✔
1322
        if (!field) {
1,183!
1323
            return null
×
1324
        }
1325
        return rec[field.name]
1,183✔
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,296✔
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,253✔
1340
        }
1341
        return index
1,296✔
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,323✔
1352
        let fields = block.fields
1,323✔
1353
        if (!context) {
1,323✔
1354
            context = params.context || this.table.context
1,287✔
1355
        }
1356
        /*
1357
            First process nested schemas recursively
1358
        */
1359
        if (this.nested && !KeysOnly[op]) {
1,323✔
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,323✔
1366
        this.setDefaults(op, fields, properties, params)
1,323✔
1367
        this.runTemplates(op, pathname, index, block.deps, properties, params)
1,323✔
1368
        this.convertNulls(op, pathname, fields, properties, params)
1,323✔
1369
        this.validateProperties(op, fields, properties, params)
1,323✔
1370
        this.selectProperties(op, block, index, properties, params, rec)
1,318✔
1371
        this.transformProperties(op, fields, properties, params, rec)
1,318✔
1372
        return rec
1,318✔
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,287!
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,318✔
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,318✔
1443
            if (field.schema) continue
13,827✔
1444
            let omit = false
13,725✔
1445

1446
            if (block == this.block) {
13,725✔
1447
                let attribute = field.attribute[0]
13,638✔
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,638✔
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,631✔
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,202✔
1464
                } else if (project && project.indexOf(attribute) < 0) {
12,429✔
1465
                    //  Attribute is not projected
1466
                    omit = true
16✔
1467
                } else if (name == this.typeField && name != index.hash && name != index.sort && op == 'find') {
12,413✔
1468
                    omit = true
79✔
1469
                } else if (field.encode) {
12,334✔
1470
                    omit = true
3✔
1471
                }
1472
            }
1473
            if (!omit && properties[name] !== undefined) {
13,718✔
1474
                rec[name] = properties[name]
9,925✔
1475
            }
1476
        }
1477
        if (block == this.block) {
1,311✔
1478
            //  Only do at top level
1479
            this.addProjectedProperties(op, properties, params, project, rec)
1,275✔
1480
        }
1481
    }
1482

1483
    getProjection(index) {
1484
        let project = index.project
1,318✔
1485
        if (project) {
1,318✔
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,318✔
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,275!
1504
        if (generic && !KeysOnly[op]) {
1,275✔
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,275✔
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,323✔
1532
            if (field.schema) continue
13,933✔
1533
            if (op == 'put' || (field.attribute[0] != index.hash && field.attribute[0] != index.sort)) {
13,831✔
1534
                if (context[field.name] !== undefined) {
13,062✔
1535
                    properties[field.name] = context[field.name]
33✔
1536
                }
1537
            }
1538
        }
1539
        if (!this.generic && fields == this.block.fields) {
1,323✔
1540
            //  Set type field for the top level only
1541
            properties[this.typeField] = this.name
1,247✔
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,323✔
1550
            return
395✔
1551
        }
1552
        for (let field of Object.values(fields)) {
928✔
1553
            if (field.schema) continue
9,617✔
1554
            let value = properties[field.name]
9,597✔
1555

1556
            //  Set defaults and uuid fields
1557
            if (value === undefined && !field.value) {
9,597✔
1558
                if (field.default !== undefined) {
1,621✔
1559
                    value = field.default
7✔
1560
                } else if (op == 'init') {
1,614!
1561
                    if (!field.generate) {
×
1562
                        //  Set non-default, non-uuid properties to null
1563
                        value = null
×
1564
                    }
1565
                } else if (field.generate) {
1,614✔
1566
                    let generate = field.generate
855✔
1567
                    if (generate === true) {
855!
1568
                        value = this.table.generate()
×
1569
                    } else if (generate == 'uuid') {
855✔
1570
                        value = this.table.uuid()
1✔
1571
                    } else if (generate == 'ulid') {
854!
1572
                        value = this.table.ulid()
854✔
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,621✔
1581
                    properties[field.name] = value
862✔
1582
                }
1583
            }
1584
        }
1585
        return properties
928✔
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,323✔
1595
            let field = fields[name]
10,380✔
1596
            if (!field || field.schema) continue
10,380✔
1597
            if (value === null && field.nulls !== true) {
10,324✔
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,314✔
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,323✔
1634
            if (field.schema) continue
13,934✔
1635
            let name = field.name
13,831✔
1636
            if (
13,831✔
1637
                field.isIndexed &&
23,469✔
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,084✔
1647

1648
            if (field.value === true && typeof this.table.params.value == 'function') {
13,084✔
1649
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1650
            } else if (properties[name] === undefined) {
13,080✔
1651
                if (field.value) {
7,668✔
1652
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
5,019✔
1653
                    if (value != null) {
5,019✔
1654
                        properties[name] = value
4,902✔
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,019✔
1670
            let [name, len, pad] = varName.split(':')
7,813✔
1671
            let v = this.getPropValue(properties, name)
7,813✔
1672
            if (v != null) {
7,813✔
1673
                if (v instanceof Date) {
7,634!
1674
                    v = this.transformWriteDate(field, v)
×
1675
                }
1676
                if (len) {
7,634!
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
179✔
1685
            }
1686
            if (typeof v == 'object' && v.toString() == '[object Object]') {
7,813!
1687
                throw new OneTableError(`Value for "${field.name}" is not a primitive value`, {code: 'TypeError'})
×
1688
            }
1689
            return v
7,813✔
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,019✔
1697
            if (index) {
164✔
1698
                if (field.attribute[0] == index.sort) {
164✔
1699
                    if (op == 'find') {
62✔
1700
                        //  Strip from first ${ onward and retain fixed prefix portion
1701
                        value = value.replace(/\${.*/g, '')
47✔
1702
                        if (value) {
47✔
1703
                            return {begins: value}
47✔
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,855✔
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,323✔
1729
            return
294✔
1730
        }
1731
        let validation = {}
1,029✔
1732
        if (typeof this.table.params.validate == 'function') {
1,029✔
1733
            validation = this.table.params.validate(this, properties, params) || {}
4!
1734
        }
1735
        for (let [name, value] of Object.entries(properties)) {
1,029✔
1736
            let field = fields[name]
9,402✔
1737
            if (!field || field.schema) continue
9,402✔
1738
            if (params.validate || field.validate || field.enum) {
9,354✔
1739
                value = this.validateProperty(field, value, validation, params)
21✔
1740
                properties[name] = value
21✔
1741
            }
1742
        }
1743
        for (let field of Object.values(fields)) {
1,029✔
1744
            //  If required and create, must be defined. If required and update, must not be null.
1745
            if (
10,676✔
1746
                field.required &&
27,085✔
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,029✔
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
21✔
1765

1766
        if (typeof params.validate == 'function') {
21!
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
21✔
1774
        if (validate) {
21✔
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) {
21✔
1801
            if (field.enum.indexOf(value) < 0) {
1!
1802
                details[fieldName] = `Bad value "${value}" for "${fieldName}"`
×
1803
            }
1804
        }
1805
        return value
21✔
1806
    }
1807

1808
    transformProperties(op, fields, properties, params, rec) {
1809
        for (let [name, field] of Object.entries(fields)) {
1,318✔
1810
            //  Nested schemas handled via collectProperties
1811
            if (field.schema) continue
13,883✔
1812
            let value = rec[name]
13,781✔
1813
            if (value !== undefined) {
13,781✔
1814
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
9,925✔
1815
            }
1816
        }
1817
        return rec
1,318✔
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,925✔
1825

1826
        if (typeof params.transform == 'function') {
9,925!
1827
            value = params.transform(this, 'write', field.name, value, properties, null)
×
1828
        } else if (value == null && field.nulls === true) {
9,925!
1829
            //  Keep the null
1830
        } else if (op == 'find' && value != null && typeof value == 'object') {
9,925✔
1831
            //  Find used {begins} for sort keys and other operators
1832
            value = this.transformNestedWriteFields(field, value)
49✔
1833
        } else if (type == 'date') {
9,876✔
1834
            value = this.transformWriteDate(field, value)
519✔
1835
        } else if (type == 'number') {
9,357✔
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,233!
1844
            if (value == 'false' || value == 'null' || value == 'undefined') {
×
1845
                value = false
×
1846
            }
1847
            value = Boolean(value)
×
1848
        } else if (type == 'string') {
9,233✔
1849
            if (value != null) {
9,134✔
1850
                value = value.toString()
9,134✔
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,925✔
1879
            value = this.encrypt(value)
1✔
1880
        }
1881
        return value
9,925✔
1882
    }
1883

1884
    transformNestedWriteFields(field, obj) {
1885
        for (let [key, value] of Object.entries(obj)) {
140✔
1886
            let type = field.type
2,159✔
1887
            if (value instanceof Date) {
2,159✔
1888
                obj[key] = this.transformWriteDate(field, value)
1✔
1889
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,158!
1890
                value = value.toString('base64')
×
1891
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,158!
1892
                value = this.transformWriteSet(type, value)
×
1893
            } else if (value == null && field.nulls !== true) {
2,158!
1894
                //  Skip nulls
1895
                continue
×
1896
            } else if (value != null && typeof value == 'object') {
2,158!
1897
                obj[key] = this.transformNestedWriteFields(field, value)
×
1898
            }
1899
        }
1900
        return obj
140✔
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
520✔
1927
        if (field.ttl) {
520!
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) {
520✔
1936
            if (value instanceof Date) {
466!
1937
                value = value.toISOString()
466✔
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
520✔
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 = {}
335✔
1960
        for (let [indexName, index] of Object.entries(indexes)) {
335✔
1961
            for (let [type, pname] of Object.entries(index)) {
793✔
1962
                if (type == 'hash' || type == 'sort') {
2,004✔
1963
                    if (properties[pname] != 'primary') {
1,555✔
1964
                        //  Let primary take precedence
1965
                        properties[pname] = indexName
1,530✔
1966
                    }
1967
                }
1968
            }
1969
        }
1970
        return properties
335✔
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,148✔
1985
        if (params.checked) {
2,381✔
1986
            //  Only need to clone once
1987
            return {properties, params}
1,121✔
1988
        }
1989
        if (!properties) {
1,260!
1990
            throw new OneTableArgError('Missing properties')
×
1991
        }
1992
        if (typeof params != 'object') {
1,260!
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,260✔
1997

1998
        params.checked = true
1,260✔
1999
        properties = this.table.assign({}, properties)
1,260✔
2000
        return {properties, params}
1,260✔
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