Permalink
Browse files

feat(project): Added joins, recursive persistence, hydrations et all.

  • Loading branch information...
RWOverdijk committed Oct 3, 2016
1 parent 0ec757e commit 576a2813f580836e778265dba8801fa00030b9e2
Showing with 1,999 additions and 346 deletions.
  1. +43 −4 src/Criteria.ts
  2. +15 −1 src/EntityInterface.ts
  3. +14 −4 src/EntityManager.ts
  4. +201 −9 src/EntityProxy.ts
  5. +73 −25 src/EntityRepository.ts
  6. +40 −19 src/Hydrator.ts
  7. +5 −4 src/IdentityMap.ts
  8. +342 −42 src/Mapping.ts
  9. +28 −71 src/Query.ts
  10. +412 −41 src/QueryBuilder.ts
  11. +84 −36 src/Scope.ts
  12. +21 −5 src/Store.ts
  13. +671 −79 src/UnitOfWork.ts
  14. +46 −3 src/Wetland.ts
  15. +4 −3 src/index.ts
@@ -44,14 +44,14 @@ export class Criteria {
*
* @type {Mapping}
*/
private hostMapping: Mapping;
private hostMapping: Mapping<any>;
/**
* Mappings for entities (joins).
*
* @type {{}}
*/
private mappings: {[key: string]: Mapping};
private mappings: {[key: string]: Mapping<any>};
/**
* Statement to apply criteria to.
@@ -60,6 +60,13 @@ export class Criteria {
*/
private statement: knex.QueryBuilder;
/**
* Criteria staged to apply.
*
* @type {Array}
*/
private staged: Array<Object> = [];
/**
* Construct a new Criteria parser.
* @constructor
@@ -68,21 +75,51 @@ export class Criteria {
* @param {Mapping} hostMapping
* @param {{}} [mappings]
*/
public constructor(statement: knex.QueryBuilder, hostMapping: Mapping, mappings?: {[key: string]: Mapping}) {
public constructor(statement: knex.QueryBuilder, hostMapping: Mapping<any>, mappings?: {[key: string]: Mapping<any>}) {
this.statement = statement;
this.mappings = mappings || {};
this.hostMapping = hostMapping;
}
/**
* Stage criteria to be applied later.
*
* @param {{}} criteria
*
* @returns {Criteria}
*/
public stage(criteria: Object): Criteria {
this.staged.push(criteria);
return this;
}
/**
* Apply staged criteria.
*
* @returns {Criteria}
*/
public applyStaged(): Criteria {
this.staged.forEach(criteria => {
this.apply(criteria);
});
this.staged = [];
return this;
}
/**
* Apply provided criteria to the statement.
*
* @param {{}} criteria
* @param {knex.QueryBuilder} [statement]
* @param {string} [parentKey]
* @param {string} [parentKnexMethodName]
*
* @return Criteria
*/
public apply(criteria: Object, statement?: knex.QueryBuilder, parentKey?: string, parentKnexMethodName?: string) {
public apply(criteria: Object, statement?: knex.QueryBuilder, parentKey?: string, parentKnexMethodName?: string): Criteria {
statement = statement || this.statement;
Object.keys(criteria).forEach(key => {
@@ -111,6 +148,8 @@ export class Criteria {
return statement[parentKnexMethodName || 'where'](key, operator, value);
});
return this;
}
/**
@@ -6,5 +6,19 @@ export interface EntityInterface {
*
* @param mapping
*/
setMapping?(mapping: Mapping): void;
setMapping?(mapping: Mapping<any>): void;
}
export interface ProxyInterface extends EntityInterface {
isEntityProxy?: boolean;
activateProxying?(): ProxyInterface;
deactivateProxying?(): ProxyInterface;
getTarget?(): EntityInterface;
isProxyingActive?(): boolean;
}
export type EntityCtor<T> = new (...args: any[]) => T;
@@ -1,7 +1,8 @@
import {Mapping} from './Mapping';
import {Wetland} from './Wetland';
import {Scope} from './Scope';
import {Scope, Entity} from './Scope';
import {EntityInterface} from './EntityInterface';
import {Homefront} from 'homefront';
/**
* The main entity manager for wetland.
@@ -33,6 +34,15 @@ export class EntityManager {
this.wetland = wetland;
}
/**
* Get the wetland config.
*
* @returns {Homefront}
*/
public getConfig(): Homefront {
return this.wetland.getConfig();
}
/**
* Create a new entity manager scope.
*
@@ -70,7 +80,7 @@ export class EntityManager {
this.entities[this.getMapping(entity).getEntityName()] = entity;
if (typeof entity.setMapping === 'function') {
entity.setMapping(Mapping.forEntity(entity));
entity.setMapping(Mapping.forEntity(this.resolveEntityReference(entity)));
}
return this;
@@ -83,7 +93,7 @@ export class EntityManager {
*
* @returns {Mapping}
*/
public getMapping(entity: EntityInterface | string | Object): Mapping {
public getMapping<T>(entity: T): Mapping<T> {
return Mapping.forEntity(this.resolveEntityReference(entity));
}
@@ -109,7 +119,7 @@ export class EntityManager {
*
* @returns {EntityInterface|null}
*/
public resolveEntityReference(hint: EntityInterface | string | {}): {new ()} {
public resolveEntityReference(hint: Entity): {new ()} {
if (typeof hint === 'string') {
return this.getEntity(hint);
}
@@ -1,28 +1,220 @@
import {UnitOfWork} from './UnitOfWork';
import {EntityInterface} from './EntityInterface';
import {EntityInterface, ProxyInterface} from './EntityInterface';
import {ArrayCollection as Collection} from './ArrayCollection';
import {Mapping} from './Mapping';
import {Scope} from './Scope';
import {MetaData} from './MetaData';
export class EntityProxy {
/**
* Patch provided entity with a proxy to track changes.
*
* @param {EntityInterface} entity
* @param {UnitOfWork} unitOfWork
* @param {Scope} entityManager
*
* @returns {Object}
*/
public static patch(entity: EntityInterface, unitOfWork: UnitOfWork): Object {
return new Proxy<Object>(entity, <Object> {
public static patchEntity<T>(entity: T, entityManager: Scope): T & ProxyInterface {
// Don't re-patch an entity.
if (entity['isEntityProxy']) {
return entity;
}
let proxyActive = false;
let metaData = MetaData.forInstance(entity);
let mapping = Mapping.forEntity(entity);
let unitOfWork = entityManager.getUnitOfWork();
let relations = mapping.getRelations();
let expected = {};
let entityProxy;
// Create collection observers
if (relations) {
Object.getOwnPropertyNames(relations).forEach(property => {
let type = relations[property].type;
if (type === Mapping.RELATION_ONE_TO_MANY || type === Mapping.RELATION_MANY_TO_MANY) {
proxyCollection(property);
}
});
}
/**
* @returns {boolean}
*/
function isProxyActive() {
return proxyActive && metaData.fetch('entityState.state') !== UnitOfWork.STATE_UNKNOWN;
}
/**
* Allow lazy loading and caching expected relations.
*
* @param {string} property
*
* @returns {any}
*/
function getExpected(property: string): {new ()} {
if (!expected[property]) {
expected[property] = entityManager.resolveEntityReference(relations[property].targetEntity);
}
return expected[property];
}
/**
* Proxy a collection.
*
* @param {string} property Where does this collection live?
* @param {boolean} [forceNew] Replace whatever is set with new collection. defaults to false.
*
* @returns {void}
*/
function proxyCollection(property: string, forceNew: boolean = false): void {
// Define what this collection consists out of.
let ExpectedEntity = getExpected(property);
let collection = (forceNew || !entity[property]) ? new Collection : entity[property];
// Create a new proxy, and ensure there's an existing collection.
entity[property] = new Proxy<Object>(collection, <Object> {
set: (collection: Collection<Object>, key: string, relationEntity: any) => {
collection[key] = relationEntity;
// If it's not a number, or we're not observing, just return.
if (isNaN(parseInt(key, 10)) || !isProxyActive()) {
return true;
}
if (typeof relationEntity !== 'object' || !(relationEntity instanceof ExpectedEntity)) {
throw new TypeError(
`Can't add to '${entity.constructor.name}.${property}'. Expected instance of '${ExpectedEntity.name}'.`
);
}
// Alright, let's stage this as a new relationship change.
unitOfWork.registerCollectionChange(UnitOfWork.RELATIONSHIP_ADDED, entityProxy, property, relationEntity);
return true;
},
get: (target, property) => {
if (property === 'isCollectionProxy') {
return true;
}
return target[property];
},
deleteProperty: (collection: Collection<Object>, key: string) => {
if (isProxyActive()) {
console.log(key, collection[key]);
unitOfWork.registerCollectionChange(UnitOfWork.RELATIONSHIP_REMOVED, entityProxy, property, collection[key]);
}
collection.splice(parseInt(key, 10), 1);
return true;
}
});
}
// Return the actual proxy for the entity.
entityProxy = new Proxy<T & ProxyInterface>(entity, <Object> {
set: (target: Object, property: string, value: any) => {
// Allow all dirty checks to be skipped.
if (typeof value === 'object' && value !== null && '_skipDirty' in value) {
target[property] = value._skipDirty;
// Allow for skipping dirty check.
if (typeof value === 'object' && '_skipDirty' in value) {
return target[property] = value._skipDirty;
return true;
}
unitOfWork.registerDirty(target, property);
// If there's no relation, or the proxy isn't active, just set value.
if (!relations || !relations[property] || !isProxyActive()) {
unitOfWork.registerDirty(target, property);
target[property] = value;
return true;
}
return target[property] = value;
// To many? Only allowed if collection is empty. Also, ensure this new collection is proxied.
if (entity[property] instanceof Collection) {
if (entity[property].length > 0) {
throw new Error(
`Can't assign to '${target.constructor.name}.${property}'. Collection is not empty.`
);
}
proxyCollection(property, true);
return true;
}
// Ensure provided value is of the type we expect for the relationship.
let ExpectedEntity = getExpected(property);
if (!(value instanceof ExpectedEntity)) {
throw new TypeError(
`Can't assign to '${target.constructor.name}.${property}'. Expected instance of '${ExpectedEntity.name}'.`
);
}
// If we already hold a value, stage its removal it.
if (target[property]) {
unitOfWork.registerRelationChange(UnitOfWork.RELATIONSHIP_REMOVED, entityProxy, property, target[property]);
}
// Now set provided entity as new.
unitOfWork.registerRelationChange(UnitOfWork.RELATIONSHIP_ADDED, entityProxy, property, value);
// Ensure that new relation is also being watched for changes, and set.
target[property] = EntityProxy.patchEntity(value, entityManager);
return true;
},
get: (target, property) => {
let methods = {
isEntityProxy : true,
activateProxying : () => {
proxyActive = true;
return this;
},
deactivateProxying: () => {
proxyActive = false;
return this;
},
getTarget : () => {
return entity;
},
isProxyingActive : () => {
return isProxyActive()
}
};
if (methods[property]) {
return methods[property];
}
return target[property];
},
deleteProperty: (target: Object, property: string) => {
if (target[property] instanceof Collection) {
throw new Error(
`It is not allowed to delete a collection. Trying to delete '${target.constructor.name}.${property}'.`
);
}
unitOfWork.registerRelationChange(UnitOfWork.RELATIONSHIP_REMOVED, entityProxy, property, target[property]);
delete target[property];
return true;
}
});
return entityProxy;
}
}
Oops, something went wrong.

0 comments on commit 576a281

Please sign in to comment.