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

sensedeep / dynamodb-onetable / #83

20 Feb 2025 11:28PM UTC coverage: 74.813% (-0.07%) from 74.885%
#83

push

Michael O'Brien
DEV: bump version

1198 of 1684 branches covered (71.14%)

Branch coverage included in aggregate %.

1909 of 2469 relevant lines covered (77.32%)

739.17 hits per line

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

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

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

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

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

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

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

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

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

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

80
        this.schema = table.schema
340✔
81
        this.indexes = this.schema.indexes
340✔
82

83
        if (!this.indexes) {
340!
84
            throw new OneTableArgError('Indexes must be defined on the Table before creating models')
×
85
        }
86
        this.indexProperties = this.getIndexProperties(this.indexes)
340✔
87

88
        let fields = options.fields || this.schema.definition.models[this.name]
340!
89
        if (fields) {
340✔
90
            this.prepModel(fields, this.block)
340✔
91
        }
92
    }
93

94
    /*
95
        Prepare a model based on the schema and compute the attribute mapping.
96
     */
97
    prepModel(schemaFields, block, parent) {
98
        let {fields} = block
354✔
99

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

119
        //  Attributes that are mapped to a different attribute. Indexed by attribute name for this block.
120
        let mapTargets = {}
354✔
121
        let map = {}
354✔
122

123
        for (let [name, field] of Object.entries(schemaFields)) {
354✔
124
            if (!field.type) {
2,578!
125
                field.type = 'string'
×
126
                this.table.log.error(`Missing type field for ${field.name}`, {field})
×
127
            }
128

129
            field.name = name
2,578✔
130
            fields[name] = field
2,578✔
131
            field.isoDates = field.isoDates != null ? field.isoDates : table.isoDates || false
2,578!
132

133
            if (field.partial == null) {
2,578✔
134
                field.partial = parent && parent.partial != null ? parent.partial : this.table.partial
2,577✔
135
            }
136

137
            if (field.uuid) {
2,578!
138
                throw new OneTableArgError(
×
139
                    'The "uuid" schema property is deprecated. Please use "generate": "uuid or ulid" instead'
140
                )
141
            }
142
            if (field.encode) {
2,578✔
143
                let schema = this.schema.definition
4✔
144
                if (typeof field.encode == 'string' && this.table.separator) {
4✔
145
                    let def = schema.models[this.name][field.encode]
1✔
146
                    if (def?.value) {
1✔
147
                        let parts = def.value.match(/\${(.*?)}/g)
1✔
148
                        let index = parts.indexOf('${' + field.name + '}')
1✔
149
                        if (index >= 0) {
1✔
150
                            field.encode = [field.encode, this.table.separator, index]
1✔
151
                        }
152
                    }
153
                    if (typeof field.encode == 'string') {
1!
154
                        throw new OneTableArgError(`Cannot resolve encoded reference for ${this.name}.${field.name}`)
×
155
                    }
156
                }
157
            }
158

159
            field.type = this.checkType(field)
2,578✔
160

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

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

244
        /*
245
            Order the fields so value templates can depend on each other safely
246
        */
247
        for (let field of Object.values(fields)) {
354✔
248
            this.orderFields(block, field)
2,578✔
249
        }
250
    }
251

252
    checkType(field) {
253
        let type = field.type
2,578✔
254
        if (typeof type == 'function') {
2,578✔
255
            type = type.name
1,028✔
256
        }
257
        type = type.toLowerCase()
2,578✔
258
        if (ValidTypes.indexOf(type) < 0) {
2,578!
259
            throw new OneTableArgError(`Unknown type "${type}" for field "${field.name}" in model "${this.name}"`)
×
260
        }
261
        return type
2,578✔
262
    }
263

264
    orderFields(block, field) {
265
        let {deps, fields} = block
2,580✔
266
        if (deps.find((i) => i.name == field.name)) {
11,457✔
267
            return
1✔
268
        }
269
        if (field.value) {
2,579✔
270
            let vars = this.table.getVars(field.value)
556✔
271
            for (let path of vars) {
556✔
272
                let name = path.split(/[.[]/g).shift().trim(']')
648✔
273
                let ref = fields[name]
648✔
274
                if (ref && ref != field) {
648✔
275
                    if (ref.schema) {
647✔
276
                        this.orderFields(ref.block, ref)
2✔
277
                    } else if (ref.value) {
645!
278
                        this.orderFields(block, ref)
×
279
                    }
280
                }
281
            }
282
        }
283
        deps.push(field)
2,579✔
284
    }
285

286
    getPropValue(properties, path) {
287
        let v = properties
7,822✔
288
        for (let part of path.split('.')) {
7,822✔
289
            if (v == null) return v
7,826!
290
            v = v[part]
7,826✔
291
        }
292
        return v
7,822✔
293
    }
294

295
    /*
296
        Run an operation on DynamodDB. The command has been parsed via Expression.
297
        Returns [] for find/scan, cmd if !execute, else returns item.
298
     */
299
    async run(op, expression) {
300
        let {index, properties, params} = expression
1,255✔
301

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

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

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

441
        /*
442
            Process the response
443
        */
444
        if (params.parse) {
1,155✔
445
            items = this.parseResponse(op, expression, items)
1,142✔
446
        }
447

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

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

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

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

557
    /*
558
        Create/Put a new item. Will overwrite existing items if exists: null.
559
    */
560
    async create(properties, params = {}) {
847✔
561
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true, exists: false}))
893✔
562
        let result
563
        if (this.hasUniqueFields) {
893✔
564
            result = await this.createUnique(properties, params)
7✔
565
        } else {
566
            result = await this.putItem(properties, params)
886✔
567
        }
568
        return result
888✔
569
    }
570

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

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

585
        let timestamp = (transaction.timestamp = transaction.timestamp || new Date())
7✔
586

587
        if (params.timestamps !== false) {
7✔
588
            if (this.timestamps === true || this.timestamps == 'create') {
7✔
589
                properties[this.createdField] = timestamp
1✔
590
            }
591
            if (this.timestamps === true || this.timestamps == 'update') {
7✔
592
                properties[this.updatedField] = timestamp
1✔
593
            }
594
        }
595
        params.prepared = properties = this.prepareProperties('put', properties, params)
7✔
596

597
        let ttlField = fields.find(f => f.ttl)
13✔
598

599
        for (let field of fields) {
7✔
600
            if (properties[field.name] !== undefined) {
13✔
601
                let scope = ''
11✔
602
                if (field.scope) {
11!
603
                    scope = this.runTemplate(null, null, field, properties, params, field.scope) + '#'
×
604
                    if (scope == undefined) {
×
605
                        throw new OneTableError('Missing properties to resolve unique scope', {
×
606
                            properties,
607
                            field,
608
                            scope: field.scope,
609
                            code: 'UniqueError',
610
                        })
611
                    }
612
                }
613
                let pk = `_unique#${scope}${this.name}#${field.attribute}#${properties[field.name]}`
11✔
614
                let sk = '_unique#'
11✔
615
                let uproperties = {[this.hash]: pk, [this.sort]: sk}
11✔
616

617
                if (ttlField) {
11!
618
                    /*
619
                        Add a TTL expiry property to the unique record
620
                     */
621
                    let value = properties[ttlField.name]
×
622
                    uproperties[ttlField.name] = new Date(new Date(value).getTime() * 1000)
×
623
                }
624
                await this.schema.uniqueModel.create(
11✔
625
                    uproperties,
626
                    {transaction, exists: false, return: 'NONE'}
627
                )
628
            }
629
        }
