added versioning column, fixed bug when dates are not added after persistment

This commit is contained in:
Umed Khudoiberdiev 2016-05-01 16:48:15 +05:00
parent 83e89a1352
commit b36937c099
17 changed files with 203 additions and 21 deletions

View File

@ -5,7 +5,7 @@ import {CompositeIndex} from "../../../src/decorator/indices/CompositeIndex";
@Table("sample16_post")
@CompositeIndex("my_index_with_id_and_text", ["id", "text"])
@CompositeIndex(["title", "likesCount"])
@CompositeIndex(["title", "likesCount"], { unique: true })
@CompositeIndex((post: Post) => [post.title, post.text])
@CompositeIndex("my_index_with_id_and_title", (post: Post) => [post.id, post.title])
export class Post {

View File

@ -0,0 +1,41 @@
import {createConnection, CreateConnectionOptions} from "../../src/typeorm";
import {Post} from "./entity/Post";
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]
};
createConnection(options).then(connection => {
let post = new Post();
post.text = "Hello how are you?";
post.title = "hello";
let postRepository = connection.getRepository(Post);
postRepository
.persist(post)
.then(post => {
console.log(`Post has been saved: `, post);
console.log(`Post's version is ${post.version}. Lets change post's text and update it:`);
post.title = "updating title";
return postRepository.persist(post);
}).then(post => {
console.log(`Post has been updated. Post's version is ${post.version}`);
});
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,20 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {VersionColumn} from "../../../src/decorator/columns/VersionColumn";
@Table("sample17_post")
export class Post {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
title: string;
@Column()
text: string;
@VersionColumn()
version: number;
}

View File

@ -0,0 +1,34 @@
import {ColumnOptions} from "../../metadata/options/ColumnOptions";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {defaultMetadataStorage} from "../../typeorm";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import "reflect-metadata";
/**
* This column will store a number - version of the entity. Every time your entity will be persisted, this number will
* be increased by one - so you can organize visioning and update strategies of your entity.
*/
export function VersionColumn(options?: ColumnOptions): Function {
return function (object: Object, propertyName: string) {
const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// if column options are not given then create a new empty options
if (!options) options = {} as ColumnOptions;
// implicitly set a type, because this column's type cannot be anything else except date
options.type = ColumnTypes.INTEGER;
// todo: check if reflectedType is number too
// create and register a new column metadata
defaultMetadataStorage().columnMetadatas.add(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isVersion: true,
options: options
}));
};
}

View File

