orm/src/schema/Schema.js

  1. /**
  2. * @module flitter-orm/src/schema/Schema
  3. */
  4. const DateType = require('./types/Date')
  5. const StringType = require('./types/String')
  6. const NumberType = require('./types/Number')
  7. const BooleanType = require('./types/Boolean')
  8. const ObjectIdType = require('./types/ObjectId')
  9. const ArrayType = require('./types/Array')
  10. const ObjectType = require('./types/Object')
  11. const ModelType = require('./types/Model')
  12. const Type = require('./Type')
  13. const { ObjectId } = require('mongodb')
  14. /**
  15. * Database schema base class. Provides logic for parsing
  16. * and recursively building a schema from a definition, and
  17. * coercing an object, recursively, to comply with the schema
  18. * definition.
  19. */
  20. class Schema {
  21. /**
  22. * Schema types supported by this Schema class.
  23. * Provides a mapping from reference name => Type class.
  24. * @type {object}
  25. */
  26. static types = {
  27. Date: DateType,
  28. String: StringType,
  29. Number: NumberType,
  30. Boolean: BooleanType,
  31. ObjectId: ObjectIdType,
  32. Array: ArrayType,
  33. Object: ObjectType,
  34. Model: ModelType,
  35. }
  36. /**
  37. * Instantiates the schema and builds the schema object from the provided definition.
  38. * @param {object} definition - the schema definition
  39. */
  40. constructor(definition = {}) {
  41. /**
  42. * The original schema definition.
  43. * @type {object}
  44. */
  45. this.definition = definition
  46. /**
  47. * The built schema object.
  48. * @type {object}
  49. */
  50. this.schema = this.build_schema(definition)
  51. }
  52. /**
  53. * Cast the provided object to the specified schema. Particularly,
  54. * this function is used externally to cast objects to this schema,
  55. * recursively.
  56. *
  57. * After this cast, the resulting object is safe to persist, at least
  58. * for what this library guarantees.
  59. *
  60. * @param {object} object - object to cast
  61. * @param {object} [level = false] - the current level of the schema object (usually leave this blank)
  62. * @returns {object} - the casted object
  63. */
  64. cast_to_schema(object, level = false) {
  65. if ( !level ) level = this.schema
  66. // Special handling for arrays, since their children property is _default
  67. if ( Array.isArray(object) && '_default' in level ) {
  68. const cast_array = []
  69. for ( const item of object ) {
  70. cast_array.push(this.cast_to_schema(item, level._default))
  71. }
  72. return cast_array
  73. }
  74. // Handle single item casts (e.g. cast 'some string' to a string schema)
  75. if ( typeof object !== 'object' || (level.type && (level.type === ObjectType || level.type === ModelType) ) ) {
  76. // Handle primitives and object types
  77. let value
  78. if ( typeof object !== 'undefined' ) value = level.type.cast(object)
  79. else {
  80. const default_value = level.default_to()
  81. if ( typeof default_value !== 'undefined' ) value = level.type.cast(default_value)
  82. }
  83. if ( typeof value !== 'undefined' ) {
  84. if (level.children) {
  85. // Parse the nested type
  86. return this.cast_to_schema(value, level.children)
  87. } else {
  88. // Store the primitive type
  89. return value
  90. }
  91. }
  92. }
  93. // Handle primitive object casts
  94. else if (typeof level === 'object' && level.type && level.default_to && level.prop && level.def ) {
  95. let value
  96. if ( object ) value = level.type.cast(object)
  97. else {
  98. const default_value = level.default_to()
  99. if (typeof default_value !== 'undefined') value = schemata.type.cast(default_value)
  100. }
  101. return value
  102. }
  103. // Handle object-type casts
  104. else {
  105. const cast_object = {}
  106. for (const prop in level) {
  107. const schemata = level[prop]
  108. if (!level.hasOwnProperty(prop)) continue
  109. // Handle primitives and object types
  110. let value
  111. if (prop in object) value = schemata.type.cast(object[prop])
  112. else {
  113. const default_value = schemata.default_to()
  114. if (typeof default_value !== 'undefined') value = schemata.type.cast(default_value)
  115. }
  116. if (typeof value !== 'undefined') {
  117. if (schemata.children) {
  118. // Parse the nested type
  119. cast_object[prop] = this.cast_to_schema(value, schemata.children)
  120. } else {
  121. // Store the primitive type
  122. cast_object[prop] = value
  123. }
  124. }
  125. }
  126. return cast_object
  127. }
  128. }
  129. /**
  130. * Recursively build a schema object from the provided definition.
  131. * @param {object} defs - the schema definition
  132. * @returns {object} - the schema object
  133. */
  134. build_schema(defs) {
  135. const level = {}
  136. for ( const prop in defs ) {
  137. const def = defs[prop]
  138. // Process primitives
  139. if ( typeof def === 'function' && def.prototype instanceof require('../model/Model') ) {
  140. const type = this.parse_type(def)
  141. const default_to = this.parse_default()
  142. level[prop] = { type, default_to, prop, def, children: this.build_schema(def.schema) }
  143. }
  144. else if ( !Array.isArray(def) && typeof def !== 'object' ) {
  145. const type = this.parse_type(def)
  146. const default_to = this.parse_default()
  147. level[prop] = { type, default_to, prop, def }
  148. }
  149. // Process primitive objects
  150. else if ( typeof def === 'object' && def.type && !Array.isArray(def.type) && typeof def.type !== 'object' ) {
  151. const type = this.parse_type(def.type)
  152. const default_to = this.parse_default(def.default)
  153. level[prop] = { type, default_to, prop, def }
  154. }
  155. // Process array types
  156. else if ( Array.isArray(def) ) {
  157. const type = this.parse_type(def)
  158. const default_to = this.parse_default()
  159. level[prop] = { type, default_to, prop, def, children: this.build_schema({_default: def[0]})}
  160. }
  161. // Process nested objects
  162. else {
  163. const type = this.parse_type(def)
  164. const default_to = this.parse_default()
  165. level[prop] = { type, default_to, prop, def, children: this.build_schema(def) }
  166. }
  167. }
  168. return level
  169. }
  170. /**
  171. * Parse the schema type from the specified type identifier.
  172. * @param {Date|String|Number|Boolean|ObjectId|Array|Object} type
  173. * @returns {module:flitter-orm/src/schema/Type~Type}
  174. */
  175. parse_type(type) {
  176. const types = this.constructor.types
  177. if ( [Date, 'date', 'Date'].includes(type) ) return types.Date
  178. if ( [String, 'string', 'String'].includes(type) ) return types.String
  179. if ( [Number, 'number', 'Number', 'int', 'Int', 'double', 'Double'].includes(type) ) return types.Number
  180. if ( [Boolean, 'boolean', 'Boolean'].includes(type) ) return types.Boolean
  181. if ( [ObjectId, 'ObjectId', 'id'].includes(type) ) return types.ObjectId
  182. if ( [Array, 'array', 'Array'].includes(type) || Array.isArray(type) ) return types.Array
  183. if ( [Object, 'object', 'Object'].includes(type) || typeof type === 'object' ) return types.Object
  184. if ( typeof type === 'function' && type.prototype instanceof require('../model/Model') ) return types.Model
  185. throw new Error('Unknown or invalid field type encountered!')
  186. }
  187. /**
  188. * Create a function that returns the specified default value.
  189. * If default_value is a function, it will be called. Otherwise,
  190. * this will return a function that evaluates to default_value.
  191. *
  192. * @param {function|*} default_value
  193. * @returns {function}
  194. */
  195. parse_default(default_value) {
  196. if ( typeof default_value === 'function' ) return default_value
  197. else return () => default_value
  198. }
  199. }
  200. module.exports = exports = Schema
JAVASCRIPT
Copied!