630
        let item = await this.putItem(properties, params)
7✔
631

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

656
    async check(properties, params) {
657
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
3✔
658
        properties = this.prepareProperties('get', properties, params)
3✔
659
        const expression = new Expression(this, 'check', properties, params)
3✔
660
        this.run('check', expression)
3✔
661
    }
662

663
    async find(properties = {}, params = {}) {
11✔
664
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
71✔
665
        return await this.queryItems(properties, params)
71✔
666
    }
667

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

694
    async load(properties = {}, params = {}) {
6!
695
        ;({properties, params} = this.checkArgs(properties, params))
6✔
696
        properties = this.prepareProperties('get', properties, params)
6✔
697
        let expression = new Expression(this, 'get', properties, params)
6✔
698
        return await this.table.batchLoad(expression)
6✔
699
    }
700

701
    init(properties = {}, params = {}) {
×
702
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
×
703
        return this.initItem(properties, params)
×
704
    }
705

706
    async remove(properties, params = {}) {
18✔
707
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, exists: null, high: true}))
42✔
708

709
        properties = this.prepareProperties('delete', properties, params)
42✔
710
        if (params.fallback || params.many) {
42✔
711
            return await this.removeByFind(properties, params)
3✔
712
        }
713
        let expression = new Expression(this, 'delete', properties, params)
39✔
714
        if (this.hasUniqueFields) {
39✔
715
            return await this.removeUnique(properties, params)
2✔
716
        } else {
717
            return await this.run('delete', expression)
37✔
718
        }
719
    }
720

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

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

762
        params.prepared = properties = this.prepareProperties('delete', properties, params)
2✔
763

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

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

823
    async scan(properties = {}, params = {}) {
63✔
824
        ;({properties, params} = this.checkArgs(properties, params, {parse: true, high: true}))
52✔
825
        return await this.scanItems(properties, params)
52✔
826
    }
827

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

841
    async upsert(properties, params = {}) {
×
842
        params.exists = null
×
843
        return await this.update(properties, params)
×
844
    }
845

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

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

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

948
        if (!transactHere) {
7✔
949
            return item
2✔
950
        }
951

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

991
    //  Low level API
992

993
    /* private */
994
    async deleteItem(properties, params = {}) {
1✔
995
        ;({properties, params} = this.checkArgs(properties, params))
3✔
996
        if (!params.prepared) {
3✔
997
            properties = this.prepareProperties('delete', properties, params)
1✔
998
        }
999
        let expression = new Expression(this, 'delete', properties, params)
3✔
1000
        return await this.run('delete', expression)
3✔
1001
    }
1002

1003
    /* private */
1004
    async getItem(properties, params = {}) {
×
1005
        ;({properties, params} = this.checkArgs(properties, params))
2✔
1006
        properties = this.prepareProperties('get', properties, params)
2✔
1007
        let expression = new Expression(this, 'get', properties, params)
2✔
1008
        return await this.run('get', expression)
2✔
1009
    }
1010

1011
    /* private */
1012
    initItem(properties, params = {}) {
×
1013
        ;({properties, params} = this.checkArgs(properties, params))
×
1014
        let fields = this.block.fields
×
1015
        this.setDefaults('init', fields, properties, params)
×
1016
        //  Ensure all fields are present
1017
        for (let key of Object.keys(fields)) {
×
1018
            if (properties[key] === undefined) {
×
1019
                properties[key] = null
×
1020
            }
1021
        }
1022
        this.runTemplates('put', '', this.indexes.primary, this.block.deps, properties, params)
×
1023
        return properties
×
1024
    }
1025

1026
    /* private */
1027
    async putItem(properties, params = {}) {
×
1028
        ;({properties, params} = this.checkArgs(properties, params))
895✔
1029
        if (!params.prepared) {
895✔
1030
            if (params.timestamps !== false) {
888✔
1031
                let timestamp = params.transaction
888✔
1032
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
36✔
1033
                    : new Date()
1034

1035
                if (this.timestamps === true || this.timestamps == 'create') {
888✔
1036
                    properties[this.createdField] = timestamp
226✔
1037
                }
1038
                if (this.timestamps === true || this.timestamps == 'update') {
888✔
1039
                    properties[this.updatedField] = timestamp
226✔
1040
                }
1041
            }
1042
            properties = this.prepareProperties('put', properties, params)
888✔
1043
        }
1044
        let expression = new Expression(this, 'put', properties, params)
892✔
1045
        return await this.run('put', expression)
891✔
1046
    }
1047

1048
    /* private */
1049
    async queryItems(properties = {}, params = {}) {
×
1050
        ;({properties, params} = this.checkArgs(properties, params))
83✔
1051
        properties = this.prepareProperties('find', properties, params)
83✔
1052
        let expression = new Expression(this, 'find', properties, params)
83✔
1053
        return await this.run('find', expression)
83✔
1054
    }
1055

1056
    //  Note: scanItems will return all model types
1057
    /* private */
1058
    async scanItems(properties = {}, params = {}) {
12✔
1059
        ;({properties, params} = this.checkArgs(properties, params))
72✔
1060
        properties = this.prepareProperties('scan', properties, params)
72✔
1061
        let expression = new Expression(this, 'scan', properties, params)
72✔
1062
        return await this.run('scan', expression)
72✔
1063
    }
1064

1065
    /* private */
1066
    async updateItem(properties, params = {}) {
×
1067
        ;({properties, params} = this.checkArgs(properties, params))
86✔
1068
        if (this.timestamps === true || this.timestamps == 'update') {
86✔
1069
            if (params.timestamps !== false) {
49✔
1070
                let timestamp = params.transaction
49✔
1071
                    ? (params.transaction.timestamp = params.transaction.timestamp || new Date())
7✔
1072
                    : new Date()
1073
                properties[this.updatedField] = timestamp
49✔
1074
                if (params.exists == null) {
49✔
1075
                    let field = this.block.fields[this.createdField] || this.table
2!
1076
                    let when = field.isoDates ? timestamp.toISOString() : timestamp.getTime()
2!
1077
                    params.set = params.set || {}
2✔
1078
                    params.set[this.createdField] = `if_not_exists(\${${this.createdField}}, {${when}})`
2✔
1079
                }
1080
            }
1081
        }
1082
        properties = this.prepareProperties('update', properties, params)
86✔
1083
        let expression = new Expression(this, 'update', properties, params)
85✔
1084
        return await this.run('update', expression)
85✔
1085
    }
1086

1087
    /* private */
