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

sensedeep / dynamodb-onetable / #65

pending completion
#65

push

Michael O'Brien
DEV: temporarily disable stream unit tests

1107 of 1601 branches covered (69.14%)

Branch coverage included in aggregate %.

1780 of 2373 relevant lines covered (75.01%)

623.2 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

126
        for (let [name, field] of Object.entries(schemaFields)) {
289✔
127
            if (!field.type) {
2,073!
128
                field.type = 'string'
×
129
                this.table.log.error(`Missing type field for ${field.name}`, {field})
×
130
            }
131
            field.name = name
2,073✔
132
            fields[name] = field
2,073✔
133
            field.isoDates = field.isoDates != null ? field.isoDates : table.isoDates || false
2,073!
134

135
            if (field.uuid) {
2,073!
136
                throw new OneTableArgError(
×
137
                    'The "uuid" schema property is deprecated. Please use "generate": "uuid or ulid" instead'
138
                )
139
            }
140

141
            field.type = this.checkType(field)
2,073✔
142

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

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

223
        /*
224
            Order the fields so value templates can depend on each other safely
225
        */
226
        for (let field of Object.values(fields)) {
289✔
227
            this.orderFields(block, field)
2,073✔
228
        }
229
    }
230

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

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

265
    getPropValue(properties, path) {
266
        let v = properties
6,172✔
267
        for (let part of path.split('.')) {
6,172✔
268
            v = v[part]
6,176✔
269
        }
270
        return v
6,172✔
271
    }
272

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

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

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

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

417
        /*
418
            Process the response
419
        */
420
        if (params.parse) {
857✔
421
            items = this.parseResponse(op, expression, items)
840✔
422
        }
423

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

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

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

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

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

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

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

561
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
6✔
562

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

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

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

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

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

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

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

656
    init(properties = {}, params = {}) {
×
657
        /* eslint-disable-next-line */
658
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
659
        return this.initItem(properties, params)
×
660
    }
661

