/**
* @module flitter-orm/src/schema/Schema
*/
const DateType = require('./types/Date')
const StringType = require('./types/String')
const NumberType = require('./types/Number')
const BooleanType = require('./types/Boolean')
const ObjectIdType = require('./types/ObjectId')
const ArrayType = require('./types/Array')
const ObjectType = require('./types/Object')
const ModelType = require('./types/Model')
const Type = require('./Type')
const { ObjectId } = require('mongodb')
/**
* Database schema base class. Provides logic for parsing
* and recursively building a schema from a definition, and
* coercing an object, recursively, to comply with the schema
* definition.
*/
class Schema {
/**
* Schema types supported by this Schema class.
* Provides a mapping from reference name => Type class.
* @type {object}
*/
static types = {
Date: DateType,
String: StringType,
Number: NumberType,
Boolean: BooleanType,
ObjectId: ObjectIdType,
Array: ArrayType,
Object: ObjectType,
Model: ModelType,
}
/**
* Instantiates the schema and builds the schema object from the provided definition.
* @param {object} definition - the schema definition
*/
constructor(definition = {}) {
/**
* The original schema definition.
* @type {object}
*/
this.definition = definition
/**
* The built schema object.
* @type {object}
*/
this.schema = this.build_schema(definition)
}
/**
* Cast the provided object to the specified schema. Particularly,
* this function is used externally to cast objects to this schema,
* recursively.
*
* After this cast, the resulting object is safe to persist, at least
* for what this library guarantees.
*
* @param {object} object - object to cast
* @param {object} [level = false] - the current level of the schema object (usually leave this blank)
* @returns {object} - the casted object
*/
cast_to_schema(object, level = false) {
if ( !level ) level = this.schema
// Special handling for arrays, since their children property is _default
if ( Array.isArray(object) && '_default' in level ) {
const cast_array = []
for ( const item of object ) {
cast_array.push(this.cast_to_schema(item, level._default))
}
return cast_array
}
// Handle single item casts (e.g. cast 'some string' to a string schema)
if ( typeof object !== 'object' || (level.type && (level.type === ObjectType || level.type === ModelType) ) ) {
// Handle primitives and object types
let value
if ( typeof object !== 'undefined' ) value = level.type.cast(object)
else {
const default_value = level.default_to()
if ( typeof default_value !== 'undefined' ) value = level.type.cast(default_value)
}
if ( typeof value !== 'undefined' ) {
if (level.children) {
// Parse the nested type
return this.cast_to_schema(value, level.children)
} else {
// Store the primitive type
return value
}
}
}
// Handle primitive object casts
else if (typeof level === 'object' && level.type && level.default_to && level.prop && level.def ) {
let value
if ( object ) value = level.type.cast(object)
else {
const default_value = level.default_to()
if (typeof default_value !== 'undefined') value = schemata.type.cast(default_value)
}
return value
}
// Handle object-type casts
else {
const cast_object = {}
for (const prop in level) {
const schemata = level[prop]
if (!level.hasOwnProperty(prop)) continue
// Handle primitives and object types
let value
if (prop in object) value = schemata.type.cast(object[prop])
else {
const default_value = schemata.default_to()
if (typeof default_value !== 'undefined') value = schemata.type.cast(default_value)
}
if (typeof value !== 'undefined') {
if (schemata.children) {
// Parse the nested type
cast_object[prop] = this.cast_to_schema(value, schemata.children)
} else {
// Store the primitive type
cast_object[prop] = value
}
}
}
return cast_object
}
}
/**
* Recursively build a schema object from the provided definition.
* @param {object} defs - the schema definition
* @returns {object} - the schema object
*/
build_schema(defs) {
const level = {}
for ( const prop in defs ) {
const def = defs[prop]
// Process primitives
if ( typeof def === 'function' && def.prototype instanceof require('../model/Model') ) {
const type = this.parse_type(def)
const default_to = this.parse_default()
level[prop] = { type, default_to, prop, def, children: this.build_schema(def.schema) }
}
else if ( !Array.isArray(def) && typeof def !== 'object' ) {
const type = this.parse_type(def)
const default_to = this.parse_default()
level[prop] = { type, default_to, prop, def }
}
// Process primitive objects
else if ( typeof def === 'object' && def.type && !Array.isArray(def.type) && typeof def.type !== 'object' ) {
const type = this.parse_type(def.type)
const default_to = this.parse_default(def.default)
level[prop] = { type, default_to, prop, def }
}
// Process array types
else if ( Array.isArray(def) ) {
const type = this.parse_type(def)
const default_to = this.parse_default()
level[prop] = { type, default_to, prop, def, children: this.build_schema({_default: def[0]})}
}
// Process nested objects
else {
const type = this.parse_type(def)
const default_to = this.parse_default()
level[prop] = { type, default_to, prop, def, children: this.build_schema(def) }
}
}
return level
}
/**
* Parse the schema type from the specified type identifier.
* @param {Date|String|Number|Boolean|ObjectId|Array|Object} type
* @returns {module:flitter-orm/src/schema/Type~Type}
*/
parse_type(type) {
const types = this.constructor.types
if ( [Date, 'date', 'Date'].includes(type) ) return types.Date
if ( [String, 'string', 'String'].includes(type) ) return types.String
if ( [Number, 'number', 'Number', 'int', 'Int', 'double', 'Double'].includes(type) ) return types.Number
if ( [Boolean, 'boolean', 'Boolean'].includes(type) ) return types.Boolean
if ( [ObjectId, 'ObjectId', 'id'].includes(type) ) return types.ObjectId
if ( [Array, 'array', 'Array'].includes(type) || Array.isArray(type) ) return types.Array
if ( [Object, 'object', 'Object'].includes(type) || typeof type === 'object' ) return types.Object
if ( typeof type === 'function' && type.prototype instanceof require('../model/Model') ) return types.Model
throw new Error('Unknown or invalid field type encountered!')
}
/**
* Create a function that returns the specified default value.
* If default_value is a function, it will be called. Otherwise,
* this will return a function that evaluates to default_value.
*
* @param {function|*} default_value
* @returns {function}
*/
parse_default(default_value) {
if ( typeof default_value === 'function' ) return default_value
else return () => default_value
}
}
module.exports = exports = Schema