1088
    async fetch(models, properties = {}, params = {}) {
2!
1089
        ;({properties, params} = this.checkArgs(properties, params))
2✔
1090
        if (models.length == 0) {
2!
1091
            return {}
×
1092
        }
1093
        let where = []
2✔
1094
        for (let model of models) {
2✔
1095
            where.push(`\${${this.typeField}} = {${model}}`)
4✔
1096
        }
1097
        if (params.where) {
2!
1098
            params.where = `(${params.where}) and (${where.join(' or ')})`
×
1099
        } else {
1100
            params.where = where.join(' or ')
2✔
1101
        }
1102
        params.parse = true
2✔
1103
        params.hidden = true
2✔
1104

1105
        let items = await this.queryItems(properties, params)
2✔
1106
        return this.table.groupByType(items)
2✔
1107
    }
1108

1109
    /*
1110
        Map Dynamo types to Javascript types after reading data
1111
     */
1112
    transformReadItem(op, raw, properties, params, expression) {
1113
        if (!raw) {
3,381!
1114
            return raw
×
1115
        }
1116
        return this.transformReadBlock(op, raw, properties, params, this.block.fields, expression)
3,381✔
1117
    }
1118

1119
    transformReadBlock(op, raw, properties, params, fields, expression) {
1120
        let rec = {}
3,472✔
1121
        for (let [name, field] of Object.entries(fields)) {
3,472✔
1122
            //  Skip hidden params. Follow needs hidden params to do the follow.
1123
            if (field.hidden && params.follow !== true) {
33,489✔
1124
                if (params.hidden === false || (params.hidden == null && this.table.hidden === false)) {
18,519✔
1125
                    continue
18,261✔
1126
                }
1127
            }
1128
            let att, sub
1129
            if (op == 'put') {
15,228✔
1130
                att = field.name
4,332✔
1131
            } else {
1132
                ;[att, sub] = field.attribute
10,896✔
1133
            }
1134
            let value = raw[att]
15,228✔
1135
            if (value === undefined) {
15,228✔
1136
                if (field.encode) {
2,629✔
1137
                    let [att, sep, index] = field.encode
5✔
1138
                    value = (raw[att] || '').split(sep)[index]
5!
1139
                }
1140
            }
1141
            if (sub && value) {
15,228✔
1142
                value = value[sub]
45✔
1143
            }
1144
            if (field.crypt && params.decrypt !== false) {
15,228✔
1145
                value = this.decrypt(value)
2✔
1146
            }
1147
            if (field.default !== undefined && value === undefined) {
15,228✔
1148
                if (!params.fields || params.fields.indexOf(name) >= 0) {
34✔
1149
                    rec[name] = field.default
32✔
1150
                }
1151
            } else if (value === undefined) {
15,194✔
1152
                if (field.required) {
2,590✔
1153
                    /*
1154
                        Transactions transform the properties to return something, but
1155
                        does not have all the properties and required fields may be missing).
1156
                        Also find operation with fields selections may not include required fields.
1157
                     */
1158
                    if (!params.transaction && !params.batch && !params.fields && !field.encode && !expression?.index?.project) {
16!
1159
                        if (params.warn || this.table.warn) {
×
1160
                            this.table.log.error(`Required field "${name}" in model "${this.name}" not defined in table item`, {model: this.name, raw, params, field})
×
1161
                        }
1162
                    }
1163
                }
1164
            } else if (field.schema && value !== null && typeof value == 'object') {
12,604✔
1165
                if (field.items && Array.isArray(value)) {
95✔
1166
                    rec[name] = []
16✔
1167
                    let i = 0
16✔
1168
                    for (let rvalue of raw[att]) {
16✔
1169
                        rec[name][i] = this.transformReadBlock(
12✔
1170
                            op,
1171
                            rvalue,
1172
                            properties[name] || [],
15✔
1173
                            params,
1174
                            field.block.fields,
1175
                            expression
1176
                        )
1177
                        i++
12✔
1178
                    }
1179
                } else {
1180
                    rec[name] = this.transformReadBlock(
79✔
1181
                        op,
1182
                        raw[att],
1183
                        properties[name] || {},
130✔
1184
                        params,
1185
                        field.block.fields,
1186
                        expression
1187
                    )
1188
                }
1189
            } else {
1190
                rec[name] = this.transformReadAttribute(field, name, value, params, properties)
12,509✔
1191
            }
1192
        }
1193
        if (this.generic) {
3,472✔
1194
            //  Generic must include attributes outside the schema.
1195
            for (let [name, value] of Object.entries(raw)) {
37✔
1196
                if (rec[name] === undefined) {
117✔
1197
                    rec[name] = value
53✔
1198
                }
1199
            }
1200
        }
1201
        if (
3,472✔
1202
            params.hidden == true &&
3,520✔
1203
            rec[this.typeField] === undefined &&
1204
            !this.generic &&
1205
            this.block.fields == fields
1206
        ) {
1207
            rec[this.typeField] = this.name
1✔
1208
        }
1209
        if (this.table.params.transform) {
3,472✔
1210
            let opForTransform = TransformParseResponseAs[op]
6✔
1211
            rec = this.table.params.transform(this, ReadWrite[opForTransform], rec, properties, params, raw)
6✔
1212
        }
1213
        return rec
3,472✔
1214
    }
1215

1216
    transformReadAttribute(field, name, value, params, properties) {
1217
        if (typeof params.transform == 'function') {
12,509!
1218
            //  Invoke custom data transform after reading
1219
            return params.transform(this, 'read', name, value, properties)
×
1220
        }
1221
        if (field.type == 'date' && value != undefined) {
12,509✔
1222
            if (field.ttl) {
1,232!
1223
                //  Parse incase stored as ISO string
1224
                return new Date(new Date(value).getTime() * 1000)
×
1225
            } else {
1226
                return new Date(value)
1,232✔
1227
            }
1228
        }
1229
        if (field.type == 'buffer' || field.type == 'arraybuffer' || field.type == 'binary') {
11,277✔
1230
            return Buffer.from(value, 'base64')
10✔
1231
        }
1232
        return value
11,267✔
1233
    }
1234

1235
    /*
1236
        Validate properties and map types if required.
1237
        Note: this does not map names to attributes or evaluate value templates, that happens in Expression.
1238
     */
1239
    prepareProperties(op, properties, params = {}) {
8✔
1240
        delete params.fallback
1,299✔
1241
        let index = this.selectIndex(op, params)
1,299✔
1242

1243
        if (this.needsFallback(op, index, params)) {
1,299✔
1244
            params.fallback = true
9✔
1245
            return properties
9✔
1246
        }
1247
        //  DEPRECATE
1248
        this.tunnelProperties(properties, params)
1,290✔
1249

1250
        if (params.filter) {
1,290!
1251
            this.convertFilter(properties, params, index)
×
1252
        }
1253
        let rec = this.collectProperties(op, '', this.block, index, properties, params)
1,290✔
1254
        if (params.fallback) {
1,286✔
1255
            return properties
7✔
1256
        }
1257
        if (op != 'scan' && this.getHash(rec, this.block.fields, index, params) == null) {
1,279!
1258
            this.table.log.error(`Empty hash key`, {properties, params, op, rec, index, model: this.name})
×
1259
            throw new OneTableError(`Empty hash key. Check hash key and any value template variable references.`, {
×
1260
                properties,
1261
                rec,
1262
                code: 'MissingError',
1263
            })
1264
        }
1265
        if (this.table.params.transform && ReadWrite[op] == 'write') {
1,279✔
1266
            rec = this.table.params.transform(this, ReadWrite[op], rec, properties, params)
5✔
1267
        }
1268
        return rec
1,279✔
1269
    }
