phibernate
Version:
Client side ORM and Delta Tracker for Relational data
613 lines (549 loc) • 21.7 kB
text/typescript
import {PH} from "../config/PH";
import {
EntityMetadata, RelationType, CascadeType, IEntity, PHQuery, QEntity, QDateField,
SQLStringQuery, PHSQLQuery, SQLDialect, SQLStringDelete, PHSQLDelete, PHSQLUpdate, SQLStringUpdate
} from "querydsl-typescript";
import {PHMetadataUtils, NameMetadataUtils} from "../core/metadata/PHMetadataUtils";
import {Observable, Subject} from "rxjs";
import {IdGenerator, IdGeneration, getIdGenerator} from "./IdGenerator";
import {UpdateCache} from "../core/repository/UpdateCache";
import {ChangeGroupApi} from "../changeList/model/ChangeGroup";
import {EntityChangeApi} from "../changeList/model/EntityChange";
import {IEntityManager} from "../core/repository/EntityManager";
import {LocalStoreType, LocalStoreSetupInfo} from "./LocalStoreApi";
import {ILocalStoreAdaptor} from "./LocalStoreAdaptor";
import {PHDelete} from "querydsl-typescript/lib/query/PHQuery";
import {AbstractEntityChangeApi} from "../changeList/model/AbstractEntityChange";
import {EntityWhereChangeApi} from "../changeList/model/EntityWhereChange";
/**
* Created by Papa on 9/9/2016.
*/
export interface CascadeRecord {
entityName: string;
mappedBy: string;
manyEntity: any;
cascadeType: 'create' | 'remove' | 'update';
}
export interface RemovalRecord {
array: any[];
index: number;
}
export abstract class SqlAdaptor implements ILocalStoreAdaptor {
public type: LocalStoreType;
protected idGenerator: IdGenerator;
protected currentChangeGroup: ChangeGroupApi;
constructor(
protected entityManager: IEntityManager,
idGeneration: IdGeneration
) {
this.idGenerator = getIdGenerator(idGeneration);
}
abstract initialize(
setupInfo: LocalStoreSetupInfo
): Promise<any>;
abstract wrapInTransaction(callback: ()=> Promise<any>): Promise<any>;
private verifyChangeGroup() {
if (this.currentChangeGroup == null) {
throw `Current change group is not defined`;
}
}
get activeChangeGroup(): ChangeGroupApi {
return this.currentChangeGroup;
}
async create<E>(
entityName: string,
entity: E,
changeGroup: ChangeGroupApi
): Promise<EntityChangeApi> {
let qEntity = PH.qEntityMap[entityName];
let entityMetadata: EntityMetadata = <EntityMetadata><any>qEntity.__entityConstructor__;
let entityRelationMap = PH.entitiesRelationPropertyMap[entityName];
if (!entityMetadata.idProperty) {
throw `@Id is not defined for entity: ${entityName}`;
}
if (entity[entityMetadata.idProperty]) {
throw `Cannot create entity: ${entityName}, id is already defined to be: ${entityMetadata.idProperty}`;
}
let entityChange = changeGroup.addNewCreateEntityChange(entityName, entity, entityMetadata.idProperty, this.idGenerator);
let columnNames: string[] = [];
let values: any[] = [];
let cascadeRecords: CascadeRecord[] = [];
for (let propertyName in entity) {
let columnName = PHMetadataUtils.getPropertyColumnName(propertyName, qEntity);
let field = qEntity.__entityFieldMap__[propertyName];
if (columnName) {
columnNames.push(columnName);
let newValue = entity[propertyName];
values.push(entity[propertyName]);
entityChange.addNewFieldChange(propertyName, null, null, newValue, field);
continue;
}
let nonPropertyValue = entity[propertyName];
// If there is no value then it doesn't have to be created
if (!nonPropertyValue && nonPropertyValue !== '' && nonPropertyValue !== 0) {
continue;
}
columnName = PHMetadataUtils.getJoinColumnName(propertyName, qEntity);
// if there is no entity data on in, don't process it (transient field)
if (!columnName) {
return;
}
// If it's not an object/array
if (typeof nonPropertyValue != 'object' || nonPropertyValue instanceof Date) {
throw `Unexpected value in relation property: ${propertyName}, of entity ${entityName}`;
}
let entityRelation = entityRelationMap[propertyName];
switch (entityRelation.relationType) {
case RelationType.MANY_TO_ONE:
if (nonPropertyValue instanceof Array) {
throw `@ManyToOne relation cannot be an array`;
}
// get the parent object's id
let parentObjectIdValue = NameMetadataUtils.getIdValue(entityRelation.entityName, nonPropertyValue);
if (!parentObjectIdValue) {
throw `Parent object's (${entityRelation.entityName}) @Id value is missing `;
}
columnNames.push(columnName);
values.push(parentObjectIdValue);
let relatingEntityIdField = NameMetadataUtils.getIdFieldName(entityRelation.entityName);
entityChange.addNewFieldChange(relatingEntityIdField, propertyName, null, parentObjectIdValue, field);
// Cascading on manyToOne is not currently implemented, nothing else needs to be done
continue;
case RelationType.ONE_TO_MANY:
if (!(nonPropertyValue instanceof Array)) {
throw `@OneToMany relation must be an array`;
}
let oneToManyConfig = PHMetadataUtils.getOneToManyConfig(propertyName, qEntity);
let cascadeType = oneToManyConfig.cascade;
switch (cascadeType) {
case CascadeType.ALL:
case CascadeType.PERSIST:
// Save for cascade operation
for (let manyEntity in nonPropertyValue) {
cascadeRecords.push({
entityName: entityRelation.entityName,
mappedBy: oneToManyConfig.mappedBy,
manyEntity: manyEntity,
cascadeType: 'create'
});
}
}
break;
}
}
await this.createNative(qEntity, columnNames, values, cascadeRecords, changeGroup);
return entityChange;
}
protected abstract async createNative(
qEntity: QEntity<any>,
columnNames: string[],
values: any[],
cascadeRecords: CascadeRecord[],
changeGroup: ChangeGroupApi
);
async insert<E>(
entityName: string,
entity: E,
changeGroup: ChangeGroupApi
): Promise<EntityChangeApi> {
let qEntity = PH.qEntityMap[entityName];
let entityMetadata: EntityMetadata = <EntityMetadata><any>qEntity.__entityConstructor__;
let entityRelationMap = PH.entitiesRelationPropertyMap[entityName];
if (!entityMetadata.idProperty) {
throw `@Id is not defined for entity: ${entityName}`;
}
if (!entity[entityMetadata.idProperty]) {
throw `Cannot insert entity: ${entityName}, id is not defined.`;
}
let entityChange = changeGroup.addNewCreateEntityChange(entityName, entity, entityMetadata.idProperty, this.idGenerator);
let columnNames: string[] = [];
let values: any[] = [];
let cascadeRecords: CascadeRecord[] = [];
for (let propertyName in entity) {
let columnName = PHMetadataUtils.getPropertyColumnName(propertyName, qEntity);
let field = qEntity.__entityFieldMap__[propertyName];
if (columnName) {
columnNames.push(columnName);
let newValue = entity[propertyName];
values.push(entity[propertyName]);
entityChange.addNewFieldChange(propertyName, null, null, newValue, field);
continue;
}
let nonPropertyValue = entity[propertyName];
// If there is no value then it doesn't have to be created
if (!nonPropertyValue && nonPropertyValue !== '' && nonPropertyValue !== 0) {
continue;
}
columnName = PHMetadataUtils.getJoinColumnName(propertyName, qEntity);
// if there is no entity data on in, don't process it (transient field)
if (!columnName) {
return;
}
// If it's not an object/array
if (typeof nonPropertyValue != 'object' || nonPropertyValue instanceof Date) {
throw `Unexpected value in relation property: ${propertyName}, of entity ${entityName}`;
}
let entityRelation = entityRelationMap[propertyName];
switch (entityRelation.relationType) {
case RelationType.MANY_TO_ONE:
if (nonPropertyValue instanceof Array) {
throw `@ManyToOne relation cannot be an array`;
}
// get the parent object's id
let parentObjectIdValue = NameMetadataUtils.getIdValue(entityRelation.entityName, nonPropertyValue);
if (!parentObjectIdValue) {
throw `Parent object's (${entityRelation.entityName}) @Id value is missing `;
}
columnNames.push(columnName);
values.push(parentObjectIdValue);
let relatingEntityIdField = NameMetadataUtils.getIdFieldName(entityRelation.entityName);
entityChange.addNewFieldChange(relatingEntityIdField, propertyName, null, parentObjectIdValue, field);
// Cascading on manyToOne is not currently implemented, nothing else needs to be done
continue;
case RelationType.ONE_TO_MANY:
throw `Cannot insert entities with @OneToMay reference ${propertyName}.`;
}
}
await this.insertNative(qEntity, columnNames, values);
return entityChange;
}
protected abstract async insertNative(
qEntity: QEntity<any>,
columnNames: string[],
values: any[]
);
async delete<E>(
entityName: string,
entity: E,
changeGroup: ChangeGroupApi
): Promise<EntityChangeApi> {
let qEntity = PH.qEntityMap[entityName];
let entityMetadata: EntityMetadata = <EntityMetadata><any>qEntity.__entityConstructor__;
let entityRelationMap = PH.entitiesRelationPropertyMap[entityName];
if (!entityMetadata.idProperty) {
throw `@Id is not defined for entity: ${entityName}`;
}
let idValue = entity[entityMetadata.idProperty];
if (!idValue) {
throw `Cannot delete entity: ${entityName}, id is not set.`;
}
let cascadeRecords: CascadeRecord[] = [];
let removalRecords: RemovalRecord[] = [];
for (let propertyName in entity) {
let entityRelation = entityRelationMap[propertyName];
// Only check relationships
if (!entityRelation) {
continue;
}
let nonPropertyValue = entity[propertyName];
// skip blank relations
if (!nonPropertyValue) {
continue;
}
// If it's not an object/array it's invalid
if (typeof nonPropertyValue != 'object' || nonPropertyValue instanceof Date) {
throw `Entity relation ${entityName}.${propertyName} is not an object or an array`;
}
switch (entityRelation.relationType) {
case RelationType.MANY_TO_ONE:
if (nonPropertyValue instanceof Array) {
throw `@ManyToOne relation cannot be an array`;
}
// get the parent object's related OneToMany
let parentObjectIdValue = NameMetadataUtils.getIdValue(entityRelation.entityName, nonPropertyValue);
let relatedOneToMany = NameMetadataUtils.getRelatedOneToManyConfig(propertyName, entityRelation.entityName);
if (!relatedOneToMany || !relatedOneToMany.config.orphanRemoval) {
continue;
}
let relatedObject = entity[propertyName];
let relatedObjectManyReference = relatedObject[relatedOneToMany.propertyName];
for (let i = 0; i < relatedObjectManyReference.length; i++) {
if (relatedObjectManyReference[i] === entity) {
removalRecords.push({
array: relatedObjectManyReference,
index: i
});
break;
}
}
// Cascading on manyToOne is not currently implemented, nothing else needs to be done
continue;
case RelationType.ONE_TO_MANY:
if (!(nonPropertyValue instanceof Array)) {
throw `@OneToMany relation must be an array`;
}
let oneToManyConfig = PHMetadataUtils.getOneToManyConfig(propertyName, qEntity);
let cascadeType = oneToManyConfig.cascade;
switch (cascadeType) {
case CascadeType.ALL:
case CascadeType.REMOVE:
// Save for cascade operation
for (let manyEntity in nonPropertyValue) {
cascadeRecords.push({
entityName: entityRelation.entityName,
mappedBy: oneToManyConfig.mappedBy,
manyEntity: manyEntity,
cascadeType: 'remove'
});
}
}
break;
}
}
let entityChange = await this.deleteNative(qEntity, entity, idValue, cascadeRecords, changeGroup);
removalRecords.forEach((removalRecord) => {
// Remove the deleted object from the related @ManyToOne objects array reference
removalRecord.array.splice(removalRecord.index, 1);
});
return entityChange;
}
protected abstract async deleteNative(
qEntity: QEntity<any>,
entity: any,
idValue: number | string,
cascadeRecords: CascadeRecord[],
changeGroup: ChangeGroupApi
);
async deleteWhere<IE extends IEntity>(
entityName: string,
phSqlDelete: PHSQLDelete<IE>,
changeGroup: ChangeGroupApi
): Promise<EntityWhereChangeApi> {
let sqlStringDelete: SQLStringDelete<IE> = new SQLStringDelete<IE>(phSqlDelete.toSQL(), phSqlDelete.qEntity, phSqlDelete.qEntityMap, phSqlDelete.entitiesRelationPropertyMap, phSqlDelete.entitiesPropertyTypeMap, this.getDialect());
return await this.deleteWhereNative(sqlStringDelete, changeGroup);
}
protected abstract async deleteWhereNative<IE extends IEntity>(
sqlStringDelete: SQLStringDelete<IE>,
changeGroup: ChangeGroupApi
): Promise<EntityWhereChangeApi>;
async update<E>(
entityName: string,
entity: E,
changeGroup: ChangeGroupApi
): Promise<EntityChangeApi> {
/**
* On an update operation, can a nested create contain an update?
* Via:
* OneToMany:
* Yes, if the child entity is itself in the update cache
* ManyToOne:
* Cascades do not travel across ManyToOne
*/
let qEntity = PH.qEntityMap[entityName];
let entityMetadata: EntityMetadata = <EntityMetadata><any>qEntity.__entityConstructor__;
let entityRelationMap = PH.entitiesRelationPropertyMap[entityName];
if (!entityMetadata.idProperty) {
throw `@Id is not defined for entity: ${entityName}`;
}
let idValue = entity[entityMetadata.idProperty];
if (!idValue) {
throw `Cannot update entity: ${entityName}, id is not set.`;
}
let entityChange = changeGroup.addNewUpdateEntityChange(entityName, entity, entityMetadata.idProperty);
let updateCache = UpdateCache.getEntityUpdateCache(entityName, entity);
let columnNames: string[] = [];
let values: any[] = [];
let cascadeRecords: CascadeRecord[] = [];
for (let propertyName in entity) {
let field = qEntity.__entityFieldMap__[propertyName];
let columnName = PHMetadataUtils.getPropertyColumnName(propertyName, qEntity);
// If the property is not a transient field and not a relation
if (columnName) {
let updatedValue = entity[propertyName];
if (typeof updatedValue === 'object' && updatedValue) {
if (!(qEntity[propertyName] instanceof QDateField)) {
throw `Unexpected object type in property: ${propertyName}, of entity: ${entityName}`;
}
if (!(updatedValue instanceof Date)) {
throw `Unexpected object in property: ${propertyName}, of entity: ${entityName}`;
}
}
if (updateCache) {
let originalValue = updateCache[propertyName];
if (!UpdateCache.valuesEqualIgnoreObjects(originalValue, updatedValue)) {
columnNames.push(columnName);
values.push(updatedValue);
entityChange.addNewFieldChange(propertyName, null, originalValue, updatedValue, field);
}
} else {
columnNames.push(columnName);
values.push(updatedValue);
entityChange.addNewFieldChange(propertyName, null, null, updatedValue, field);
}
continue;
}
let nonPropertyValue = entity[propertyName];
columnName = PHMetadataUtils.getJoinColumnName(propertyName, qEntity);
// if there is no entity data on in, don't process it (transient field)
if (!columnName) {
return;
}
// If it's not an object/array
if (typeof nonPropertyValue != 'object' || nonPropertyValue instanceof Date) {
throw `Unexpected value in relation property: ${propertyName}, of entity ${entityName}`;
}
let entityRelation = entityRelationMap[propertyName];
switch (entityRelation.relationType) {
case RelationType.MANY_TO_ONE:
if (nonPropertyValue instanceof Array) {
throw `@ManyToOne relation cannot be an array`;
}
// get the parent object's id
let parentObjectIdValue = NameMetadataUtils.getIdValue(entityRelation.entityName, nonPropertyValue);
if (!parentObjectIdValue) {
throw `Parent object's (${entityRelation.entityName}) @Id value is missing `;
}
let relatingEntityIdField = NameMetadataUtils.getIdFieldName(entityRelation.entityName);
if (updateCache) {
let originalValue = updateCache[propertyName];
if (!UpdateCache.valuesEqualIgnoreObjects(originalValue, parentObjectIdValue)) {
columnNames.push(columnName);
values.push(parentObjectIdValue);
entityChange.addNewFieldChange(relatingEntityIdField, propertyName, originalValue, parentObjectIdValue, field);
}
} else {
columnNames.push(columnName);
values.push(parentObjectIdValue);
entityChange.addNewFieldChange(relatingEntityIdField, propertyName, null, parentObjectIdValue, field);
}
// Cascading on manyToOne is not currently implemented, nothing else needs to be done
continue;
case RelationType.ONE_TO_MANY:
if (!(nonPropertyValue instanceof Array)) {
throw `@OneToMany relation must be an array`;
}
let oneToManyConfig = PHMetadataUtils.getOneToManyConfig(propertyName, qEntity);
let cascadeType = oneToManyConfig.cascade;
switch (cascadeType) {
case CascadeType.ALL:
case CascadeType.MERGE:
// Save for cascade operation
for (let manyEntity in nonPropertyValue) {
let parentObjectIdValue = NameMetadataUtils.getIdValue(entityRelation.entityName, manyEntity);
// If the child record does not exist, cascade a create operation
if (!parentObjectIdValue) {
cascadeRecords.push({
entityName: entityRelation.entityName,
mappedBy: oneToManyConfig.mappedBy,
manyEntity: manyEntity,
cascadeType: 'create'
});
} else {
let updateCache = UpdateCache.getEntityUpdateCache(entityName, entity);
// Cannot cascade update operations without an update cache
if (!updateCache) {
continue;
}
cascadeRecords.push({
entityName: entityRelation.entityName,
mappedBy: oneToManyConfig.mappedBy,
manyEntity: manyEntity,
cascadeType: 'update'
});
}
}
}
break;
}
}
await this.updateNative(
qEntity,
columnNames,
values,
entityMetadata.idProperty,
idValue,
cascadeRecords,
changeGroup);
return entityChange;
}
protected abstract async updateNative(
qEntity: QEntity<any>,
columnNames: string[],
values: any[],
idProperty: string,
idValue: number | string,
cascadeRecords: CascadeRecord[],
changeGroup: ChangeGroupApi
);
async updateWhere<IE extends IEntity>(
entityName: string,
phSqlUpdate: PHSQLUpdate<IE>,
changeGroup: ChangeGroupApi
): Promise<EntityWhereChangeApi> {
let sqlStringUpdate: SQLStringUpdate<IE> = new SQLStringUpdate<IE>(phSqlUpdate.toSQL(), phSqlUpdate.qEntity, phSqlUpdate.qEntityMap, phSqlUpdate.entitiesRelationPropertyMap, phSqlUpdate.entitiesPropertyTypeMap, this.getDialect());
return await this.updateWhereNative(sqlStringUpdate, changeGroup);
}
protected abstract async updateWhereNative<IE extends IEntity>(
sqlStringUpdate: SQLStringUpdate<IE>,
changeGroup: ChangeGroupApi
): Promise<EntityWhereChangeApi>;
async find < E, IE extends IEntity >(
entityName: string,
phSqlQuery: PHSQLQuery < IE >
): Promise < E[] > {
let query: SQLStringQuery<IE> = new SQLStringQuery<IE>(phSqlQuery.toSQL(), phSqlQuery.qEntity, phSqlQuery.qEntityMap, phSqlQuery.entitiesRelationPropertyMap, phSqlQuery.entitiesPropertyTypeMap, this.getDialect());
let parameters = [];
let sql = query.toSQL(true, parameters);
let rawResults = await this.findNative(sql, parameters);
return query.parseQueryResults(rawResults);
}
protected abstract getDialect(): SQLDialect;
protected abstract async findNative(
sqlQuery: string,
parameters: any[]
): Promise<any[]>;
async findOne < E, IE extends IEntity >(
entityName: string,
phSqlQuery: PHSQLQuery < IE >
): Promise < E > {
let results = await this.find(entityName, phSqlQuery);
if (results.length > 0) {
throw `Expecting a single result, got ${results.length}`;
}
if (results.length == 1) {
return <E>results[0];
}
return null;
}
async save<E>(
entityName: string,
entity: E,
changeGroup: ChangeGroupApi
): Promise < EntityChangeApi > {
let qEntity = PH.qEntityMap[entityName];
let entityMetadata: EntityMetadata = <EntityMetadata><any>qEntity.__entityConstructor__;
if (!entityMetadata.idProperty) {
throw `@Id is not defined for entity: ${entityName}`;
}
if (entity[entityMetadata.idProperty]) {
return await this.update(entityName, entity, changeGroup);
} else {
return await this.create(entityName, entity, changeGroup);
}
}
search < E, IE extends IEntity >(
entityName: string,
phSqlQuery: PHSQLQuery < IE >,
subject ?: Subject < E[] >
): Observable < E[] > {
if (!subject) {
subject = new Subject<E[]>();
}
this.find(entityName, phSqlQuery).then((results: E[]) => {
subject.next(results);
});
return subject;
}
searchOne < E, IE extends IEntity >(
entityName: string,
phQuery: PHQuery < IE >,
subject ?: Subject < E >
): Observable < E > {
return null;
}
warn(
message: string
) {
console.log(message);
}
}