'use strict';
const Type = require('./Type');
const Permission = require('../util/Permission');
const Validator = require('../util/Validator');
const binding = require('../binding');
const deprecated = require('../util/deprecated');
const VALIDATION_CODE = Symbol('ValidationCode');
const TYPE_CONSTRUCTOR = Type.TYPE_CONSTRUCTOR;
/**
* @alias metamodel.ManagedType
* @extends metamodel.Type
*/
class ManagedType extends Type {
/**
* @type Function
*/
get validationCode() {
return this[VALIDATION_CODE];
}
/**
* @param {string=} code
*/
set validationCode(code) {
if (!code) {
this[VALIDATION_CODE] = null;
} else {
this[VALIDATION_CODE] = Validator.compile(this, code);
}
}
/**
* The Managed class
* @type Class<binding.Managed>
*/
get typeConstructor() {
if (!this[TYPE_CONSTRUCTOR]) {
this.typeConstructor = this.createProxyClass();
}
return this[TYPE_CONSTRUCTOR];
}
/**
* The Managed class constructor
* @param {Class<binding.Managed>} typeConstructor The managed class constructor
*/
set typeConstructor(typeConstructor) {
if (this[TYPE_CONSTRUCTOR]) {
throw new Error('Type constructor has already been set.');
}
const isEntity = typeConstructor.prototype instanceof binding.Entity;
if (this.isEntity) {
if (!isEntity) {
throw new TypeError('Entity classes must extends the Entity class.');
}
} else if (!(typeConstructor.prototype instanceof binding.Managed) || isEntity) {
throw new TypeError('Embeddable classes must extends the Managed class.');
}
this.enhancer.enhance(this, typeConstructor);
this[TYPE_CONSTRUCTOR] = typeConstructor;
}
/**
* @param {string} ref or full class name
* @param {Class<binding.Managed>=} typeConstructor
*/
constructor(ref, typeConstructor) {
super(ref.indexOf('/db/') !== 0 ? '/db/' + ref : ref, typeConstructor);
/** @type binding.Enhancer */
this.enhancer = null;
/** @type {metamodel.Attribute[]} */
this.declaredAttributes = [];
/** @type util.Permission */
this.schemaAddPermission = new Permission();
/** @type util.Permission */
this.schemaReplacePermission = new Permission();
/** @type Object<string,string>|null */
this.metadata = null;
}
/**
* Initialize this type
* @param {binding.Enhancer} enhancer The class enhancer used to instantiate an instance of this managed class
* @return {void}
*/
init(enhancer) {
this.enhancer = enhancer;
if (this[TYPE_CONSTRUCTOR] && !binding.Enhancer.getIdentifier(this[TYPE_CONSTRUCTOR])) {
binding.Enhancer.setIdentifier(this[TYPE_CONSTRUCTOR], this.ref);
}
}
/**
* Creates an ProxyClass for this type
* @return {Class<*>} the crated proxy class for this type
* @abstract
*/
createProxyClass() {}
/**
* Creates an ObjectFactory for this type and the given EntityManager
* @param {EntityManager} db The created instances will be attached to this EntityManager
* @return {binding.ManagedFactory<*>} the crated object factory for the given EntityManager
* @abstract
*/
createObjectFactory(db) {} // eslint-disable-line no-unused-vars
/**
* Creates a new instance of the managed type, without invoking any constructors
*
* This method is used to create object instances which are loaded form the backend.
*
* @return {object} The created instance
*/
create() {
const instance = Object.create(this.typeConstructor.prototype);
binding.Managed.init(instance);
return instance;
}
/**
* An iterator which returns all attributes declared by this type and inherited form all super types
* @return {Iterator<metamodel.Attribute>}
*/
attributes() {
let iter;
let index = 0;
const type = this;
if (this.superType) {
iter = this.superType.attributes();
}
return {
[Symbol.iterator]() {
return this;
},
/**
* @return {Object} item
* @return {boolean} item.done
* @return {metamodel.Attribute} item.value
*/
next() {
if (iter) {
const item = iter.next();
if (!item.done) {
return item;
}
iter = null;
}
if (index < type.declaredAttributes.length) {
const value = type.declaredAttributes[index];
index += 1;
return { value, done: false };
}
return { done: true };
},
};
}
/**
* Adds an attribute to this type
* @param {metamodel.Attribute} attr The attribute to add
* @param {number=} order Position of the attribute
* @return {void}
*/
addAttribute(attr, order) {
if (this.getAttribute(attr.name)) {
throw new Error('An attribute with the name ' + attr.name + ' is already declared.');
}
let initOrder;
if (!attr.order) {
initOrder = typeof order === 'undefined' ? this.declaredAttributes.length : order;
} else {
initOrder = attr.order;
}
attr.init(this, initOrder);
this.declaredAttributes.push(attr);
if (this[TYPE_CONSTRUCTOR] && this.name !== 'Object') {
this.enhancer.enhanceProperty(this[TYPE_CONSTRUCTOR], attr);
}
}
/**
* Removes an attribute from this type
* @param {string} name The Name of the attribute which will be removed
* @return {void}
*/
removeAttribute(name) {
const length = this.declaredAttributes.length;
this.declaredAttributes = this.declaredAttributes.filter(val => val.name !== name);
if (length === this.declaredAttributes.length) {
throw new Error('An Attribute with the name ' + name + ' is not declared.');
}
}
/**
* @param {!string} name
* @return {metamodel.Attribute}
*/
getAttribute(name) {
let attr = this.getDeclaredAttribute(name);
if (!attr && this.superType) {
attr = this.superType.getAttribute(name);
}
return attr;
}
/**
* @param {string|number} val Name or order of the attribute
* @return {metamodel.Attribute}
*/
getDeclaredAttribute(val) {
return this.declaredAttributes.filter(attr => attr.name === val || attr.order === val)[0] || null;
}
/**
* @inheritDoc
*/
fromJsonValue(state, jsonObject, currentObject, options) {
if (!jsonObject) {
return null;
}
const iter = this.attributes();
for (let el = iter.next(); !el.done; el = iter.next()) {
const attribute = el.value;
if (!options.onlyMetadata || attribute.isMetadata) {
attribute.setJsonValue(state, currentObject, jsonObject[attribute.name], options);
}
}
return currentObject;
}
/**
* @inheritDoc
*/
toJsonValue(state, object, options) {
if (!(object instanceof this.typeConstructor)) {
return null;
}
const value = {};
const iter = this.attributes();
for (let el = iter.next(); !el.done; el = iter.next()) {
const attribute = el.value;
if (!options.excludeMetadata || !attribute.isMetadata) {
value[attribute.name] = attribute.getJsonValue(state, object, options);
}
}
return value;
}
/**
* Converts ths type schema to json
* @return {json}
*/
toJSON() {
const fields = {};
this.declaredAttributes.forEach((attribute) => {
if (!attribute.isMetadata) {
fields[attribute.name] = attribute;
}
});
const json = {
class: this.ref,
fields,
acl: {
load: this.loadPermission,
schemaAdd: this.schemaAddPermission,
schemaReplace: this.schemaReplacePermission,
},
};
if (this.superType) {
json.superClass = this.superType.ref;
}
if (this.isEmbeddable) {
json.embedded = true;
}
if (this.metadata) {
json.metadata = this.metadata;
}
return json;
}
/**
* Returns iterator to get all referenced entities
* @return {Iterator<EntityType>}
*/
references() {
const attributes = this.attributes();
let attribute;
let embeddedAttributes;
return {
[Symbol.iterator]() {
return this;
},
/**
* @return {Object} item
* @return {boolean} item.done
* @return {Object} item.value
*/
next() {
for (;;) {
if (embeddedAttributes) {
const item = embeddedAttributes.next();
if (!item.done) {
return { value: { path: [attribute.name].concat(item.value.path) } };
}
embeddedAttributes = null;
}
const item = attributes.next();
if (item.done) {
return item;
}
attribute = item.value;
const type = attribute.isCollection ? attribute.elementType : attribute.type;
if (type.isEntity) {
return { value: { path: [attribute.name] } };
} else if (type.isEmbeddable) {
embeddedAttributes = type.references();
}
}
},
};
}
/**
* Retrieves whether this type has specific metadata
*
* @param {string} key
* @return {boolean}
*/
hasMetadata(key) {
return this.metadata && !!this.metadata[key];
}
/**
* Gets some metadata of this type
*
* @param {string} key
* @return {null|string}
*/
getMetadata(key) {
if (!this.hasMetadata(key)) {
return null;
}
return this.metadata[key];
}
}
deprecated(ManagedType.prototype, '_enhancer', 'enhancer');
module.exports = ManagedType;