1270

1271
    /*
1272
        Convert a full text params.filter into a smart params.where
1273
        NOTE: this is prototype code and definitely not perfect! Use at own risk.
1274
     */
1275
    convertFilter(properties, params, index) {
1276
        let filter = params.filter.toString()
×
1277
        let fields = this.block.fields
×
1278
        let where
1279
        //  TODO support > >= < <= ..., AND or ...
1280
        let [name, value] = filter.split('=')
×
1281
        if (value) {
×
1282
            name = name.trim()
×
1283
            value = value.trim()
×
1284
            let field = fields[name]
×
1285
            if (field) {
×
1286
                name = field.map ? field.map : name
×
1287
                if (field.encode) {
×
1288
                    properties[name] = value
×
1289
                } else {
1290
                    where = `\${${name}} = {"${value}"}`
×
1291
                }
1292
            } else {
1293
                where = `\${${name}} = {"${value}"}`
×
1294
            }
1295
        } else {
1296
            value = name
×
1297
            where = []
×
1298
            for (let [name, field] of Object.entries(fields)) {
×
1299
                let primary = this.indexes.primary
×
1300
                if (primary.hash == name || primary.sort == name || index.hash == name || index.sort == name) {
×
1301
                    continue
×
1302
                }
1303
                if (field.encode) {
×
1304
                    continue
×
1305
                }
1306
                name = field.map ? field.map : name
×
1307
                where.push(`(contains(\${${name}}, {"${filter}"}))`)
×
1308
            }
1309
            if (where) {
×
1310
                where = where.join(' or ')
×
1311
            }
1312
        }
1313
        params.where = where
×
1314
        params.maxPages = params.maxPages || 25
×
1315
        //  Remove limit otherwise the search will only search "limit" items at a time
1316
        delete params.limit
×
1317
    }
1318

1319
    //  Handle fallback for get/delete as GSIs only support find and scan
1320
    needsFallback(op, index, params) {
1321
        if (index != this.indexes.primary && op != 'find' && op != 'scan') {
1,299✔
1322
            if (params.low) {
9!
1323
                throw new OneTableArgError('Cannot use non-primary index for "${op}" operation')
×
1324
            }
1325
            return true
9✔
1326
        }
1327
        return false
1,290✔
1328
    }
1329

1330
    /*
1331
        Return the hash property name for the selected index.
1332
    */
1333
    getHash(rec, fields, index, params) {
1334
        let generic = params.generic != null ? params.generic : this.generic
1,207!
1335
        if (generic) {
1,207✔
1336
            return rec[index.hash]
20✔
1337
        }
1338
        let field = Object.values(fields).find((f) => f.attribute[0] == index.hash)
1,658✔
1339
        if (!field) {
1,187!
1340
            return null
×
1341
        }
1342
        return rec[field.name]
1,187✔
1343
    }
1344

1345
    /*
1346
        Get the index for the request
1347
    */
1348
    selectIndex(op, params) {
1349
        let index
1350
        if (params.index && params.index != 'primary') {
1,299✔
1351
            index = this.indexes[params.index]
43✔
1352
            if (!index) {
43!
1353
                throw new OneTableError(`Cannot find index ${params.index}`, {code: 'MissingError'})
×
1354
            }
1355
        } else {
1356
            index = this.indexes.primary
1,256✔
1357
        }
1358
        return index
1,299✔
1359
    }
1360

1361
    /*
1362
        Collect the required attribute from the properties and context.
1363
        This handles tunneled properties, blends context properties, resolves default values,
1364
        handles Nulls and empty strings, and invokes validations. Nested schemas are handled here.
1365

1366
        NOTE: pathname is only needed for DEPRECATED and undocumented callbacks.
1367
    */
1368
    collectProperties(op, pathname, block, index, properties, params, context, rec = {}) {
1,326✔
1369
        let fields = block.fields
1,326✔
1370
        if (!context) {
1,326✔
1371
            context = params.context || this.table.context
1,290✔
1372
        }
1373
        /*
1374
            First process nested schemas recursively
1375
        */
1376
        if (this.nested && !KeysOnly[op]) {
1,326✔
1377
            this.collectNested(op, pathname, fields, index, properties, params, context, rec)
96✔
1378
        }
1379
        /*
1380
            Then process the non-schema properties at this level (non-recursive)
1381
        */
1382
        this.addContext(op, fields, index, properties, params, context)
1,326✔
1383
        this.setDefaults(op, fields, properties, params)
1,326✔
1384
        this.runTemplates(op, pathname, index, block.deps, properties, params)
1,326✔
1385
        this.convertNulls(op, pathname, fields, properties, params)
1,326✔
1386
        this.validateProperties(op, fields, properties, params)
1,326✔
1387
        this.selectProperties(op, block, index, properties, params, rec)
1,322✔
1388
        this.transformProperties(op, fields, properties, params, rec)
1,322✔
1389
        return rec
1,322✔
1390
    }
1391

1392
    /*
1393
        Process nested schema recursively
1394
    */
1395
    collectNested(op, pathname, fields, index, properties, params, context, rec) {
1396
        for (let field of Object.values(fields)) {
96✔
1397
            let schema = field.schema || field?.items?.schema
829✔
1398
            if (schema) {
829✔
1399
                let name = field.name
83✔
1400
                let value = properties[name]
83✔
1401
                if (op == 'put' && value === undefined) {
83✔
1402
                    value = field.required ? (field.type == 'array' ? [] : {}) : field.default
2!
1403
                }
1404
                let ctx = context[name] || {}
83✔
1405
                let partial = this.getPartial(field, params)
83✔
1406

1407
                if (value === null && field.nulls === true) {
83!
1408
                    rec[name] = null
×
1409
                } else if (value !== undefined) {
83✔
1410
                    if (field.items && Array.isArray(value)) {
39✔
1411
                        rec[name] = []
11✔
1412
                        let i = 0
11✔
1413
                        for (let rvalue of value) {
11✔
1414
                            let path = pathname ? `${pathname}.${name}[${i}]` : `${name}[${i}]`
8!
1415
                            let obj = this.collectProperties(op, path, field.block, index, rvalue, params, ctx)
8✔
1416
                            //  Don't update properties if empty and partial and no default
1417
                            if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
8✔
1418
                                rec[name][i++] = obj
7✔
1419
                            }
1420
                        }
1421
                    } else {
1422
                        let path = pathname ? `${pathname}.${field.name}` : field.name
28✔
1423
                        let obj = this.collectProperties(op, path, field.block, index, value, params, ctx)
28✔
1424
                        if (!partial || Object.keys(obj).length > 0 || field.default !== undefined) {
28✔
1425
                            rec[name] = obj
28✔
1426
                        }
1427
                    }
1428
                }
1429
            }
1430
        }
