aurelia-orm
Version:
Makes working with entities and calling your Rest API simple.
1,625 lines (1,378 loc) • 37.1 kB
JavaScript
import typer from 'typer';
import {inject,transient,Container} from 'aurelia-dependency-injection';
import {Config} from 'aurelia-api';
import {metadata} from 'aurelia-metadata';
import {Validator,ValidationRules} from 'aurelia-validation';
import {getLogger} from 'aurelia-logging';
import {Config as ViewManagerConfig} from 'aurelia-view-manager';
/**
* The Repository basis class
*/
export class Repository {
transport = null;
/**
* Construct.
*
* @param {Config} clientConfig
*
* @constructor
*/
constructor(clientConfig) {
this.clientConfig = clientConfig;
}
/**
* Get the transport for the resource this repository represents.
*
* @return {Rest}
*/
getTransport() {
if (this.transport === null) {
this.transport = this.clientConfig.getEndpoint(this.getMeta().fetch('endpoint'));
if (!this.transport) {
throw new Error(`No transport found for '${this.getMeta().fetch('endpoint') || 'default'}'.`);
}
}
return this.transport;
}
/**
* Set the associated entity's meta data
*
* @param {{}} meta
*/
setMeta(meta) {
this.meta = meta;
}
/**
* Get the associated entity's meta data.
* @return {{}}
*/
getMeta() {
return this.meta;
}
/**
* Set the identifier
*
* @param {string} identifier
* @return {Repository} this
* @chainable
*/
setIdentifier(identifier) {
this.identifier = identifier;
return this;
}
/**
* Get the identifier
*
* @return {string|null}
*/
getIdentifier() {
return this.identifier;
}
/**
* Set the resource
*
* @param {string} resource
* @return {Repository} this
* @chainable
*/
setResource(resource) {
this.resource = resource;
return this;
}
/**
* Get the resource
*
* @return {string|null}
*/
getResource() {
return this.resource;
}
/**
* Perform a find query and populate entities with the retrieved data.
*
* @param {{}|number|string} criteria Criteria to add to the query. A plain string or number will be used as relative path.
* @param {boolean} [raw] Set to true to get a POJO in stead of populated entities.
*
* @return {Promise<Entity|[Entity]>}
*/
find(criteria, raw) {
return this.findPath(this.resource, criteria, raw);
}
/**
* Perform a find query and populate entities with the retrieved data, limited to one result.
*
* @param {{}|number|string} criteria Criteria to add to the query. A plain string or number will be used as relative path.
* @param {boolean} [raw] Set to true to get a POJO in stead of populated entities.
*
* @return {Promise<Entity|[Entity]>}
*/
findOne(criteria, raw) {
return this.findPath(this.resource, criteria, raw, true);
}
/**
* Perform a find query for `path` and populate entities with the retrieved data.
*
* @param {string} path
* @param {{}|number|string} criteria Criteria to add to the query. A plain string or number will be used as relative path.
* @param {boolean} [raw] Set to true to get a POJO in stead of populated entities.
* @param {boolean} [single] Whether or not this is a findOne.
*
* @return {Promise<Entity|[Entity]>}
*/
findPath(path, criteria, raw, single) {
let transport = this.getTransport();
let findQuery;
if (single) {
if (typeof criteria === 'object' && criteria !== null) {
criteria.limit = 1;
}
findQuery = transport.findOne(path, criteria);
} else {
findQuery = transport.find(path, criteria);
}
if (raw) {
return findQuery;
}
return findQuery
.then(response => {
return this.populateEntities(response);
})
.then(populated => {
if (!populated) {
return null;
}
if (!Array.isArray(populated)) {
return populated.markClean();
}
populated.forEach(entity => entity.markClean());
return populated;
});
}
/**
* Perform a count on the resource.
*
* @param {null|{}} criteria
*
* @return {Promise<number>}
*/
count(criteria) {
return this.getTransport().find(this.resource + '/count', criteria);
}
/**
* Get new populated entity or entities based on supplied data including associations
*
* @param {{}|[{}]} data|[data] The data to populate with
* @param {boolean} [clean] Mark the entities as clean or not
*
* @return {Entity|[Entity]}
*/
populateEntities(data, clean) {
if (!data) {
return null;
}
if (!Array.isArray(data)) {
return this.getPopulatedEntity(data, null, clean);
}
let collection = [];
data.forEach(source => {
collection.push(this.getPopulatedEntity(source, null, clean));
});
return collection;
}
/**
* Populate a (new) entity including associations
*
* @param {{}} data The data to populate with
* @param {Entity} [entity] optional. if not set, a new entity is returned
* @param {boolean} [clean] Mark the entities as clean or not
*
* @return {Entity}
*/
getPopulatedEntity(data, entity, clean) {
entity = entity || this.getNewEntity();
let entityMetadata = entity.getMeta();
let populatedData = {};
let key;
for (key in data) {
if (!data.hasOwnProperty(key)) {
continue;
}
let value = data[key];
if (entityMetadata.has('types', key)) {
const dataType = entityMetadata.fetch('types', key);
if ((dataType === 'date' || dataType === 'datetime') && !value) {
continue;
}
populatedData[key] = typer.cast(value, dataType);
continue;
}
if (!entityMetadata.has('associations', key) || typeof value !== 'object') {
// Not an association, or not an object. clean copy.
populatedData[key] = value;
continue;
}
let repository = this.entityManager.getRepository(entityMetadata.fetch('associations', key).entity);
populatedData[key] = repository.populateEntities(value, clean);
}
return entity.setData(populatedData, clean);
}
/**
* Get a new instance for entityReference.
*
* @return {Entity}
*/
getNewEntity() {
return this.entityManager.getEntity(this.identifier || this.resource);
}
/**
* Populate a new entity with the empty associations set.
*
* @return {Entity}
*/
getNewPopulatedEntity() {
let entity = this.getNewEntity();
let associations = entity.getMeta().fetch('associations');
for (let property in associations) {
if (associations.hasOwnProperty(property)) {
let assocMeta = associations[property];
if (assocMeta.type !== 'entity') {
continue;
}
entity[property] = this.entityManager.getRepository(assocMeta.entity).getNewEntity();
}
}
return entity;
}
}
/**
* The DefaultRepository class
* @transient
*/
export class DefaultRepository extends Repository {
}
export class OrmMetadata {
static forTarget(target) {
return metadata.getOrCreateOwn(Metadata.key, Metadata, target, target.name);
}
}
/**
* The MetaData class for Entity and Repository
*
*/
export class Metadata {
// The key used to identify this specific metadata
static key = 'spoonx:orm:metadata';
/**
* Construct metadata with sensible defaults (so we can make assumptions in the code).
*/
constructor() {
this.metadata = {
repository : DefaultRepository,
identifier : null,
resource : null,
endpoint : null,
name : null,
idProperty : 'id',
associations: {}
};
}
/**
* Add a value to an array.
*
* @param {string} key
* @param {*} value
*
* @return {Metadata} itself
* @chainable
*/
addTo(key, value) {
if (typeof this.metadata[key] === 'undefined') {
this.metadata[key] = [];
} else if (!Array.isArray(this.metadata[key])) {
this.metadata[key] = [this.metadata[key]];
}
this.metadata[key].push(value);
return this;
}
/**
* Set a value for key, or one level deeper (key.key).
*
* @param {string} key
* @param {string|*} valueOrNestedKey
* @param {null|*} [valueOrNull]
*
* @return {Metadata} itself
* @chainable
*/
put(key, valueOrNestedKey, valueOrNull) {
if (!valueOrNull) {
this.metadata[key] = valueOrNestedKey;
return this;
}
if (typeof this.metadata[key] !== 'object') {
this.metadata[key] = {};
}
this.metadata[key][valueOrNestedKey] = valueOrNull;
return this;
}
/**
* Check if key, or key.nested exists.
*
* @param {string} key
* @param {string} [nested]
*
* @return {boolean}
*/
has(key, nested) {
if (typeof nested === 'undefined') {
return typeof this.metadata[key] !== 'undefined';
}
return typeof this.metadata[key] !== 'undefined' && typeof this.metadata[key][nested] !== 'undefined';
}
/**
* Fetch key or key.nested from metadata.
*
* @param {string} key
* @param {string} [nested]
*
* @return {*}
*/
fetch(key, nested) {
if (!nested) {
return this.has(key) ? this.metadata[key] : null;
}
if (!this.has(key, nested)) {
return null;
}
return this.metadata[key][nested];
}
}
/* eslint-disable max-lines */
/**
* The Entity basis class
* @transient
*/
export class Entity {
/**
* Construct a new entity.
*
* @param {Validator} validator
*/
constructor() {
this
.define('__meta', OrmMetadata.forTarget(this.constructor))
.define('__cleanValues', {}, true);
}
/**
* Get the transport for the resource this repository represents.
*
* @return {Rest}
*/
getTransport() {
return this.getRepository().getTransport();
}
/**
* Get reference to the repository.
*
* @return {Repository}
*/
getRepository() {
return this.__repository;
}
/**
* Set reference to the repository.
*
* @param {Repository} repository
*
* @return {Entity} this
* @chainable
*/
setRepository(repository) {
return this.define('__repository', repository);
}
/**
* Define a non-enumerable property on the entity.
*
* @param {string} property
* @param {*} value
* @param {boolean} [writable]
* @chainable
*
* @return {Entity} this
* @chainable
*/
define(property, value, writable) {
Object.defineProperty(this, property, {
value : value,
writable : !!writable,
enumerable: false
});
return this;
}
/**
* Get the metadata for this entity.
*
* @return {Metadata}
*/
getMeta() {
return this.__meta;
}
/**
* Get the id property name for this entity.
*
* @return {string}
*/
getIdProperty() {
return this.getMeta().fetch('idProperty');
}
/**
* Get the id property name of the entity (static).
*
* @return {string}
*/
static getIdProperty() {
let idProperty = OrmMetadata.forTarget(this).fetch('idProperty');
return idProperty;
}
/**
* Get the Id value for this entity.
*
* @return {number|string}
*/
getId() {
return this[this.getIdProperty()];
}
/**
* Set the Id value for this entity.
*
* @param {number|string} id
*
* @return {Entity} this
* @chainable
*/
setId(id) {
this[this.getIdProperty()] = id;
return this;
}
/**
* Persist the entity's state to the server.
* Either creates a new record (POST) or updates an existing one (PUT) based on the entity's state,
*
* @return {Promise}
*/
save() {
if (!this.isNew()) {
return this.update();
}
let response;
return this.getTransport()
.create(this.getResource(), this.asObject(true))
.then(created => {
this.setId(created[this.getIdProperty()]);
response = created;
})
.then(() => this.saveCollections())
.then(() => this.markClean())
.then(() => response);
}
/**
* Persist the changes made to this entity to the server.
*
* @see .save()
*
* @return {Promise}
*
* @throws {Error}
*/
update() {
if (this.isNew()) {
throw new Error('Required value "id" missing on entity.');
}
// We're clean, no need to update.
if (this.isClean()) {
// Always save collections (might have new).
return this.saveCollections()
.then(() => this.markClean())
.then(() => null);
}
let requestBody = this.asObject(true);
let response;
return this.getTransport()
.update(this.getResource(), this.getId(), requestBody)
.then(updated => { response = updated; })
.then(() => this.saveCollections())
.then(() => this.markClean())
.then(() => response);
}
/**
* Add an entity to a collection (persist).
*
* When given entity has data, create the entity and set up the relation.
*
* @param {Entity|number} entity Entity or id
* @param {string} [property] The name of the property
*
* @return {Promise}
*/
addCollectionAssociation(entity, property) {
property = property || getPropertyForAssociation(this, entity);
let url = [this.getResource(), this.getId(), property];
if (this.isNew()) {
throw new Error('Cannot add association to entity that does not have an id.');
}
if (!(entity instanceof Entity)) {
url.push(entity);
return this.getTransport().create(url.join('/'));
}
if (entity.isNew()) {
let associationProperty = getPropertyForAssociation(entity, this);
let relation = entity.getMeta().fetch('association', associationProperty);
if (!relation || relation.type !== 'entity') {
// Many relation, create and then link.
return entity.save()
.then(() => {
if (entity.isNew()) {
throw new Error('Entity did not return return an id on saving.');
}
return this.addCollectionAssociation(entity, property);
});
}
// toOne relation, pass in ID to prevent extra request. Something something performance.
entity[associationProperty] = this.getId();
return entity.save()
.then(() => entity);
}
// Entity isn't new, just add id to url.
url.push(entity.getId());
return this.getTransport()
.create(url.join('/'))
.then(() => entity);
}
/**
* Remove an entity from a collection.
*
* @param {Entity|number} entity Entity or id
* @param {string} [property] The name of the property
*
* @return {Promise}
*/
removeCollectionAssociation(entity, property) {
property = property || getPropertyForAssociation(this, entity);
let idToRemove = entity;
if (entity instanceof Entity) {
if (!entity.getId()) {
return Promise.resolve(null);
}
idToRemove = entity.getId();
}
return this.getTransport().destroy([this.getResource(), this.getId(), property, idToRemove].join('/'));
}
/**
* Persist the collections on the entity.
*
* @return {Promise} itself
*/
saveCollections() {
let tasks = [];
let currentCollections = getCollectionsCompact(this, true);
let cleanCollections = this.__cleanValues.data ? this.__cleanValues.data.collections : null;
let addTasksForDifferences = (base, candidate, method) => {
if (base === null) {
return;
}
Object.getOwnPropertyNames(base).forEach(property => {
base[property].forEach(id => {
if (candidate === null || !Array.isArray(candidate[property]) || candidate[property].indexOf(id) === -1) {
tasks.push(method.call(this, id, property));
}
});
});
};
// Something to add?
addTasksForDifferences(currentCollections, cleanCollections, this.addCollectionAssociation);
// Something to remove?
addTasksForDifferences(cleanCollections, currentCollections, this.removeCollectionAssociation);
return Promise.all(tasks).then(results => this);
}
/**
* Mark this entity as clean, in its current state.
*
* @return {Entity} itself
* @chainable
*/
markClean() {
let cleanValues = getFlat(this);
this.__cleanValues = {
checksum: JSON.stringify(cleanValues),
data : cleanValues
};
return this;
}
/**
* Return if the entity is clean.
*
* @return {boolean}
*/
isClean() {
return getFlat(this, true) === this.__cleanValues.checksum;
}
/**
* Return if the entity is dirty.
*
* @return {boolean}
*/
isDirty() {
return !this.isClean();
}
/**
* Return if the entity is new (ergo, hasn't been persisted to the server).
*
* @return {boolean}
*/
isNew() {
return !this.getId();
}
/**
* Resets the entity to the clean state
*
* @param {boolean} [shallow]
*
* @return {Entity} itself
*/
reset(shallow) {
let pojo = {};
let metadata = this.getMeta();
Object.keys(this).forEach(propertyName => {
let value = this[propertyName];
let association = metadata.fetch('associations', propertyName);
// No meta data, no value or no association property: simple assignment.
if (!association || !value) {
pojo[propertyName] = value;
return;
}
});
if (this.isClean()) {
return this;
}
let isNew = this.isNew();
let associations = this.getMeta().fetch('associations');
Object.keys(this).forEach(propertyName => {
if (Object.getOwnPropertyNames(associations).indexOf(propertyName) === -1) {
delete this[propertyName];
}
});
if (isNew) {
return this.markClean();
}
this.setData(this.__cleanValues.data.entity);
if (shallow) {
return this.markClean();
}
let collections = this.__cleanValues.data.collections;
Object.getOwnPropertyNames(collections).forEach(index => {
this[index] = [];
collections[index].forEach(entity => {
if (typeof entity === 'number') {
this[index].push(entity);
}
});
});
return this.markClean();
}
/**
* Sets the entity's properties to their clean values
*
* @return {Entity} itself
* @chainable
*/
clear() {
if (!this.isNew()) {
return this.setData(this.__cleanValues.data.entity);
}
return this;
}
/**
* Get the identifier name of this entity's reference (static).
*
* @return {string|null}
*/
static getIdentifier() {
return OrmMetadata.forTarget(this).fetch('identifier');
}
/**
* Get the identifier name of this entity instance
*
* @return {string|null}
*/
getIdentifier() {
return this.__identifier || this.getMeta().fetch('identifier');
}
/**
* Set this instance's identifier.
*
* @param {string} identifier
*
* @return {Entity} itself
* @chainable
*/
setIdentifier(identifier) {
return this.define('__identifier', identifier);
}
/**
* Get the resource name of this entity's reference (static).
*
* @return {string|null}
*/
static getResource() {
return OrmMetadata.forTarget(this).fetch('resource');
}
/**
* Get the resource name of this entity instance
*
* @return {string|null}
*/
getResource() {
return this.__resource || this.getMeta().fetch('resource');
}
/**
* Set this instance's resource.
*
* @param {string} resource
*
* @return {Entity} itself
* @chainable
*/
setResource(resource) {
return this.define('__resource', resource);
}
/**
* Destroy this entity (DELETE request to the server).
*
* @return {Promise}
*/
destroy() {
if (!this.getId()) {
throw new Error('Required value "id" missing on entity.');
}
return this.getTransport().destroy(this.getResource(), this.getId());
}
/**
* Get the name of the entity. This is useful for labels in texts.
*
* @return {string}
*/
getName() {
let metaName = this.getMeta().fetch('name');
if (metaName) {
return metaName;
}
return this.getResource();
}
/**
* Get the name of the entity (static). This is useful for labels in texts.
*
* @return {string}
*/
static getName() {
let metaName = OrmMetadata.forTarget(this).fetch('name');
if (metaName) {
return metaName;
}
return this.getResource();
}
/**
* Set data on this entity.
*
* @param {{}} data
* @param {boolean} markClean
* @return {Entity} itself
* @chainable
*/
setData(data, markClean) {
Object.assign(this, data);
if (markClean) {
this.markClean();
}
return this;
}
/**
* Set the validator instance.
*
* @param {Validator} validator
* @return {Entity} itself
* @chainable
*/
setValidator(validator) {
this.define('__validator', validator);
return this;
}
/**
* Get the validator instance.
*
* @return {Validator}
*/
getValidator() {
if (!this.hasValidation()) {
return null;
}
return this.__validator;
}
/**
* Check if entity has validation enabled.
*
* @return {boolean}
*/
hasValidation() {
return !!this.getMeta().fetch('validation');
}
/**
* Validates the entity
*
* @param {string|null} propertyName Optional. The name of the property to validate. If unspecified,
* all properties will be validated.
* @param {Rule<*, *>[]|null} rules Optional. If unspecified, the rules will be looked up using
* the metadata for the object created by ValidationRules....on(class/object)
* @return {Promise<ValidateResult[]>}
*/
validate(propertyName, rules) {
// entities without validation are to be considered valid
if (!this.hasValidation()) {
return Promise.resolve([]);
}
return propertyName ? this.getValidator().validateProperty(this, propertyName, rules)
: this.getValidator().validateObject(this, rules);
}
/**
* Get the data in this entity as a POJO.
*
* @param {boolean} [shallow]
*
* @return {{}}
*/
asObject(shallow) {
return asObject(this, shallow);
}
/**
* Get the data in this entity as a json string.
*
* @param {boolean} [shallow]
*
* @return {string}
*/
asJson(shallow) {
return asJson(this, shallow);
}
}
/**
* Entity representation as pojo.
*
* @param {Entity} entity
* @param {boolean} [shallow]
*
* @return {{}}
*/
function asObject(entity, shallow) {
let pojo = {};
let metadata = entity.getMeta();
Object.keys(entity).forEach(propertyName => {
let value = entity[propertyName];
let association = metadata.fetch('associations', propertyName);
// No meta data, no value or no association property: simple assignment.
if (!association || !value) {
pojo[propertyName] = value;
return;
}
// When shallow, we only assign toOne associations.
if (shallow) {
if (association.type === 'collection') {
return;
}
if (value instanceof Entity && value.getId()) {
pojo[propertyName] = value.getId();
return;
}
if (value instanceof Entity) {
pojo[propertyName] = value.asObject();
return;
}
if (['string', 'number', 'boolean'].indexOf(typeof value) > -1 || value.constructor === Object) {
pojo[propertyName] = value;
return;
}
}
// Array, treat children as potential entities.
if (!Array.isArray(value)) {
// Single value not an instance of entity? Simple assignment.
pojo[propertyName] = !(value instanceof Entity) ? value : value.asObject(shallow);
return;
}
let asObjects = [];
value.forEach(childValue => {
if (typeof childValue !== 'object') {
return;
}
if (!(childValue instanceof Entity)) {
asObjects.push(childValue);
return;
}
// If shallow, we don't handle toMany.
if (!shallow || (typeof childValue === 'object' && !childValue.getId())) {
asObjects.push(childValue.asObject(shallow));
}
});
pojo[propertyName] = asObjects;
});
return pojo;
}
/**
* Entity representation as json
*
* @param {Entity} entity
* @param {boolean} [shallow]
*
* @return {string}
*/
function asJson(entity, shallow) {
let json;
try {
json = JSON.stringify(asObject(entity, shallow));
} catch (error) {
json = '';
}
return json;
}
/**
* Get a compact object of collections (arrays of ids)
*
* @param {Entity} forEntity
* @param {boolean} [includeNew]
*
* @return {{}}
*/
function getCollectionsCompact(forEntity, includeNew) {
let associations = forEntity.getMeta().fetch('associations');
let collections = {};
Object.getOwnPropertyNames(associations).forEach(index => {
let association = associations[index];
if (association.type !== 'collection') {
return;
}
collections[index] = [];
if (!Array.isArray(forEntity[index])) {
return;
}
forEntity[index].forEach(entity => {
if (typeof entity === 'number') {
collections[index].push(entity);
return;
}
if (!(entity instanceof Entity)) {
return;
}
if (entity.getId()) {
collections[index].push(entity.getId());
return;
}
if (includeNew) {
collections[index].push(entity);
return;
}
});
});
return collections;
}
/**
* Get a flat, plain representation of the entity and its associations.
*
* @param {Entity} entity
* @param {boolean} [json]
*
* @return {{}} {entity, collections}
*/
function getFlat(entity, json) {
let flat = {
entity : asObject(entity, true),
collections: getCollectionsCompact(entity)
};
if (json) {
flat = JSON.stringify(flat);
}
return flat;
}
/**
* Get the property of the association on this entity.
*
* @param {Entity} forEntity
* @param {Entity} entity
*
* @return {string}
*/
function getPropertyForAssociation(forEntity, entity) {
let associations = forEntity.getMeta().fetch('associations');
return Object.keys(associations).filter(key => {
return associations[key].entity === entity.getResource();
})[0];
}
/**
* Set the id property for en entity
*
* @export
* @param {string} propertyName
* @returns {function}
*
* @decorator
*/
export function idProperty(propertyName) {
return function(target) {
OrmMetadata.forTarget(target).put('idProperty', propertyName);
};
}
/**
* Set the 'identifierName' metadata on the entity
*
* @param {string} identifierName The name of the identifier
*
* @return {function}
*
* @decorator
*/
export function identifier(identifierName) {
return function(target) {
OrmMetadata.forTarget(target).put('identifier', identifierName || target.name.toLowerCase());
};
}
/**
* Set the 'name' metadata on the entity
*
* @param {string} entityName=target.name.toLowerCase The (custom) name to use
*
* @return {function}
*
* @decorator
*/
export function name(entityName) {
return function(target) {
OrmMetadata.forTarget(target).put('name', entityName || target.name.toLowerCase());
};
}
/**
* Set the repositoryReference metadata on the entity
*
* @param {string} repositoryReference The repository reference to use
*
* @return {function}
*
* @decorator
*/
export function repository(repositoryReference) {
return function(target) {
OrmMetadata.forTarget(target).put('repository', repositoryReference);
};
}
/**
* Set the 'resourceName' metadata on the entity
*
* @param {string} resourceName The name of the resource
*
* @return {function}
*
* @decorator
*/
export function resource(resourceName) {
return function(target) {
OrmMetadata.forTarget(target).put('resource', resourceName || target.name.toLowerCase());
};
}
/**
* Set the 'validation' metadata to 'true'
*
* @param {[function]} ValidatorClass = Validator
*
* @return {function}
*
* @decorator
*/
export function validation(ValidatorClass = Validator) {
return function(target) {
OrmMetadata.forTarget(target).put('validation', ValidatorClass);
};
}
/**
* The EntityManager class
*/
export class EntityManager {
repositories = {};
entities = {};
/**
* Construct a new EntityManager.
*
* @param {Container} container
*/
constructor(container) {
this.container = container;
}
/**
* Register an array of entity classes.
*
* @param {function[]|function} EntityClasses Array or object of Entity constructors.
*
* @return {EntityManager} itself
* @chainable
*/
registerEntities(EntityClasses) {
for (let property in EntityClasses) {
if (EntityClasses.hasOwnProperty(property)) {
this.registerEntity(EntityClasses[property]);
}
}
return this;
}
/**
* Register an Entity class.
*
* @param {function} EntityClass
*
* @return {EntityManager} itself
* @chainable
*/
registerEntity(EntityClass) {
let meta = OrmMetadata.forTarget(EntityClass);
this.entities[meta.fetch('identifier') || meta.fetch('resource')] = EntityClass;
return this;
}
/**
* Get a repository instance.
*
* @param {Entity|string} entity
*
* @return {Repository}
* @throws {Error}
*/
getRepository(entity) {
let reference = this.resolveEntityReference(entity);
let identifier = entity;
let resource = entity;
if (typeof reference.getResource === 'function') {
resource = reference.getResource() || resource;
}
if (typeof reference.getIdentifier === 'function') {
identifier = reference.getIdentifier() || resource;
}
if (typeof resource !== 'string') {
throw new Error('Unable to find resource for entity.');
}
// Cached instance available. Return.
if (this.repositories[identifier]) {
return this.repositories[identifier];
}
// Get instance of repository
let metaData = OrmMetadata.forTarget(reference);
let repository = metaData.fetch('repository');
let instance = this.container.get(repository);
// Already setup instance? Return.
if (instance.meta && instance.resource && instance.entityManager) {
return instance;
}
// Tell the repository instance what resource it should use.
instance.setMeta(metaData);
instance.resource = resource;
instance.identifier = identifier;
instance.entityManager = this;
if (instance instanceof DefaultRepository) {
// This is a default repository. We'll cache this instance.
this.repositories[identifier] = instance;
}
return instance;
}
/**
* Resolve given resource value to an entityReference
*
* @param {Entity|string} resource
*
* @return {Entity}
* @throws {Error}
*/
resolveEntityReference(resource) {
let entityReference = resource;
if (typeof resource === 'string') {
entityReference = this.entities[resource] || Entity;
}
if (typeof entityReference === 'function') {
return entityReference;
}
throw new Error('Unable to resolve to entity reference. Expected string or function.');
}
/**
* Get an instance for `entity`
*
* @param {string|Entity} entity
*
* @return {Entity}
*/
getEntity(entity) {
let reference = this.resolveEntityReference(entity);
let instance = this.container.get(reference);
let resource = reference.getResource();
let identifier = reference.getIdentifier() || resource;
if (!resource) {
if (typeof entity !== 'string') {
throw new Error('Unable to find resource for entity.');
}
resource = entity;
identifier = entity;
}
// Set the validator.
if (instance.hasValidation() && !(instance.getValidator())) {
let validator = this.container.get(OrmMetadata.forTarget(reference).fetch('validation'));
instance.setValidator(validator);
}
return instance.setResource(resource)
.setIdentifier(identifier)
.setRepository(this.getRepository(identifier));
}
}
/**
* Set the 'resource' metadata and enables validation on the entity
*
* @param {string} resourceName The name of the resource
* @param {[function]} ValidatorClass = Validator
*
* @return {function}
*
* @decorator
*/
export function validatedResource(resourceName, ValidatorClass) {
return function(target, propertyName) {
resource(resourceName)(target);
validation(ValidatorClass)(target, propertyName);
};
}
// eslint-disable-line no-unused-vars
// eslint-disable-line no-unused-vars
/**
* Plugin configure
*
* @export
* @param {*} frameworkConfig
* @param {*} configCallback
*/
export function configure(frameworkConfig, configCallback) {
// add hasAssociation custom validation rule
ValidationRules.customRule(
'hasAssociation',
value => (value instanceof Entity && typeof value.id === 'number') || typeof value === 'number',
`\${$displayName} must be an association.` // eslint-disable-line quotes
);
let entityManagerInstance = frameworkConfig.container.get(EntityManager);
configCallback(entityManagerInstance);
frameworkConfig.container.get(ViewManagerConfig).configureNamespace('spoonx/orm', {
location: './view/{{framework}}/{{view}}.html'
});
frameworkConfig.globalResources('./component/association-select');
frameworkConfig.globalResources('./component/paged');
}
export const logger = getLogger('aurelia-orm');
/**
* Set generic 'data' metadata.
*
* @param {{}} metaData The data to set
*
* @returns {function}
*
* @decorator
*/
export function data(metaData) {
/**
* @param {function} target
* @param {string} propertyName
*/
return function(target, propertyName) {
if (typeof metaData !== 'object') {
logger.error('data must be an object, ' + typeof metaData + ' given.');
}
OrmMetadata.forTarget(target.constructor).put('data', propertyName, metaData);
};
}
/**
* Set the 'endpoint' metadta of an entity. Needs a set resource
*
* @param {string} entityEndpoint The endpoint name to use
*
* @return {function}
*
* @decorator
*/
export function endpoint(entityEndpoint) {
return function(target) {
if (!OrmMetadata.forTarget(target).fetch('resource')) {
logger.warn('Need to set the resource before setting the endpoint!');
}
OrmMetadata.forTarget(target).put('endpoint', entityEndpoint);
};
}
// fix for babels property decorator
export function ensurePropertyIsConfigurable(target, propertyName, descriptor) {
if (descriptor && descriptor.configurable === false) {
descriptor.configurable = true;
if (!Reflect.defineProperty(target, propertyName, descriptor)) {
logger.warn(`Cannot make configurable property '${propertyName}' of object`, target);
}
}
}
/**
* Associate a property with an entity (toOne) or a collection (toMany)
*
* @param {undefined|string|{}} associationData undefined={entity:propertyName}, string={entity:string}, Object={entity: string, collection: string}
*
* @return {function}
*
* @decorator
*/
export function association(associationData) {
return function(target, propertyName, descriptor) {
ensurePropertyIsConfigurable(target, propertyName, descriptor);
if (!associationData) {
associationData = {entity: propertyName};
} else if (typeof associationData === 'string') {
associationData = {entity: associationData};
}
OrmMetadata.forTarget(target.constructor).put('associations', propertyName, {
type : associationData.entity ? 'entity' : 'collection',
entity: associationData.entity || associationData.collection
});
};
}
/**
* Registers the 'enumerations' for an attribute's values
*
* @param {*[]} values - a list of valid values for the entity's attribute
*
* @return {Function}
*
* @decorator
*/
export function enumeration(values) {
return function(target, propertyName, descriptor) {
ensurePropertyIsConfigurable(target, propertyName, descriptor);
OrmMetadata.forTarget(target.constructor).put('enumerations', propertyName, values);
};
}
/**
* Set the 'types' metadata on the entity
*
* @param {string} typeValue The type(text,string,date,datetime,integer,int,number,float,boolean,bool,smart,autodetect (based on value)) to use for this property using typer
*
* @return {function}
*
* @decorator
*/
export function type(typeValue) {
return function(target, propertyName, descriptor) {
ensurePropertyIsConfigurable(target, propertyName, descriptor);
OrmMetadata.forTarget(target.constructor).put('types', propertyName, typeValue);
};
}