662
    async remove(properties, params = {}) {
18✔
663
        /* eslint-disable-next-line */
664
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
41✔
665

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

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

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

719
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
720

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

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

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

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

791
    async upsert(properties, params = {}) {
×
792
        params.exists = null
×
793
        return await this.update(properties, params)
×
794
    }
795

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

809
        params.prepared = properties = this.prepareProperties('update', properties, params)
5✔
810
        let keys = {
5✔
811
            [index.hash]: properties[index.hash],
812
        }
813
        if (index.sort) {
5✔
814
            keys[index.sort] = properties[index.sort]
5✔
815
        }
816

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

834
        for (let field of fields) {
5✔
835
            let toBeRemoved = params.remove && params.remove.includes(field.name)
15✔
836
            let isUnchanged = prior && properties[field.name] === prior[field.name]
15✔
837
            if (isUnchanged) {
15✔
838
                continue
3✔
839
            }
840

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

883
        if (!transactHere) {
5!
884
            return item
×
885
        }
886

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

926
    //  Low level API
927

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

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

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

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

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

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

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

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

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

1048
        let items = await this.queryItems(properties, params)
2✔
1049
        return this.table.groupByType(items)
2✔
1050
    }
1051

1052
    /*
1053
        Map Dynamo types to Javascript types after reading data
1054
     */
1055
    transformReadItem(op, raw, properties, params) {
1056
        if (!raw) {
2,552!
1057
            return raw
×
1058
        }
1059
        return this.transformReadBlock(op, raw, properties, params, this.block.fields)
2,552✔
1060
    }
1061

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

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

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

1182
        if (this.needsFallback(op, index, params)) {
990✔
1183
            params.fallback = true
9✔
1184
            return properties
9✔
1185
        }
1186
        //  DEPRECATE
1187
        this.tunnelProperties(properties, params)
981✔
1188

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

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

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

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

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

1301
    /*
1302
        Collect the required attribute from the properties and context.
1303
        This handles tunneled properties, blends context properties, resolves default values,
1304
        handles Nulls and empty strings, and invokes validations. Nested schemas are handled here.
1305

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

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

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

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

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

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

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

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

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

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

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

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

1579
    /*
1580
        Process value templates and property values that are functions
1581
     */
1582
    runTemplates(op, pathname, index, deps, properties, params) {
1583
        for (let field of deps) {
1,008✔
1584
            if (field.schema) continue
11,531✔
1585
            let name = field.name
11,450✔
1586
            if (
11,450✔
1587
                field.isIndexed &&
19,989✔
1588
                op != 'put' &&
1589
                op != 'update' &&
1590
                field.attribute[0] != index.hash &&
1591
                field.attribute[0] != index.sort
1592
            ) {
1593
                //  Ignore indexes not being used for this call
1594
                continue
727✔
1595
            }
1596
            let path = pathname ? `${pathname}.${field.name}` : field.name
10,723✔
1597

1598
            if (field.value === true && typeof this.table.params.value == 'function') {
10,723✔
1599
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1600
            } else if (properties[name] === undefined) {
10,719✔
1601
                if (field.value) {
6,475✔
1602
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
3,925✔
1603
                    if (value != null) {
3,925✔
1604
                        properties[name] = value
3,809✔
1605
                    }
1606
                }
1607
            }
1608
        }
1609
    }
1610

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

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

1665
    //  Public routine to run templates
1666
    template(name, properties, params = {}) {
×
1667
        let fields = this.block.fields
×
1668
        let field = fields[name]
×
1669
        if (!field) {
×
1670
            throw new OneTableError('Cannot find field', {name})
×
1671
        }
1672
        return this.runTemplate('find', null, field, properties, params, field.value)
×
1673
    }
1674

1675
    validateProperties(op, fields, properties, params) {
1676
        if (op != 'put' && op != 'update') {
1,008✔
1677
            return
269✔
1678
        }
1679
        let validation = {}
739✔
1680
        if (typeof this.table.params.validate == 'function') {
739✔
1681
            validation = this.table.params.validate(this, properties, params) || {}
4!
1682
        }
1683
        for (let [name, value] of Object.entries(properties)) {
739✔
1684
            let field = fields[name]
7,219✔
1685
            if (!field || field.schema) continue
7,219✔
1686
            if (params.validate || field.validate || field.enum) {
7,182✔
1687
                value = this.validateProperty(field, value, validation, params)
20✔
1688
                properties[name] = value
20✔
1689
            }
1690
        }
1691
        for (let field of Object.values(fields)) {
739✔
1692
            //  If required and create, must be defined. If required and update, must not be null.
1693
            if (
8,461✔
1694
                field.required &&
20,202✔
1695
                !field.schema &&
1696
                ((op == 'put' && properties[field.name] == null) || (op == 'update' && properties[field.name] === null))
1697
            ) {
1698
                validation[field.name] = `Value not defined for required field "${field.name}"`
4✔
1699
            }
1700
        }
1701

1702
        if (Object.keys(validation).length > 0) {
739✔
1703
            throw new OneTableError(`Validation Error in "${this.name}" for "${Object.keys(validation).join(', ')}"`, {
5✔
1704
                validation,
1705
                code: 'ValidationError',
1706
                properties,
1707
            })
1708
        }
1709
    }
1710

1711
    validateProperty(field, value, details, params) {
1712
        let fieldName = field.name
20✔
1713

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

1756
    transformProperties(op, fields, properties, params, rec) {
1757
        for (let [name, field] of Object.entries(fields)) {
1,003✔
1758
            //  Nested schemas handled via collectProperties
1759
            if (field.schema) continue
11,480✔
1760
            let value = rec[name]
11,400✔
1761
            if (value !== undefined) {
11,400✔
1762
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
7,687✔
1763
            }
1764
        }
1765
        return rec
1,003✔
1766
    }
1767

1768
    /*
1769
        Transform an attribute before writing. This invokes transform callbacks and handles nested objects.
1770
     */
1771
    transformWriteAttribute(op, field, value, properties, params) {
1772
        let type = field.type
7,687✔
1773

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

1826
        if (field.crypt && value != null) {
7,687✔
1827
            value = this.encrypt(value)
1✔
1828
        }
1829
        return value
7,687✔
1830
    }
1831

1832
    transformNestedWriteFields(field, obj) {
1833
        for (let [key, value] of Object.entries(obj)) {
132✔
1834
            let type = field.type
2,151✔
1835
            if (value instanceof Date) {
2,151✔
1836
                obj[key] = this.transformWriteDate(field, value)
1✔
1837
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,150!
1838
                value = value.toString('base64')
×
1839
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,150!
1840
                value = this.transformWriteSet(type, value)
×
1841
            } else if (value == null && field.nulls !== true) {
2,150!
1842
                //  Skip nulls
1843
                continue
×
1844
            } else if (value != null && typeof value == 'object') {
2,150!
1845
                obj[key] = this.transformNestedWriteFields(field, value)
×
1846
            }
1847
        }
1848
        return obj
132✔
1849
    }
1850

1851
    transformWriteSet(type, value) {
1852
        if (!Array.isArray(value)) {
×
1853
            throw new OneTableError('Set values must be arrays', {code: 'TypeError'})
×
1854
        }
1855
        if (type == Set || type == 'Set' || type == 'set') {
×
1856
            let v = value.values().next().value
×
1857
            if (typeof v == 'string') {
×
1858
                value = value.map((v) => v.toString())
×
1859
            } else if (typeof v == 'number') {
×
1860
                value = value.map((v) => Number(v))
×
1861
            } else if (v instanceof Buffer || v instanceof ArrayBuffer || v instanceof DataView) {
×
1862
                value = value.map((v) => v.toString('base64'))
×
1863
            }
1864
        } else {
1865
            throw new OneTableError('Unknown type', {code: 'TypeError'})
×
1866
        }
1867
        return value
×
1868
    }
1869

1870
    /*
1871
        Handle dates. Supports epoch and ISO date transformations.
1872
    */
1873
    transformWriteDate(field, value) {
1874
        let isoDates = field.isoDates || this.table.isoDates
504✔
1875
        if (field.ttl) {
504!
1876
            //  Convert dates to DynamoDB TTL
1877
            if (value instanceof Date) {
×
1878
                value = value.getTime()
×
1879
            } else if (typeof value == 'string') {
×
1880
                value = new Date(Date.parse(value)).getTime()
×
1881
            }
1882
            value = Math.ceil(value / 1000)
×
1883
        } else if (isoDates) {
504✔
1884
            if (value instanceof Date) {
450!
1885
                value = value.toISOString()
450✔
1886
            } else if (typeof value == 'string') {
×
1887
                value = new Date(Date.parse(value)).toISOString()
×
1888
            } else if (typeof value == 'number') {
×
1889
                value = new Date(value).toISOString()
×
1890
            }
1891
        } else {
1892
            //  Convert dates to unix epoch in milliseconds
1893
            if (value instanceof Date) {
54!
1894
                value = value.getTime()
54✔
1895
            } else if (typeof value == 'string') {
×
1896
                value = new Date(Date.parse(value)).getTime()
×
1897
            }
1898
        }
1899
        return value
504✔
1900
    }
1901

1902
    /*
1903
        Get a hash of all the property names of the indexes. Keys are properties, values are index names.
1904
        Primary takes precedence if property used in multiple indexes (LSIs)
1905
     */
1906
    getIndexProperties(indexes) {
1907
        let properties = {}
278✔
1908
        for (let [indexName, index] of Object.entries(indexes)) {
278✔
1909
            for (let [type, pname] of Object.entries(index)) {
655✔
1910
                if (type == 'hash' || type == 'sort') {
1,657✔
1911
                    if (properties[pname] != 'primary') {
1,284✔
1912
                        //  Let primary take precedence
1913
                        properties[pname] = indexName
1,259✔
1914
                    }
1915
                }
1916
            }
1917
        }
1918
        return properties
278✔
1919
    }
1920

1921
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1922
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1923
    }
1924

1925
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1926
        return this.table.decrypt(text, inCode, outCode)
2✔
1927
    }
