added embedded decorator and metadata

This commit is contained in:
Umed Khudoiberdiev 2016-05-27 10:12:18 +05:00
parent 0264d1fc10
commit 249b30bc91
15 changed files with 316 additions and 23 deletions

View File

@ -0,0 +1,63 @@
import "reflect-metadata";
import {createConnection, CreateConnectionOptions} from "../../src/index";
import {Post} from "./entity/Post";
import {Question} from "./entity/Question";
import {Counters} from "./entity/Counters";
const options: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
logging: {
logOnlyFailedQueries: true,
logFailedQueryError: true
}
},
entities: [Post, Question, Counters]
};
createConnection(options).then(connection => {
let postRepository = connection.getRepository(Post);
let questionRepository = connection.getRepository(Question);
const questionPromise = questionRepository.findOneById(1).then(question => {
if (!question) {
question = new Question();
question.title = "Umed";
return questionRepository.persist(question).then(savedAuthor => {
return questionRepository.findOneById(1);
});
}
return question;
});
const postPromise = postRepository.findOneById(1).then(post => {
if (!post) {
post = new Post();
post.title = "Hello post";
post.text = "This is post contents";
return postRepository.persist(post).then(savedPost => {
return postRepository.findOneById(1);
});
}
return post;
});
return Promise.all<any>([questionPromise, postPromise])
.then(results => {
const [question, post] = results;
question.posts = [post];
return questionRepository.persist(question);
})
.then(savedAuthor => {
console.log("Question has been saved: ", savedAuthor);
})
.catch(error => console.log(error.stack));
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,19 @@
import {Column} from "../../../src/columns";
import {EmbeddableTable} from "../../../src/decorator/tables/EmbeddableTable";
@EmbeddableTable()
export class Counters {
@Column()
raiting: number;
@Column()
stars: number;
@Column()
commentCount: number;
@Column()
metadata: string;
}

View File

@ -0,0 +1,21 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {Counters} from "./Counters";
import {Embedded} from "../../../src/decorator/Embedded";
@Table("sample26_post")
export class Post {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
title: string;
@Column()
text: string;
@Embedded(type => Counters)
counters: Counters;
}

View File

@ -0,0 +1,18 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {Counters} from "./Counters";
import {Embedded} from "../../../src/decorator/Embedded";
@Table("sample26_question")
export class Question {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
title: string;
@Embedded(type => Counters)
counters: Counters;
}

22
src/decorator/Embedded.ts Normal file
View File

@ -0,0 +1,22 @@
import {ConstructorFunction} from "../common/ConstructorFunction";
import {getMetadataArgsStorage} from "../index";
import {EmbeddedMetadataArgs} from "../metadata-args/EmbeddedMetadataArgs";
/**
* Property in entity can be marked as Embedded, and on persist all columns from the embedded are mapped to the
* single table of the entity where Embedded is used. And on hydration all columns which supposed to be in the
* embedded will be mapped to it from the single table.
*/
export function Embedded<T>(typeFunction: (type?: any) => ConstructorFunction<T>) {
return function (object: Object, propertyName: string) {
// const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
const args: EmbeddedMetadataArgs = {
target: object.constructor,
propertyName: propertyName,
// propertyType: reflectedType,
type: typeFunction
};
getMetadataArgsStorage().embeddeds.add(args);
};
}

View File

@ -0,0 +1,16 @@
import {getMetadataArgsStorage} from "../../index";
import {TableMetadataArgs} from "../../metadata-args/TableMetadataArgs";
/**
* This decorators is used on the entities that must be embedded into the tables.
*/
export function EmbeddableTable(): Function {
return function (target: Function) {
const args: TableMetadataArgs = {
target: target,
name: name,
type: "embeddable"
};
getMetadataArgsStorage().tables.add(args);
};
}

View File

@ -0,0 +1,21 @@
/**
* Arguments for EmbeddedMetadata class.
*/
export interface EmbeddedMetadataArgs {
/**
* Class to which this column is applied.
*/
readonly target: Function;
/**
* Class's property name to which this column is applied.
*/
readonly propertyName: string;
/**
* Type of the class to be embedded.
*/
readonly type: ((type?: any) => Function);
}

View File

@ -4,6 +4,7 @@ import {TableMetadata} from "../metadata/TableMetadata";
import {RelationMetadata} from "../metadata/RelationMetadata";
import {IndexMetadata} from "../metadata/IndexMetadata";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
/**
* Arguments for EntityMetadata class.
@ -16,5 +17,6 @@ export interface EntityMetadataArgs {
readonly relationMetadatas?: RelationMetadata[];
readonly indexMetadatas?: IndexMetadata[];
readonly foreignKeyMetadatas?: ForeignKeyMetadata[];
readonly embeddedMetadatas?: EmbeddedMetadata[];
}

View File

@ -10,6 +10,7 @@ import {NamingStrategyMetadataArgs} from "./NamingStrategyMetadataArgs";
import {EventSubscriberMetadataArgs} from "./EventSubscriberMetadataArgs";
import {JoinTableMetadataArgs} from "./JoinTableMetadataArgs";
import {JoinColumnMetadataArgs} from "./JoinColumnMetadataArgs";
import {EmbeddedMetadataArgs} from "./EmbeddedMetadataArgs";
/**
* Storage all metadatas of all available types: tables, fields, subscribers, relations, etc.
@ -36,6 +37,7 @@ export class MetadataArgsStorage {
readonly joinTables = new PropertyMetadataArgsCollection<JoinTableMetadataArgs>();
readonly entityListeners = new PropertyMetadataArgsCollection<EntityListenerMetadataArgs>();
readonly relationCounts = new PropertyMetadataArgsCollection<RelationsCountMetadataArgs>();
readonly embeddeds = new PropertyMetadataArgsCollection<EmbeddedMetadataArgs>();
// -------------------------------------------------------------------------
// Public Methods
@ -46,20 +48,29 @@ export class MetadataArgsStorage {
*/
getMergedTableMetadatas(classes: Function[]) {
const allTableMetadataArgs = this.tables.filterByClasses(classes);
const tableMetadatas = this.tables.filterByClasses(classes).filter(table => table.type !== "abstract");
const tableMetadatas = this.tables.filterByClasses(classes).filter(table => table.type === "regular" || table.type === "closure");
return tableMetadatas.map(tableMetadata => {
return this.mergeWithAbstract(allTableMetadataArgs, tableMetadata);
});
}
/**
* Gets merged (with all abstract classes) embeddable table metadatas for the given classes.
*/
getMergedEmbeddableTableMetadatas(classes: Function[]) {
const embeddableTableMetadatas = this.tables.filterByClasses(classes).filter(table => table.type === "embeddable");
return embeddableTableMetadatas.map(embeddableTableMetadata => {
return this.mergeWithEmbeddable(embeddableTableMetadatas, embeddableTableMetadata);
});
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
/**
* Creates a new copy of the MetadataStorage with same metadatas as in current metadata storage, but filtered
* by classes.
*/
private mergeWithAbstract(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
tableMetadata: TableMetadataArgs) {
@ -71,6 +82,7 @@ export class MetadataArgsStorage {
const joinTables = this.joinTables.filterByClass(tableMetadata.target);
const entityListeners = this.entityListeners.filterByClass(tableMetadata.target);
const relationCounts = this.relationCounts.filterByClass(tableMetadata.target);
const embeddeds = this.embeddeds.filterByClass(tableMetadata.target);
allTableMetadatas
.filter(metadata => {
@ -103,6 +115,10 @@ export class MetadataArgsStorage {
metadatasFromAbstract.relationCounts
.filterRepeatedMetadatas(relationCounts)
.forEach(metadata => relationCounts.push(metadata));
metadatasFromAbstract.embeddeds
.filterRepeatedMetadatas(embeddeds)
.forEach(metadata => embeddeds.push(metadata));
});
return {
@ -113,7 +129,33 @@ export class MetadataArgsStorage {
joinColumns: joinColumns,
joinTables: joinTables,
entityListeners: entityListeners,
relationCounts: relationCounts
relationCounts: relationCounts,
embeddeds: embeddeds
};
}
/**
*/
private mergeWithEmbeddable(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
tableMetadata: TableMetadataArgs) {
const columns = this.columns.filterByClass(tableMetadata.target);
allTableMetadatas
.filter(metadata => {
if (!tableMetadata.target || !metadata.target) return false;
return this.isInherited(tableMetadata.target, metadata.target);
})
.forEach(parentMetadata => {
const metadatasFromParents = this.mergeWithEmbeddable(allTableMetadatas, parentMetadata);
metadatasFromParents.columns
.filterRepeatedMetadatas(columns)
.forEach(metadata => columns.push(metadata));
});
return {
table: tableMetadata,
columns: columns
};
}

View File

@ -7,14 +7,9 @@ export class PropertyMetadataArgsCollection<T extends { target?: Function, prope
// -------------------------------------------------------------------------
filterRepeatedMetadatas(existsMetadatas: T[]): this {
const collection = new (<any> this.constructor)();
this
.filter(metadata => {
return !existsMetadatas.find(fieldFromDocument => fieldFromDocument.propertyName === metadata.propertyName);
})
.forEach(metadata => collection.add(metadata));
return collection;
return this.filter(metadata => {
return !existsMetadatas.find(fieldFromDocument => fieldFromDocument.propertyName === metadata.propertyName);
});
}
findByProperty(propertyName: string) {

View File

@ -6,6 +6,13 @@ export class TargetMetadataArgsCollection<T extends { target?: Function }> exten
// Public Methods
// -------------------------------------------------------------------------
filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): this {
const collection = new (<any> this.constructor)();
super.filter(callbackfn)
.forEach(metadata => collection.add(metadata));
return collection;
}
filterByClass(cls?: Function): this {
// if no class specified then simply return empty collection
@ -16,14 +23,10 @@ export class TargetMetadataArgsCollection<T extends { target?: Function }> exten
}
filterByClasses(classes: Function[]): this {
const collection = new (<any> this.constructor)();
this
.filter(metadata => {
if (!metadata.target) return false;
return classes.indexOf(metadata.target) !== -1;
})
.forEach(metadata => collection.add(metadata));
return collection;
return this.filter(metadata => {
if (!metadata.target) return false;
return classes.indexOf(metadata.target) !== -1;
});
}
add(metadata: T, checkForDuplicateTargets = false) {

View File

@ -12,6 +12,7 @@ import {RelationMetadata} from "../metadata/RelationMetadata";
import {JoinTableMetadata} from "../metadata/JoinTableMetadata";
import {JunctionEntityMetadataBuilder} from "./JunctionEntityMetadataBuilder";
import {ClosureJunctionEntityMetadataBuilder} from "./ClosureJunctionEntityMetadataBuilder";
import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
/**
* Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes.
@ -36,8 +37,20 @@ export class EntityMetadataBuilder {
*/
build(namingStrategy: NamingStrategyInterface, entityClasses: Function[]): EntityMetadata[] {
const embeddableMergedArgs = getMetadataArgsStorage().getMergedEmbeddableTableMetadatas(entityClasses);
const entityMetadatas = getMetadataArgsStorage().getMergedTableMetadatas(entityClasses).map(mergedArgs => {
// find embeddable tables for embeddeds registered in this table and create EmbeddedMetadatas from them
const embeddeds: EmbeddedMetadata[] = [];
mergedArgs.embeddeds.forEach(embedded => {
const embeddableTable = embeddableMergedArgs.find(mergedArs => mergedArgs.table.target === embedded.type);
if (embeddableTable) {
const table = new TableMetadata(mergedArgs.table);
const columns = mergedArgs.columns.map(args => new ColumnMetadata(args));
embeddeds.push(new EmbeddedMetadata(table, columns));
}
});
// create metadatas from args
const table = new TableMetadata(mergedArgs.table);
const columns = mergedArgs.columns.map(args => new ColumnMetadata(args));
@ -50,7 +63,8 @@ export class EntityMetadataBuilder {
tableMetadata: table,
columnMetadatas: columns,
relationMetadatas: relations,
indexMetadatas: indices
indexMetadatas: indices,
embeddedMetadatas: embeddeds
});
// create entity's relations join tables

View File

@ -0,0 +1,42 @@
import {EntityMetadata} from "./EntityMetadata";
import {TableMetadata} from "./TableMetadata";
import {ColumnMetadata} from "./ColumnMetadata";
/**
* Contains all information about entity's embedded property.
*/
export class EmbeddedMetadata {
// ---------------------------------------------------------------------
// Public Properties
// ---------------------------------------------------------------------
/**
* Its own entity metadata.
*/
entityMetadata: EntityMetadata;
// ---------------------------------------------------------------------
// Private Properties
// ---------------------------------------------------------------------
/**
* Embeddable table.
*/
readonly table: TableMetadata;
/**
* Embeddable table's columns.
*/
readonly columns: ColumnMetadata[];
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(table: TableMetadata, columns: ColumnMetadata[]) {
this.table = table;
this.columns = columns;
}
}

View File

@ -6,6 +6,7 @@ import {RelationTypes} from "./types/RelationTypes";
import {ForeignKeyMetadata} from "./ForeignKeyMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {EntityMetadataArgs} from "../metadata-args/EntityMetadataArgs";
import {EmbeddedMetadata} from "./EmbeddedMetadata";
/**
* Contains all entity metadata.
@ -55,6 +56,11 @@ export class EntityMetadata {
*/
readonly foreignKeys: ForeignKeyMetadata[] = [];
/**
* Entity's embedded metadatas.
*/
readonly embeddeds: EmbeddedMetadata[];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
@ -66,12 +72,14 @@ export class EntityMetadata {
this.relations = args.relationMetadatas || [];
this.indices = args.indexMetadatas || [];
this.foreignKeys = args.foreignKeyMetadatas || [];
this.embeddeds = args.embeddedMetadatas || [];
this.table.entityMetadata = this;
this.columns.forEach(column => column.entityMetadata = this);
this.relations.forEach(relation => relation.entityMetadata = this);
this.foreignKeys.forEach(foreignKey => foreignKey.entityMetadata = this);
this.indices.forEach(index => index.entityMetadata = this);
this.embeddeds.forEach(embedded => embedded.entityMetadata = this);
}
// -------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@ import {TableMetadataArgs} from "../metadata-args/TableMetadataArgs";
/**
* Table type.
*/
export type TableType = "regular"|"abstract"|"junction"|"closure"|"closureJunction";
export type TableType = "regular"|"abstract"|"junction"|"closure"|"closureJunction"|"embeddable";
/**
* This metadata interface contains all information about specific table.
@ -98,5 +98,12 @@ export class TableMetadata extends TargetMetadata {
get isClosure() {
return this.tableType === "closure";
}
/**
* Checks if this table is an embeddable table.
*/
get isEmbeddable() {
return this.tableType === "embeddable";
}
}