1431
    }
1432

1433
    /*
1434
        DEPRECATE - not needed anymore
1435
    */
1436
    tunnelProperties(properties, params) {
1437
        if (params.tunnel) {
1,290!
1438
            if (this.table.warn) {
×
1439
                console.warn(
×
1440
                    'WARNING: tunnel properties should not be required for typescript and will be removed soon.'
1441
                )
1442
            }
1443
            for (let [kind, settings] of Object.entries(params.tunnel)) {
×
1444
                for (let [key, value] of Object.entries(settings)) {
×
1445
                    properties[key] = {[kind]: value}
×
1446
                }
1447
            }
1448
        }
1449
    }
1450

1451
    /*
1452
        Select the attributes to include in the request
1453
    */
1454
    selectProperties(op, block, index, properties, params, rec) {
1455
        let project = this.getProjection(index)
1,322✔
1456
        /*
1457
            NOTE: Value templates for unique items may need other properties when removing unique items
1458
        */
1459
        for (let [name, field] of Object.entries(block.fields)) {
1,322✔
1460
            if (field.schema) continue
13,853✔
1461
            let omit = false
13,751✔
1462

1463
            if (block == this.block) {
13,751✔
1464
                let attribute = field.attribute[0]
13,664✔
1465
                //  Missing sort key on a high-level API for get/delete
1466
                if (properties[name] == null && attribute == index.sort && params.high && KeysOnly[op]) {
13,664✔
1467
                    if (op == 'delete' && !params.many) {
7!
1468
                        throw new OneTableError('Missing sort key', {code: 'MissingError', properties, params})
×
1469
                    }
1470
                    /*
1471
                        Missing sort key for high level get, or delete without "any".
1472
                        Fallback to find to select the items of interest. Get will throw if more than one result is returned.
1473
                    */
1474
                    params.fallback = true
7✔
1475
                    return
7✔
1476
                }
1477
                if (KeysOnly[op] && attribute != index.hash && attribute != index.sort && !this.hasUniqueFields) {
13,657✔
1478
                    //  Keys only for get and delete. Must include unique properties and all properties if unique value templates.
1479
                    //  FUTURE: could have a "strict" mode where we warn for other properties instead of ignoring.
1480
                    omit = true
1,209✔
1481
                } else if (project && project.indexOf(attribute) < 0) {
12,448✔
1482
                    //  Attribute is not projected
1483
                    omit = true
16✔
1484
                } else if (name == this.typeField && name != index.hash && name != index.sort && op == 'find') {
12,432✔
1485
                    omit = true
79✔
1486
                } else if (field.encode) {
12,353✔
1487
                    omit = true
3✔
1488
                }
1489
            }
1490
            if (!omit && properties[name] !== undefined) {
13,744✔
1491
                rec[name] = properties[name]
9,943✔
1492
            }
1493
        }
1494
        if (block == this.block) {
1,315✔
1495
            //  Only do at top level
1496
            this.addProjectedProperties(op, properties, params, project, rec)
1,279✔
1497
        }
1498
    }
1499

1500
    getProjection(index) {
1501
        let project = index.project
1,322✔
1502
        if (project) {
1,322✔
1503
            if (project == 'all') {
51✔
1504
                project = null
47✔
1505
            } else if (project == 'keys') {
4!
1506
                let primary = this.indexes.primary
×
1507
                project = [primary.hash, primary.sort, index.hash, index.sort]
×
1508
                project = project.filter((v, i, a) => a.indexOf(v) === i)
×
1509
            } else if (Array.isArray(project)) {
4✔
1510
                let primary = this.indexes.primary
4✔
1511
                project = project.concat([primary.hash, primary.sort, index.hash, index.sort])
4✔
1512
                project = project.filter((v, i, a) => a.indexOf(v) === i)
28✔
1513
            }
1514
        }
1515
        return project
1,322✔
1516
    }
1517

1518
    //  For generic (table low level APIs), add all properties that are projected
1519
    addProjectedProperties(op, properties, params, project, rec) {
1520
        let generic = params.generic != null ? params.generic : this.generic
1,279!
1521
        if (generic && !KeysOnly[op]) {
1,279✔
1522
            for (let [name, value] of Object.entries(properties)) {
37✔
1523
                if (project && project.indexOf(name) < 0) {
31!
1524
                    continue
×
1525
                }
1526
                if (rec[name] === undefined) {
31✔
1527
                    //  Cannot do all type transformations - don't have enough info without fields
1528
                    if (value instanceof Date) {
9!
1529
                        if (this.isoDates) {
×
1530
                            rec[name] = value.toISOString()
×
1531
                        } else {
1532
                            rec[name] = value.getTime()
×
1533
                        }
1534
                    } else {
1535
                        rec[name] = value
9✔
1536
                    }
1537
                }
1538
            }
1539
        }
1540
        return rec
1,279✔
1541
    }
1542

1543
    /*
1544
        Add context to properties. If 'put', then for all fields, otherwise just key fields.
1545
        Context overrides properties.
1546
     */
1547
    addContext(op, fields, index, properties, params, context) {
1548
        for (let field of Object.values(fields)) {
1,326✔
1549
            if (field.schema) continue
13,951✔
1550
            if (op == 'put' || (field.attribute[0] != index.hash && field.attribute[0] != index.sort)) {
13,849✔
1551
                if (context[field.name] !== undefined) {
13,076✔
1552
                    properties[field.name] = context[field.name]
33✔
1553
                }
1554
            }
1555
        }
1556
        if (!this.generic && fields == this.block.fields) {
1,326✔
1557
            //  Set type field for the top level only
1558
            properties[this.typeField] = this.name
1,250✔
1559
        }
1560
    }
1561

1562
    /*
1563
        Set default property values on Put.
1564
    */
