refactored columns decorators

This commit is contained in:
Umed Khudoiberdiev 2016-03-19 00:44:02 +05:00
parent 2f5a79a1a6
commit da8963c5e4
11 changed files with 575 additions and 143 deletions

View File

@ -0,0 +1,70 @@
## Tables and columns
### Creating a basic table
Every object that you want to be saved in the database must have `@Table`
decorator and each property you want to be saved from your object must
have `@Column` decorator. Let's start with an example:
```typescript
@Table("photo")
class Photo {
@PrimaryColumn()
id: number;
@Column()
name: string;
@Column()
filename: string;
@Column()
description: string;
}
```
* `@Table` decorator registers your class in ORM, and allows you to make
different operations with instances of this class directly in the db.
After you mark your class with this decorator and run schema update
process, ORM will create a table in the db for you, with all proper
columns, keys and so on. After you define your class as a @Table you
can do various operations like inserting, updating, removing, fining
objects of this class in the database. In this example we also specified
a name for this table (`@Table("photo")`). ORM will create a table in
the database with such name - "photos".
* `@Column` decorator marks properties of your class to be persisted into
the database. For each property of your class decorated by `@Table`
decorator ORM will create a column in the table, and this property's
value will be saved to database when you'll save a class instance.
* `@PrimaryColumn` decorator marks a property of your class that must
be a primary-key in your table. It works the same way as @Column decorator,
but the main difference is that it also creates a PRIMARY KEY for this
column. This decorator is always used when you want to create an id-based
column, including auto-increment id.
### Primary and regular columns
By default column type used in the database is automatically guessed
from property type. But type that is set for property is not desirable
for the type used in the database. For example type `number
You can specify a type of column that will be used `@Column`
```typescript
@Table("photo")
class Photo {
@Column("string")
name: string;
@Column()
filename: string;
@Column()
description: string;
}
```

View File

@ -1,72 +1,2 @@
import "reflect-metadata";
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {ColumnOptions} from "../metadata-builder/options/ColumnOptions";
/**
* Column decorator is used to mark a specific class property as a table column. Only table columns will be
* persisted to the database when document is being saved.
*/
export function Column(options?: ColumnOptions): Function;
export function Column(type?: string, options?: ColumnOptions): Function;
export function Column(typeOrOptions?: string|ColumnOptions, options?: ColumnOptions): Function {
let type: string;
if (typeof typeOrOptions === "string") {
type = <string> typeOrOptions;
} else {
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
if (!type)
type = Reflect.getMetadata("design:type", object, propertyName);
if (!options)
options = {};
if (!options.type)
options.type = type;
if (options.autoIncrement)
throw new Error(`Column for property ${propertyName} in ${(<any>object.constructor).name} cannot have auto increment. To have this ability you need to use @PrimaryColumn decorator.`);
// todo: need proper type validation here
const metadata = new ColumnMetadata(object.constructor, propertyName, false, false, false, false, options);
defaultMetadataStorage.addColumnMetadata(metadata);
};
}
/**
* Column decorator is used to mark a specific class property as a table column. Only table columns will be
* persisted to the database when document is being saved.
*/
export function PrimaryColumn(options?: ColumnOptions): Function;
export function PrimaryColumn(type?: string, options?: ColumnOptions): Function;
export function PrimaryColumn(typeOrOptions?: string|ColumnOptions, options?: ColumnOptions): Function {
let type: string;
if (typeof typeOrOptions === "string") {
type = <string> typeOrOptions;
} else {
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
if (!type)
type = Reflect.getMetadata("design:type", object, propertyName);
if (!options)
options = {};
if (!options.type)
options.type = type;
if (options.nullable)
throw new Error(`Primary column for property ${propertyName} in ${(<any>object.constructor).name} cannot be nullable. Its not allowed for primary keys. Please remove isNullable option.`);
// todo: need proper type validation here
const metadata = new ColumnMetadata(object.constructor, propertyName, true, false, false, false, options);
defaultMetadataStorage.addColumnMetadata(metadata);
};
}
export * from "./columns/Column";
export * from "./columns/PrimaryColumn";

View File

@ -0,0 +1,50 @@
import "reflect-metadata";
import {ColumnOptions, ColumnTypeString, ColumnTypes} from "../../metadata-builder/options/ColumnOptions";
import {ColumnTypeUndefinedError} from "../error/ColumnTypeUndefinedError";
import {AutoIncrementOnlyForPrimaryError} from "../error/AutoIncrementOnlyForPrimaryError";
import {defaultMetadataStorage} from "../../metadata-builder/MetadataStorage";
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(options?: ColumnOptions): Function;
export function Column(type?: ColumnTypeString, options?: ColumnOptions): Function;
export function Column(typeOrOptions?: ColumnTypeString|ColumnOptions, options?: ColumnOptions): Function {
let type: ColumnTypeString;
if (typeof typeOrOptions === "string") {
type = <ColumnTypeString> typeOrOptions;
} else {
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
// if type is not given implicitly then try to guess it
if (!type)
type = ColumnTypes.determineTypeFromFunction(Reflect.getMetadata("design:type", object, propertyName));
// if column options are not given then create a new empty options
if (!options)
options = {};
// check if there is no type in column options then set type from first function argument, or guessed one
if (!options.type)
options.type = type;
// if we still don't have a type then we need to give error to user that type is required
if (!options.type)
throw new ColumnTypeUndefinedError(object, propertyName);
// check if auto increment is not set for simple column
if (options.autoIncrement)
throw new AutoIncrementOnlyForPrimaryError(object, propertyName);
// create and register a new column metadata
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
options: options
}));
};
}

View File

@ -0,0 +1,53 @@
import "reflect-metadata";
import {ColumnOptions, ColumnTypeString, ColumnTypes} from "../../metadata-builder/options/ColumnOptions";
import {ColumnTypeUndefinedError} from "../error/ColumnTypeUndefinedError";
import {defaultMetadataStorage} from "../../metadata-builder/MetadataStorage";
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
import {PrimaryColumnCannotBeNullableError} from "../error/PrimaryColumnCannotBeNullableError";
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved. Primary columns also creates a PRIMARY KEY for
* this column in a db.
*/
export function PrimaryColumn(options?: ColumnOptions): Function;
export function PrimaryColumn(type?: ColumnTypeString, options?: ColumnOptions): Function;
export function PrimaryColumn(typeOrOptions?: ColumnTypeString|ColumnOptions, options?: ColumnOptions): Function {
let type: ColumnTypeString;
if (typeof typeOrOptions === "string") {
type = <ColumnTypeString> typeOrOptions;
} else {
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
// if type is not given implicitly then try to guess it
if (!type)
type = ColumnTypes.determineTypeFromFunction(Reflect.getMetadata("design:type", object, propertyName));
// if column options are not given then create a new empty options
if (!options)
options = {};
// check if there is no type in column options then set type from first function argument, or guessed one
if (!options.type)
options.type = type;
// if we still don't have a type then we need to give error to user that type is required
if (!options.type)
throw new ColumnTypeUndefinedError(object, propertyName);
// check if column is not nullable, because we cannot allow a primary key to be nullable
if (options.nullable)
throw new PrimaryColumnCannotBeNullableError(object, propertyName);
// create and register a new column metadata
defaultMetadataStorage.addColumnMetadata(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
isPrimaryKey: true,
options: options
}));
};
}

View File

@ -0,0 +1,10 @@
export class AutoIncrementOnlyForPrimaryError extends Error {
name = "AutoIncrementOnlyForPrimaryError";
constructor(object: Object, propertyName: string) {
super();
this.message = `Column for property ${(<any>object.constructor).name}#${propertyName} cannot have an auto ` +
`increment because its not a primary column. Try to use @PrimaryColumn decorator.`;
}
}