1928

1929
    /*
1930
        Clone properties and params to callers objects are not polluted
1931
    */
1932
    checkArgs(properties, params, overrides = {}) {
856✔
1933
        if (params.checked) {
1,790✔
1934
            //  Only need to clone once
1935
            return {properties, params}
832✔
1936
        }
1937
        if (!properties) {
958!
1938
            throw new OneTableArgError('Missing properties')
×
1939
        }
1940
        if (typeof params != 'object') {
958!
1941
            throw new OneTableError('Invalid type for params', {code: 'TypeError'})
×
1942
        }
1943
        //  Must not use merge as we need to modify the callers batch/transaction objects
1944
        params = Object.assign(overrides, params)
958✔
1945

1946
        params.checked = true
958✔
1947
        properties = this.table.assign({}, properties)
958✔
1948
        return {properties, params}
958✔
1949
    }
1950

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

1983
    getPartial(field, params) {
1984
        let partial = params.partial
7,657✔
1985
        if (partial === undefined) {
7,657✔
1986
            partial = field.partial
7,641✔
1987
            if (partial == undefined) {
7,641✔
1988
                partial = this.table.partial
7,636✔
1989
            }
1990
        }
1991
        return partial
7,657✔
1992
    }
1993

1994
    /*  KEEP
1995
    captureStack() {
1996
        let limit = Error.stackTraceLimit
1997
        Error.stackTraceLimit = 1
1998

1999
        let obj = {}
2000
        let v8Handler = Error.prepareStackTrace
2001
        Error.prepareStackTrace = function(obj, stack) { return stack }
2002
        Error.captureStackTrace(obj, this.captureStack)
2003

2004
        let stack = obj.stack
2005
        Error.prepareStackTrace = v8Handler
2006
        Error.stackTraceLimit = limit
2007

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

© 2025 Coveralls, Inc