1565
    setDefaults(op, fields, properties, params) {
1566
        if (op != 'put' && op != 'init' && !(op == 'update' && params.exists == null)) {
1,326✔
1567
            return
397✔
1568
        }
1569
        for (let field of Object.values(fields)) {
929✔
1570
            if (field.schema) continue
9,623✔
1571
            let value = properties[field.name]
9,603✔
1572

1573
            //  Set defaults and uuid fields
1574
            if (value === undefined && !field.value) {
9,603✔
1575
                if (field.default !== undefined) {
1,621✔
1576
                    value = field.default
7✔
1577
                } else if (op == 'init') {
1,614!
1578
                    if (!field.generate) {
×
1579
                        //  Set non-default, non-uuid properties to null
1580
                        value = null
×
1581
                    }
1582
                } else if (field.generate) {
1,614✔
1583
                    let generate = field.generate
856✔
1584
                    if (generate === true) {
856!
1585
                        value = this.table.generate()
×
1586
                    } else if (typeof generate === 'function') {
856✔
1587
                        value = generate()
1✔
1588
                    } else if (generate == 'uuid') {
855✔
1589
                        value = this.table.uuid()
1✔
1590
                    } else if (generate == 'ulid') {
854!
1591
                        value = this.table.ulid()
854✔
1592
                    } else if (generate == 'uid') {
×
1593
                        value = this.table.uid(10)
×
1594
                    } else if (generate.indexOf('uid') == 0) {
×
1595
                        let [, size] = generate.split('(')
×
1596
                        value = this.table.uid(parseInt(size) || 10)
×
1597
                    }
1598
                }
1599
                if (value !== undefined) {
1,621✔
1600
                    properties[field.name] = value
863✔
1601
                }
1602
            }
1603
        }
1604
        return properties
929✔
1605
    }
1606

1607
    /*
1608
        Remove null properties from the table unless Table.nulls == true
1609
        TODO - null conversion would be better done in Expression then pathnames would not be needed.
1610
        NOTE: pathname is only needed for DEPRECATED callbacks.
1611
    */
