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

sensedeep / dynamodb-onetable / #65

pending completion
#65

push

Michael O'Brien
DEV: temporarily disable stream unit tests

1107 of 1601 branches covered (69.14%)

Branch coverage included in aggregate %.

1780 of 2373 relevant lines covered (75.01%)

623.2 hits per line

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

44.44
/src/Schema.js
1
/*
2
    Schema.js - Utility class to manage schemas
3
 */
4

5
import {Model} from './Model.js'
50✔
6
import {OneTableArgError} from './Error.js'
50✔
7

8
const GenericModel = '_Generic'
50✔
9
const MigrationModel = '_Migration'
50✔
10
const SchemaModel = '_Schema'
50✔
11
const UniqueModel = '_Unique'
50✔
12
const MigrationKey = '_migration'
50✔
13
const SchemaKey = '_schema'
50✔
14
const SchemaFormat = 'onetable:1.1.0'
50✔
15

16
export class Schema {
50✔
17
    constructor(table, schema) {
18
        this.table = table
55✔
19
        this.keyTypes = {}
55✔
20
        this.process = {}
55✔
21
        table.schema = this
55✔
22
        Object.defineProperty(this, 'table', {enumerable: false})
55✔
23
        this.params = table.getSchemaParams()
55✔
24
        this.setSchemaInner(schema)
55✔
25
    }
26

27
    getCurrentSchema() {
28
        if (this.definition) {
5✔
29
            let schema = this.table.assign({}, this.definition, {params: this.params})
5✔
30
            schema = this.transformSchemaForWrite(schema)
5✔
31
            schema.process = Object.assign({}, this.process)
5✔
32
            return schema
5✔
33
        }
34
        return null
×
35
    }
36

37
    /* private */
38
    setSchemaInner(schema) {
39
        this.models = {}
55✔
40
        this.indexes = null
55✔
41
        if (schema) {
55✔
42
            this.validateSchema(schema)
54✔
43
            this.definition = schema
54✔
44
            let {models, indexes, params} = schema
54✔
45
            if (!models) {
54!
46
                models = {}
×
47
            }
48
            this.indexes = indexes
54✔
49
            //  Must set before creating models
50
            this.table.setSchemaParams(params)
54✔
51

52
            for (let [name, model] of Object.entries(models)) {
54✔
53
                if (name == SchemaModel || name == MigrationModel) continue
60!
54
                this.models[name] = new Model(this.table, name, {fields: model})
60✔
55
            }
56
            this.createStandardModels()
54✔
57
            this.process = schema.process
54✔
58
        }
59
        return this.indexes
55✔
60
    }
61

62
    /*
63
        Set the schema to use. If undefined, get the table keys.
64
    */
65
    async setSchema(schema) {
66
        if (schema) {
×
67
            this.setSchemaInner(schema)
×
68
        } else {
69
            await this.getKeys()
×
70
        }
71
        return this.indexes
×
72
    }
73

74
    //  Start of a function to better validate schemas. More to do.
75
    validateSchema(schema) {
76
        let {indexes} = schema
54✔
77
        if (!schema.version) {
54!
78
            throw new Error('Schema is missing a version')
×
79
        }
80
        if (!schema.indexes) {
54!
81
            throw new Error('Schema is missing indexes')
×
82
        }
83
        let primary = indexes.primary
54✔
84
        if (!primary) {
54!
85
            throw new Error('Schema is missing a primary index')
×
86
        }
87
        let lsi = 0
54✔
88
        for (let [name, index] of Object.entries(schema.indexes)) {
54✔
89
            if (name != 'primary') {
128✔
90
                if (index.type == 'local') {
74✔
91
                    if (index.hash && index.hash != primary.hash) {
4!
92
                        throw new OneTableArgError(
×
93
                            `LSI "${name}" should not define a hash attribute that is different to the primary index`
94
                        )
95
                    }
96
                    if (index.sort == null) {
4!
97
                        throw new OneTableArgError('LSIs must define a sort attribute')
×
98
                    }
99
                    index.hash = primary.hash
4✔
100
                    lsi++
4✔
101
                } else if (index.hash == null) {
70!
102
                    if (index.type == null) {
×
103
                        console.warn(`Must use explicit "type": "local" in "${name}" LSI index definitions`)
×
104
                        index.type = 'local'
×
105
                    } else {
106
                        throw new OneTableArgError(`Index "${name}" is missing a hash attribute`)
×
107
                    }
108
                }
109
            }
110
        }
111
        if (lsi > 5) {
54!
112
            throw new Error('Schema has too many LSIs')
×
113
        }
114
    }
115

116
    createStandardModels() {
117
        this.createUniqueModel()
54✔
118
        this.createGenericModel()
54✔
119
        this.createSchemaModel()
54✔
120
        this.createMigrationModel()
54✔
121
    }
122

123
    /*
124
        Model for unique attributes. Free standing and not in models[]
125
     */
126
    createUniqueModel() {
127
        let {indexes, table} = this
54✔
128
        let primary = indexes.primary
54✔
129
        let type = this.keyTypes[primary.hash] || 'string'
54✔
130
        let fields = {
54✔
131
            [primary.hash]: {type},
132
        }
133
        if (primary.sort) {
54✔
134
            let type = this.keyTypes[primary.sort] || 'string'
51✔
135
            fields[primary.sort] = {type}
51✔
136
        }
137
        this.uniqueModel = new Model(table, UniqueModel, {fields, timestamps: false})
54✔
138
    }
139

140
    /*
141
        Model for genric low-level API access. Generic models allow reading attributes that are not defined on the schema.
142
        NOTE: there is not items created based on this model.
143
     */
144
    createGenericModel() {
145
        let {indexes, table} = this
54✔
146
        let primary = indexes.primary
54✔
147
        let type = this.keyTypes[primary.hash] || 'string'
54✔
148
        let fields = {[primary.hash]: {type}}
54✔
149
        if (primary.sort) {
54✔
150
            type = this.keyTypes[primary.sort] || 'string'
51✔
151
            fields[primary.sort] = {type}
51✔
152
        }
153
        this.genericModel = new Model(table, GenericModel, {fields, timestamps: false, generic: true})
54✔
154
    }
155

156
    createSchemaModel() {
157
        let {indexes, table} = this
54✔
158
        let primary = indexes.primary
54✔
159
        let fields = (this.schemaModelFields = {
54✔
160
            [primary.hash]: {type: 'string', required: true, value: `${SchemaKey}`},
161
            format: {type: 'string', required: true},
162
            indexes: {type: 'object', required: true},
163
            name: {type: 'string', required: true},
164
            models: {type: 'object', required: true},
165
            params: {type: 'object', required: true},
166
            queries: {type: 'object', required: true},
167
            process: {type: 'object'},
168
            version: {type: 'string', required: true},
169
        })
170
        if (primary.sort) {
54✔
171
            fields[primary.sort] = {type: 'string', required: true, value: `${SchemaKey}:\${name}`}
51✔
172
        }
173
        this.models[SchemaModel] = new Model(table, SchemaModel, {fields})
54✔
174
    }
175

176
    createMigrationModel() {
177
        let {indexes} = this
54✔
178
        let primary = indexes.primary
54✔
179
        let fields = (this.migrationModelFields = {
54✔
180
            [primary.hash]: {type: 'string', value: `${MigrationKey}`},
181
            date: {type: 'date', required: true},
182
            description: {type: 'string', required: true},
183
            path: {type: 'string', required: true},
184
            version: {type: 'string', required: true},
185
        })
186
        if (primary.sort) {
54✔
187
            fields[primary.sort] = {type: 'string', value: `${MigrationKey}:\${version}`}
51✔
188
        }
189
        this.models[MigrationModel] = new Model(this.table, MigrationModel, {fields, indexes})
54✔
190
    }
191

192
    addModel(name, fields) {
193
        this.models[name] = new Model(this.table, name, {indexes: this.indexes, fields})
1✔
194
    }
195

196
    listModels() {
197
        return Object.keys(this.models)
8✔
198
    }
199

200
    /*
201
        Thows exception if model cannot be found
202
     */
203
    getModel(name) {
204
        if (!name) {
127!
205
            throw new Error('Undefined model name')
×
206
        }
207
        let model = this.models[name.toString()]
127✔
208
        if (!model) {
127✔
209
            if (name == UniqueModel) {
2!
210
                return this.uniqueModel
×
211
            }
212
            throw new Error(`Cannot find model ${name}`)
2✔
213
        }
214
        return model
125✔
215
    }
216

217
    removeModel(name) {
218
        let model = this.models[name.toString()]
2✔
219
        if (!model) {
2✔
220
            throw new Error(`Cannot find model ${name}`)
1✔
221
        }
222
        delete this.models[name.toString()]
1✔
223
    }
224

225
    async getKeys(refresh = false) {
×
226
        if (this.indexes && !refresh) {
×
227
            return this.indexes
×
228
        }
229
        let info = await this.table.describeTable()
×
230
        for (let def of info.Table.AttributeDefinitions) {
×
231
            this.keyTypes[def.AttributeName] = def.AttributeType == 'N' ? 'number' : 'string'
×
232
        }
233
        let indexes = {primary: {}}
×
234
        for (let key of info.Table.KeySchema) {
×
235
            let type = key.KeyType.toLowerCase() == 'hash' ? 'hash' : 'sort'
×
236
            indexes.primary[type] = key.AttributeName
×
237
        }
238
        if (info.Table.GlobalSecondaryIndexes) {
×
239
            for (let index of info.Table.GlobalSecondaryIndexes) {
×
240
                let keys = (indexes[index.IndexName] = {})
×
241
                for (let key of index.KeySchema) {
×
242
                    let type = key.KeyType.toLowerCase() == 'hash' ? 'hash' : 'sort'
×
243
                    keys[type] = key.AttributeName
×
244
                }
245
                indexes[index.IndexName] = keys
×
246
            }
247
        }
248
        this.indexes = indexes
×
249
        this.createStandardModels()
×
250
        return indexes
×
251
    }
252

253
    setDefaultParams(params) {
254
        if (params.typeField == null) {
5!
255
            params.typeField = '_type'
×
256
        }
257
        if (params.isoDates == null) {
5!
258
            params.isoDates = false
×
259
        }
260
        if (params.nulls == null) {
5!
261
            params.nulls = false
×
262
        }
263
        if (params.timestamps == null) {
5!
264
            params.timestamps = false
×
265
        }
266
        return params
5✔
267
    }
268

269
    /*
270
        Prepare for persisting the schema. Convert types and regexp to strings.
271
    */
272
    transformSchemaForWrite(schema) {
273
        for (let model of Object.values(schema.models)) {
5✔
274
            for (let field of Object.values(model)) {
5✔
275
                this.transformFieldForWrite(field)
60✔
276
            }
277
        }
278
        schema.params = this.setDefaultParams(schema.params || this.params)
5!
279
        return schema
5✔
280
    }
281

282
    transformFieldForWrite(field) {
283
        if (field.validate && field.validate instanceof RegExp) {
64✔
284
            field.validate = `/${field.validate.source}/${field.validate.flags}`
4✔
285
        }
286
        if (field.encode) {
64!
287
            field.encode = field.encode.map((e) => (e instanceof RegExp ? `/${e.source}/${e.flags}` : e))
×
288
        }
289
        let type = typeof field.type == 'function' ? field.type.name : field.type
64!
290
        field.type = type.toLowerCase()
64✔
291
        //  DEPRECATE
292
        if (field.uuid) {
64!
293
            field.generate = field.generate || field.uuid
×
294
            delete field.uuid
×
295
        }
296
        if (field.schema) {
64✔
297
            for (let f of Object.values(field.schema)) {
1✔
298
                this.transformFieldForWrite(f)
4✔
299
            }
300
        }
301
        return field
64✔
302
    }
303

304
    /*
305
        Replace Schema and Migration models, timestamp fields and type field
306
    */
307
    transformSchemaAfterRead(schema) {
308
        if (!schema) {
×
309
            return null
×
310
        }
311
        if (!schema.name) {
×
312
            schema.name == 'Current'
×
313
        }
314
        //  Add internal models
315
        schema.models[SchemaModel] = this.schemaModelFields
×
316
        schema.models[MigrationModel] = this.migrationModelFields
×
317

318
        let params = schema.params || this.params
×
319

320
        for (let mdef of Object.values(schema.models)) {
×
321
            if (params.timestamps === true || params.timestamps == 'create') {
×
322
                let createdField = params.createdField || 'created'
×
323
                mdef[createdField] = {name: createdField, type: 'date'}
×
324
            }
325
            if (params.timestamps === true || params.timestamps == 'update') {
×
326
                let updatedField = params.updatedField || 'updated'
×
327
                mdef[updatedField] = {name: updatedField, type: 'date'}
×
328
            }
329
            mdef[params.typeField] = {name: params.typeField, type: 'string', required: true}
×
330

331
            for (let [, field] of Object.entries(mdef)) {
×
332
                //  DEPRECATE
333
                if (field.uuid) {
×
334
                    console.warn(`OneTable: Using deprecated field "uuid". Use "generate" instead.`)
×
335
                    field.generate = field.generate || field.uuid
×
336
                }
337
            }
338
        }
339
        this.setDefaultParams(params)
×
340
        return schema
×
341
    }
342

343
    /*
344
        Read the current schema saved in the table
345
    */
346
    async readSchema() {
347
        let indexes = this.indexes || (await this.getKeys())
×
348
        let primary = indexes.primary
×
349
        let params = {
×
350
            [primary.hash]: SchemaKey,
351
        }
352
        if (primary.sort) {
×
353
            params[primary.sort] = `${SchemaKey}:Current`
×
354
        }
355
        let schema = await this.table.getItem(params, {hidden: true, parse: true})
×
356
        return this.transformSchemaAfterRead(schema)
×
357
    }
358

359
    async readSchemas() {
360
        let indexes = this.indexes || (await this.getKeys())
×
361
        let primary = indexes.primary
×
362
        let params = {
×
363
            [primary.hash]: `${SchemaKey}`,
364
        }
365
        let schemas = await this.table.queryItems(params, {hidden: true, parse: true})
×
366
        for (let [index, schema] of Object.entries(schemas)) {
×
367
            schemas[index] = this.transformSchemaAfterRead(schema)
×
368
        }
369
        return schemas
×
370
    }
371

372
    async removeSchema(schema) {
373
        if (!this.indexes) {
×
374
            await this.getKeys()
×
375
        }
376
        let model = this.getModel(SchemaModel)
×
377
        await model.remove(schema)
×
378
    }
379

380
    /*
381
        Update the schema model saved in the database _Schema model.
382
        NOTE: this does not update the current schema used by the Table instance.
383
    */
384
    async saveSchema(schema) {
385
        if (!this.indexes) {
×
386
            await this.getKeys()
×
387
        }
388
        if (schema) {
×
389
            schema = this.table.assign({}, schema)
×
390
            if (!schema.params) {
×
391
                schema.params = this.params
×
392
            }
393
            if (!schema.models) {
×
394
                schema.models = {}
×
395
            }
396
            if (!schema.indexes) {
×
397
                schema.indexes = this.indexes || (await this.getKeys())
×
398
            }
399
            if (!schema.queries) {
×
400
                schema.queries = {}
×
401
            }
402
            schema = this.transformSchemaForWrite(schema)
×
403
        } else {
404
            schema = this.getCurrentSchema()
×
405
        }
406
        if (!schema) {
×
407
            throw new Error('No schema to save')
×
408
        }
409
        if (!schema.name) {
×
410
            schema.name = 'Current'
×
411
        }
412
        schema.version = schema.version || '0.0.1'
×
413
        schema.format = SchemaFormat
×
414

415
        let model = this.getModel(SchemaModel)
×
416
        return await model.create(schema, {exists: null})
×
417
    }
418
}
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