View File

@ -0,0 +1,10 @@
export class ColumnTypeUndefinedError extends Error {
name = "ColumnTypeUndefinedError";
constructor(object: Object, propertyName: string) {
super();
this.message = `Column type for ${(<any>object.constructor).name}#${propertyName} is not defined or cannot be guessed. ` +
`Try to implicitly provide a column type to @Column decorator.`;
}
}

View File

@ -0,0 +1,10 @@
export class PrimaryColumnCannotBeNullableError extends Error {
name = "PrimaryColumnCannotBeNullableError";
constructor(object: Object, propertyName: string) {
super();
this.message = `Primary column ${(<any>object.constructor).name}#${propertyName} cannot be nullable. ` +
`Its not allowed for primary keys. Try to remove nullable option.`;
}
}

View File

@ -108,7 +108,12 @@ export class EntityMetadataBuilder {
oldColumnName: relation.oldColumnName,
nullable: relation.isNullable
};
relationalColumn = new ColumnMetadata(metadata.target, relation.name, false, false, false, true, options);
relationalColumn = new ColumnMetadata({
target: metadata.target,
propertyName: relation.name,
isVirtual: true,
options: options
});
metadata.columns.push(relationalColumn);
}
@ -145,8 +150,16 @@ export class EntityMetadataBuilder {
name: inverseSideMetadata.table.name + "_" + inverseSideMetadata.primaryColumn.name
};
const columns = [
new ColumnMetadata(null, null, false, false, false, false, column1options),
new ColumnMetadata(null, null, false, false, false, false, column2options)
new ColumnMetadata({
target: null,
propertyName: null,
options: column1options
}),
new ColumnMetadata({
target: null,
propertyName: null,
options: column2options
})
];
const foreignKeys = [
new ForeignKeyMetadata(tableMetadata, [columns[0]], metadata.table, [metadata.primaryColumn]),

View File

@ -1,9 +1,22 @@
import {PropertyMetadata} from "./PropertyMetadata";
import {ColumnOptions} from "../options/ColumnOptions";
import {ColumnOptions, ColumnTypeString} from "../options/ColumnOptions";
import {NamingStrategy} from "../../naming-strategy/NamingStrategy";
/**
* This metadata interface contains all information about some document's column.
* Constructor arguments for ColumnMetadata class.
*/
export interface ColumnMetadataArgs {
target: Function;
propertyName: string;
isPrimaryKey?: boolean;
isCreateDate?: boolean;
isUpdateDate?: boolean;
isVirtual?: boolean;
options: ColumnOptions;
}
/**
* This metadata contains all information about class's column.
*/
export class ColumnMetadata extends PropertyMetadata {
@ -11,6 +24,9 @@ export class ColumnMetadata extends PropertyMetadata {
// Public Properties
// ---------------------------------------------------------------------
/**
* Naming strategy to be used to generate column name.
*/
namingStrategy: NamingStrategy;
// ---------------------------------------------------------------------
@ -25,112 +41,132 @@ export class ColumnMetadata extends PropertyMetadata {
/**
* The type of the column.
*/
private _type: string = "";
private _type: ColumnTypeString;
/**
* Maximum length in the database.
*/
private _length: string = "";
private _length = "";
/**
* Indicates if this column is primary key.
*/
private _isPrimary: boolean = false;
private _isPrimary = false;
/**
* Indicates if this column is auto increment.
*/
private _isAutoIncrement: boolean = false;
private _isAutoIncrement = false;
/**
* Indicates if value should be unqiue or not.
* Indicates if value should be unique or not.
*/
private _isUnique: boolean = false;
private _isUnique = false;
/**
* Indicates if can contain nulls or not.
*/
private _isNullable: boolean = false;
private _isNullable = false;
/**
* Indicates if column will contain a created date or not.
*/
private _isCreateDate: boolean = false;
private _isCreateDate = false;
/**
* Indicates if column will contain an updated date or not.
*/
private _isUpdateDate: boolean = false;
private _isUpdateDate = false;
/**
* Indicates if column will contain an updated date or not.
*/
private _isVirtual: boolean = false;
private _isVirtual = false;
/**
* Extra sql definition for the given column.
*/
private _columnDefinition: string = "";
private _columnDefinition = "";
/**
* Column comment.
*/
private _comment: string = "";
private _comment = "";
/**
* Old column name. Used to correctly alter tables when column name is changed.
*/
private _oldColumnName: string;
/**
* The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum
* number of digits that are stored for the values.
*/
private _precision: number;
/**
* The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number
* of digits to the right of the decimal point and must not be greater than precision.
*/
private _scale: number;
/**
* Column collation. Note that not all databases support it.
*/
private _collation: string;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(target: Function,
propertyName: string,
isPrimaryKey: boolean,
isCreateDate: boolean,
isUpdateDate: boolean,
isVirtual: boolean,
options: ColumnOptions) {
super(target, propertyName);
constructor(args: ColumnMetadataArgs) {
super(args.target, args.propertyName);
if (isPrimaryKey)
this._isPrimary = isPrimaryKey;
if (isCreateDate)
this._isCreateDate = isCreateDate;
if (isUpdateDate)
this._isUpdateDate = isUpdateDate;
if (isVirtual)
this._isVirtual = isVirtual;
if (options.name)
this._name = options.name;
if (options.type)
this._type = this.convertType(options.type);
if (args.isPrimaryKey)
this._isPrimary = args.isPrimaryKey;
if (args.isCreateDate)
this._isCreateDate = args.isCreateDate;
if (args.isUpdateDate)
this._isUpdateDate = args.isUpdateDate;
if (args.isVirtual)
this._isVirtual = args.isVirtual;
if (args.options.name)
this._name = args.options.name;
if (args.options.type)
this._type = args.options.type;
if (options.length)
this._length = options.length;
if (options.autoIncrement)
this._isAutoIncrement = options.autoIncrement;
if (options.unique)
this._isUnique = options.unique;
if (options.nullable)
this._isNullable = options.nullable;
if (options.columnDefinition)
this._columnDefinition = options.columnDefinition;
if (options.comment)
this._comment = options.comment;
if (options.oldColumnName)
this._oldColumnName = options.oldColumnName;
if (args.options.length)
this._length = args.options.length;
if (args.options.autoIncrement)
this._isAutoIncrement = args.options.autoIncrement;
if (args.options.unique)
this._isUnique = args.options.unique;
if (args.options.nullable)
this._isNullable = args.options.nullable;
if (args.options.columnDefinition)
this._columnDefinition = args.options.columnDefinition;
if (args.options.comment)
this._comment = args.options.comment;
if (args.options.oldColumnName)
this._oldColumnName = args.options.oldColumnName;
if (args.options.scale)
this._scale = args.options.scale;
if (args.options.precision)
this._precision = args.options.precision;
if (args.options.collation)
this._collation = args.options.collation;
if (!this._name)
this._name = propertyName;
this._name = args.propertyName;
}
// ---------------------------------------------------------------------
// Accessors
// ---------------------------------------------------------------------
/**
* Column name in the database.
*/
get name(): string {
return this.namingStrategy ? this.namingStrategy.columnName(this._name) : this._name;
}
@ -138,70 +174,111 @@ export class ColumnMetadata extends PropertyMetadata {
/**
* Type of the column.
*/
get type(): string {
get type(): ColumnTypeString {
return this._type;
}
/**
* Column type's length. For example type = "string" and length = 100 means that ORM will create a column with
* type varchar(100).
*/
get length(): string {
return this._length;
}
/**
* Indicates if this column is a primary key.
*/
get isPrimary(): boolean {
return this._isPrimary;
}
/**
* Indicates if this column's value is auto incremented.
*/
get isAutoIncrement(): boolean {
return this._isAutoIncrement;
}
/**
* Indicates if this column has unique key.
*/
get isUnique(): boolean {
return this._isUnique;
}
/**
* Indicates if this column can have a NULL value.
*/
get isNullable(): boolean {
return this._isNullable;
}
/**
* Indicates if this column is special and contains object create date.
*/
get isCreateDate(): boolean {
return this._isCreateDate;
}
/**
* Indicates if this column is special and contains object last update date.
*/
get isUpdateDate(): boolean {
return this._isUpdateDate;
}
/**
* Indicates if this column is virtual. Virtual column mean that it does not really exist in class. Virtual columns
* are used for many-to-many tables.
*/
get isVirtual(): boolean {
return this._isVirtual;
}
/**
* Extra column definition value.
*/
get columnDefinition(): string {
return this._columnDefinition;
}
/**
* Extra column's comment.
*/
get comment(): string {
return this._comment;
}
/**
* Column name used previously for this column. Used to make safe schema updates. Experimental and most probably
* will be removed in the future. Avoid using it.
*/
get oldColumnName(): string {
return this._oldColumnName;
}
// ---------------------------------------------------------------------
// Private Methods
// ---------------------------------------------------------------------
/**
* The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum
* number of digits that are stored for the values.
*/
get precision(): number {
return this._precision;
}
private convertType(type: Function|string): string {
// todo: throw exception if no type in type function
if (type instanceof Function) {
let typeName = (<any>type).name.toLowerCase();
switch (typeName) {
case "number":
case "boolean":
case "string":
return typeName;
}
}
return <string> type;
/**
* The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number
* of digits to the right of the decimal point and must not be greater than precision.
*/
get scale(): number {
return this._scale;
}
/**
* Column collation. Note that not all databases support it.
*/
get collation(): string {
return this._collation;
}
}

View File

@ -1,11 +1,220 @@
/**
* Describes all column's options.
*/
export interface ColumnOptions {
/**
* Column name.
*/
name?: string;
type?: string;
/**
* Column type. Must be one of the value from the ColumnTypes class.
*/
type?: ColumnTypeString;
/**
* Column type's length. For example type = "string" and length = 100 means that ORM will create a column with
* type varchar(100).
*/
length?: string;
/**
* Specifies if this column will use AUTO_INCREMENT or not (e.g. generated number).
*/
autoIncrement?: boolean;
/**
* Specifies if column's value must be unqiue or not.
*/
unique?: boolean;
/**
* Indicates if column must be nullable or not.
*/
nullable?: boolean;
/**
* Extra column definition. Should be used only in emergency situations. Note that if you'll use this property
* auto schema generation will not work properly anymore.
*/
columnDefinition?: string;
/**
* Column comment.
*/
comment?: string;
/**
* Column name used previously for this column. Used to make safe schema updates. Experimental and most probably
* will be removed in the future. Avoid using it.
*/
oldColumnName?: string;
/**
* The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum
* number of digits that are stored for the values.
*/
precision?: number;
/**
* The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number
* of digits to the right of the decimal point and must not be greater than precision.
*/
scale?: number;
/**
* Column collation. Note that not all databases support it.
*/
collation?: string;
}
/**
* All types that column can be.
*/
export type ColumnTypeString = "string"|"text"|"number"|"integer"|"int"|"smallint"|"bigint"|"float"|"double"|
"decimal"|"date"|"time"|"datetime"|"boolean"|"json"|"simple_array";
/**
* All types that column can be.
*/
export class ColumnTypes {
/**
* SQL VARCHAR type. Your class's property type should be a "string".
*/
static STRING = "string";
/**
* SQL CLOB type. Your class's property type should be a "string".
*/
static TEXT = "text";
/**
* SQL FLOAT type. Your class's property type should be a "number".
*/
static NUMBER = "number";
/**
* SQL INT type. Your class's property type should be a "number".
*/
static INTEGER = "integer";
/**
* SQL INT type. Your class's property type should be a "number".
*/
static INT = "int";
/**
* SQL SMALLINT type. Your class's property type should be a "number".
*/
static SMALLINT = "smallint";
/**
* SQL BIGINT type. Your class's property type should be a "number".
*/
static BIGINT = "bigint";
/**
* SQL FLOAT type. Your class's property type should be a "number".
*/
static FLOAT = "float";
/**
* SQL FLOAT type. Your class's property type should be a "number".
*/
static DOUBLE = "double";
/**
* SQL DECIMAL type. Your class's property type should be a "string".
*/
static DECIMAL = "decimal";
/**
* SQL DATETIME type. Your class's property type should be a "Date" object.
*/
static DATE = "date";
/**
* SQL TIME type. Your class's property type should be a "Date" object.
*/
static TIME = "time";
/**
* SQL DATETIME/TIMESTAMP type. Your class's property type should be a "Date" object.
*/
static DATETIME = "datetime";
/**
* SQL BOOLEAN type. Your class's property type should be a "boolean".
*/
static BOOLEAN = "boolean";
/**
* SQL CLOB type. Your class's property type should be any Object.
*/
static JSON = "json";
/**
* SQL CLOB type. Your class's property type should be array of string. Note: value in this column should not contain
* a comma (",") since this symbol is used to create a string from the array, using .join(",") operator.
*/
static SIMPLE_ARRAY = "simple_array";
/**
* Checks if given type in a string format is supported by ORM.
*/
static isTypeSupported(type: string) {
return this.supportedTypes.indexOf(type) !== -1;
}
/**
* Returns list of all supported types by the ORM.
*/
static get supportedTypes() {
return [
this.STRING,
this.TEXT,
this.NUMBER,
this.INTEGER,
this.INT,
this.SMALLINT,
this.BIGINT,
this.FLOAT,
this.DOUBLE,
this.DECIMAL,
this.DATE,
this.TIME,
this.DATETIME,
this.BOOLEAN,
this.JSON,
this.SIMPLE_ARRAY
];
}
/**
* Tries to guess a column type from the given function.
*/
static determineTypeFromFunction(type: Function): ColumnTypeString {
if (type instanceof Date) {
return "datetime";
} else if (type instanceof Function) {
const typeName = (<any>type).name.toLowerCase();
switch (typeName) {
case "number":
return "number";
case "boolean":
return "boolean";
case "string":
return "string";
}
} else if (type instanceof Object) {
return "json";
}
return undefined;
}
}

View File

@ -6,8 +6,8 @@ export class CascadesNotAllowedError extends Error {
constructor(type: "insert"|"update"|"remove", metadata: EntityMetadata, relation: RelationMetadata) {
super();
const name = entityClassOrName instanceof Function ? (<any> entityClassOrName).name : entityClassOrName;
this.message = `No broadcaster for "${name}" was found. Looks like this entity is not registered in your connection?`;
const cls = (<any> metadata.target).name;
this.message = `Cascades (${type}) are not allowed for the given relation ${cls}#${relation.name}`;
}
}