@ -1,18 +1,22 @@
import {CompositeIndexMetadata} from "../../metadata/CompositeIndexMetadata";
import {defaultMetadataStorage} from "../../typeorm";
import {CompositeIndexOptions} from "../../metadata/options/CompositeIndexOptions";
/**
* Composite indexes must be set on entity classes and must specify fields to be indexed.
*/
export function CompositeIndex(name: string, fields: string[]): Function;
export function CompositeIndex(fields: string[]): Function;
export function CompositeIndex(fields: (object: any) => any[]): Function;
export function CompositeIndex(name: string, fields: (object: any) => any[]): Function;
export function CompositeIndex(nameOrFields: string|string[]|((object: any) => any[]), maybeFields?: ((object: any) => any[])|string[]): Function {
export function CompositeIndex(name: string, fields: string[], options?: CompositeIndexOptions): Function;
export function CompositeIndex(fields: string[], options?: CompositeIndexOptions): Function;
export function CompositeIndex(fields: (object: any) => any[], options?: CompositeIndexOptions): Function;
export function CompositeIndex(name: string, fields: (object: any) => any[], options?: CompositeIndexOptions): Function;
export function CompositeIndex(nameOrFields: string|string[]|((object: any) => any[]),
maybeFieldsOrOptions?: ((object: any) => any[])|CompositeIndexOptions|string[],
maybeOptions?: CompositeIndexOptions): Function {
const name = typeof nameOrFields === "string" ? nameOrFields : undefined;
const fields = typeof nameOrFields === "string" ? <((object: any) => any[])|string[]> maybeFields : nameOrFields;
const fields = typeof nameOrFields === "string" ? <((object: any) => any[])|string[]> maybeFieldsOrOptions : nameOrFields;
const options = typeof maybeFieldsOrOptions === "object" ? <CompositeIndexOptions> maybeFieldsOrOptions : maybeOptions;
return function (cls: Function) {
defaultMetadataStorage().compositeIndexMetadatas.add(new CompositeIndexMetadata(cls, name, fields));
defaultMetadataStorage().compositeIndexMetadatas.add(new CompositeIndexMetadata(cls, name, fields, options));
};
}

View File

@ -66,6 +66,11 @@ export class ColumnMetadata extends PropertyMetadata {
*/
readonly isUpdateDate = false;
/**
* Indicates if column will contain a version.
*/
readonly isVersion = false;
/**
* Indicates if column will contain an updated date or not.
*/
@ -125,6 +130,8 @@ export class ColumnMetadata extends PropertyMetadata {
this.isCreateDate = args.isCreateDate;
if (args.isUpdateDate)
this.isUpdateDate = args.isUpdateDate;
if (args.isVersion)
this.isVersion = args.isVersion;
if (args.isVirtual)
this.isVirtual = args.isVirtual;
if (args.propertyType)

View File

@ -1,6 +1,7 @@
import {TargetMetadata} from "./TargetMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {EntityMetadata} from "./EntityMetadata";
import {CompositeIndexOptions} from "./options/CompositeIndexOptions";
/**
* This metadata interface contains all information about table's composite index.
@ -25,6 +26,11 @@ export class CompositeIndexMetadata extends TargetMetadata {
// Readonly Properties
// ---------------------------------------------------------------------
/**
* Indicates if this index must be unique.
*/
readonly isUnique: boolean;
// ---------------------------------------------------------------------
// Private Properties
// ---------------------------------------------------------------------
@ -43,11 +49,16 @@ export class CompositeIndexMetadata extends TargetMetadata {
// Constructor
// ---------------------------------------------------------------------
constructor(target: Function, name: string|undefined, columns: ((object: any) => any[])|string[]) {
constructor(target: Function,
name: string|undefined,
columns: ((object: any) => any[])|string[],
options?: CompositeIndexOptions) {
super(target);
this._columns = columns;
if (name)
this._name = name;
if (options && options.unique)
this.isUnique = options.unique;
}
// ---------------------------------------------------------------------

View File

@ -91,6 +91,10 @@ export class EntityMetadata {
return this.columns.find(column => column.isUpdateDate);
}
get versionColumn(): ColumnMetadata {
return this.columns.find(column => column.isVersion);
}
get hasPrimaryKey(): boolean {
return !!this.primaryColumn;
}

View File

@ -40,6 +40,16 @@ export interface ColumnMetadataArgs {
*/
isVirtual?: boolean;
/**
* Indicates if this column is version column.
*/
isVersion?: boolean;
/**
* Indicates if this column is order id column.
*/
isOrderId?: boolean;
/**
* Extra column options.
*/

View File

@ -0,0 +1,11 @@
/**
* Describes all composite index's options.
*/
export interface CompositeIndexOptions {
/**
* Indicates if this composite index must be unique or not.
*/
unique?: boolean;
}

View File

@ -328,7 +328,7 @@ export class EntityPersistOperationBuilder {
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual && !column.isUpdateDate && !column.isCreateDate)
.filter(column => !column.isVirtual && !column.isUpdateDate && !column.isVersion && !column.isCreateDate)
.filter(column => newEntity[column.propertyName] !== dbEntity[column.propertyName]);
}

View File

@ -46,6 +46,7 @@ export class PersistOperationExecutor {
.then(() => this.driver.endTransaction())
.then(() => this.updateIdsOfInsertedEntities(persistOperation))
.then(() => this.updateIdsOfRemovedEntities(persistOperation))
.then(() => this.updateSpecialColumnsInEntities(persistOperation))
.then(() => this.broadcastAfterEvents(persistOperation));
}
@ -106,7 +107,7 @@ export class PersistOperationExecutor {
*/
private executeInsertOperations(persistOperation: PersistOperation) {
return Promise.all(persistOperation.inserts.map(operation => {
return this.insert(operation.entity).then((insertId: any) => {
return this.insert(operation).then((insertId: any) => {
operation.entityId = insertId;
});
}));
@ -177,7 +178,31 @@ export class PersistOperationExecutor {
private updateIdsOfInsertedEntities(persistOperation: PersistOperation) {
persistOperation.inserts.forEach(insertOperation => {
const metadata = this.entityMetadatas.findByTarget(insertOperation.entity.constructor);
insertOperation.entity[metadata.primaryColumn.name] = insertOperation.entityId;
insertOperation.entity[metadata.primaryColumn.propertyName] = insertOperation.entityId;
});
}
/**
* Updates all special columns of the saving entities (create date, update date, versioning).
*/
private updateSpecialColumnsInEntities(persistOperation: PersistOperation) {
persistOperation.inserts.forEach(insertOperation => {
const metadata = this.entityMetadatas.findByTarget(insertOperation.entity.constructor);
if (metadata.updateDateColumn)
insertOperation.entity[metadata.updateDateColumn.propertyName] = insertOperation.date;
if (metadata.createDateColumn)
insertOperation.entity[metadata.createDateColumn.propertyName] = insertOperation.date;
if (metadata.versionColumn)
insertOperation.entity[metadata.versionColumn.propertyName] = 1;
});
persistOperation.updates.forEach(updateOperation => {
const metadata = this.entityMetadatas.findByTarget(updateOperation.entity.constructor);
if (metadata.updateDateColumn)
updateOperation.entity[metadata.updateDateColumn.propertyName] = updateOperation.date;
if (metadata.createDateColumn)
updateOperation.entity[metadata.createDateColumn.propertyName] = updateOperation.date;
if (metadata.versionColumn)
updateOperation.entity[metadata.versionColumn.propertyName]++;
});
}
@ -226,8 +251,13 @@ export class PersistOperationExecutor {
return object;
}, {});
// todo: what if number of updated columns = 0 ? + probably no need to update updated date and version columns
if (metadata.updateDateColumn)
values[metadata.updateDateColumn.name] = this.driver.preparePersistentValue(new Date(), metadata.updateDateColumn);
if (metadata.versionColumn)
values[metadata.versionColumn.name] = this.driver.preparePersistentValue(entity[metadata.versionColumn.propertyName] + 1, metadata.versionColumn);
return this.driver.update(metadata.table.name, values, { [metadata.primaryColumn.name]: metadata.getEntityId(entity) });
}
@ -249,7 +279,8 @@ export class PersistOperationExecutor {
return this.driver.delete(metadata.table.name, { [metadata.primaryColumn.name]: entity[metadata.primaryColumn.propertyName] });
}
private insert(entity: any) {
private insert(operation: InsertOperation) {
const entity = operation.entity;
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
const columns = metadata.columns
.filter(column => !column.isVirtual)
@ -276,12 +307,17 @@ export class PersistOperationExecutor {
if (metadata.createDateColumn) {
allColumns.push(metadata.createDateColumn.name);
allValues.push(this.driver.preparePersistentValue(new Date(), metadata.createDateColumn));
allValues.push(this.driver.preparePersistentValue(operation.date, metadata.createDateColumn));
}
if (metadata.updateDateColumn) {
allColumns.push(metadata.updateDateColumn.name);
allValues.push(this.driver.preparePersistentValue(new Date(), metadata.updateDateColumn));
allValues.push(this.driver.preparePersistentValue(operation.date, metadata.updateDateColumn));
}
if (metadata.versionColumn) {
allColumns.push(metadata.versionColumn.name);
allValues.push(this.driver.preparePersistentValue(1, metadata.versionColumn));
}
return this.driver.insert(metadata.table.name, this.zipObject(allColumns, allValues));

View File

@ -3,6 +3,7 @@
*/
export class InsertOperation {
constructor(public entity: any,
public entityId?: number) {
public entityId?: number,
public date = new Date()) {
}
}

View File

@ -6,6 +6,7 @@ import {ColumnMetadata} from "../../metadata/ColumnMetadata";
export class UpdateOperation {
constructor(public entity: any,
public entityId: any,
public columns: ColumnMetadata[]) {
public columns: ColumnMetadata[],
public date = new Date()) {
}
}

View File

@ -3,6 +3,7 @@ import {MysqlDriver} from "../driver/MysqlDriver";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {TableMetadata} from "../metadata/TableMetadata";
import {CompositeIndexMetadata} from "../metadata/CompositeIndexMetadata";
/**
* @internal
@ -117,8 +118,8 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
return this.query(sql).then(() => {});
}
createIndex(tableName: string, indexName: string, columns: string[]): Promise<void> {
const sql = `CREATE INDEX \`${indexName}\` ON ${tableName}(${columns.join(", ")})`;
createIndex(tableName: string, index: CompositeIndexMetadata): Promise<void> {
const sql = `CREATE ${index.isUnique ? "UNIQUE" : ""} INDEX \`${index.name}\` ON ${tableName}(${index.columns.join(", ")})`;
return this.query(sql).then(() => {});
}

View File

@ -1,6 +1,7 @@
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {TableMetadata} from "../metadata/TableMetadata";
import {CompositeIndexMetadata} from "../metadata/CompositeIndexMetadata";
/**
* todo: make internal too (need to refactor driver).
@ -19,7 +20,7 @@ export abstract class SchemaBuilder {
abstract getTableIndicesQuery(tableName: string): Promise<{ key: string, sequence: number, column: string }[]>;
abstract getPrimaryConstraintName(tableName: string): Promise<string>;
abstract dropIndex(tableName: string, indexName: string): Promise<void>;
abstract createIndex(tableName: string, indexName: string, columns: string[]): Promise<void>;
abstract createIndex(tableName: string, index: CompositeIndexMetadata): Promise<void>;
abstract addUniqueKey(tableName: string, columnName: string, keyName: string): Promise<void>;
abstract getTableColumns(tableName: string): Promise<string[]>;
abstract changeColumnQuery(tableName: string, columnName: string, newColumn: ColumnMetadata, skipPrimary?: boolean): Promise<void>;

View File

@ -222,7 +222,7 @@ export class SchemaCreator {
// then create table indices for all composite indices we have
const addQueries = compositeIndices
.filter(compositeIndex => !tableIndices.find(i => i.key === compositeIndex.name))
.map(compositeIndex => this.schemaBuilder.createIndex(table.name, compositeIndex.name, compositeIndex.columns));
.map(compositeIndex => this.schemaBuilder.createIndex(table.name, compositeIndex));
return Promise.all([dropQueries, addQueries]);
});