baqend
Version:
Baqend JavaScript SDK
358 lines (303 loc) • 9.74 kB
text/typescript
import { Type } from './Type';
import { Class, Json, JsonMap } from '../util';
import {
Enhancer, Entity, Managed, ManagedFactory,
} from '../binding';
import type { Attribute } from './Attribute';
import type { EntityManager } from '../EntityManager';
import type { EntityType } from './EntityType';
import type { PluralAttribute } from './PluralAttribute';
import type { SingularAttribute } from './SingularAttribute';
import type { EmbeddableType } from './index';
import { Permission, ManagedState } from '../intersection';
export abstract class ManagedType<T extends Managed> extends Type<T> {
public enhancer: Enhancer | null = null;
public declaredAttributes: Attribute<any>[] = [];
public schemaAddPermission: Permission = new Permission();
public schemaReplacePermission: Permission = new Permission();
public metadata: { [key: string]: string } | null = null;
public superType: EntityType<any> | null = null;
public _validationCode: Function | null = null;
/**
* @type Function
*/
get validationCode(): Function | null {
return this._validationCode;
}
/**
* @param code
*/
set validationCode(code: Function | null) {
this._validationCode = code;
}
/**
* The Managed class
*/
get typeConstructor(): Class<T> {
if (!this._typeConstructor) {
this.typeConstructor = this.createProxyClass();
}
return this._typeConstructor!!;
}
/**
* The Managed class constructor
* @param typeConstructor The managed class constructor
*/
set typeConstructor(typeConstructor: Class<T>) {
if (this._typeConstructor) {
throw new Error('Type constructor has already been set.');
}
const isEntity = typeConstructor.prototype instanceof Entity;
if (this.isEntity) {
if (!isEntity) {
throw new TypeError('Entity classes must extends the Entity class.');
}
} else if (!(typeConstructor.prototype instanceof Managed) || isEntity) {
throw new TypeError('Embeddable classes must extends the Managed class.');
}
this.enhancer!!.enhance(this, typeConstructor);
this._typeConstructor = typeConstructor;
}
/**
* @param ref or full class name
* @param typeConstructor The type constructor of the managed lass
*/
constructor(ref: string, typeConstructor?: Class<T>) {
super(ref.indexOf('/db/') !== 0 ? `/db/${ref}` : ref, typeConstructor);
}
/**
* Initialize this type
* @param enhancer The class enhancer used to instantiate an instance of this managed class
*/
init(enhancer: Enhancer): void {
this.enhancer = enhancer;
if (this._typeConstructor && !Enhancer.getIdentifier(this._typeConstructor!!)) {
Enhancer.setIdentifier(this._typeConstructor!!, this.ref);
}
}
/**
* Creates an ProxyClass for this type
* @return the crated proxy class for this type
*/
abstract createProxyClass(): Class<T>;
/**
* Creates an ObjectFactory for this type and the given EntityManager
* @param db The created instances will be attached to this EntityManager
* @return the crated object factory for the given EntityManager
*/
abstract createObjectFactory(db: EntityManager): ManagedFactory<T>;
/**
* 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 The created instance
*/
create(): T {
const instance = Object.create(this.typeConstructor.prototype);
Managed.init(instance);
return instance;
}
/**
* An iterator which returns all attributes declared by this type and inherited form all super types
* @return
*/
attributes(): IterableIterator<Attribute<any>> {
let iter: Iterator<Attribute<any>> | null;
let index = 0;
const type = this;
if (this.superType) {
iter = this.superType.attributes();
}
return {
[Symbol.iterator]() {
return this;
},
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, value: undefined };
},
};
}
/**
* Adds an attribute to this type
* @param attr The attribute to add
* @param order Position of the attribute
* @return
*/
addAttribute(attr: Attribute<any>, order?: number): void {
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._typeConstructor && this.name !== 'Object') {
this.enhancer!!.enhanceProperty(this._typeConstructor, attr);
}
}
/**
* Removes an attribute from this type
* @param name The Name of the attribute which will be removed
* @return
*/
removeAttribute(name: string): void {
const { length } = this.declaredAttributes;
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 name
* @return
*/
getAttribute(name: string): Attribute<any> | null {
let attr = this.getDeclaredAttribute(name);
if (!attr && this.superType) {
attr = this.superType.getAttribute(name);
}
return attr;
}
/**
* @param val Name or order of the attribute
* @return
*/
getDeclaredAttribute(val: string | number): Attribute<any> | null {
return this.declaredAttributes.filter((attr) => attr.name === val || attr.order === val)[0] || null;
}
/**
* @inheritDoc
*/
fromJsonValue(state: ManagedState, jsonObject: Json, currentObject: T | null,
options: { onlyMetadata?: boolean, persisting: boolean }) {
if (!jsonObject || !currentObject) {
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 as JsonMap)[attribute.name], options);
}
}
return currentObject;
}
/**
* @inheritDoc
*/
toJsonValue(state: ManagedState, object: T | null,
options: { excludeMetadata?: boolean; depth?: number | boolean, persisting: boolean }): Json {
if (!(object instanceof this.typeConstructor)) {
return null;
}
const value: { [attr: string]: any } = {};
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
*/
toJSON(): JsonMap {
const fields: { [attr: string]: any } = {};
this.declaredAttributes.forEach((attribute) => {
if (!attribute.isMetadata) {
fields[attribute.name] = attribute;
}
});
return {
class: this.ref,
fields,
acl: {
schemaAdd: this.schemaAddPermission.toJSON(),
schemaReplace: this.schemaReplacePermission.toJSON(),
},
...(this.superType && { superClass: this.superType.ref }),
...(this.isEmbeddable && { embedded: true }),
...(this.metadata && { metadata: this.metadata }),
};
}
/**
* Returns iterator to get all referenced entities
* @return
*/
references(): IterableIterator<{ path: string[] }> {
const attributes = this.attributes();
let attribute: Attribute<any>;
let embeddedAttributes: IterableIterator<{ path: string[] }> | null;
return {
[Symbol.iterator]() {
return this;
},
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) {
// currently TS requires a undefined value here https://github.com/microsoft/TypeScript/issues/38479
return { done: true, value: undefined };
}
attribute = item.value;
const type = attribute.isCollection
? (attribute as PluralAttribute<any, any>).elementType
: (attribute as SingularAttribute<any>).type;
if (type.isEntity) {
return { value: { path: [attribute.name] } };
} if (type.isEmbeddable) {
embeddedAttributes = (type as EmbeddableType<any>).references();
}
}
},
};
}
/**
* Retrieves whether this type has specific metadata
*
* @param key
* @return
*/
hasMetadata(key: string): boolean {
return !!this.metadata && !!this.metadata[key];
}
/**
* Gets some metadata of this type
*
* @param key
* @return
*/
getMetadata(key: string): string | null {
if (!this.hasMetadata(key)) {
return null;
}
return this.metadata!![key];
}
}