property implemented date types, escaping and types persisment/hydration

This commit is contained in:
Umed Khudoiberdiev 2016-04-21 15:15:04 +05:00
parent f686515646
commit b919ef8bd7
20 changed files with 378 additions and 27 deletions

View File

@ -163,8 +163,6 @@ If you want to load photos from the database, you can use `repository.find*`
methods:
```typescript
let repository = connection.getRepository(Photo);
// here we load one photo by id:
let photoId = 1;
let repository = connection.getRepository(Photo);

View File

@ -65,6 +65,7 @@ ColumnType can be one of:
* `date` will be mapped to db's `datetime`
* `time` will be mapped to db's `time`
* `datetime` will be mapped to db's `datetime`
* `timestamp` will be mapped to db's `timestamp`
* `boolean` will be mapped to db's `boolean`
* `json` will be mapped to db's `text`
* `simple_array` will be mapped to db's `text`

View File

@ -1,7 +1,7 @@
{
"name": "typeorm",
"private": true,
"version": "0.0.2-alpha",
"version": "0.0.2-alpha.1",
"description": "Data-mapper ORM for Typescript",
"license": "Apache-2.0",
"readmeFilename": "README.md",
@ -44,6 +44,7 @@
"dependencies": {
"fs": "^0.0.2",
"lodash": "^4.11.1",
"moment": "^2.13.0",
"mysql": "^2.10.2",
"path": "^0.12.7",
"reflect-metadata": "^0.1.3",

View File

@ -0,0 +1,95 @@
import {createConnection, CreateConnectionOptions} from "../../src/typeorm";
import {EverythingEntity} from "./entity/EverythingEntity";
const options: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
logging: {
logOnlyFailedQueries: true
}
},
entities: [EverythingEntity]
};
createConnection(options).then(connection => {
let entity = new EverythingEntity();
entity.date = new Date(1980, 11, 1);
entity.name = "max 255 chars name";
entity.text = "this is pretty long text";
entity.shortTextColumn = "TJ";
entity.numberColumn = 123.5;
entity.intColumn = 1000000;
entity.integerColumn = 2000000;
entity.smallintColumn = 12345;
entity.bigintColumn = 123456789012345;
entity.floatColumn = 100.5;
entity.doubleColumn = 200.6;
entity.decimalColumn = 1000000;
entity.dateColumn = new Date();
entity.timeColumn = new Date();
entity.isBooleanColumn = true;
entity.isSecondBooleanColumn = false;
entity.simpleArrayColumn = ["hello", "world", "of", "typescript"];
entity.jsonColumn = [{ hello: "olleh" }, { world: "dlrow" }];
entity.alsoJson = { hello: "olleh", world: "dlrow" };
let postRepository = connection.getRepository(EverythingEntity);
postRepository
.persist(entity)
.then(entity => {
console.log("EverythingEntity has been saved. Lets insert a new one to update it later");
entity.id = null;
return postRepository.persist(entity);
})
.then(entity => {
console.log("Second entity has been inserted. Lets update it");
entity.date = new Date(2000, 12, 5);
entity.name = "updated short name";
entity.text = "loooooong text updated";
entity.shortTextColumn = "RU";
entity.numberColumn = 1.1;
entity.intColumn = 1000001;
entity.integerColumn = 2000002;
entity.smallintColumn = 12342;
entity.bigintColumn = 12345678922222;
entity.floatColumn = 200.2;
entity.doubleColumn = 400.12;
entity.decimalColumn = 2000000;
entity.dateColumn = new Date();
entity.timeColumn = new Date();
entity.isBooleanColumn = false;
entity.isSecondBooleanColumn = true;
entity.simpleArrayColumn = ["hello!", "world!", "of!", "typescript!"];
entity.jsonColumn = [{ olleh: "hello" }, { dlrow: "world" }];
entity.alsoJson = { olleh: "hello", dlrow: "world" };
return postRepository.persist(entity);
})
.then(entity => {
console.log("Entity has been updated. Persist once again to make find and remove then");
entity.id = null;
return postRepository.persist(entity);
})
.then(entity => {
return postRepository.findById(entity.id);
})
.then(entity => {
console.log("Entity is loaded: ", entity);
console.log("Now remove it");
return postRepository.remove(entity);
})
.then(entity => {
console.log("Entity has been removed");
})
.catch(error => console.log("Cannot save. Error: ", error, error.stack));
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,75 @@
import {PrimaryColumn, Column} from "../../../src/decorator/columns";
import {Table} from "../../../src/decorator/tables";
import {CreateDateColumn} from "../../../src/decorator/columns/CreateDateColumn";
import {UpdateDateColumn} from "../../../src/decorator/columns/UpdateDateColumn";
@Table("sample11_everything_entity")
export class EverythingEntity {
@PrimaryColumn("int", { autoIncrement: true })
id: number;
@Column()
name: string;
@Column("text")
text: string;
@Column({ length: "32" })
shortTextColumn: string;
@Column()
numberColumn: number;
@Column("integer")
integerColumn: number;
@Column("int")
intColumn: number;
@Column("smallint")
smallintColumn: number;
@Column("bigint")
bigintColumn: number;
@Column("float")
floatColumn: number;
@Column("double")
doubleColumn: number;
@Column("decimal")
decimalColumn: number;
@Column()
date: Date;
@Column("date")
dateColumn: Date;
@Column("time")
timeColumn: Date;
@Column("boolean")
isBooleanColumn: boolean;
@Column("boolean")
isSecondBooleanColumn: boolean;
@Column("json")
jsonColumn: any;
@Column()
alsoJson: Object;
@Column("simple_array")
simpleArrayColumn: string[];
@CreateDateColumn()
createdDate: Date;
@UpdateDateColumn()
updatedDate: Date;
}

View File

@ -30,6 +30,8 @@ export function Column(typeOrOptions?: ColumnType|ColumnOptions, options?: Colum
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// if type is not given implicitly then try to guess it
if (!type)
@ -55,6 +57,7 @@ export function Column(typeOrOptions?: ColumnType|ColumnOptions, options?: Colum
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
options: options
}));
};

View File

@ -11,6 +11,8 @@ import "reflect-metadata";
export function CreateDateColumn(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 = {};
@ -22,6 +24,7 @@ export function CreateDateColumn(options?: ColumnOptions): Function {
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isCreateDate: true,
options: options
}));

View File

@ -34,6 +34,8 @@ export function PrimaryColumn(typeOrOptions?: ColumnType|ColumnOptions, options?
}
return function (object: Object, propertyName: string) {
const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// if type is not given implicitly then try to guess it
if (!type)
type = ColumnTypes.determineTypeFromFunction(Reflect.getMetadata("design:type", object, propertyName));
@ -58,6 +60,7 @@ export function PrimaryColumn(typeOrOptions?: ColumnType|ColumnOptions, options?
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isPrimaryKey: true,
options: options
}));

View File

@ -11,6 +11,8 @@ import "reflect-metadata";
export function UpdateDateColumn(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 = {};
@ -22,6 +24,7 @@ export function UpdateDateColumn(options?: ColumnOptions): Function {
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isUpdateDate: true,
options: options
}));

View File

@ -2,6 +2,7 @@ import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {Connection} from "../connection/Connection";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
/**
* Driver communicates with specific database.
@ -83,4 +84,19 @@ export interface Driver {
*/
endTransaction(): Promise<void>;
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be hydrated, based on its column type and metadata.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Escapes given value.
*/
escape(value: any): any;
}

View File

@ -1,11 +1,13 @@
import {Driver} from "./Driver";
import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {MysqlSchemaBuilder} from "../schema-builder/MysqlSchemaBuilder";
import {Connection} from "../connection/Connection";
import {ConnectionIsNotSetError} from "./error/ConnectionIsNotSetError";
import {BaseDriver} from "./BaseDriver";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {ColumnTypes} from "../metadata-builder/types/ColumnTypes";
import * as moment from "moment";
/**
* This driver organizes work with mysql database.
@ -215,4 +217,65 @@ export class MysqlDriver extends BaseDriver implements Driver {
return this.query("COMMIT").then(() => {});
}
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
case ColumnTypes.TIME:
return moment(value).format("hh:mm:ss");
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD hh:mm:ss");
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as Array<any>)
.map(i => String(i))
.join(",");
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "hh:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD hh:mm:ss").toDate();
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
}
return value;
}
/**
* Escapes given value.
*/
escape(value: any): any {
return this.mysqlConnection.escape(value);
}
}

View File

@ -97,6 +97,7 @@ export class EntityMetadataBuilder {
relationalColumn = new ColumnMetadata({
target: metadata.target,
propertyName: relation.name,
propertyType: inverseSideMetadata.primaryColumn.type,
isVirtual: true,
options: options
});
@ -139,11 +140,13 @@ export class EntityMetadataBuilder {
new ColumnMetadata({
target: null,
propertyName: null,
propertyType: inverseSideMetadata.primaryColumn.type,
options: column1options
}),
new ColumnMetadata({
target: null,
propertyName: null,
propertyType: inverseSideMetadata.primaryColumn.type,
options: column2options
})
];

View File

@ -18,6 +18,11 @@ export interface ColumnMetadataArgs {
*/
propertyName: string;
/**
* Class's property type (reflected) to which this column is applied.
*/
propertyType: string;
/**
* Indicates if this column is primary key or not.
*/
@ -67,6 +72,11 @@ export class ColumnMetadata extends PropertyMetadata {
*/
private _name: string;
/**
* The real reflected property type.
*/
private _propertyType: string;
/**
* The type of the column.
*/
@ -159,6 +169,8 @@ export class ColumnMetadata extends PropertyMetadata {
this._isUpdateDate = args.isUpdateDate;
if (args.isVirtual)
this._isVirtual = args.isVirtual;
if (args.propertyType)
this._propertyType = args.propertyType;
if (args.options.name)
this._name = args.options.name;
if (args.options.type)
@ -200,6 +212,13 @@ export class ColumnMetadata extends PropertyMetadata {
return this.namingStrategy ? this.namingStrategy.columnName(this._name) : this._name;
}
/**
* The real reflected property type.
*/
get propertyType(): string {
return this._propertyType.toLowerCase();
}
/**
* Type of the column.
*/

View File

@ -29,7 +29,7 @@ export interface ColumnOptions {
/**
* Specifies if column's value must be unique or not.
*/
unique?: boolean;
unique?: boolean;
/**
* Indicates if column's value can be set to NULL.

View File

@ -138,6 +138,10 @@ export class ColumnTypes {
return ColumnTypes.BOOLEAN;
case "string":
return ColumnTypes.STRING;
case "date":
return ColumnTypes.DATETIME;
case "object":
return ColumnTypes.JSON;
}
} else if (type instanceof Object) {
@ -146,5 +150,9 @@ export class ColumnTypes {
}
return undefined;
}
static typeToString(type: Function) {
return (<any>type).name.toLowerCase();
}
}

View File

@ -215,10 +215,15 @@ export class PersistOperationExecutor {
private update(updateOperation: UpdateOperation) {
const entity = updateOperation.entity;
const metadata = this.connection.getEntityMetadata(entity.constructor);
const values = updateOperation.columns.reduce((object, column) => {
(<any> object)[column.name] = entity[column.propertyName];
const values: any = updateOperation.columns.reduce((object, column) => {
const value = this.connection.driver.preparePersistentValue(entity[column.propertyName], column);
(<any> object)[column.name] = value;
return object;
}, {});
if (metadata.updateDateColumn)
values[metadata.updateDateColumn.name] = this.connection.driver.preparePersistentValue(new Date(), metadata.updateDateColumn);
return this.connection.driver.update(metadata.table.name, values, { [metadata.primaryColumn.name]: metadata.getEntityId(entity) });
}
@ -244,7 +249,14 @@ export class PersistOperationExecutor {
const values = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => "'" + entity[column.propertyName] + "'");
.map(column => this.connection.driver.preparePersistentValue(entity[column.propertyName], column))
.map(value => {
if (value === null || value === undefined) {
return "NULL";
} else {
return this.connection.driver.escape(value);
}
});
const relationColumns = metadata.relations
.filter(relation => relation.isOwning && !!relation.relatedEntityMetadata)
.filter(relation => entity.hasOwnProperty(relation.propertyName))
@ -255,10 +267,20 @@ export class PersistOperationExecutor {
.filter(relation => relation.isOwning && !!relation.relatedEntityMetadata)
.filter(relation => entity.hasOwnProperty(relation.propertyName))
.filter(relation => entity[relation.propertyName].hasOwnProperty(relation.relatedEntityMetadata.primaryColumn.name))
.map(relation => "'" + entity[relation.propertyName][relation.relatedEntityMetadata.primaryColumn.name] + "'");
.map(relation => this.connection.driver.escape(entity[relation.propertyName][relation.relatedEntityMetadata.primaryColumn.name]));
const allColumns = columns.concat(relationColumns);
const allValues = values.concat(relationValues);
if (metadata.createDateColumn) {
allColumns.push(metadata.createDateColumn.name);
allValues.push(this.connection.driver.escape(this.connection.driver.preparePersistentValue(new Date(), metadata.createDateColumn)));
}
if (metadata.updateDateColumn) {
allColumns.push(metadata.updateDateColumn.name);
allValues.push(this.connection.driver.escape(this.connection.driver.preparePersistentValue(new Date(), metadata.updateDateColumn)));
}
return this.connection.driver.insert(metadata.table.name, this.zipObject(allColumns, allValues));
}

View File

@ -419,7 +419,7 @@ export class QueryBuilder<Entity> {
// -------------------------------------------------------------------------
protected rawResultsToEntities(results: any[]) {
const transformer = new RawSqlResultsToEntityTransformer(this._aliasMap);
const transformer = new RawSqlResultsToEntityTransformer(this.connection, this._aliasMap);
return transformer.transform(results);
}
@ -576,8 +576,8 @@ export class QueryBuilder<Entity> {
protected replaceParameters(sql: string) {
// todo: proper escape values and prevent sql injection
Object.keys(this.parameters).forEach(key => {
const value = this.parameters[key] !== null && this.parameters[key] !== undefined ? "\"" + this.parameters[key] + "\"" : "NULL";
sql = sql.replace(":" + key, value); // .replace('"', '')
const value = this.parameters[key] !== null && this.parameters[key] !== undefined ? this.parameters[key] : "NULL";
sql = sql.replace(":" + key, this.connection.driver.escape(value)); // .replace('"', '')
});
return sql;
}

View File

@ -2,6 +2,7 @@ import {AliasMap} from "../alias/AliasMap";
import {Alias} from "../alias/Alias";
import * as _ from "lodash";
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {Connection} from "../../connection/Connection";
/**
* Transforms raw sql results returned from the database into entity object.
@ -13,7 +14,8 @@ export class RawSqlResultsToEntityTransformer {
// Constructor
// -------------------------------------------------------------------------
constructor(private aliasMap: AliasMap) {
constructor(private connection: Connection,
private aliasMap: AliasMap) {
}
// -------------------------------------------------------------------------
@ -55,7 +57,7 @@ export class RawSqlResultsToEntityTransformer {
metadata.columns.forEach(column => {
const valueInObject = alias.getColumnValue(rawSqlResults[0], column); // we use zero index since its grouped data
if (valueInObject && column.propertyName && !column.isVirtual) {
entity[column.propertyName] = valueInObject;
entity[column.propertyName] = this.connection.driver.prepareHydratedValue(valueInObject, column);
hasData = true;
}
});

View File

@ -28,7 +28,11 @@ export class Repository<Entity> {
* Checks if entity has an id.
*/
hasId(entity: Entity): boolean {
return entity && this.metadata.primaryColumn && entity.hasOwnProperty(this.metadata.primaryColumn.propertyName);
return entity &&
this.metadata.primaryColumn &&
entity.hasOwnProperty(this.metadata.primaryColumn.propertyName) &&
(<any> entity)[this.metadata.primaryColumn.propertyName] !== null &&
(<any> entity)[this.metadata.primaryColumn.propertyName] !== undefined;
}
/**

View File

@ -19,7 +19,7 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
const dbData = results.find(result => result.COLUMN_NAME === column.name);
if (!dbData) return false;
const newType = this.normalizeType(column.type, column.length);
const newType = this.normalizeType(column);
const isNullable = column.isNullable === true ? "YES" : "NO";
const hasDbColumnAutoIncrement = dbData.EXTRA.indexOf("auto_increment") !== -1;
const hasDbColumnPrimaryIndex = dbData.COLUMN_KEY.indexOf("PRI") !== -1;
@ -126,7 +126,7 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
}
private buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean) {
let c = column.name + " " + this.normalizeType(column.type, column.length);
let c = column.name + " " + this.normalizeType(column);
if (column.isNullable !== true)
c += " NOT NULL";
if (column.isPrimary === true && !skipPrimary)
@ -140,31 +140,63 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
return c;
}
private normalizeType(type: any, length?: string) {
private normalizeType(column: ColumnMetadata) {
let realType: string;
if (typeof type === "string") {
realType = type.toLowerCase();
if (typeof column.type === "string") {
realType = column.type.toLowerCase();
} else if (type.name && typeof type.name === "string") {
realType = type.name.toLowerCase();
// todo: remove casting to any after upgrade to typescript 2
} else if (typeof column.type === "object" && (<any>column.type).name && typeof (<any>column.type).name === "string") {
realType = (<any>column.type).toLowerCase();
}
switch (realType) {
case "string":
return "varchar(" + (length ? length : 255) + ")";
return "varchar(" + (column.length ? column.length : 255) + ")";
case "text":
return "text";
case "number":
return "double";
case "boolean":
return "boolean";
case "integer":
case "int":
return "int(" + (length ? length : 11) + ")";
return "INT(" + (column.length ? column.length : 11) + ")";
case "smallint":
return "SMALLINT(" + (column.length ? column.length : 11) + ")";
case "bigint":
return "BIGINT(" + (column.length ? column.length : 11) + ")";
case "float":
return "FLOAT";
case "double":
case "number":
return "DOUBLE";
case "decimal":
if (column.precision && column.scale) {
return `DECIMAL(${column.precision},${column.scale})`;
} else if (column.scale) {
return `DECIMAL(${column.scale})`;
} else if (column.precision) {
return `DECIMAL(${column.precision})`;
} else {
return "DECIMAL";
}
case "date":
return "DATE";
case "time":
return "TIME";
case "datetime":
return "DATETIME";
case "json":
return "text";
case "simple_array":
return column.length ? "varchar(" + column.length + ")" : "text";
}
throw new Error("Specified type (" + type + ") is not supported by current driver.");
throw new Error("Specified type (" + column.type + ") is not supported by current driver.");
}
}