1612
    convertNulls(op, pathname, fields, properties, params) {
1613
        for (let [name, value] of Object.entries(properties)) {
1,326✔
1614
            let field = fields[name]
10,395✔
1615
            if (!field || field.schema) continue
10,395✔
1616
            if (value === null && field.nulls !== true) {
10,339✔
1617
                //  create with null/undefined, or update with null property
1618
                if (
10✔
1619
                    field.required &&
13!
1620
                    ((op == 'put' && properties[field.name] == null) ||
1621
                        (op == 'update' && properties[field.name] === null))
1622
                ) {
1623
                    //  Validation will catch this
1624
                    continue
1✔
1625
                }
1626
                delete properties[name]
9✔
1627
                if (this.getPartial(field, params) === false && pathname.match(/[[.]/)) {
9!
1628
                    /*
1629
                        Partial disabled for a nested object
1630
                        Don't create remove entry as the entire object is being created/updated
1631
                     */
1632
                    continue
×
1633
                }
1634
                if (params.remove && !Array.isArray(params.remove)) {
9!
1635
                    params.remove = [params.remove]
×
1636
                } else {
1637
                    params.remove = params.remove || []
9✔
1638
                }
1639
                let path = pathname ? `${pathname}.${field.name}` : field.name
9✔
1640
                params.remove.push(path)
9✔
1641
            } else if (typeof value == 'object' && (field.type == 'object' || field.type == 'array')) {
10,329✔
1642
                //  LEGACY: Remove nested empty strings because DynamoDB cannot handle these nested in objects or arrays
1643
                if (this.table.params.legacyEmpties === true) {
93!
1644
                    properties[name] = this.handleEmpties(field, value)
×
1645
                }
1646
            }
1647
        }
1648
    }
1649

1650
    /*
1651
        Process value templates and property values that are functions
1652
     */
1653
    runTemplates(op, pathname, index, deps, properties, params) {
1654
        for (let field of deps) {
1,326✔
1655
            if (field.schema) continue
13,952✔
1656
            let name = field.name
13,849✔
1657
            if (
13,849✔
1658
                field.isIndexed &&
23,503✔
1659
                op != 'put' &&
1660
                op != 'update' &&
1661
                field.attribute[0] != index.hash &&
1662
                field.attribute[0] != index.sort
1663
            ) {
1664
                //  Ignore indexes not being used for this call
1665
                continue
747✔
1666
            }
1667
            let path = pathname ? `${pathname}.${field.name}` : field.name
13,102✔
1668

1669
            if (field.value === true && typeof this.table.params.value == 'function') {
13,102✔
1670
                properties[name] = this.table.params.value(this, path, properties, params)
4✔
1671
            } else if (properties[name] === undefined) {
13,098✔
1672
                if (field.value) {
7,677✔
1673
                    let value = this.runTemplate(op, index, field, properties, params, field.value)
5,025✔
1674
                    if (value != null) {
5,025✔
1675
                        properties[name] = value
4,908✔
1676
                    }
1677
                }
1678
            }
1679
        }
1680
    }
1681

1682
    /*
1683
        Expand a value template by substituting ${variable} values from context and properties.
1684
     */
1685
    runTemplate(op, index, field, properties, params, value) {
1686
        /*
1687
            Replace property references in ${var}
1688
            Support ${var:length:pad-character} which is useful for sorting.
1689
        */
1690
        value = value.replace(/\${(.*?)}/g, (match, varName) => {
5,025✔
1691
            let [name, len, pad] = varName.split(':')
7,822✔
1692
            let v = this.getPropValue(properties, name)
7,822✔
1693
            if (v != null) {
7,822✔
1694
                if (v instanceof Date) {
7,643!
1695
                    v = this.transformWriteDate(field, v)
×
1696
                }
1697
                if (len) {
7,643!
1698
                    //  Add leading padding for sorting numerics
1699
                    pad = pad || '0'
×
1700
                    let s = v + ''
×
1701
                    while (s.length < len) s = pad + s
×
1702
                    v = s
×
1703
                }
1704
            } else {
1705
                v = match
179✔
1706
            }
1707
            if (typeof v == 'object' && v.toString() == '[object Object]') {
7,822!
1708
                throw new OneTableError(`Value for "${field.name}" is not a primitive value`, {code: 'TypeError'})
×
1709
            }
1710
            return v
7,822✔
1711
        })
1712

1713
        /*
1714
            Consider unresolved template variables. If field is the sort key and doing find,
1715
            then use sort key prefix and begins_with, (provide no where clause).
1716
         */
1717
        if (value.indexOf('${') >= 0) {
5,025✔
1718
            if (index) {
164✔
1719
                if (field.attribute[0] == index.sort) {
164✔
1720
                    if (op == 'find') {
62✔
1721
                        //  Strip from first ${ onward and retain fixed prefix portion
1722
                        value = value.replace(/\${.*/g, '')
47✔
1723
                        if (value) {
47✔
1724
                            return {begins: value}
47✔
1725
                        }
1726
                    }
1727
                }
1728
            }
1729
            /*
1730
                Return undefined if any variables remain undefined. This is critical to stop updating
1731
                templates which do not have all the required properties to complete.
1732
            */
1733
            return undefined
117✔
1734
        }
1735
        return value
4,861✔
1736
    }
1737

1738
    //  Public routine to run templates
1739
    template(name, properties, params = {}) {
×
1740
        let fields = this.block.fields
×
1741
        let field = fields[name]
×
1742
        if (!field) {
×
1743
            throw new OneTableError('Cannot find field', {name})
×
1744
        }
1745
        return this.runTemplate('find', null, field, properties, params, field.value)
×
1746
    }
1747

1748
    validateProperties(op, fields, properties, params) {
1749
        if (op != 'put' && op != 'update') {
1,326✔
1750
            return
296✔
1751
        }
1752
        let validation = {}
1,030✔
1753
        if (typeof this.table.params.validate == 'function') {
1,030✔
1754
            validation = this.table.params.validate(this, properties, params) || {}
4!
1755
        }
1756
        for (let [name, value] of Object.entries(properties)) {
1,030✔
1757
            let field = fields[name]
9,409✔
1758
            if (!field || field.schema) continue
9,409✔
1759
            if (params.validate || field.validate || field.enum) {
9,361✔
1760
                value = this.validateProperty(field, value, validation, params)
21✔
1761
                properties[name] = value
21✔
1762
            }
1763
        }
1764
        for (let field of Object.values(fields)) {
1,030✔
1765
            //  If required and create, must be defined. If required and update, must not be null.
1766
            if (
10,683✔
1767
                field.required &&
27,105✔
1768
                !field.schema &&
1769
                ((op == 'put' && properties[field.name] == null) || (op == 'update' && properties[field.name] === null))
1770
            ) {
1771
                validation[field.name] = `Value not defined for required field "${field.name}"`
3✔
1772
            }
1773
        }
1774

1775
        if (Object.keys(validation).length > 0) {
1,030✔
1776
            throw new OneTableError(`Validation Error in "${this.name}" for "${Object.keys(validation).join(', ')}"`, {
4✔
1777
                validation,
1778
                code: 'ValidationError',
1779
                properties,
1780
            })
1781
        }
1782
    }
1783

1784
    validateProperty(field, value, details, params) {
1785
        let fieldName = field.name
21✔
1786

1787
        if (typeof params.validate == 'function') {
21!
1788
            let error
1789
            ;({error, value} = params.validate(this, field, value))
×
1790
            if (error) {
×
1791
                details[fieldName] = error
×
1792
            }
1793
        }
1794
        let validate = field.validate
21✔
1795
        if (validate) {
21✔
1796
            if (value === null) {
20✔
1797
                if (field.required && field.value == null) {
1✔
1798
                    details[fieldName] = `Value not defined for "${fieldName}"`
1✔
1799
                }
1800
            } else if (validate instanceof RegExp) {
19✔
1801
                if (!validate.exec(value)) {
13✔
1802
                    details[fieldName] = `Bad value "${value}" for "${fieldName}"`
3✔
1803
                }
1804
            } else {
1805
                let pattern = validate.toString()
6✔
1806
                if (pattern[0] == '/' && pattern.lastIndexOf('/') > 0) {
6✔
1807
                    let parts = pattern.split('/')
4✔
1808
                    let qualifiers = parts.pop()
4✔
1809
                    let pat = parts.slice(1).join('/')
4✔
1810
                    validate = new RegExp(pat, qualifiers)
4✔
1811
                    if (!validate.exec(value)) {
4✔
1812
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
2✔
1813
                    }
1814
                } else {
1815
                    if (!value.match(pattern)) {
2✔
1816
                        details[fieldName] = `Bad value "${value}" for "${fieldName}"`
1✔
1817
                    }
1818
                }
1819
            }
1820
        }
1821
        if (field.enum) {
21✔
1822
            if (field.enum.indexOf(value) < 0) {
1!
1823
                details[fieldName] = `Bad value "${value}" for "${fieldName}"`
×
1824
            }
1825
        }
1826
        return value
21✔
1827
    }
1828

1829
    transformProperties(op, fields, properties, params, rec) {
1830
        for (let [name, field] of Object.entries(fields)) {
1,322✔
1831
            //  Nested schemas handled via collectProperties
1832
            if (field.schema) continue
13,909✔
1833
            let value = rec[name]
13,807✔
1834
            if (value !== undefined) {
13,807✔
1835
                rec[name] = this.transformWriteAttribute(op, field, value, properties, params)
9,943✔
1836
            }
1837
        }
1838
        return rec
1,322✔
1839
    }
1840

1841
    /*
1842
        Transform an attribute before writing. This invokes transform callbacks and handles nested objects.
1843
     */
1844
    transformWriteAttribute(op, field, value, properties, params) {
1845
        let type = field.type
9,943✔
1846

1847
        if (typeof params.transform == 'function') {
9,943!
1848
            value = params.transform(this, 'write', field.name, value, properties, null)
×
1849
        } else if (value == null && field.nulls === true) {
9,943!
1850
            //  Keep the null
1851
        } else if (op == 'find' && value != null && typeof value == 'object') {
9,943✔
1852
            //  Find used {begins} for sort keys and other operators
1853
            value = this.transformNestedWriteFields(field, value)
49✔
1854
        } else if (type == 'date') {
9,894✔
1855
            value = this.transformWriteDate(field, value)
519✔
1856
        } else if (type == 'number') {
9,375✔
1857
            let num = Number(value)
124✔
1858
            if (isNaN(num)) {
124!
1859
                throw new OneTableError(`Invalid value "${value}" provided for field "${field.name}"`, {
×
1860
                    code: 'ValidationError',
1861
                })
1862
            }
1863
            value = num
124✔
1864
        } else if (type == 'boolean') {
9,251!
1865
            if (value == 'false' || value == 'null' || value == 'undefined') {
×
1866
                value = false
×
1867
            }
1868
            value = Boolean(value)
×
1869
        } else if (type == 'string') {
9,251✔
1870
            if (value != null) {
9,151✔
1871
                value = value.toString()
9,151✔
1872
            }
1873
        } else if (type == 'buffer' || type == 'arraybuffer' || type == 'binary') {
100✔
1874
            if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
5!
1875
                value = value.toString('base64')
5✔
1876
            }
1877
        } else if (type == 'array') {
95✔
1878
            if (value != null) {
11✔
1879
                if (Array.isArray(value)) {
11!
1880
                    value = this.transformNestedWriteFields(field, value)
11✔
1881
                } else {
1882
                    //  Heursistics to accept legacy string values for array types. Note: TS would catch this also.
1883
                    if (value == '') {
×
1884
                        value = []
×
1885
                    } else {
1886
                        //  FUTURE: should be moved to validations
1887
                        throw new OneTableArgError(
×
1888
                            `Invalid data type for Array field "${field.name}" in "${this.name}"`
1889
                        )
1890
                    }
1891
                }
1892
            }
1893
        } else if (type == 'set' && Array.isArray(value)) {
84!
1894
            value = this.transformWriteSet(type, value)
×
1895
        } else if (type == 'object' && value != null && typeof value == 'object') {
84✔
1896
            value = this.transformNestedWriteFields(field, value)
81✔
1897
        }
1898

1899
        if (field.crypt && value != null) {
9,943✔
1900
            value = this.encrypt(value)
1✔
1901
        }
1902
        return value
9,943✔
1903
    }
1904

1905
    transformNestedWriteFields(field, obj) {
1906
        for (let [key, value] of Object.entries(obj)) {
141✔
1907
            let type = field.type
2,162✔
1908
            if (value instanceof Date) {
2,162✔
1909
                obj[key] = this.transformWriteDate(field, value)
1✔
1910
            } else if (value instanceof Buffer || value instanceof ArrayBuffer || value instanceof DataView) {
2,161!
1911
                value = value.toString('base64')
×
1912
            } else if (Array.isArray(value) && (field.type == Set || type == Set)) {
2,161!
1913
                value = this.transformWriteSet(type, value)
×
1914
            } else if (value == null && field.nulls !== true) {
2,161!
1915
                //  Skip nulls
1916
                continue
×
1917
            } else if (value != null && typeof value == 'object') {
2,161!
1918
                obj[key] = this.transformNestedWriteFields(field, value)
×
1919
            }
1920
        }
1921
        return obj
141✔
1922
    }
1923

1924
    transformWriteSet(type, value) {
1925
        if (!Array.isArray(value)) {
×
1926
            throw new OneTableError('Set values must be arrays', {code: 'TypeError'})
×
1927
        }
1928
        if (type == Set || type == 'Set' || type == 'set') {
×
1929
            let v = value.values().next().value
×
1930
            if (typeof v == 'string') {
×
1931
                value = value.map((v) => v.toString())
×
1932
            } else if (typeof v == 'number') {
×
1933
                value = value.map((v) => Number(v))
×
1934
            } else if (v instanceof Buffer || v instanceof ArrayBuffer || v instanceof DataView) {
×
1935
                value = value.map((v) => v.toString('base64'))
×
1936
            }
1937
        } else {
1938
            throw new OneTableError('Unknown type', {code: 'TypeError'})
×
1939
        }
1940
        return value
×
1941
    }
1942

1943
    /*
1944
        Handle dates. Supports epoch and ISO date transformations.
1945
    */
1946
    transformWriteDate(field, value) {
1947
        let isoDates = field.isoDates || this.table.isoDates
520✔
1948
        if (field.ttl) {
520!
1949
            //  Convert dates to DynamoDB TTL
1950
            if (value instanceof Date) {
×
1951
                value = value.getTime()
×
1952
            } else if (typeof value == 'string') {
×
1953
                value = new Date(Date.parse(value)).getTime()
×
1954
            }
1955
            value = Math.ceil(value / 1000)
×
1956
        } else if (isoDates) {
520✔
1957
            if (value instanceof Date) {
466!
1958
                value = value.toISOString()
466✔
1959
            } else if (typeof value == 'string') {
×
1960
                value = new Date(Date.parse(value)).toISOString()
×
1961
            } else if (typeof value == 'number') {
×
1962
                value = new Date(value).toISOString()
×
1963
            }
1964
        } else {
1965
            //  Convert dates to unix epoch in milliseconds
1966
            if (value instanceof Date) {
54!
1967
                value = value.getTime()
54✔
1968
            } else if (typeof value == 'string') {
×
1969
                value = new Date(Date.parse(value)).getTime()
×
1970
            }
1971
        }
1972
        return value
520✔
1973
    }
1974

1975
    /*
1976
        Get a hash of all the property names of the indexes. Keys are properties, values are index names.
1977
        Primary takes precedence if property used in multiple indexes (LSIs)
1978
     */
1979
    getIndexProperties(indexes) {
1980
        let properties = {}
340✔
1981
        for (let [indexName, index] of Object.entries(indexes)) {
340✔
1982
            for (let [type, pname] of Object.entries(index)) {
798✔
1983
                if (type == 'hash' || type == 'sort') {
2,014✔
1984
                    if (properties[pname] != 'primary') {
1,565✔
1985
                        //  Let primary take precedence
1986
                        properties[pname] = indexName
1,540✔
1987
                    }
1988
                }
1989
            }
1990
        }
1991
        return properties
340✔
1992
    }
1993

1994
    encrypt(text, name = 'primary', inCode = 'utf8', outCode = 'base64') {
3✔
1995
        return this.table.encrypt(text, name, inCode, outCode)
1✔
1996
    }
1997

1998
    decrypt(text, inCode = 'base64', outCode = 'utf8') {
4✔
1999
        return this.table.decrypt(text, inCode, outCode)
2✔
2000
    }
2001

2002
    /*
2003
        Clone properties and params to callers objects are not polluted
2004
    */
2005
    checkArgs(properties, params, overrides = {}) {
1,149✔
2006
        if (params.checked) {
2,385✔
2007
            //  Only need to clone once
2008
            return {properties, params}
1,122✔
2009
        }
2010
        if (!properties) {
1,263!
2011
            throw new OneTableArgError('Missing properties')
×
2012
        }
2013
        if (typeof params != 'object') {
1,263!
2014
            throw new OneTableError('Invalid type for params', {code: 'TypeError'})
×
2015
        }
2016
        //  Must not use merge as we need to modify the callers batch/transaction objects
2017
        params = Object.assign(overrides, params)
1,263✔
2018

2019
        params.checked = true
1,263✔
2020
        properties = this.table.assign({}, properties)
1,263✔
2021
        return {properties, params}
1,263✔
2022
    }
2023

2024
    /*
2025
        Handle nulls and empty strings properly according to nulls preference in plain objects and arrays.
2026
        NOTE: DynamoDB can handle empty strings as top level non-key string attributes, but not nested in lists or maps. Ugh!
2027
    */
2028
    handleEmpties(field, obj) {
2029
        let result
2030
        if (
×
2031
            obj !== null &&
×
2032
            typeof obj == 'object' &&
2033
            (obj.constructor.name == 'Object' || obj.constructor.name == 'Array')
2034
        ) {
2035
            result = Array.isArray(obj) ? [] : {}
×
2036
            for (let [key, value] of Object.entries(obj)) {
×
2037
                if (value === '') {
×
2038
                    //  Convert to null and handle according to field.nulls
2039
                    value = null
×
2040
                }
2041
                if (value == null && field.nulls !== true) {
×
2042
                    //  Match null and undefined
2043
                    continue
×
2044
                } else if (typeof value == 'object') {
×
2045
                    result[key] = this.handleEmpties(field, value)
×
2046
                } else {
2047
                    result[key] = value
×
2048
                }
2049
            }
2050
        } else {
2051
            result = obj
×
2052
        }
2053
        return result
×
2054
    }
2055

2056
    /*
2057
        Return if a field supports partial updates of its children.
2058
        Only relevant for fields with nested schema
2059
     */
2060
    getPartial(field, params) {
2061
        let partial = params.partial
131✔
2062
        if (partial === undefined) {
131✔
2063
            partial = field.partial
107✔
2064
        }
2065
        return partial ? true : false
131✔
2066
    }
2067

2068
    /*  KEEP
2069
    captureStack() {
2070
        let limit = Error.stackTraceLimit
2071
        Error.stackTraceLimit = 1
2072

2073
        let obj = {}
2074
        let v8Handler = Error.prepareStackTrace
2075
        Error.prepareStackTrace = function(obj, stack) { return stack }
2076
        Error.captureStackTrace(obj, this.captureStack)
2077

2078
        let stack = obj.stack
2079
        Error.prepareStackTrace = v8Handler
2080
        Error.stackTraceLimit = limit
2081

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

© 2026 Coveralls, Inc