fix(postgres,mysql): better support for enum and enum array

This commit is contained in:
Vladimir Poluch 2019-01-12 22:23:47 +01:00
parent 905ecd737d
commit ebfbb009e2
12 changed files with 421 additions and 67 deletions

View File

@ -10,6 +10,7 @@
* [Column types for `postgres`](#column-types-for-postgres)
* [Column types for `sqlite` / `cordova` / `react-native` / `expo`](#column-types-for-sqlite--cordova--react-native--expo)
* [Column types for `mssql`](#column-types-for-mssql)
* [`enum` column type](#enum-column-type)
* [`simple-array` column type](#simple-array-column-type)
* [`simple-json` column type](#simple-json-column-type)
* [Columns with generated values](#columns-with-generated-values)
@ -327,6 +328,55 @@ or
`timestamp with local time zone`, `interval year to month`, `interval day to second`, `bfile`, `blob`, `clob`,
`nclob`, `rowid`, `urowid`
### `enum` column type
`enum` column type is supported by `postgres` and `mysql`. There are various possible column definitions:
Using typescript enums:
```typescript
export enum UserRole {
ADMIN = "admin",
EDITOR = "editor"
GHOST = "ghost"
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: "enum",
enum: UserRole,
default: UserRole.GHOST
})
role: UserRole
}
```
> Note: String, numeric and heterogeneous enums are supported.
Using array with enum values:
```typescript
export type UserRoleType = "admin" | "editor" | "ghost",
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
role: UserRoleType
}
```
### `simple-array` column type
There is a special column type called `simple-array` which can store primitive array values in a single string column.

View File

@ -1,6 +1,6 @@
import "reflect-metadata";
import {ConnectionOptions, createConnection} from "../../src/index";
import {EverythingEntity} from "./entity/EverythingEntity";
import {EverythingEntity, SampleEnum} from "./entity/EverythingEntity";
const options: ConnectionOptions = {
type: "mysql",
@ -36,6 +36,7 @@ createConnection(options).then(connection => {
entity.simpleArrayColumn = ["hello", "world", "of", "typescript"];
entity.jsonColumn = [{ hello: "olleh" }, { world: "dlrow" }];
entity.alsoJson = { hello: "olleh", world: "dlrow" };
entity.enum = SampleEnum.ONE;
let postRepository = connection.getRepository(EverythingEntity);
@ -67,6 +68,7 @@ createConnection(options).then(connection => {
entity.simpleArrayColumn = ["hello!", "world!", "of!", "typescript!"];
entity.jsonColumn = [{ olleh: "hello" }, { dlrow: "world" }];
entity.alsoJson = { olleh: "hello", dlrow: "world" };
entity.enum = SampleEnum.TWO;
return postRepository.save(entity);
})

View File

@ -2,6 +2,11 @@ import {Column, Entity, PrimaryGeneratedColumn} from "../../../src/index";
import {CreateDateColumn} from "../../../src/decorator/columns/CreateDateColumn";
import {UpdateDateColumn} from "../../../src/decorator/columns/UpdateDateColumn";
export enum SampleEnum {
ONE = "one",
TWO = "two"
}
@Entity("sample11_everything_entity")
export class EverythingEntity {
@ -65,6 +70,9 @@ export class EverythingEntity {
@Column("simple_array")
simpleArrayColumn: string[];
@Column("enum", { enum: SampleEnum })
enum: SampleEnum;
@CreateDateColumn()
createdDate: Date;

View File

@ -439,6 +439,8 @@ export class MysqlDriver implements Driver {
} else if (columnMetadata.type === "simple-json") {
return DateUtils.simpleJsonToString(value);
} else if (columnMetadata.type === "enum") {
return "" + value;
}
return value;
@ -471,6 +473,15 @@ export class MysqlDriver implements Driver {
} else if (columnMetadata.type === "simple-json") {
value = DateUtils.stringToSimpleJson(value);
} else if (
columnMetadata.type === "enum"
&& columnMetadata.enum
&& !isNaN(value)
&& columnMetadata.enum.indexOf(parseInt(value)) >= 0
) {
// convert to number if that exists in poosible enum options
value = parseInt(value);
}
if (columnMetadata.transformer)
@ -530,6 +541,10 @@ export class MysqlDriver implements Driver {
normalizeDefault(columnMetadata: ColumnMetadata): string {
const defaultValue = columnMetadata.default;
if (columnMetadata.type === "enum" && defaultValue !== undefined) {
return `'${defaultValue}'`;
}
if (typeof defaultValue === "number") {
return "" + defaultValue;

View File

@ -411,6 +411,9 @@ export class PostgresDriver implements Driver {
} else if (columnMetadata.type === "simple-json") {
return DateUtils.simpleJsonToString(value);
} else if (columnMetadata.type === "enum" && !columnMetadata.isArray) {
return "" + value;
}
return value;
@ -459,12 +462,20 @@ export class PostgresDriver implements Driver {
} else if (columnMetadata.type === "simple-json") {
value = DateUtils.stringToSimpleJson(value);
} else if (columnMetadata.type === "enum" ) {
if (columnMetadata.isArray) {
// manually convert enum array to array of values (pg does not support, see https://github.com/brianc/node-pg-types/issues/56)
value = value !== "{}" ? (value as string).substr(1, (value as string).length - 2).split(",") : [];
// convert to number if that exists in poosible enum options
value = value.map((val: string) => {
return !isNaN(+val) && columnMetadata.enum!.indexOf(parseInt(val)) >= 0 ? parseInt(val) : val;
});
} else {
// convert to number if that exists in poosible enum options
value = !isNaN(+value) && columnMetadata.enum!.indexOf(parseInt(value)) >= 0 ? parseInt(value) : value;
}
}
// manually convert enum array to array of values (pg does not support, see https://github.com/brianc/node-pg-types/issues/56)
if (columnMetadata.enum && columnMetadata.isArray)
value = value !== "{}" ? (value as string).substr(1, (value as string).length - 2).split(",") : [];
if (columnMetadata.transformer)
value = columnMetadata.transformer.from(value);
@ -587,6 +598,13 @@ export class PostgresDriver implements Driver {
const defaultValue = columnMetadata.default;
const arrayCast = columnMetadata.isArray ? `::${columnMetadata.type}[]` : "";
if (columnMetadata.type === "enum" && defaultValue !== undefined) {
if (columnMetadata.isArray && Array.isArray(defaultValue)) {
return `'{${defaultValue.map((val: string) => `${val}`).join(",")}}'`;
}
return `'${defaultValue}'`;
}
if (typeof defaultValue === "number") {
return "" + defaultValue;

View File

@ -349,10 +349,10 @@ export class ColumnMetadata {
if (options.args.options.precision !== undefined)
this.precision = options.args.options.precision;
if (options.args.options.enum) {
if (options.args.options.enum instanceof Object) {
this.enum = Object.keys(options.args.options.enum).map(key => {
return (options.args.options.enum as ObjectLiteral)[key];
});
if (options.args.options.enum instanceof Object && !Array.isArray(options.args.options.enum)) {
this.enum = Object.keys(options.args.options.enum)
.filter(key => isNaN(+key)) // remove numeric keys - typescript numeric enum types generate them
.map(key => (options.args.options.enum as ObjectLiteral)[key]);
} else {
this.enum = options.args.options.enum;

View File

@ -1,37 +0,0 @@
import "reflect-metadata";
import {Post, PostType} from "./entity/Post";
import {Connection} from "../../../../src";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
describe("database schema > enum arrays", () => {
let connections: Connection[];
before(async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"],
});
});
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should correctly create column with collation option", () => Promise.all(connections.map(async connection => {
const postRepository = connection.getRepository(Post);
const post = new Post();
post.id = 1;
post.type = [PostType.advertising, PostType.blog];
post.numbers = [1, 2, 3];
await postRepository.save(post);
const loadedPost = await postRepository.findOne(1);
loadedPost!.should.be.eql({
id: 1,
type: [PostType.advertising, PostType.blog],
numbers: [ 1, 2, 3 ]
});
})));
});

View File

@ -1,21 +0,0 @@
import {Column, Entity, PrimaryGeneratedColumn} from "../../../../../src";
export enum PostType {
blog = "blog",
news = "news",
advertising = "advertising"
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: "enum", enum: PostType, array: true })
type: PostType[];
@Column({ type: "int", array: true })
numbers: number[];
}

View File

@ -0,0 +1,105 @@
import { Entity, Column, PrimaryColumn } from "../../../../../src";
export enum NumericEnum {
ADMIN,
EDITOR,
MODERATOR,
GHOST
}
export enum StringEnum {
ADMIN = "a",
EDITOR = "e",
MODERATOR = "m",
GHOST = "g"
}
export enum StringNumericEnum {
ONE = "1",
TWO = "2",
THREE = "3",
FOUR = "4"
}
export enum HeterogeneousEnum {
NO = 0,
YES = "YES",
}
export type ArrayDefinedStringEnumType = "admin" | "editor" | "ghost";
export type ArrayDefinedNumericEnumType = 11 | 12 | 13;
@Entity()
export class EnumArrayEntity {
@PrimaryColumn()
id: number;
@Column({
type: "enum",
enum: NumericEnum,
array: true,
default: [NumericEnum.GHOST, NumericEnum.ADMIN]
})
numericEnums: NumericEnum[];
@Column({
type: "enum",
enum: StringEnum,
array: true,
default: []
})
stringEnums: StringEnum[];
@Column({
type: "enum",
enum: StringNumericEnum,
array: true,
default: [StringNumericEnum.THREE, StringNumericEnum.ONE]
})
stringNumericEnums: StringNumericEnum[];
@Column({
type: "enum",
enum: HeterogeneousEnum,
array: true,
default: [HeterogeneousEnum.YES]
})
heterogeneousEnums: HeterogeneousEnum[];
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
array: true,
default: ["admin"]
})
arrayDefinedStringEnums: ArrayDefinedStringEnumType[];
@Column({
type: "enum",
enum: [11, 12, 13],
array: true,
default: [11, 13]
})
arrayDefinedNumericEnums: ArrayDefinedNumericEnumType[];
@Column({
type: "enum",
enum: StringEnum,
array: true,
nullable: true
})
enumWithoutDefault: StringEnum[];
@Column({
type: "enum",
enum: StringEnum,
array: true,
default: "{}"
})
legacyDefaultAsString: StringEnum[];
}

View File

@ -0,0 +1,62 @@
import "reflect-metadata";
import { Connection } from "../../../../src";
import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../../utils/test-utils";
import { EnumArrayEntity, NumericEnum, StringEnum, HeterogeneousEnum, StringNumericEnum } from "./entity/EnumArrayEntity";
describe("database schema > enum arrays", () => {
let connections: Connection[];
before(async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres"]
});
});
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should correctly create default values", () => Promise.all(connections.map(async connection => {
const enumEntityRepository = connection.getRepository(EnumArrayEntity);
const enumEntity = new EnumArrayEntity();
enumEntity.id = 1;
await enumEntityRepository.save(enumEntity);
const loadedEnumEntity = await enumEntityRepository.findOne(1);
loadedEnumEntity!.numericEnums.should.be.eql([NumericEnum.GHOST, NumericEnum.ADMIN]);
loadedEnumEntity!.stringEnums.should.be.eql([]);
loadedEnumEntity!.stringNumericEnums.should.be.eql([StringNumericEnum.THREE, StringNumericEnum.ONE]);
loadedEnumEntity!.heterogeneousEnums.should.be.eql([HeterogeneousEnum.YES]);
loadedEnumEntity!.arrayDefinedStringEnums.should.be.eql(["admin"]);
loadedEnumEntity!.arrayDefinedNumericEnums.should.be.eql([11, 13]);
})));
it("should correctly save and retrieve", () => Promise.all(connections.map(async connection => {
const enumEntityRepository = connection.getRepository(EnumArrayEntity);
const enumEntity = new EnumArrayEntity();
enumEntity.id = 1;
enumEntity.numericEnums = [NumericEnum.GHOST, NumericEnum.EDITOR];
enumEntity.stringEnums = [StringEnum.MODERATOR];
enumEntity.stringNumericEnums = [StringNumericEnum.FOUR];
enumEntity.heterogeneousEnums = [HeterogeneousEnum.NO];
enumEntity.arrayDefinedStringEnums = ["editor"];
enumEntity.arrayDefinedNumericEnums = [12, 13];
await enumEntityRepository.save(enumEntity);
const loadedEnumEntity = await enumEntityRepository.findOne(1);
loadedEnumEntity!.numericEnums.should.be.eql([NumericEnum.GHOST, NumericEnum.EDITOR]);
loadedEnumEntity!.stringEnums.should.be.eql([StringEnum.MODERATOR]);
loadedEnumEntity!.stringNumericEnums.should.be.eql([StringNumericEnum.FOUR]);
loadedEnumEntity!.heterogeneousEnums.should.be.eql([HeterogeneousEnum.NO]);
loadedEnumEntity!.arrayDefinedStringEnums.should.be.eql(["editor"]);
loadedEnumEntity!.arrayDefinedNumericEnums.should.be.eql([12, 13]);
})));
});

View File

@ -0,0 +1,89 @@
import { Entity, Column, PrimaryColumn } from "../../../../../src";
export enum NumericEnum {
ADMIN,
EDITOR,
MODERATOR,
GHOST
}
export enum StringEnum {
ADMIN = "a",
EDITOR = "e",
MODERATOR = "m",
GHOST = "g"
}
export enum StringNumericEnum {
ONE = "1",
TWO = "2",
THREE = "3",
FOUR = "4"
}
export enum HeterogeneousEnum {
NO = 0,
YES = "YES",
}
export type ArrayDefinedStringEnumType = "admin" | "editor" | "ghost";
export type ArrayDefinedNumericEnumType = 11 | 12 | 13;
@Entity()
export class EnumEntity {
@PrimaryColumn()
id: number;
@Column({
type: "enum",
enum: NumericEnum,
default: NumericEnum.MODERATOR
})
numericEnum: NumericEnum;
@Column({
type: "enum",
enum: StringEnum,
default: StringEnum.GHOST
})
stringEnum: StringEnum;
@Column({
type: "enum",
enum: StringNumericEnum,
default: StringNumericEnum.FOUR
})
stringNumericEnum: StringNumericEnum;
@Column({
type: "enum",
enum: HeterogeneousEnum,
default: HeterogeneousEnum.NO
})
heterogeneousEnum: HeterogeneousEnum;
@Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
arrayDefinedStringEnum: ArrayDefinedStringEnumType;
@Column({
type: "enum",
enum: [11, 12, 13],
default: 12
})
arrayDefinedNumericEnum: ArrayDefinedNumericEnumType;
@Column({
type: "enum",
enum: StringEnum,
})
enumWithoutdefault: StringEnum;
}

View File

@ -0,0 +1,63 @@
import "reflect-metadata";
import { Connection } from "../../../../src";
import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../../utils/test-utils";
import { EnumEntity, NumericEnum, StringEnum, HeterogeneousEnum, StringNumericEnum } from "./entity/EnumEntity";
describe("database schema > enums", () => {
let connections: Connection[];
before(async () => {
connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
enabledDrivers: ["postgres", "mysql"]
});
});
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should correctly use default values", () => Promise.all(connections.map(async connection => {
const enumEntityRepository = connection.getRepository(EnumEntity);
const enumEntity = new EnumEntity();
enumEntity.id = 1;
enumEntity.enumWithoutdefault = StringEnum.EDITOR;
await enumEntityRepository.save(enumEntity);
const loadedEnumEntity = await enumEntityRepository.findOne(1);
loadedEnumEntity!.numericEnum.should.be.eq(NumericEnum.MODERATOR);
loadedEnumEntity!.stringEnum.should.be.eq(StringEnum.GHOST);
loadedEnumEntity!.stringNumericEnum.should.be.eq(StringNumericEnum.FOUR);
loadedEnumEntity!.heterogeneousEnum.should.be.eq(HeterogeneousEnum.NO);
loadedEnumEntity!.arrayDefinedStringEnum.should.be.eq("ghost");
loadedEnumEntity!.arrayDefinedNumericEnum.should.be.eq(12);
})));
it("should correctly save and retrieve", () => Promise.all(connections.map(async connection => {
const enumEntityRepository = connection.getRepository(EnumEntity);
const enumEntity = new EnumEntity();
enumEntity.id = 1;
enumEntity.numericEnum = NumericEnum.EDITOR;
enumEntity.stringEnum = StringEnum.ADMIN;
enumEntity.stringNumericEnum = StringNumericEnum.TWO;
enumEntity.heterogeneousEnum = HeterogeneousEnum.YES;
enumEntity.arrayDefinedStringEnum = "editor";
enumEntity.arrayDefinedNumericEnum = 13;
enumEntity.enumWithoutdefault = StringEnum.ADMIN;
await enumEntityRepository.save(enumEntity);
const loadedEnumEntity = await enumEntityRepository.findOne(1);
loadedEnumEntity!.numericEnum.should.be.eq(NumericEnum.EDITOR);
loadedEnumEntity!.stringEnum.should.be.eq(StringEnum.ADMIN);
loadedEnumEntity!.stringNumericEnum.should.be.eq(StringNumericEnum.TWO);
loadedEnumEntity!.heterogeneousEnum.should.be.eq(HeterogeneousEnum.YES);
loadedEnumEntity!.arrayDefinedStringEnum.should.be.eq("editor");
loadedEnumEntity!.arrayDefinedNumericEnum.should.be.eq(13);
})));
});