diff --git a/.travis.yml b/.travis.yml index 16dc5462b8..78d8db8c74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,8 @@ language: node_js node_js: -# - stable + - 12 - 10 - - 9 - 8 -# - 6 -# - 4 -# - 0.12 -# - 0.11 services: - docker diff --git a/CHANGELOG.md b/CHANGELOG.md index e93f859881..36b00c8859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,36 @@ -# Changelog +## [0.2.19](https://github.com/typeorm/typeorm/compare/0.2.18...0.2.19) (2019-09-13) -TypeORM follows a semantic versioning and until `1.0.0` breaking changes may appear in `0.x.x` versions, -however since API is already quite stable we don't expect too much breaking changes. -If we missed a note on some change or you have a questions on migrating from old version, -feel free to ask us and community. +### Bug Fixes + +* "database" option error in driver when use "url" option for connection ([690e6f5](https://github.com/typeorm/typeorm/commit/690e6f5)) +* "hstore injection" & properly handle NULL, empty string, backslashes & quotes in hstore key/value pairs ([#4720](https://github.com/typeorm/typeorm/issues/4720)) ([3abe5b9](https://github.com/typeorm/typeorm/commit/3abe5b9)) +* add SaveOptions and RemoveOptions into ActiveRecord ([#4318](https://github.com/typeorm/typeorm/issues/4318)) ([a6d7ba2](https://github.com/typeorm/typeorm/commit/a6d7ba2)) +* apostrophe in Postgres enum strings breaks query ([#4631](https://github.com/typeorm/typeorm/issues/4631)) ([445c740](https://github.com/typeorm/typeorm/commit/445c740)) +* change PrimaryColumn decorator to clone passed options ([#4571](https://github.com/typeorm/typeorm/issues/4571)) ([3cf470d](https://github.com/typeorm/typeorm/commit/3cf470d)), closes [#4570](https://github.com/typeorm/typeorm/issues/4570) +* createQueryBuilder relation remove works only if using ID ([#2632](https://github.com/typeorm/typeorm/issues/2632)) ([#4734](https://github.com/typeorm/typeorm/issues/4734)) ([1d73a90](https://github.com/typeorm/typeorm/commit/1d73a90)) +* resolve issue with conversion string to simple-json ([#4476](https://github.com/typeorm/typeorm/issues/4476)) ([d1594f5](https://github.com/typeorm/typeorm/commit/d1594f5)), closes [#4440](https://github.com/typeorm/typeorm/issues/4440) +* sqlite connections don't ignore the schema property ([#4599](https://github.com/typeorm/typeorm/issues/4599)) ([d8f1c81](https://github.com/typeorm/typeorm/commit/d8f1c81)) +* the excessive stack depth comparing types `FindConditions` and `FindConditions` problem ([#4470](https://github.com/typeorm/typeorm/issues/4470)) ([7a0beed](https://github.com/typeorm/typeorm/commit/7a0beed)) +* views generating broken Migrations ([#4726](https://github.com/typeorm/typeorm/issues/4726)) ([c52b3d2](https://github.com/typeorm/typeorm/commit/c52b3d2)), closes [#4123](https://github.com/typeorm/typeorm/issues/4123) + + +### Features -## 0.2.18 (UNRELEASED) +* add `set` datatype support for MySQL/MariaDB ([#4538](https://github.com/typeorm/typeorm/issues/4538)) ([19e2179](https://github.com/typeorm/typeorm/commit/19e2179)), closes [#2779](https://github.com/typeorm/typeorm/issues/2779) +* add materialized View support for Postgres ([#4478](https://github.com/typeorm/typeorm/issues/4478)) ([dacac83](https://github.com/typeorm/typeorm/commit/dacac83)), closes [#4317](https://github.com/typeorm/typeorm/issues/4317) [#3996](https://github.com/typeorm/typeorm/issues/3996) +* add mongodb `useUnifiedTopology` config parameter ([#4684](https://github.com/typeorm/typeorm/issues/4684)) ([92e4270](https://github.com/typeorm/typeorm/commit/92e4270)) +* add multi-dimensional cube support for PostgreSQL ([#4378](https://github.com/typeorm/typeorm/issues/4378)) ([b6d6278](https://github.com/typeorm/typeorm/commit/b6d6278)) +* add options to input init config for sql.js ([#4560](https://github.com/typeorm/typeorm/issues/4560)) ([5c311ed](https://github.com/typeorm/typeorm/commit/5c311ed)) +* add postgres pool error handler ([#4474](https://github.com/typeorm/typeorm/issues/4474)) ([a925be9](https://github.com/typeorm/typeorm/commit/a925be9)) +* add referenced table metadata to NamingStrategy to resolve foreign key name ([#4274](https://github.com/typeorm/typeorm/issues/4274)) ([0094f61](https://github.com/typeorm/typeorm/commit/0094f61)), closes [#3847](https://github.com/typeorm/typeorm/issues/3847) [#1355](https://github.com/typeorm/typeorm/issues/1355) +* add support for ON CONFLICT for cockroach ([#4518](https://github.com/typeorm/typeorm/issues/4518)) ([db8074a](https://github.com/typeorm/typeorm/commit/db8074a)), closes [#4513](https://github.com/typeorm/typeorm/issues/4513) +* Added support for DISTINCT queries ([#4109](https://github.com/typeorm/typeorm/issues/4109)) ([39a8e34](https://github.com/typeorm/typeorm/commit/39a8e34)) +* Aurora Data API ([#4375](https://github.com/typeorm/typeorm/issues/4375)) ([c321562](https://github.com/typeorm/typeorm/commit/c321562)) +* export additional schema builder classes ([#4325](https://github.com/typeorm/typeorm/issues/4325)) ([e589fda](https://github.com/typeorm/typeorm/commit/e589fda)) +* log files loaded from glob patterns ([#4346](https://github.com/typeorm/typeorm/issues/4346)) ([e12479e](https://github.com/typeorm/typeorm/commit/e12479e)), closes [#4162](https://github.com/typeorm/typeorm/issues/4162) +* UpdateResult returns affected rows in postgresql ([#4432](https://github.com/typeorm/typeorm/issues/4432)) ([7808bba](https://github.com/typeorm/typeorm/commit/7808bba)), closes [#1308](https://github.com/typeorm/typeorm/issues/1308) + +## 0.2.18 ### Bug fixes @@ -24,6 +49,7 @@ feel free to ask us and community. * extend afterLoad() subscriber interface to take LoadEvent ([issue #4185](https://github.com/typeorm/typeorm/issues/4185)) * relation decorators (e.g. `@OneToMany`) now also accept `string` instead of `typeFunction`, which prevents circular dependency issues in the frontend/browser ([issue #4190](https://github.com/typeorm/typeorm/issues/4190)) * added support for metadata reflection in typeorm-class-transformer-shim.js ([issue #4219](https://github.com/typeorm/typeorm/issues/4219)) +* added `sqlJsConfig` to input config when initializing sql.js ([issue #4559](https://github.com/typeorm/typeorm/issues/4559)) ## 0.2.17 (2019-05-01) diff --git a/README.md b/README.md index 954a8ed7db..b5a82b21fc 100755 --- a/README.md +++ b/README.md @@ -1261,6 +1261,8 @@ There are a few repositories which you can clone and start with: * [Example how to use TypeORM in a Cordova/PhoneGap app](https://github.com/typeorm/cordova-example) * [Example how to use TypeORM with an Ionic app](https://github.com/typeorm/ionic-example) * [Example how to use TypeORM with React Native](https://github.com/typeorm/react-native-example) +* [Example how to use TypeORM with Nativescript-Vue](https://github.com/typeorm/nativescript-vue-typeorm-sample) +* [Example how to use TypeORM with Nativescript-Angular](https://github.com/betov18x/nativescript-angular-typeorm-example) * [Example how to use TypeORM with Electron using JavaScript](https://github.com/typeorm/electron-javascript-example) * [Example how to use TypeORM with Electron using TypeScript](https://github.com/typeorm/electron-typescript-example) diff --git a/docs/active-record-data-mapper.md b/docs/active-record-data-mapper.md index f103d7117f..0190c79c2b 100644 --- a/docs/active-record-data-mapper.md +++ b/docs/active-record-data-mapper.md @@ -185,7 +185,7 @@ Learn more about [custom repositories](custom-repository.md). The decision is up to you. Both strategies have their own cons and pros. -One thing we should always keep in mind in software development is how we are going to maintain it. -The `Data Mapper` approach helps you with maintainability of your software which is more effective in bigger apps. -The `Active record` approach helps you to keep things simple which works good in small apps. +One thing we should always keep in mind in with software development is how we are going to maintain our applications. +The `Data Mapper` approach helps with maintainability, which is more effective in bigger apps. +The `Active record` approach helps keep things simple which works well in smaller apps. And simplicity is always a key to better maintainability. diff --git a/docs/connection-options.md b/docs/connection-options.md index c222aaa24c..daa7cc207d 100644 --- a/docs/connection-options.md +++ b/docs/connection-options.md @@ -180,6 +180,8 @@ See [SSL options](https://github.com/mysqljs/mysql#ssl-options). * `uuidExtension` - The Postgres extension to use when generating UUIDs. Defaults to `uuid-ossp`. Can be changed to `pgcrypto` if the `uuid-ossp` extension is unavailable. +* `poolErrorHandler` - A function that get's called when underlying pool emits `'error'` event. Takes single parameter (error instance) and defaults to logging with `warn` level. + ## `sqlite` connection options * `database` - Database path. For example "./mydb.sql" @@ -258,6 +260,8 @@ See [SSL options](https://github.com/mysqljs/mysql#ssl-options). * `pool.idleTimeoutMillis` - the minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due to idle time. Supersedes `softIdleTimeoutMillis`. Default: `30000`. + * `pool.errorHandler` - A function that get's called when underlying pool emits `'error'` event. Takes single parameter (error instance) and defaults to logging with `warn` level. + * `options.fallbackToDefaultDb` - By default, if the database requestion by `options.database` cannot be accessed, the connection will fail with an error. However, if `options.fallbackToDefaultDb` is set to `true`, then the user's default database will be used instead (Default: `false`). @@ -461,6 +465,8 @@ See [SSL options](https://github.com/mysqljs/mysql#ssl-options). * `database`: The raw UInt8Array database that should be imported. +* `sqlJsConfig`: Optional initialize config for sql.js. + * `autoSave`: Whether or not autoSave should be disabled. If set to true the database will be saved to the given file location (Node.js) or LocalStorage element (browser) when a change happens and `location` is specified. Otherwise `autoSaveCallback` can be used. * `autoSaveCallback`: A function that get's called when changes to the database are made and `autoSave` is enabled. The function gets a `UInt8Array` that represents the database. diff --git a/docs/entities.md b/docs/entities.md index 1f8b808b04..7f212b5160 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -294,7 +294,7 @@ or `bit`, `int`, `integer`, `tinyint`, `smallint`, `mediumint`, `bigint`, `float`, `double`, `double precision`, `dec`, `decimal`, `numeric`, `fixed`, `bool`, `boolean`, `date`, `datetime`, `timestamp`, `time`, `year`, `char`, `nchar`, `national char`, `varchar`, `nvarchar`, `national varchar`, -`text`, `tinytext`, `mediumtext`, `blob`, `longtext`, `tinyblob`, `mediumblob`, `longblob`, `enum`, +`text`, `tinytext`, `mediumtext`, `blob`, `longtext`, `tinyblob`, `mediumblob`, `longblob`, `enum`, `set`, `json`, `binary`, `varbinary`, `geometry`, `point`, `linestring`, `polygon`, `multipoint`, `multilinestring`, `multipolygon`, `geometrycollection` @@ -307,7 +307,7 @@ or `date`, `time`, `time without time zone`, `time with time zone`, `interval`, `bool`, `boolean`, `enum`, `point`, `line`, `lseg`, `box`, `path`, `polygon`, `circle`, `cidr`, `inet`, `macaddr`, `tsvector`, `tsquery`, `uuid`, `xml`, `json`, `jsonb`, `int4range`, `int8range`, `numrange`, -`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography` +`tsrange`, `tstzrange`, `daterange`, `geometry`, `geography`, `cube` ### Column types for `cockroachdb` @@ -390,6 +390,52 @@ export class User { } ``` +### `set` column type + +`set` column type is supported by `mariadb` 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: "set", + enum: UserRole, + default: [UserRole.GHOST, UserRole.EDITOR] + }) + roles: UserRole[] + +} +``` + +Using array with `set` values: +```typescript +export type UserRoleType = "admin" | "editor" | "ghost", + +@Entity() +export class User { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ + type: "set", + enum: ["admin", "editor", "ghost"], + default: ["ghost", "editor"] + }) + roles: UserRoleType[] +} +``` ### `simple-array` column type diff --git a/docs/example-with-express.md b/docs/example-with-express.md index 1f4b98be62..cfbafe7e04 100644 --- a/docs/example-with-express.md +++ b/docs/example-with-express.md @@ -233,7 +233,7 @@ createConnection().then(connection => { }); app.delete("/users/:id", async function(req: Request, res: Response) { - const results = await userRepository.remove(req.params.id); + const results = await userRepository.delete(req.params.id); return res.send(results); }); diff --git a/docs/faq.md b/docs/faq.md index 24cf6d6bfe..8f4fd5f1ee 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -131,7 +131,7 @@ join column / junction table settings, like join column name or junction table n It's not possible to add extra columns into a table created by a many-to-many relation. You'll need to create a separate entity and bind it using two many-to-one relations with the target entities (the effect will be same as creating a many-to-many table), -and add extra columns in there. +and add extra columns in there. You can read more about this in [Many-to-Many relations](./many-to-many-relations.md#many-to-many-relations-with-custom-properties). ## How to use TypeORM with a dependency injection tool? diff --git a/docs/many-to-many-relations.md b/docs/many-to-many-relations.md index 3070692b50..e066fd1b75 100644 --- a/docs/many-to-many-relations.md +++ b/docs/many-to-many-relations.md @@ -169,3 +169,50 @@ const categoriesWithQuestions = await connection .leftJoinAndSelect("category.questions", "question") .getMany(); ``` + +## many-to-many relations with custom properties + +In case you need to have additional properties to your many-to-many relationship you have to create a new entity yourself. +For example if you would like entities `Post` and `Category` to have a many-to-many relationship with a `createdAt` property +associated to it you have to create entity `PostToCategory` like the following: + +```typescript +import { Entity, Column, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Post } from "./post"; +import { Category } from "./category"; + +@Entity() +export class PostToCategory { + @PrimaryGeneratedColumn() + public postToCategoryId!: number; + + @Column() + public postId!: number; + + @Column() + public categoryId!: number; + + @Column() + public order!: number; + + @ManyToOne(type => Post, post => post.postToCategories) + public post!: Post; + + @ManyToOne(type => Category, category => category.postToCategories) + public category!: Category; +} +``` + +Additionally you will have to add a relationship like the following to `Post` and `Category`: + +```typescript +// category.ts +... +@OneToMany((type) => PostToCategory, (postToCategory) => postToCategory.category) +public postToCategories!: PostToCategory[]; + +// post.ts +... +@OneToMany((type) => PostToCategory, (postToCategory) => postToCategory.post) +public postToCategories!: PostToCategory[]; +``` diff --git a/docs/many-to-one-one-to-many-relations.md b/docs/many-to-one-one-to-many-relations.md index a8a08b37dc..c2e4472877 100644 --- a/docs/many-to-one-one-to-many-relations.md +++ b/docs/many-to-one-one-to-many-relations.md @@ -45,7 +45,7 @@ export class User { Here we added `@OneToMany` to the `photos` property and specified the target relation type to be `Photo`. You can omit `@JoinColumn` in a `@ManyToOne` / `@OneToMany` relation. `@OneToMany` cannot exist without `@ManyToOne`. -If you want to use `@OneToMany`, `@ManyToOne` is required. +If you want to use `@OneToMany`, `@ManyToOne` is required. However, the inverse is not required: If you only care about the `@ManyToOne` relationship, you can define it without having `@OneToMany` on the related entity. Where you set `@ManyToOne` - its related entity will have "relation id" and foreign key. This example will produce following tables: diff --git a/docs/relations.md b/docs/relations.md index 92bdd06c46..0e50a2e593 100644 --- a/docs/relations.md +++ b/docs/relations.md @@ -141,7 +141,7 @@ You can also change the name of the generated "junction" table. ```typescript @ManyToMany(type => Category) @JoinTable({ - name: "question_categories" // table name for the junction table of this relation + name: "question_categories", // table name for the junction table of this relation joinColumn: { name: "question", referencedColumnName: "id" diff --git a/docs/select-query-builder.md b/docs/select-query-builder.md index b1942e315a..5c075c6811 100644 --- a/docs/select-query-builder.md +++ b/docs/select-query-builder.md @@ -304,7 +304,7 @@ Which will produce: SELECT ... FROM users user WHERE user.name = 'Timber' ``` -You can add `AND` into an exist `WHERE` expression: +You can add `AND` into an existing `WHERE` expression: ```typescript createQueryBuilder("user") diff --git a/docs/separating-entity-definition.md b/docs/separating-entity-definition.md index db7e6be5dd..a45f35abe8 100644 --- a/docs/separating-entity-definition.md +++ b/docs/separating-entity-definition.md @@ -187,7 +187,7 @@ export const CategoryEntity = new EntitySchema({ }); ``` -Be sure to add the `extended` columns also to the `Categeory` interface (e.g., via `export interface Category extend BaseEntity`). +Be sure to add the `extended` columns also to the `Category` interface (e.g., via `export interface Category extend BaseEntity`). ## Using Schemas to Query / Insert Data diff --git a/docs/supported-platforms.md b/docs/supported-platforms.md index 64dffa361d..3753dbbff1 100644 --- a/docs/supported-platforms.md +++ b/docs/supported-platforms.md @@ -63,7 +63,7 @@ TypeORM is able to on React Native apps using the [react-native-sqlite-storage]( ## Expo -TypeORM is able to run on Expo apps using the [Expo SQLite API](https://docs.expo.io/versions/latest/sdk/sqlite.html). For an example how to use TypeORM in Expo see [typeorm/react-native-example](https://github.com/typeorm/react-native-example). +TypeORM is able to run on Expo apps using the [Expo SQLite API](https://docs.expo.io/versions/latest/sdk/sqlite/). For an example how to use TypeORM in Expo see [typeorm/expo-example](https://github.com/typeorm/expo-example). ## NativeScript diff --git a/docs/transactions.md b/docs/transactions.md index d27b1659a7..0c03fb6efe 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -13,7 +13,7 @@ Examples: ```typescript import {getConnection} from "typeorm"; -await getConnection().transaction(transactionalEntityManager => { +await getConnection().transaction(async transactionalEntityManager => { }); ``` @@ -23,7 +23,7 @@ or ```typescript import {getManager} from "typeorm"; -await getManager().transaction(transactionalEntityManager => { +await getManager().transaction(async transactionalEntityManager => { }); ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000000..4baf4d8921 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,16 @@ +# Troubleshooting + +* [Glob patterns](#glob-patterns) + +## Glob Patterns + +Glob patterns are used in the TypeOrm to specify the locations of entities, migrations, subscriber and other information. Errors in the patterns can lead to the common `RepositoryNotFoundError` and familiar errors. In order to check if any files were loaded by TypeOrm using the glob patterns, all you need to do is set the logging level to `info` such as explained in the [Logging](./logging.md) section of the documentation. This will allow you to have logs in the console that may look like this: + +```bash +# in case of an error + INFO: No classes were found using the provided glob pattern: "dist/**/*.entity{.ts}" +``` +```bash +# when files are found +INFO: All classes found using provided glob pattern "dist/**/*.entity{.js,.ts}" : "dist/app/user/user.entity.js | dist/app/common/common.entity.js" +``` \ No newline at end of file diff --git a/docs/using-cli.md b/docs/using-cli.md index e56a9b338c..8b44459850 100644 --- a/docs/using-cli.md +++ b/docs/using-cli.md @@ -5,7 +5,7 @@ * [Create a new entity](#create-a-new-entity) * [Create a new subscriber](#create-a-new-subscriber) * [Create a new migration](#create-a-new-migration) -* [Generate a migration from exist table schema](#generate-a-migration-from-exist-table-schema) +* [Generate a migration from existing table schema](#generate-a-migration-from-exist-table-schema) * [Run migrations](#run-migrations) * [Revert migrations](#revert-migrations) * [Show migrations](#show-migrations) @@ -196,7 +196,7 @@ typeorm migration:create -n UserMigration -d src/user/migration Learn more about [Migrations](./migrations.md). -## Generate a migration from exist table schema +## Generate a migration from existing table schema Automatic migration generation creates a new migration file and writes all sql queries that must be executed to update the database. diff --git a/docs/using-ormconfig.md b/docs/using-ormconfig.md index ae011efad9..67faa69aec 100644 --- a/docs/using-ormconfig.md +++ b/docs/using-ormconfig.md @@ -1,4 +1,4 @@ -# ormconfig.json +# Using Configuration Sources - [Creating a new connection from the configuration file](#creating-a-new-connection-from-the-configuration-file) - [Using `ormconfig.json`](#using-ormconfigjson) @@ -96,7 +96,7 @@ TYPEORM_DATABASE = test TYPEORM_PORT = 3000 TYPEORM_SYNCHRONIZE = true TYPEORM_LOGGING = true -TYPEORM_ENTITIES = entity/.*js,modules/**/entity/.*js +TYPEORM_ENTITIES = entity/*.js,modules/**/entity/*.js ``` List of available env variables you can set: diff --git a/docs/zh_CN/connection-options.md b/docs/zh_CN/connection-options.md index d287e6be19..b7273998f1 100644 --- a/docs/zh_CN/connection-options.md +++ b/docs/zh_CN/connection-options.md @@ -372,6 +372,8 @@ - `database`: 应导入的原始 UInt8Array 数据库。 +- `sqlJsConfig`: sql.js可选启动配置 + - `autoSave`: 是否应禁用 autoSave。如果设置为 true,则在发生更改并指定`location`时,数据库将保存到给定的文件位置(Node.js)或 LocalStorage(浏览器)。否则可以使用`autoSaveCallback`。 - `autoSaveCallback`: 在对数据库进行更改并启用`autoSave`时调用的函数。该函数获取表示数据库的`UInt8Array`。 diff --git a/docs/zh_CN/entities.md b/docs/zh_CN/entities.md index f56d8f7c3c..95f47560b4 100644 --- a/docs/zh_CN/entities.md +++ b/docs/zh_CN/entities.md @@ -346,49 +346,6 @@ export class User { } ``` -### `simple-array` column type - -`postgres`和`mysql`支持`enum`列类型。 有多种列定义方式: - -使用 typescript 枚举: - -```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 - } -``` - -> 注意:支持字符串,数字和异构枚举。 -> 使用带枚举值的数组: - -```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`的列类型 有一种称为`simple-array`的特殊列类型,它可以将原始数组值存储在单个字符串列中。 diff --git a/docs/zh_CN/troubleshooting.md b/docs/zh_CN/troubleshooting.md new file mode 100644 index 0000000000..eda40222aa --- /dev/null +++ b/docs/zh_CN/troubleshooting.md @@ -0,0 +1,16 @@ +# 故障排除 + +* [全球模式](#全球模式) + +## 全球模式 + +在类型中使用全局模式来指定实体,迁移,订户和其他信息的位置。模式中的错误可能导致常见的`RepositoryNotFoundError`和熟悉的错误。为了检查TypeOrm是否使用glob模式加载了任何文件,您需要做的就是将日志级别设置为`info`,如文档的[Logging](./logging.md)部分所述。 这将允许您拥有可能如下所示的日志: + +```bash +# 如果出错 + INFO: No classes were found using the provided glob pattern: "dist/**/*.entity{.ts}" +``` +```bash +# 何时找到文件 +INFO: All classes found using provided glob pattern "dist/**/*.entity{.js,.ts}" : "dist/app/user/user.entity.js | dist/app/common/common.entity.js" +``` \ No newline at end of file diff --git a/extra/typeorm-model-shim.js b/extra/typeorm-model-shim.js index c869dabc19..6de41344d3 100644 --- a/extra/typeorm-model-shim.js +++ b/extra/typeorm-model-shim.js @@ -14,7 +14,7 @@ // } // for webpack this is resolved this way: -// resolve: { // see: http://webpack.github.io/docs/configuration.html#resolve +// resolve: { // see: https://webpack.js.org/configuration/resolve/ // alias: { // typeorm: path.resolve(__dirname, "../node_modules/typeorm/typeorm-model-shim") // } @@ -229,4 +229,4 @@ exports.Generated = Generated; return function (object, propertyName) { }; } -exports.Index = Index; \ No newline at end of file +exports.Index = Index; diff --git a/ormconfig.circleci.json b/ormconfig.circleci.json index f56a69db61..d52b7b1d13 100644 --- a/ormconfig.circleci.json +++ b/ormconfig.circleci.json @@ -75,6 +75,7 @@ "name": "mongodb", "type": "mongodb", "database": "test", - "useNewUrlParser": true + "useNewUrlParser": true, + "useUnifiedTopology": true } ] diff --git a/ormconfig.json.dist b/ormconfig.json.dist index 1365721d32..db4e2321bb 100644 --- a/ormconfig.json.dist +++ b/ormconfig.json.dist @@ -77,6 +77,7 @@ "type": "mongodb", "database": "test", "logging": false, - "useNewUrlParser": true + "useNewUrlParser": true, + "useUnifiedTopology": true } ] diff --git a/ormconfig.travis.json b/ormconfig.travis.json index bb61533d70..fb1a1d47eb 100644 --- a/ormconfig.travis.json +++ b/ormconfig.travis.json @@ -76,6 +76,7 @@ "name": "mongodb", "type": "mongodb", "database": "test", - "useNewUrlParser": true + "useNewUrlParser": true, + "useUnifiedTopology": true } ] diff --git a/package-lock.json b/package-lock.json index fe0418a203..a512d64b05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@haggholm/typeorm", - "version": "0.2.18-7", + "version": "0.2.19-1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1816,6 +1816,15 @@ "assert-plus": "^1.0.0" } }, + "data-api-client": { + "version": "1.0.0-beta", + "resolved": "https://registry.npmjs.org/data-api-client/-/data-api-client-1.0.0-beta.tgz", + "integrity": "sha512-sBC6pGooj59FhKhND7aj24a+pI4qFd0K08WtF6X7ZtthMy5x5ezWC6VDuMUfwMrvA0qGXttFdT6/2U1JTgSN2g==", + "dev": true, + "requires": { + "sqlstring": "^2.3.1" + } + }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -2673,28 +2682,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": "", + "resolved": false, "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "dev": true, "optional": true, @@ -2705,14 +2714,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": "", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -2723,35 +2732,35 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true }, "debug": { "version": "2.6.9", - "resolved": "", + "resolved": false, "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "optional": true, @@ -2761,35 +2770,35 @@ }, "deep-extend": { "version": "0.5.1", - "resolved": "", + "resolved": false, "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true }, "fs.realpath": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -2806,7 +2815,7 @@ }, "glob": { "version": "7.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "optional": true, @@ -2821,14 +2830,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.21", - "resolved": "", + "resolved": false, "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "dev": true, "optional": true, @@ -2838,7 +2847,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "dev": true, "optional": true, @@ -2848,7 +2857,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -2859,21 +2868,21 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "optional": true }, "ini": { "version": "1.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -2883,14 +2892,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -2900,14 +2909,14 @@ }, "minimist": { "version": "0.0.8", - "resolved": "", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true }, "mkdirp": { "version": "0.5.1", - "resolved": "", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -2917,14 +2926,14 @@ }, "ms": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true, "optional": true }, "needle": { "version": "2.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "dev": true, "optional": true, @@ -2936,7 +2945,7 @@ }, "node-pre-gyp": { "version": "0.10.0", - "resolved": "", + "resolved": false, "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "dev": true, "optional": true, @@ -2955,7 +2964,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "dev": true, "optional": true, @@ -2966,14 +2975,14 @@ }, "npm-bundled": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "dev": true, "optional": true }, "npm-packlist": { "version": "1.1.10", - "resolved": "", + "resolved": false, "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "dev": true, "optional": true, @@ -2984,7 +2993,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -2997,21 +3006,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": "", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -3021,21 +3030,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -3046,21 +3055,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true, "optional": true }, "rc": { "version": "1.2.7", - "resolved": "", + "resolved": false, "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "dev": true, "optional": true, @@ -3073,7 +3082,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true @@ -3082,7 +3091,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -3098,7 +3107,7 @@ }, "rimraf": { "version": "2.6.2", - "resolved": "", + "resolved": false, "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "optional": true, @@ -3108,49 +3117,49 @@ }, "safe-buffer": { "version": "5.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": "", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true }, "semver": { "version": "5.5.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true, "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -3162,7 +3171,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -3172,7 +3181,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -3182,7 +3191,7 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true @@ -3214,14 +3223,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "dev": true, "optional": true, @@ -3231,7 +3240,7 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true @@ -5300,9 +5309,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "lodash._reinterpolate": { @@ -5330,12 +5339,12 @@ "dev": true }, "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, "requires": { - "lodash._reinterpolate": "~3.0.0", + "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" } }, @@ -5703,9 +5712,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -7890,9 +7899,9 @@ "dev": true }, "sqlite3": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.8.tgz", - "integrity": "sha512-kgwHu4j10KhpCHtx//dejd/tVQot7jc3sw+Sn0vMuKOw0X00Ckyg9VceKgzPyGmmz+zEoYue9tOLriWTvYy0ww==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.0.9.tgz", + "integrity": "sha512-IkvzjmsWQl9BuBiM4xKpl5X8WCR4w0AeJHRdobCdXZ8dT/lNc1XS6WqvY35N6+YzIIgzSBeY5prdFObID9F9tA==", "dev": true, "requires": { "nan": "^2.12.1", @@ -8398,6 +8407,15 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typeorm-aurora-data-api-driver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/typeorm-aurora-data-api-driver/-/typeorm-aurora-data-api-driver-1.1.1.tgz", + "integrity": "sha512-KqqMiwf/YrT0/YIPL0D97zEAt2TtRyxZGVo1UJusnO3o+3FoLbzFLp3x0Jg3KapOq8EyzYGeLRDsWVUSJQ6MkQ==", + "dev": true, + "requires": { + "data-api-client": "^1.0.0-beta" + } + }, "typescript": { "version": "3.3.3333", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", diff --git a/package.json b/package.json index 8ced5bec41..9a930cb00b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@haggholm/typeorm", "private": true, - "version": "0.2.18-7", + "version": "0.2.19-1", "description": "Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, MongoDB databases.", "license": "MIT", "readmeFilename": "README.md", @@ -86,10 +86,11 @@ "sinon-chai": "^3.3.0", "source-map-support": "^0.5.10", "sql.js": "^1.0.0", - "sqlite3": "^4.0.8", + "sqlite3": "^4.0.9", "ts-node": "^8.0.2", "tslint": "^5.13.1", - "typescript": "^3.3.3333" + "typescript": "^3.3.3333", + "typeorm-aurora-data-api-driver": "^1.1.1" }, "dependencies": { "app-root-path": "^2.0.1", @@ -120,7 +121,7 @@ "compile": "rimraf ./build && tsc", "package": "gulp package", "lint": "tslint -p .", - "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -u" + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 2" }, "bin": { "typeorm": "./cli.js" diff --git a/src/commands/MigrationGenerateCommand.ts b/src/commands/MigrationGenerateCommand.ts index 3816ccd78a..375f5b518b 100644 --- a/src/commands/MigrationGenerateCommand.ts +++ b/src/commands/MigrationGenerateCommand.ts @@ -81,17 +81,17 @@ export class MigrationGenerateCommand implements yargs.CommandModule { // we are using simple quoted string instead of template string syntax if (connection.driver instanceof MysqlDriver) { sqlInMemory.upQueries.forEach(upQuery => { - upSqls.push(" await queryRunner.query(\"" + upQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\");"); + upSqls.push(" await queryRunner.query(\"" + upQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\", " + JSON.stringify(upQuery.parameters) + ");"); }); sqlInMemory.downQueries.forEach(downQuery => { - downSqls.push(" await queryRunner.query(\"" + downQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\");"); + downSqls.push(" await queryRunner.query(\"" + downQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\", " + JSON.stringify(downQuery.parameters) + ");"); }); } else { sqlInMemory.upQueries.forEach(upQuery => { - upSqls.push(" await queryRunner.query(`" + upQuery.query.replace(new RegExp("`", "g"), "\\`") + "`);"); + upSqls.push(" await queryRunner.query(`" + upQuery.query.replace(new RegExp("`", "g"), "\\`") + "`, " + JSON.stringify(upQuery.parameters) + ");"); }); sqlInMemory.downQueries.forEach(downQuery => { - downSqls.push(" await queryRunner.query(`" + downQuery.query.replace(new RegExp("`", "g"), "\\`") + "`);"); + downSqls.push(" await queryRunner.query(`" + downQuery.query.replace(new RegExp("`", "g"), "\\`") + "`, " + JSON.stringify(downQuery.parameters) + ");"); }); } diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index ba089eb5ce..e736b8db9c 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -307,7 +307,7 @@ export class Connection { /** * Lists all migrations and whether they have been run. - * Returns true if there are no pending migrations + * Returns true if there are pending migrations */ async showMigrations(): Promise { if (!this.isConnected) { @@ -517,6 +517,8 @@ export class Connection { const migrations = connectionMetadataBuilder.buildMigrations(this.options.migrations || []); ObjectUtils.assign(this, { migrations: migrations }); + this.driver.database = this.options.database; + // validate all created entity metadatas to make sure user created entities are valid and correct entityMetadataValidator.validateMany(this.entityMetadatas.filter(metadata => metadata.tableType !== "view"), this.driver); } diff --git a/src/connection/ConnectionMetadataBuilder.ts b/src/connection/ConnectionMetadataBuilder.ts index d67577365d..e8c77ba61b 100644 --- a/src/connection/ConnectionMetadataBuilder.ts +++ b/src/connection/ConnectionMetadataBuilder.ts @@ -31,7 +31,7 @@ export class ConnectionMetadataBuilder { */ buildMigrations(migrations: (Function|string)[]): MigrationInterface[] { const [migrationClasses, migrationDirectories] = OrmUtils.splitClassesAndStrings(migrations); - const allMigrationClasses = [...migrationClasses, ...importClassesFromDirectories(migrationDirectories)]; + const allMigrationClasses = [...migrationClasses, ...importClassesFromDirectories(this.connection.logger, migrationDirectories)]; return allMigrationClasses.map(migrationClass => getFromContainer(migrationClass)); } @@ -40,7 +40,7 @@ export class ConnectionMetadataBuilder { */ buildSubscribers(subscribers: (Function|string)[]): EntitySubscriberInterface[] { const [subscriberClasses, subscriberDirectories] = OrmUtils.splitClassesAndStrings(subscribers || []); - const allSubscriberClasses = [...subscriberClasses, ...importClassesFromDirectories(subscriberDirectories)]; + const allSubscriberClasses = [...subscriberClasses, ...importClassesFromDirectories(this.connection.logger, subscriberDirectories)]; return getMetadataArgsStorage() .filterSubscribers(allSubscriberClasses) .map(metadata => getFromContainer>(metadata.target)); @@ -56,7 +56,7 @@ export class ConnectionMetadataBuilder { const entityClasses: Function[] = entityClassesOrSchemas.filter(entityClass => (entityClass instanceof EntitySchema) === false) as any; const entitySchemas: EntitySchema[] = entityClassesOrSchemas.filter(entityClass => entityClass instanceof EntitySchema) as any; - const allEntityClasses = [...entityClasses, ...importClassesFromDirectories(entityDirectories)]; + const allEntityClasses = [...entityClasses, ...importClassesFromDirectories(this.connection.logger, entityDirectories)]; allEntityClasses.forEach(entityClass => { // if we have entity schemas loaded from directories if (entityClass instanceof EntitySchema) { entitySchemas.push(entityClass); diff --git a/src/connection/ConnectionOptions.ts b/src/connection/ConnectionOptions.ts index 39bfbc7945..11317c01da 100644 --- a/src/connection/ConnectionOptions.ts +++ b/src/connection/ConnectionOptions.ts @@ -10,6 +10,8 @@ import {SqljsConnectionOptions} from "../driver/sqljs/SqljsConnectionOptions"; import {ReactNativeConnectionOptions} from "../driver/react-native/ReactNativeConnectionOptions"; import {NativescriptConnectionOptions} from "../driver/nativescript/NativescriptConnectionOptions"; import {ExpoConnectionOptions} from "../driver/expo/ExpoConnectionOptions"; +import {AuroraDataApiConnectionOptions} from "../driver/aurora-data-api/AuroraDataApiConnectionOptions"; + /** * ConnectionOptions is an interface with settings and options for specific connection. @@ -28,4 +30,5 @@ export type ConnectionOptions = ReactNativeConnectionOptions| SqljsConnectionOptions| MongoConnectionOptions| + AuroraDataApiConnectionOptions| ExpoConnectionOptions; diff --git a/src/decorator/columns/Column.ts b/src/decorator/columns/Column.ts index ead237747d..834c33119e 100644 --- a/src/decorator/columns/Column.ts +++ b/src/decorator/columns/Column.ts @@ -70,6 +70,12 @@ export function Column(type: "enum", options?: ColumnCommonOptions & ColumnEnumO */ export function Column(type: "simple-enum", options?: ColumnCommonOptions & ColumnEnumOptions): Function; +/** + * 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(type: "set", options?: ColumnCommonOptions & ColumnEnumOptions): Function; + /** * 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. diff --git a/src/decorator/columns/PrimaryColumn.ts b/src/decorator/columns/PrimaryColumn.ts index 3d479a9faf..9c56828bf6 100644 --- a/src/decorator/columns/PrimaryColumn.ts +++ b/src/decorator/columns/PrimaryColumn.ts @@ -31,7 +31,7 @@ export function PrimaryColumn(typeOrOptions?: ColumnType|ColumnOptions, options? if (typeof typeOrOptions === "string") { type = typeOrOptions; } else { - options = typeOrOptions; + options = Object.assign({}, typeOrOptions); } if (!options) options = {} as ColumnOptions; diff --git a/src/decorator/entity-view/ViewEntity.ts b/src/decorator/entity-view/ViewEntity.ts index 376ccdb771..50cb0f96a4 100644 --- a/src/decorator/entity-view/ViewEntity.ts +++ b/src/decorator/entity-view/ViewEntity.ts @@ -30,6 +30,8 @@ export function ViewEntity(nameOrOptions?: string|ViewEntityOptions, maybeOption type: "view", database: options.database ? options.database : undefined, schema: options.schema ? options.schema : undefined, + synchronize: options.synchronize === false ? false : true, + materialized: !!options.materialized } as TableMetadataArgs); }; } diff --git a/src/decorator/options/ViewEntityOptions.ts b/src/decorator/options/ViewEntityOptions.ts index 68d8e851e3..ad4aeaf6be 100644 --- a/src/decorator/options/ViewEntityOptions.ts +++ b/src/decorator/options/ViewEntityOptions.ts @@ -25,4 +25,17 @@ export interface ViewEntityOptions { * Schema name. Used in Postgres and Sql Server. */ schema?: string; + + /** + * Indicates if schema synchronization is enabled or disabled for this entity. + * If it will be set to false then schema sync will and migrations ignore this entity. + * By default schema synchronization is enabled for all entities. + */ + synchronize?: boolean; + + /** + * Indicates if view should be materialized view. + * It's supported by Postgres and Oracle. + */ + materialized?: boolean; } diff --git a/src/driver/DriverFactory.ts b/src/driver/DriverFactory.ts index 0580adedc8..332d555c47 100644 --- a/src/driver/DriverFactory.ts +++ b/src/driver/DriverFactory.ts @@ -11,6 +11,7 @@ import {SqljsDriver} from "./sqljs/SqljsDriver"; import {MysqlDriver} from "./mysql/MysqlDriver"; import {PostgresDriver} from "./postgres/PostgresDriver"; import {ExpoDriver} from "./expo/ExpoDriver"; +import {AuroraDataApiDriver} from "./aurora-data-api/AuroraDataApiDriver"; import {Driver} from "./Driver"; import {Connection} from "../connection/Connection"; @@ -51,6 +52,8 @@ export class DriverFactory { return new MongoDriver(connection); case "expo": return new ExpoDriver(connection); + case "aurora-data-api": + return new AuroraDataApiDriver(connection); default: throw new MissingDriverError(type); } diff --git a/src/driver/aurora-data-api/AuroraDataApiConnection.ts b/src/driver/aurora-data-api/AuroraDataApiConnection.ts new file mode 100644 index 0000000000..386248809f --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnection.ts @@ -0,0 +1,20 @@ +import {AuroraDataApiQueryRunner} from "./AuroraDataApiQueryRunner"; +import {Connection} from "../../connection/Connection"; +import {ConnectionOptions, QueryRunner} from "../.."; + +/** + * Organizes communication with MySQL DBMS. + */ +export class AuroraDataApiConnection extends Connection { + queryRunnter: AuroraDataApiQueryRunner; + + constructor(options: ConnectionOptions, queryRunner: AuroraDataApiQueryRunner) { + super(options); + this.queryRunnter = queryRunner; + } + + public createQueryRunner(mode: "master" | "slave" = "master"): QueryRunner { + return this.queryRunnter; + } + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts b/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts new file mode 100644 index 0000000000..dc8be9db29 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts @@ -0,0 +1,43 @@ +/** + * MySQL specific connection credential options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface AuroraDataApiConnectionCredentialsOptions { + + /** + * Connection url where perform connection to. + */ + readonly url?: string; + + /** + * Database host. + */ + readonly host?: string; + + /** + * Database host port. + */ + readonly port?: number; + + /** + * Database username. + */ + readonly username?: string; + + /** + * Database password. + */ + readonly password?: string; + + /** + * Database name to connect to. + */ + readonly database?: string; + + /** + * Object with ssl parameters or a string containing name of ssl profile. + */ + readonly ssl?: any; + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts b/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts new file mode 100644 index 0000000000..c79c850ff2 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts @@ -0,0 +1,23 @@ +import {BaseConnectionOptions} from "../../connection/BaseConnectionOptions"; +import {AuroraDataApiConnectionCredentialsOptions} from "./AuroraDataApiConnectionCredentialsOptions"; + +/** + * MySQL specific connection options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface AuroraDataApiConnectionOptions extends BaseConnectionOptions, AuroraDataApiConnectionCredentialsOptions { + + /** + * Database type. + */ + readonly type: "aurora-data-api"; + + readonly region: string; + + readonly secretArn: string; + + readonly resourceArn: string; + + readonly database: string; +} diff --git a/src/driver/aurora-data-api/AuroraDataApiDriver.ts b/src/driver/aurora-data-api/AuroraDataApiDriver.ts new file mode 100644 index 0000000000..e2248a324c --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiDriver.ts @@ -0,0 +1,828 @@ +import {Driver} from "../Driver"; +import {DriverUtils} from "../DriverUtils"; +import {AuroraDataApiQueryRunner} from "./AuroraDataApiQueryRunner"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {ColumnMetadata} from "../../metadata/ColumnMetadata"; +import {DateUtils} from "../../util/DateUtils"; +import {PlatformTools} from "../../platform/PlatformTools"; +import {Connection} from "../../connection/Connection"; +import {RdbmsSchemaBuilder} from "../../schema-builder/RdbmsSchemaBuilder"; +import {AuroraDataApiConnectionOptions} from "./AuroraDataApiConnectionOptions"; +import {MappedColumnTypes} from "../types/MappedColumnTypes"; +import {ColumnType} from "../types/ColumnTypes"; +import {DataTypeDefaults} from "../types/DataTypeDefaults"; +import {TableColumn} from "../../schema-builder/table/TableColumn"; +import {AuroraDataApiConnectionCredentialsOptions} from "./AuroraDataApiConnectionCredentialsOptions"; +import {EntityMetadata} from "../../metadata/EntityMetadata"; +import {OrmUtils} from "../../util/OrmUtils"; +import {ApplyValueTransformers} from "../../util/ApplyValueTransformers"; + +/** + * Organizes communication with MySQL DBMS. + */ +export class AuroraDataApiDriver implements Driver { + + // ------------------------------------------------------------------------- + // Public Properties + // ------------------------------------------------------------------------- + + connection: Connection; + /** + * Aurora Data API underlying library. + */ + DataApiDriver: any; + + client: any; + + /** + * Connection pool. + * Used in non-replication mode. + */ + pool: any; + + /** + * Pool cluster used in replication mode. + */ + poolCluster: any; + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Connection options. + */ + options: AuroraDataApiConnectionOptions; + + /** + * Master database used to perform all write queries. + */ + database?: string; + + /** + * Indicates if replication is enabled. + */ + isReplicated: boolean = false; + + /** + * Indicates if tree tables are supported by this driver. + */ + treeSupport = true; + + /** + * Gets list of supported column data types by a driver. + * + * @see https://www.tutorialspoint.com/mysql/mysql-data-types.htm + * @see https://dev.mysql.com/doc/refman/8.0/en/data-types.html + */ + supportedDataTypes: ColumnType[] = [ + // numeric types + "bit", + "int", + "integer", // synonym for int + "tinyint", + "smallint", + "mediumint", + "bigint", + "float", + "double", + "double precision", // synonym for double + "real", // synonym for double + "decimal", + "dec", // synonym for decimal + "numeric", // synonym for decimal + "fixed", // synonym for decimal + "bool", // synonym for tinyint + "boolean", // synonym for tinyint + // date and time types + "date", + "datetime", + "timestamp", + "time", + "year", + // string types + "char", + "nchar", // synonym for national char + "national char", + "varchar", + "nvarchar", // synonym for national varchar + "national varchar", + "blob", + "text", + "tinyblob", + "tinytext", + "mediumblob", + "mediumtext", + "longblob", + "longtext", + "enum", + "binary", + "varbinary", + // json data type + "json", + // spatial data types + "geometry", + "point", + "linestring", + "polygon", + "multipoint", + "multilinestring", + "multipolygon", + "geometrycollection" + ]; + + /** + * Gets list of spatial column data types. + */ + spatialTypes: ColumnType[] = [ + "geometry", + "point", + "linestring", + "polygon", + "multipoint", + "multilinestring", + "multipolygon", + "geometrycollection" + ]; + + /** + * Gets list of column data types that support length by a driver. + */ + withLengthColumnTypes: ColumnType[] = [ + "char", + "varchar", + "nvarchar", + "binary", + "varbinary" + ]; + + /** + * Gets list of column data types that support length by a driver. + */ + withWidthColumnTypes: ColumnType[] = [ + "bit", + "tinyint", + "smallint", + "mediumint", + "int", + "integer", + "bigint" + ]; + + /** + * Gets list of column data types that support precision by a driver. + */ + withPrecisionColumnTypes: ColumnType[] = [ + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real", + "time", + "datetime", + "timestamp" + ]; + + /** + * Gets list of column data types that supports scale by a driver. + */ + withScaleColumnTypes: ColumnType[] = [ + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real" + ]; + + /** + * Gets list of column data types that supports UNSIGNED and ZEROFILL attributes. + */ + unsignedAndZerofillTypes: ColumnType[] = [ + "int", + "integer", + "smallint", + "tinyint", + "mediumint", + "bigint", + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real" + ]; + + /** + * ORM has special columns and we need to know what database column types should be for those columns. + * Column types are driver dependant. + */ + mappedDataTypes: MappedColumnTypes = { + createDate: "datetime", + createDatePrecision: 6, + createDateDefault: "CURRENT_TIMESTAMP(6)", + updateDate: "datetime", + updateDatePrecision: 6, + updateDateDefault: "CURRENT_TIMESTAMP(6)", + version: "int", + treeLevel: "int", + migrationId: "int", + migrationName: "varchar", + migrationTimestamp: "bigint", + cacheId: "int", + cacheIdentifier: "varchar", + cacheTime: "bigint", + cacheDuration: "int", + cacheQuery: "text", + cacheResult: "text", + metadataType: "varchar", + metadataDatabase: "varchar", + metadataSchema: "varchar", + metadataTable: "varchar", + metadataName: "varchar", + metadataValue: "text", + }; + + /** + * Default values of length, precision and scale depends on column data type. + * Used in the cases when length/precision/scale is not specified by user. + */ + dataTypeDefaults: DataTypeDefaults = { + "varchar": { length: 255 }, + "nvarchar": { length: 255 }, + "national varchar": { length: 255 }, + "char": { length: 1 }, + "binary": { length: 1 }, + "varbinary": { length: 255 }, + "decimal": { precision: 10, scale: 0 }, + "dec": { precision: 10, scale: 0 }, + "numeric": { precision: 10, scale: 0 }, + "fixed": { precision: 10, scale: 0 }, + "float": { precision: 12 }, + "double": { precision: 22 }, + "time": { precision: 0 }, + "datetime": { precision: 0 }, + "timestamp": { precision: 0 }, + "bit": { width: 1 }, + "int": { width: 11 }, + "integer": { width: 11 }, + "tinyint": { width: 4 }, + "smallint": { width: 6 }, + "mediumint": { width: 9 }, + "bigint": { width: 20 } + }; + + + /** + * Max length allowed by MySQL for aliases. + * @see https://dev.mysql.com/doc/refman/5.5/en/identifiers.html + */ + maxAliasLength = 63; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(connection: Connection) { + this.connection = connection; + this.options = connection.options as AuroraDataApiConnectionOptions; + + // load mysql package + this.loadDependencies(); + + this.client = new this.DataApiDriver( + this.options.region, + this.options.secretArn, + this.options.resourceArn, + this.options.database, + (query: string, parameters?: any[]) => this.connection.logger.logQuery(query, parameters), + ); + + // validate options to make sure everything is set + // todo: revisit validation with replication in mind + // if (!(this.options.host || (this.options.extra && this.options.extra.socketPath)) && !this.options.socketPath) + // throw new DriverOptionNotSetError("socketPath and host"); + // if (!this.options.username) + // throw new DriverOptionNotSetError("username"); + // if (!this.options.database) + // throw new DriverOptionNotSetError("database"); + // todo: check what is going on when connection is setup without database and how to connect to a database then? + // todo: provide options to auto-create a database if it does not exist yet + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Performs connection to the database. + */ + async connect(): Promise { + } + + /** + * Makes any action after connection (e.g. create extensions in Postgres driver). + */ + afterConnect(): Promise { + return Promise.resolve(); + } + + /** + * Closes connection with the database. + */ + async disconnect(): Promise { + } + + /** + * Creates a schema builder used to build and sync a schema. + */ + createSchemaBuilder() { + return new RdbmsSchemaBuilder(this.connection); + } + + /** + * Creates a query runner used to execute database queries. + */ + createQueryRunner(mode: "master"|"slave" = "master") { + return new AuroraDataApiQueryRunner(this); + } + + /** + * Replaces parameters in the given sql with special escaping character + * and an array of parameter names to be passed to a query. + */ + escapeQueryWithParameters(sql: string, parameters: ObjectLiteral, nativeParameters: ObjectLiteral): [string, any[]] { + const escapedParameters: any[] = Object.keys(nativeParameters).map(key => nativeParameters[key]); + if (!parameters || !Object.keys(parameters).length) + return [sql, escapedParameters]; + + const keys = Object.keys(parameters).map(parameter => "(:(\\.\\.\\.)?" + parameter + "\\b)").join("|"); + sql = sql.replace(new RegExp(keys, "g"), (key: string) => { + let value: any; + if (key.substr(0, 4) === ":...") { + value = parameters[key.substr(4)]; + } else { + value = parameters[key.substr(1)]; + } + + if (value instanceof Function) { + return value(); + + } else { + escapedParameters.push(value); + return "?"; + } + }); // todo: make replace only in value statements, otherwise problems + return [sql, escapedParameters]; + } + + /** + * Escapes a column name. + */ + escape(columnName: string): string { + return "`" + columnName + "`"; + } + + /** + * Build full table name with database name, schema name and table name. + * E.g. "myDB"."mySchema"."myTable" + */ + buildTableName(tableName: string, schema?: string, database?: string): string { + return database ? `${database}.${tableName}` : tableName; + } + + /** + * Prepares given value to a value to be persisted, based on its column type and metadata. + */ + preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any { + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformTo(columnMetadata.transformer, value); + + if (value === null || value === undefined) + return value; + + if (columnMetadata.type === Boolean) { + return value === true ? 1 : 0; + + } else if (columnMetadata.type === "date") { + return DateUtils.mixedDateToDateString(value); + + } else if (columnMetadata.type === "time") { + return DateUtils.mixedDateToTimeString(value); + + } else if (columnMetadata.type === "json") { + return JSON.stringify(value); + + } else if (columnMetadata.type === "timestamp" || columnMetadata.type === "datetime" || columnMetadata.type === Date) { + return DateUtils.mixedDateToDate(value); + + } else if (columnMetadata.type === "simple-array") { + return DateUtils.simpleArrayToString(value); + + } else if (columnMetadata.type === "simple-json") { + return DateUtils.simpleJsonToString(value); + + } else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") { + return "" + value; + } + + return value; + } + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any { + if (value === null || value === undefined) + return columnMetadata.transformer ? ApplyValueTransformers.transformFrom(columnMetadata.transformer, value) : value; + + if (columnMetadata.type === Boolean || columnMetadata.type === "bool" || columnMetadata.type === "boolean") { + value = value ? true : false; + + } else if (columnMetadata.type === "datetime" || columnMetadata.type === Date) { + value = DateUtils.normalizeHydratedDate(value); + + } else if (columnMetadata.type === "date") { + value = DateUtils.mixedDateToDateString(value); + + } else if (columnMetadata.type === "json") { + value = typeof value === "string" ? JSON.parse(value) : value; + + } else if (columnMetadata.type === "time") { + value = DateUtils.mixedTimeToString(value); + + } else if (columnMetadata.type === "simple-array") { + value = DateUtils.stringToSimpleArray(value); + + } else if (columnMetadata.type === "simple-json") { + value = DateUtils.stringToSimpleJson(value); + + } else if ((columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") + && columnMetadata.enum + && !isNaN(value) + && columnMetadata.enum.indexOf(parseInt(value)) >= 0) { + // convert to number if that exists in possible enum options + value = parseInt(value); + } + + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformFrom(columnMetadata.transformer, value); + + return value; + } + + /** + * Creates a database type from a given column metadata. + */ + normalizeType(column: { type: ColumnType, length?: number|string, precision?: number|null, scale?: number }): string { + if (column.type === Number || column.type === "integer") { + return "int"; + + } else if (column.type === String) { + return "varchar"; + + } else if (column.type === Date) { + return "datetime"; + + } else if ((column.type as any) === Buffer) { + return "blob"; + + } else if (column.type === Boolean) { + return "tinyint"; + + } else if (column.type === "uuid") { + return "varchar"; + + } else if (column.type === "simple-array" || column.type === "simple-json") { + return "text"; + + } else if (column.type === "simple-enum") { + return "enum"; + + } else if (column.type === "double precision" || column.type === "real") { + return "double"; + + } else if (column.type === "dec" || column.type === "numeric" || column.type === "fixed") { + return "decimal"; + + } else if (column.type === "bool" || column.type === "boolean") { + return "tinyint"; + + } else if (column.type === "nvarchar" || column.type === "national varchar") { + return "varchar"; + + } else if (column.type === "nchar" || column.type === "national char") { + return "char"; + + } else { + return column.type as string || ""; + } + } + + /** + * Normalizes "default" value of the column. + */ + normalizeDefault(columnMetadata: ColumnMetadata): string { + const defaultValue = columnMetadata.default; + + if ((columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") && defaultValue !== undefined) { + return `'${defaultValue}'`; + } + + if (typeof defaultValue === "number") { + return "" + defaultValue; + + } else if (typeof defaultValue === "boolean") { + return defaultValue === true ? "1" : "0"; + + } else if (typeof defaultValue === "function") { + return defaultValue(); + + } else if (typeof defaultValue === "string") { + return `'${defaultValue}'`; + + } else if (defaultValue === null) { + return `null`; + + } else { + return defaultValue; + } + } + + /** + * Normalizes "isUnique" value of the column. + */ + normalizeIsUnique(column: ColumnMetadata): boolean { + return column.entityMetadata.indices.some(idx => idx.isUnique && idx.columns.length === 1 && idx.columns[0] === column); + } + + /** + * Returns default column lengths, which is required on column creation. + */ + getColumnLength(column: ColumnMetadata|TableColumn): string { + if (column.length) + return column.length.toString(); + + /** + * fix https://github.com/typeorm/typeorm/issues/1139 + */ + if (column.generationStrategy === "uuid") + return "36"; + + switch (column.type) { + case String: + case "varchar": + case "nvarchar": + case "national varchar": + return "255"; + case "varbinary": + return "255"; + default: + return ""; + } + } + + /** + * Creates column type definition including length, precision and scale + */ + createFullType(column: TableColumn): string { + let type = column.type; + + // used 'getColumnLength()' method, because MySQL requires column length for `varchar`, `nvarchar` and `varbinary` data types + if (this.getColumnLength(column)) { + type += `(${this.getColumnLength(column)})`; + + } else if (column.width) { + type += `(${column.width})`; + + } else if (column.precision !== null && column.precision !== undefined && column.scale !== null && column.scale !== undefined) { + type += `(${column.precision},${column.scale})`; + + } else if (column.precision !== null && column.precision !== undefined) { + type += `(${column.precision})`; + } + + if (column.isArray) + type += " array"; + + return type; + } + + /** + * Obtains a new database connection to a master server. + * Used for replication. + * If replication is not setup then returns default connection's database connection. + */ + obtainMasterConnection(): Promise { + return new Promise((ok, fail) => { + if (this.poolCluster) { + this.poolCluster.getConnection("MASTER", (err: any, dbConnection: any) => { + err ? fail(err) : ok(this.prepareDbConnection(dbConnection)); + }); + + } else if (this.pool) { + this.pool.getConnection((err: any, dbConnection: any) => { + err ? fail(err) : ok(this.prepareDbConnection(dbConnection)); + }); + } else { + fail(new Error(`Connection is not established with mysql database`)); + } + }); + } + + /** + * Obtains a new database connection to a slave server. + * Used for replication. + * If replication is not setup then returns master (default) connection's database connection. + */ + obtainSlaveConnection(): Promise { + if (!this.poolCluster) + return this.obtainMasterConnection(); + + return new Promise((ok, fail) => { + this.poolCluster.getConnection("SLAVE*", (err: any, dbConnection: any) => { + err ? fail(err) : ok(dbConnection); + }); + }); + } + + /** + * Creates generated map of values generated or returned by database after INSERT query. + */ + createGeneratedMap(metadata: EntityMetadata, insertResult: any) { + const generatedMap = metadata.generatedColumns.reduce((map, generatedColumn) => { + let value: any; + if (generatedColumn.generationStrategy === "increment" && insertResult.insertId) { + value = insertResult.insertId; + // } else if (generatedColumn.generationStrategy === "uuid") { + // console.log("getting db value:", generatedColumn.databaseName); + // value = generatedColumn.getEntityValue(uuidMap); + } + + return OrmUtils.mergeDeep(map, generatedColumn.createValueMap(value)); + }, {} as ObjectLiteral); + + return Object.keys(generatedMap).length > 0 ? generatedMap : undefined; + } + + /** + * Differentiate columns of this table and columns from the given column metadatas columns + * and returns only changed. + */ + findChangedColumns(tableColumns: TableColumn[], columnMetadatas: ColumnMetadata[]): ColumnMetadata[] { + return columnMetadatas.filter(columnMetadata => { + const tableColumn = tableColumns.find(c => c.name === columnMetadata.databaseName); + if (!tableColumn) + return false; // we don't need new columns, we only need exist and changed + + // console.log("table:", columnMetadata.entityMetadata.tableName); + // console.log("name:", tableColumn.name, columnMetadata.databaseName); + // console.log("type:", tableColumn.type, this.normalizeType(columnMetadata)); + // console.log("length:", tableColumn.length, columnMetadata.length); + // console.log("width:", tableColumn.width, columnMetadata.width); + // console.log("precision:", tableColumn.precision, columnMetadata.precision); + // console.log("scale:", tableColumn.scale, columnMetadata.scale); + // console.log("zerofill:", tableColumn.zerofill, columnMetadata.zerofill); + // console.log("unsigned:", tableColumn.unsigned, columnMetadata.unsigned); + // console.log("asExpression:", tableColumn.asExpression, columnMetadata.asExpression); + // console.log("generatedType:", tableColumn.generatedType, columnMetadata.generatedType); + // console.log("comment:", tableColumn.comment, columnMetadata.comment); + // console.log("default:", tableColumn.default, columnMetadata.default); + // console.log("enum:", tableColumn.enum, columnMetadata.enum); + // console.log("default changed:", !this.compareDefaultValues(this.normalizeDefault(columnMetadata), tableColumn.default)); + // console.log("onUpdate:", tableColumn.onUpdate, columnMetadata.onUpdate); + // console.log("isPrimary:", tableColumn.isPrimary, columnMetadata.isPrimary); + // console.log("isNullable:", tableColumn.isNullable, columnMetadata.isNullable); + // console.log("isUnique:", tableColumn.isUnique, this.normalizeIsUnique(columnMetadata)); + // console.log("isGenerated:", tableColumn.isGenerated, columnMetadata.isGenerated); + // console.log((columnMetadata.generationStrategy !== "uuid" && tableColumn.isGenerated !== columnMetadata.isGenerated)); + // console.log("=========================================="); + + let columnMetadataLength = columnMetadata.length; + if (!columnMetadataLength && columnMetadata.generationStrategy === "uuid") { // fixing #3374 + columnMetadataLength = this.getColumnLength(columnMetadata); + } + + return tableColumn.name !== columnMetadata.databaseName + || tableColumn.type !== this.normalizeType(columnMetadata) + || tableColumn.length !== columnMetadataLength + || tableColumn.width !== columnMetadata.width + || tableColumn.precision !== columnMetadata.precision + || tableColumn.scale !== columnMetadata.scale + || tableColumn.zerofill !== columnMetadata.zerofill + || tableColumn.unsigned !== columnMetadata.unsigned + || tableColumn.asExpression !== columnMetadata.asExpression + || tableColumn.generatedType !== columnMetadata.generatedType + // || tableColumn.comment !== columnMetadata.comment // todo + || !this.compareDefaultValues(this.normalizeDefault(columnMetadata), tableColumn.default) + || (tableColumn.enum && columnMetadata.enum && !OrmUtils.isArraysEqual(tableColumn.enum, columnMetadata.enum.map(val => val + ""))) + || tableColumn.onUpdate !== columnMetadata.onUpdate + || tableColumn.isPrimary !== columnMetadata.isPrimary + || tableColumn.isNullable !== columnMetadata.isNullable + || tableColumn.isUnique !== this.normalizeIsUnique(columnMetadata) + || (columnMetadata.generationStrategy !== "uuid" && tableColumn.isGenerated !== columnMetadata.isGenerated); + }); + } + + /** + * Returns true if driver supports RETURNING / OUTPUT statement. + */ + isReturningSqlSupported(): boolean { + return false; + } + + /** + * Returns true if driver supports uuid values generation on its own. + */ + isUUIDGenerationSupported(): boolean { + return false; + } + + /** + * Creates an escaped parameter. + */ + createParameter(parameterName: string, index: number): string { + return "?"; + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Loads all driver dependencies. + */ + protected loadDependencies(): void { + this.DataApiDriver = PlatformTools.load("typeorm-aurora-data-api-driver"); + } + + /** + * Creates a new connection pool for a given database credentials. + */ + protected createConnectionOptions(options: AuroraDataApiConnectionOptions, credentials: AuroraDataApiConnectionCredentialsOptions): Promise { + + credentials = Object.assign(credentials, DriverUtils.buildDriverOptions(credentials)); // todo: do it better way + + // build connection options for the driver + return Object.assign({}, { + resourceArn: options.resourceArn, + secretArn: options.secretArn, + database: options.database, + region: options.region, + type: options.type, + }, { + host: credentials.host, + user: credentials.username, + password: credentials.password, + database: credentials.database, + port: credentials.port, + ssl: options.ssl + }, + + options.extra || {}); + } + + /** + * Creates a new connection pool for a given database credentials. + */ + protected async createPool(connectionOptions: any): Promise { + return {}; + } + + /** + * Attaches all required base handlers to a database connection, such as the unhandled error handler. + */ + private prepareDbConnection(connection: any): any { + const { logger } = this.connection; + /* + Attaching an error handler to connection errors is essential, as, otherwise, errors raised will go unhandled and + cause the hosting app to crash. + */ + if (connection.listeners("error").length === 0) { + connection.on("error", (error: any) => logger.log("warn", `MySQL connection raised an error. ${error}`)); + } + return connection; + } + + /** + * Checks if "DEFAULT" values in the column metadata and in the database are equal. + */ + protected compareDefaultValues(columnMetadataValue: string, databaseValue: string): boolean { + if (typeof columnMetadataValue === "string" && typeof databaseValue === "string") { + // we need to cut out "'" because in mysql we can understand returned value is a string or a function + // as result compare cannot understand if default is really changed or not + columnMetadataValue = columnMetadataValue.replace(/^'+|'+$/g, ""); + databaseValue = databaseValue.replace(/^'+|'+$/g, ""); + } + + return columnMetadataValue === databaseValue; + } + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts new file mode 100644 index 0000000000..2c38e16c22 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts @@ -0,0 +1,1614 @@ +import {QueryRunner} from "../../query-runner/QueryRunner"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {TransactionAlreadyStartedError} from "../../error/TransactionAlreadyStartedError"; +import {TransactionNotStartedError} from "../../error/TransactionNotStartedError"; +import {TableColumn} from "../../schema-builder/table/TableColumn"; +import {Table} from "../../schema-builder/table/Table"; +import {TableForeignKey} from "../../schema-builder/table/TableForeignKey"; +import {TableIndex} from "../../schema-builder/table/TableIndex"; +import {QueryRunnerAlreadyReleasedError} from "../../error/QueryRunnerAlreadyReleasedError"; +import {View} from "../../schema-builder/view/View"; +import {Query} from "../Query"; +import {AuroraDataApiDriver} from "./AuroraDataApiDriver"; +import {ReadStream} from "../../platform/PlatformTools"; +import {OrmUtils} from "../../util/OrmUtils"; +import {TableIndexOptions} from "../../schema-builder/options/TableIndexOptions"; +import {TableUnique} from "../../schema-builder/table/TableUnique"; +import {BaseQueryRunner} from "../../query-runner/BaseQueryRunner"; +import {Broadcaster} from "../../subscriber/Broadcaster"; +import {ColumnType, PromiseUtils} from "../../index"; +import {TableCheck} from "../../schema-builder/table/TableCheck"; +import {IsolationLevel} from "../types/IsolationLevel"; +import {TableExclusion} from "../../schema-builder/table/TableExclusion"; + +/** + * Runs queries on a single mysql database connection. + */ +export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRunner { + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Database driver used by connection. + */ + + driver: AuroraDataApiDriver; + + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * Promise used to obtain a database connection from a pool for a first time. + */ + protected databaseConnectionPromise: Promise; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(driver: AuroraDataApiDriver) { + super(); + this.driver = driver; + this.connection = driver.connection; + this.broadcaster = new Broadcaster(this); + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Creates/uses database connection from the connection pool to perform further operations. + * Returns obtained database connection. + */ + async connect(): Promise { + return {}; + } + + /** + * Releases used database connection. + * You cannot use query runner methods once its released. + */ + release(): Promise { + this.isReleased = true; + if (this.databaseConnection) + this.databaseConnection.release(); + return Promise.resolve(); + } + + /** + * Starts transaction on the current connection. + */ + async startTransaction(isolationLevel?: IsolationLevel): Promise { + if (this.isTransactionActive) + throw new TransactionAlreadyStartedError(); + + this.isTransactionActive = true; + await this.driver.client.startTransaction(); + } + + /** + * Commits transaction. + * Error will be thrown if transaction was not started. + */ + async commitTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.commitTransaction(); + this.isTransactionActive = false; + } + + /** + * Rollbacks transaction. + * Error will be thrown if transaction was not started. + */ + async rollbackTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.rollbackTransaction(); + this.isTransactionActive = false; + } + + /** + * Executes a raw SQL query. + */ + async query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const result = await this.driver.client.query(query, parameters); + + if (result.records) { + return result.records; + } + + return result; + } + + /** + * Returns raw data stream. + */ + stream(query: string, parameters?: any[], onEnd?: Function, onError?: Function): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + return new Promise(async (ok, fail) => { + try { + const databaseConnection = await this.connect(); + const stream = databaseConnection.query(query, parameters); + if (onEnd) stream.on("end", onEnd); + if (onError) stream.on("error", onError); + ok(stream); + + } catch (err) { + fail(err); + } + }); + } + + /** + * Returns all available database names including system databases. + */ + async getDatabases(): Promise { + return Promise.resolve([]); + } + + /** + * Returns all available schema names including system schemas. + * If database parameter specified, returns schemas of that database. + */ + async getSchemas(database?: string): Promise { + throw new Error(`MySql driver does not support table schemas`); + } + + /** + * Checks if database with the given name exist. + */ + async hasDatabase(database: string): Promise { + const result = await this.query(`SELECT * FROM \`INFORMATION_SCHEMA\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\` = '${database}'`); + return result.length ? true : false; + } + + /** + * Checks if schema with the given name exist. + */ + async hasSchema(schema: string): Promise { + throw new Error(`MySql driver does not support table schemas`); + } + + /** + * Checks if table with the given name exist in the database. + */ + async hasTable(tableOrName: Table|string): Promise { + const parsedTableName = this.parseTableName(tableOrName); + const sql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE \`TABLE_SCHEMA\` = '${parsedTableName.database}' AND \`TABLE_NAME\` = '${parsedTableName.tableName}'`; + const result = await this.query(sql); + return result.length ? true : false; + } + + /** + * Checks if column with the given name exist in the given table. + */ + async hasColumn(tableOrName: Table|string, column: TableColumn|string): Promise { + const parsedTableName = this.parseTableName(tableOrName); + const columnName = column instanceof TableColumn ? column.name : column; + const sql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE \`TABLE_SCHEMA\` = '${parsedTableName.database}' AND \`TABLE_NAME\` = '${parsedTableName.tableName}' AND \`COLUMN_NAME\` = '${columnName}'`; + const result = await this.query(sql); + return result.length ? true : false; + } + + /** + * Creates a new database. + */ + async createDatabase(database: string, ifNotExist?: boolean): Promise { + const up = ifNotExist ? `CREATE DATABASE IF NOT EXISTS \`${database}\`` : `CREATE DATABASE \`${database}\``; + const down = `DROP DATABASE \`${database}\``; + await this.executeQueries(new Query(up), new Query(down)); + } + + /** + * Drops database. + */ + async dropDatabase(database: string, ifExist?: boolean): Promise { + const up = ifExist ? `DROP DATABASE IF EXISTS \`${database}\`` : `DROP DATABASE \`${database}\``; + const down = `CREATE DATABASE \`${database}\``; + await this.executeQueries(new Query(up), new Query(down)); + } + + /** + * Creates a new table schema. + */ + async createSchema(schema: string, ifNotExist?: boolean): Promise { + throw new Error(`Schema create queries are not supported by MySql driver.`); + } + + /** + * Drops table schema. + */ + async dropSchema(schemaPath: string, ifExist?: boolean): Promise { + throw new Error(`Schema drop queries are not supported by MySql driver.`); + } + + /** + * Creates a new table. + */ + async createTable(table: Table, ifNotExist: boolean = false, createForeignKeys: boolean = true): Promise { + if (ifNotExist) { + const isTableExist = await this.hasTable(table); + if (isTableExist) return Promise.resolve(); + } + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + upQueries.push(this.createTableSql(table, createForeignKeys)); + downQueries.push(this.dropTableSql(table)); + + // we must first drop indices, than drop foreign keys, because drop queries runs in reversed order + // and foreign keys will be dropped first as indices. This order is very important, because we can't drop index + // if it related to the foreign key. + + // createTable does not need separate method to create indices, because it create indices in the same query with table creation. + table.indices.forEach(index => downQueries.push(this.dropIndexSql(table, index))); + + // if createForeignKeys is true, we must drop created foreign keys in down query. + // createTable does not need separate method to create foreign keys, because it create fk's in the same query with table creation. + if (createForeignKeys) + table.foreignKeys.forEach(foreignKey => downQueries.push(this.dropForeignKeySql(table, foreignKey))); + + return this.executeQueries(upQueries, downQueries); + } + + /** + * Drop the table. + */ + async dropTable(target: Table|string, ifExist?: boolean, dropForeignKeys: boolean = true): Promise { + // It needs because if table does not exist and dropForeignKeys or dropIndices is true, we don't need + // to perform drop queries for foreign keys and indices. + if (ifExist) { + const isTableExist = await this.hasTable(target); + if (!isTableExist) return Promise.resolve(); + } + + // if dropTable called with dropForeignKeys = true, we must create foreign keys in down query. + const createForeignKeys: boolean = dropForeignKeys; + const tableName = target instanceof Table ? target.name : target; + const table = await this.getCachedTable(tableName); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + if (dropForeignKeys) + table.foreignKeys.forEach(foreignKey => upQueries.push(this.dropForeignKeySql(table, foreignKey))); + + table.indices.forEach(index => upQueries.push(this.dropIndexSql(table, index))); + + upQueries.push(this.dropTableSql(table)); + downQueries.push(this.createTableSql(table, createForeignKeys)); + + await this.executeQueries(upQueries, downQueries); + } + + /** + * Creates a new view. + */ + async createView(view: View): Promise { + const upQueries: Query[] = []; + const downQueries: Query[] = []; + upQueries.push(this.createViewSql(view)); + upQueries.push(await this.insertViewDefinitionSql(view)); + downQueries.push(this.dropViewSql(view)); + downQueries.push(await this.deleteViewDefinitionSql(view)); + await this.executeQueries(upQueries, downQueries); + } + + /** + * Drops the view. + */ + async dropView(target: View|string): Promise { + const viewName = target instanceof View ? target.name : target; + const view = await this.getCachedView(viewName); + + const upQueries: Query[] = []; + const downQueries: Query[] = []; + upQueries.push(await this.deleteViewDefinitionSql(view)); + upQueries.push(this.dropViewSql(view)); + downQueries.push(await this.insertViewDefinitionSql(view)); + downQueries.push(this.createViewSql(view)); + await this.executeQueries(upQueries, downQueries); + } + + /** + * Renames a table. + */ + async renameTable(oldTableOrName: Table|string, newTableName: string): Promise { + const upQueries: Query[] = []; + const downQueries: Query[] = []; + const oldTable = oldTableOrName instanceof Table ? oldTableOrName : await this.getCachedTable(oldTableOrName); + const newTable = oldTable.clone(); + const dbName = oldTable.name.indexOf(".") === -1 ? undefined : oldTable.name.split(".")[0]; + newTable.name = dbName ? `${dbName}.${newTableName}` : newTableName; + + // rename table + upQueries.push(new Query(`RENAME TABLE ${this.escapePath(oldTable.name)} TO ${this.escapePath(newTable.name)}`)); + downQueries.push(new Query(`RENAME TABLE ${this.escapePath(newTable.name)} TO ${this.escapePath(oldTable.name)}`)); + + // rename index constraints + newTable.indices.forEach(index => { + // build new constraint name + const columnNames = index.columnNames.map(column => `\`${column}\``).join(", "); + const newIndexName = this.connection.namingStrategy.indexName(newTable, index.columnNames, index.where); + + // build queries + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} DROP INDEX \`${index.name}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${index.name}\` (${columnNames})`)); + + // replace constraint name + index.name = newIndexName; + }); + + // rename foreign key constraint + newTable.foreignKeys.forEach(foreignKey => { + // build new constraint name + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + + // build queries + let up = `ALTER TABLE ${this.escapePath(newTable)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + up += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + up += ` ON UPDATE ${foreignKey.onUpdate}`; + + let down = `ALTER TABLE ${this.escapePath(newTable)} DROP FOREIGN KEY \`${newForeignKeyName}\`, ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + down += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + down += ` ON UPDATE ${foreignKey.onUpdate}`; + + upQueries.push(new Query(up)); + downQueries.push(new Query(down)); + + // replace constraint name + foreignKey.name = newForeignKeyName; + }); + + await this.executeQueries(upQueries, downQueries); + + // rename old table and replace it in cached tabled; + oldTable.name = newTable.name; + this.replaceCachedTable(oldTable, newTable); + } + + /** + * Creates a new column from the column in the table. + */ + async addColumn(tableOrName: Table|string, column: TableColumn): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + const skipColumnLevelPrimary = clonedTable.primaryColumns.length > 0; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(column, skipColumnLevelPrimary, false)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN \`${column.name}\``)); + + // create or update primary key constraint + if (column.isPrimary && skipColumnLevelPrimary) { + // if we already have generated column, we must temporary drop AUTO_INCREMENT property. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${column.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(column, true)}`)); + } + + const primaryColumns = clonedTable.primaryColumns; + let columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + + primaryColumns.push(column); + columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + // if we previously dropped AUTO_INCREMENT property, we must bring it back + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(column, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${column.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + // create column index + const columnIndex = clonedTable.indices.find(index => index.columnNames.length === 1 && index.columnNames[0] === column.name); + if (columnIndex) { + upQueries.push(this.createIndexSql(table, columnIndex)); + downQueries.push(this.dropIndexSql(table, columnIndex)); + + } else if (column.isUnique) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table.name, [column.name]), + columnNames: [column.name], + isUnique: true + }); + clonedTable.indices.push(uniqueIndex); + clonedTable.uniques.push(new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames + })); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex.name}\` (\`${column.name}\`)`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex.name}\``)); + } + + await this.executeQueries(upQueries, downQueries); + + clonedTable.addColumn(column); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Creates a new columns from the column in the table. + */ + async addColumns(tableOrName: Table|string, columns: TableColumn[]): Promise { + await PromiseUtils.runInSequence(columns, column => this.addColumn(tableOrName, column)); + } + + /** + * Renames column in the given table. + */ + async renameColumn(tableOrName: Table|string, oldTableColumnOrName: TableColumn|string, newTableColumnOrName: TableColumn|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const oldColumn = oldTableColumnOrName instanceof TableColumn ? oldTableColumnOrName : table.columns.find(c => c.name === oldTableColumnOrName); + if (!oldColumn) + throw new Error(`Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`); + + let newColumn: TableColumn|undefined = undefined; + if (newTableColumnOrName instanceof TableColumn) { + newColumn = newTableColumnOrName; + } else { + newColumn = oldColumn.clone(); + newColumn.name = newTableColumnOrName; + } + + await this.changeColumn(table, oldColumn, newColumn); + } + + /** + * Changes a column in the table. + */ + async changeColumn(tableOrName: Table|string, oldColumnOrName: TableColumn|string, newColumn: TableColumn): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + let clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + const oldColumn = oldColumnOrName instanceof TableColumn + ? oldColumnOrName + : table.columns.find(column => column.name === oldColumnOrName); + if (!oldColumn) + throw new Error(`Column "${oldColumnOrName}" was not found in the "${table.name}" table.`); + + if ((newColumn.isGenerated !== oldColumn.isGenerated && newColumn.generationStrategy !== "uuid") + || oldColumn.type !== newColumn.type + || oldColumn.length !== newColumn.length + || oldColumn.generatedType !== newColumn.generatedType) { + await this.dropColumn(table, oldColumn); + await this.addColumn(table, newColumn); + + // update cloned table + clonedTable = table.clone(); + + } else { + if (newColumn.name !== oldColumn.name) { + // We don't change any column properties, just rename it. + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${oldColumn.name}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newColumn.name}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true, true)}`)); + + // rename index constraints + clonedTable.findColumnIndices(oldColumn).forEach(index => { + // build new constraint name + index.columnNames.splice(index.columnNames.indexOf(oldColumn.name), 1); + index.columnNames.push(newColumn.name); + const columnNames = index.columnNames.map(column => `\`${column}\``).join(", "); + const newIndexName = this.connection.namingStrategy.indexName(clonedTable, index.columnNames, index.where); + + // build queries + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${index.name}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${index.name}\` (${columnNames})`)); + + // replace constraint name + index.name = newIndexName; + }); + + // rename foreign key constraints + clonedTable.findColumnForeignKeys(oldColumn).forEach(foreignKey => { + // build new constraint name + foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); + foreignKey.columnNames.push(newColumn.name); + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + + // build queries + let up = `ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + up += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + up += ` ON UPDATE ${foreignKey.onUpdate}`; + + let down = `ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${newForeignKeyName}\`, ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + down += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + down += ` ON UPDATE ${foreignKey.onUpdate}`; + + upQueries.push(new Query(up)); + downQueries.push(new Query(down)); + + // replace constraint name + foreignKey.name = newForeignKeyName; + }); + + // rename old column in the Table object + const oldTableColumn = clonedTable.columns.find(column => column.name === oldColumn.name); + clonedTable.columns[clonedTable.columns.indexOf(oldTableColumn!)].name = newColumn.name; + oldColumn.name = newColumn.name; + } + + if (this.isColumnChanged(oldColumn, newColumn, true)) { + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${oldColumn.name}\` ${this.buildCreateColumnSql(newColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true)}`)); + } + + if (newColumn.isPrimary !== oldColumn.isPrimary) { + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + const primaryColumns = clonedTable.primaryColumns; + + // if primary column state changed, we must always drop existed constraint. + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + } + + if (newColumn.isPrimary === true) { + primaryColumns.push(newColumn); + // update column in table + const column = clonedTable.columns.find(column => column.name === newColumn.name); + column!.isPrimary = true; + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + } else { + const primaryColumn = primaryColumns.find(c => c.name === newColumn.name); + primaryColumns.splice(primaryColumns.indexOf(primaryColumn!), 1); + // update column in table + const column = clonedTable.columns.find(column => column.name === newColumn.name); + column!.isPrimary = false; + + // if we have another primary keys, we must recreate constraint. + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + } + } + + // if we have generated column, and we dropped AUTO_INCREMENT property before, we must bring it back + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + if (newColumn.isUnique !== oldColumn.isUnique) { + if (newColumn.isUnique === true) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table.name, [newColumn.name]), + columnNames: [newColumn.name], + isUnique: true + }); + clonedTable.indices.push(uniqueIndex); + clonedTable.uniques.push(new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames + })); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex.name}\` (\`${newColumn.name}\`)`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex.name}\``)); + + } else { + const uniqueIndex = clonedTable.indices.find(index => { + return index.columnNames.length === 1 && index.isUnique === true && !!index.columnNames.find(columnName => columnName === newColumn.name); + }); + clonedTable.indices.splice(clonedTable.indices.indexOf(uniqueIndex!), 1); + + const tableUnique = clonedTable.uniques.find(unique => unique.name === uniqueIndex!.name); + clonedTable.uniques.splice(clonedTable.uniques.indexOf(tableUnique!), 1); + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex!.name}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex!.name}\` (\`${newColumn.name}\`)`)); + } + } + } + + await this.executeQueries(upQueries, downQueries); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Changes a column in the table. + */ + async changeColumns(tableOrName: Table|string, changedColumns: { newColumn: TableColumn, oldColumn: TableColumn }[]): Promise { + await PromiseUtils.runInSequence(changedColumns, changedColumn => this.changeColumn(tableOrName, changedColumn.oldColumn, changedColumn.newColumn)); + } + + /** + * Drops column in the table. + */ + async dropColumn(tableOrName: Table|string, columnOrName: TableColumn|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const column = columnOrName instanceof TableColumn ? columnOrName : table.findColumnByName(columnOrName); + if (!column) + throw new Error(`Column "${columnOrName}" was not found in table "${table.name}"`); + + const clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + // drop primary key constraint + if (column.isPrimary) { + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + // dropping primary key constraint + const columnNames = clonedTable.primaryColumns.map(primaryColumn => `\`${primaryColumn.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} ADD PRIMARY KEY (${columnNames})`)); + + // update column in table + const tableColumn = clonedTable.findColumnByName(column.name); + tableColumn!.isPrimary = false; + + // if primary key have multiple columns, we must recreate it without dropped column + if (clonedTable.primaryColumns.length > 0) { + const columnNames = clonedTable.primaryColumns.map(primaryColumn => `\`${primaryColumn.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} DROP PRIMARY KEY`)); + } + + // if we have generated column, and we dropped AUTO_INCREMENT property before, and this column is not current dropping column, we must bring it back + if (generatedColumn && generatedColumn.name !== column.name) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + // drop column index + const columnIndex = clonedTable.indices.find(index => index.columnNames.length === 1 && index.columnNames[0] === column.name); + if (columnIndex) { + clonedTable.indices.splice(clonedTable.indices.indexOf(columnIndex), 1); + upQueries.push(this.dropIndexSql(table, columnIndex)); + downQueries.push(this.createIndexSql(table, columnIndex)); + + } else if (column.isUnique) { + // we splice constraints both from table uniques and indices. + const uniqueName = this.connection.namingStrategy.uniqueConstraintName(table.name, [column.name]); + const foundUnique = clonedTable.uniques.find(unique => unique.name === uniqueName); + if (foundUnique) + clonedTable.uniques.splice(clonedTable.uniques.indexOf(foundUnique), 1); + + const indexName = this.connection.namingStrategy.indexName(table.name, [column.name]); + const foundIndex = clonedTable.indices.find(index => index.name === indexName); + if (foundIndex) + clonedTable.indices.splice(clonedTable.indices.indexOf(foundIndex), 1); + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${indexName}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${indexName}\` (\`${column.name}\`)`)); + } + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN \`${column.name}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(column, true)}`)); + + await this.executeQueries(upQueries, downQueries); + + clonedTable.removeColumn(column); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Drops the columns in the table. + */ + async dropColumns(tableOrName: Table|string, columns: TableColumn[]): Promise { + await PromiseUtils.runInSequence(columns, column => this.dropColumn(tableOrName, column)); + } + + /** + * Creates a new primary key. + */ + async createPrimaryKey(tableOrName: Table|string, columnNames: string[]): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + + const up = this.createPrimaryKeySql(table, columnNames); + const down = this.dropPrimaryKeySql(table); + + await this.executeQueries(up, down); + clonedTable.columns.forEach(column => { + if (columnNames.find(columnName => columnName === column.name)) + column.isPrimary = true; + }); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Updates composite primary keys. + */ + async updatePrimaryKeys(tableOrName: Table|string, columns: TableColumn[]): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + const columnNames = columns.map(column => column.name); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + // if table already have primary columns, we must drop them. + const primaryColumns = clonedTable.primaryColumns; + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + } + + // update columns in table. + clonedTable.columns + .filter(column => columnNames.indexOf(column.name) !== -1) + .forEach(column => column.isPrimary = true); + + const columnNamesString = columnNames.map(columnName => `\`${columnName}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNamesString})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + // if we already have generated column or column is changed to generated, and we dropped AUTO_INCREMENT property before, we must bring it back + const newOrExistGeneratedColumn = generatedColumn ? generatedColumn : columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (newOrExistGeneratedColumn) { + const nonGeneratedColumn = newOrExistGeneratedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(newOrExistGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newOrExistGeneratedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + + // if column changed to generated, we must update it in table + const changedGeneratedColumn = clonedTable.columns.find(column => column.name === newOrExistGeneratedColumn.name); + changedGeneratedColumn!.isGenerated = true; + changedGeneratedColumn!.generationStrategy = "increment"; + } + + await this.executeQueries(upQueries, downQueries); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Drops a primary key. + */ + async dropPrimaryKey(tableOrName: Table|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const up = this.dropPrimaryKeySql(table); + const down = this.createPrimaryKeySql(table, table.primaryColumns.map(column => column.name)); + await this.executeQueries(up, down); + table.primaryColumns.forEach(column => { + column.isPrimary = false; + }); + } + + /** + * Creates a new unique constraint. + */ + async createUniqueConstraint(tableOrName: Table|string, uniqueConstraint: TableUnique): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Creates a new unique constraints. + */ + async createUniqueConstraints(tableOrName: Table|string, uniqueConstraints: TableUnique[]): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Drops an unique constraint. + */ + async dropUniqueConstraint(tableOrName: Table|string, uniqueOrName: TableUnique|string): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Drops an unique constraints. + */ + async dropUniqueConstraints(tableOrName: Table|string, uniqueConstraints: TableUnique[]): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Creates a new check constraint. + */ + async createCheckConstraint(tableOrName: Table|string, checkConstraint: TableCheck): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Creates a new check constraints. + */ + async createCheckConstraints(tableOrName: Table|string, checkConstraints: TableCheck[]): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Drops check constraint. + */ + async dropCheckConstraint(tableOrName: Table|string, checkOrName: TableCheck|string): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Drops check constraints. + */ + async dropCheckConstraints(tableOrName: Table|string, checkConstraints: TableCheck[]): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Creates a new exclusion constraint. + */ + async createExclusionConstraint(tableOrName: Table|string, exclusionConstraint: TableExclusion): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Creates a new exclusion constraints. + */ + async createExclusionConstraints(tableOrName: Table|string, exclusionConstraints: TableExclusion[]): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Drops exclusion constraint. + */ + async dropExclusionConstraint(tableOrName: Table|string, exclusionOrName: TableExclusion|string): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Drops exclusion constraints. + */ + async dropExclusionConstraints(tableOrName: Table|string, exclusionConstraints: TableExclusion[]): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Creates a new foreign key. + */ + async createForeignKey(tableOrName: Table|string, foreignKey: TableForeignKey): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + + // new FK may be passed without name. In this case we generate FK name manually. + if (!foreignKey.name) + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + + const up = this.createForeignKeySql(table, foreignKey); + const down = this.dropForeignKeySql(table, foreignKey); + await this.executeQueries(up, down); + table.addForeignKey(foreignKey); + } + + /** + * Creates a new foreign keys. + */ + async createForeignKeys(tableOrName: Table|string, foreignKeys: TableForeignKey[]): Promise { + const promises = foreignKeys.map(foreignKey => this.createForeignKey(tableOrName, foreignKey)); + await Promise.all(promises); + } + + /** + * Drops a foreign key. + */ + async dropForeignKey(tableOrName: Table|string, foreignKeyOrName: TableForeignKey|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const foreignKey = foreignKeyOrName instanceof TableForeignKey ? foreignKeyOrName : table.foreignKeys.find(fk => fk.name === foreignKeyOrName); + if (!foreignKey) + throw new Error(`Supplied foreign key was not found in table ${table.name}`); + + const up = this.dropForeignKeySql(table, foreignKey); + const down = this.createForeignKeySql(table, foreignKey); + await this.executeQueries(up, down); + table.removeForeignKey(foreignKey); + } + + /** + * Drops a foreign keys from the table. + */ + async dropForeignKeys(tableOrName: Table|string, foreignKeys: TableForeignKey[]): Promise { + const promises = foreignKeys.map(foreignKey => this.dropForeignKey(tableOrName, foreignKey)); + await Promise.all(promises); + } + + /** + * Creates a new index. + */ + async createIndex(tableOrName: Table|string, index: TableIndex): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + + // new index may be passed without name. In this case we generate index name manually. + if (!index.name) + index.name = this.connection.namingStrategy.indexName(table.name, index.columnNames, index.where); + + const up = this.createIndexSql(table, index); + const down = this.dropIndexSql(table, index); + await this.executeQueries(up, down); + table.addIndex(index, true); + } + + /** + * Creates a new indices + */ + async createIndices(tableOrName: Table|string, indices: TableIndex[]): Promise { + const promises = indices.map(index => this.createIndex(tableOrName, index)); + await Promise.all(promises); + } + + /** + * Drops an index. + */ + async dropIndex(tableOrName: Table|string, indexOrName: TableIndex|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const index = indexOrName instanceof TableIndex ? indexOrName : table.indices.find(i => i.name === indexOrName); + if (!index) + throw new Error(`Supplied index was not found in table ${table.name}`); + + const up = this.dropIndexSql(table, index); + const down = this.createIndexSql(table, index); + await this.executeQueries(up, down); + table.removeIndex(index, true); + } + + /** + * Drops an indices from the table. + */ + async dropIndices(tableOrName: Table|string, indices: TableIndex[]): Promise { + const promises = indices.map(index => this.dropIndex(tableOrName, index)); + await Promise.all(promises); + } + + /** + * Clears all table contents. + * Note: this operation uses SQL's TRUNCATE query which cannot be reverted in transactions. + */ + async clearTable(tableOrName: Table|string): Promise { + await this.query(`TRUNCATE TABLE ${this.escapePath(tableOrName)}`); + } + + /** + * Removes all tables from the currently connected database. + * Be careful using this method and avoid using it in production or migrations + * (because it can clear all your database). + */ + async clearDatabase(database?: string): Promise { + const dbName = database ? database : this.driver.database; + if (dbName) { + const isDatabaseExist = await this.hasDatabase(dbName); + if (!isDatabaseExist) + return Promise.resolve(); + } else { + throw new Error(`Can not clear database. No database is specified`); + } + + await this.startTransaction(); + try { + + const selectViewDropsQuery = `SELECT concat('DROP VIEW IF EXISTS \`', table_schema, '\`.\`', table_name, '\`') AS \`query\` FROM \`INFORMATION_SCHEMA\`.\`VIEWS\` WHERE \`TABLE_SCHEMA\` = '${dbName}'`; + const dropViewQueries: ObjectLiteral[] = await this.query(selectViewDropsQuery); + await Promise.all(dropViewQueries.map(q => this.query(q["query"]))); + + const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`; + const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS \`', table_schema, '\`.\`', table_name, '\`') AS \`query\` FROM \`INFORMATION_SCHEMA\`.\`TABLES\` WHERE \`TABLE_SCHEMA\` = '${dbName}'`; + const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`; + + await this.query(disableForeignKeysCheckQuery); + const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery); + await Promise.all(dropQueries.map(query => this.query(query["query"]))); + await this.query(enableForeignKeysCheckQuery); + + await this.commitTransaction(); + + } catch (error) { + try { // we throw original error even if rollback thrown an error + await this.rollbackTransaction(); + } catch (rollbackError) { } + throw error; + } + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Returns current database. + */ + protected async getCurrentDatabase(): Promise { + const currentDBQuery = await this.query(`SELECT DATABASE() AS \`db_name\``); + return currentDBQuery[0]["db_name"]; + } + + protected async loadViews(viewNames: string[]): Promise { + const hasTable = await this.hasTable(this.getTypeormMetadataTableName()); + if (!hasTable) + return Promise.resolve([]); + + const currentDatabase = await this.getCurrentDatabase(); + const viewsCondition = viewNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`t\`.\`schema\` = '${database}' AND \`t\`.\`name\` = '${name}')`; + }).join(" OR "); + + const query = `SELECT \`t\`.*, \`v\`.\`check_option\` FROM ${this.escapePath(this.getTypeormMetadataTableName())} \`t\` ` + + `INNER JOIN \`information_schema\`.\`views\` \`v\` ON \`v\`.\`table_schema\` = \`t\`.\`schema\` AND \`v\`.\`table_name\` = \`t\`.\`name\` WHERE \`t\`.\`type\` = 'VIEW' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; + const dbViews = await this.query(query); + return dbViews.map((dbView: any) => { + const view = new View(); + const db = dbView["schema"] === currentDatabase ? undefined : dbView["schema"]; + view.name = this.driver.buildTableName(dbView["name"], undefined, db); + view.expression = dbView["value"]; + return view; + }); + } + + /** + * Loads all tables (with given names) from the database and creates a Table from them. + */ + protected async loadTables(tableNames: string[]): Promise { + + // if no tables given then no need to proceed + if (!tableNames || !tableNames.length) + return []; + + const currentDatabase = await this.getCurrentDatabase(); + const tablesCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`TABLE_SCHEMA\` = '${database}' AND \`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const tablesSql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`TABLES\` WHERE ` + tablesCondition; + + const columnsSql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE ` + tablesCondition; + + const primaryKeySql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` WHERE \`CONSTRAINT_NAME\` = 'PRIMARY' AND (${tablesCondition})`; + + const collationsSql = `SELECT \`SCHEMA_NAME\`, \`DEFAULT_CHARACTER_SET_NAME\` as \`CHARSET\`, \`DEFAULT_COLLATION_NAME\` AS \`COLLATION\` FROM \`INFORMATION_SCHEMA\`.\`SCHEMATA\``; + + const indicesCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`s\`.\`TABLE_SCHEMA\` = '${database}' AND \`s\`.\`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const indicesSql = `SELECT \`s\`.* FROM \`INFORMATION_SCHEMA\`.\`STATISTICS\` \`s\` ` + + `LEFT JOIN \`INFORMATION_SCHEMA\`.\`REFERENTIAL_CONSTRAINTS\` \`rc\` ON \`s\`.\`INDEX_NAME\` = \`rc\`.\`CONSTRAINT_NAME\` ` + + `WHERE (${indicesCondition}) AND \`s\`.\`INDEX_NAME\` != 'PRIMARY' AND \`rc\`.\`CONSTRAINT_NAME\` IS NULL`; + + const foreignKeysCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`kcu\`.\`TABLE_SCHEMA\` = '${database}' AND \`kcu\`.\`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const foreignKeysSql = `SELECT \`kcu\`.\`TABLE_SCHEMA\`, \`kcu\`.\`TABLE_NAME\`, \`kcu\`.\`CONSTRAINT_NAME\`, \`kcu\`.\`COLUMN_NAME\`, \`kcu\`.\`REFERENCED_TABLE_SCHEMA\`, ` + + `\`kcu\`.\`REFERENCED_TABLE_NAME\`, \`kcu\`.\`REFERENCED_COLUMN_NAME\`, \`rc\`.\`DELETE_RULE\` \`ON_DELETE\`, \`rc\`.\`UPDATE_RULE\` \`ON_UPDATE\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` \`kcu\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`REFERENTIAL_CONSTRAINTS\` \`rc\` ON \`rc\`.\`constraint_name\` = \`kcu\`.\`constraint_name\` ` + + `WHERE ` + foreignKeysCondition; + const [dbTables, dbColumns, dbPrimaryKeys, dbCollations, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([ + this.query(tablesSql), + this.query(columnsSql), + this.query(primaryKeySql), + this.query(collationsSql), + this.query(indicesSql), + this.query(foreignKeysSql) + ]); + + // if tables were not found in the db, no need to proceed + if (!dbTables.length) + return []; + + + // create tables for loaded tables + return Promise.all(dbTables.map(async dbTable => { + const table = new Table(); + + const dbCollation = dbCollations.find(coll => coll["SCHEMA_NAME"] === dbTable["TABLE_SCHEMA"])!; + const defaultCollation = dbCollation["COLLATION"]; + const defaultCharset = dbCollation["CHARSET"]; + + // We do not need to join database name, when database is by default. + // In this case we need local variable `tableFullName` for below comparision. + const db = dbTable["TABLE_SCHEMA"] === currentDatabase ? undefined : dbTable["TABLE_SCHEMA"]; + table.name = this.driver.buildTableName(dbTable["TABLE_NAME"], undefined, db); + const tableFullName = this.driver.buildTableName(dbTable["TABLE_NAME"], undefined, dbTable["TABLE_SCHEMA"]); + + // create columns from the loaded columns + table.columns = dbColumns + .filter(dbColumn => this.driver.buildTableName(dbColumn["TABLE_NAME"], undefined, dbColumn["TABLE_SCHEMA"]) === tableFullName) + .map(dbColumn => { + + const columnUniqueIndex = dbIndices.find(dbIndex => { + return this.driver.buildTableName(dbIndex["TABLE_NAME"], undefined, dbIndex["TABLE_SCHEMA"]) === tableFullName + && dbIndex["COLUMN_NAME"] === dbColumn["COLUMN_NAME"] && dbIndex["NON_UNIQUE"] === "0"; + }); + + const tableMetadata = this.connection.entityMetadatas.find(metadata => metadata.tablePath === table.name); + const hasIgnoredIndex = columnUniqueIndex && tableMetadata && tableMetadata.indices + .some(index => index.name === columnUniqueIndex["INDEX_NAME"] && index.synchronize === false); + + const isConstraintComposite = columnUniqueIndex + ? !!dbIndices.find(dbIndex => dbIndex["INDEX_NAME"] === columnUniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]) + : false; + + const tableColumn = new TableColumn(); + tableColumn.name = dbColumn["COLUMN_NAME"]; + tableColumn.type = dbColumn["DATA_TYPE"].toLowerCase(); + + if (this.driver.withWidthColumnTypes.indexOf(tableColumn.type as ColumnType) !== -1) { + const width = dbColumn["COLUMN_TYPE"].substring(dbColumn["COLUMN_TYPE"].indexOf("(") + 1, dbColumn["COLUMN_TYPE"].indexOf(")")); + tableColumn.width = width && !this.isDefaultColumnWidth(table, tableColumn, parseInt(width)) ? parseInt(width) : undefined; + } + + if (dbColumn["COLUMN_DEFAULT"] === null + || dbColumn["COLUMN_DEFAULT"] === undefined) { + tableColumn.default = undefined; + + } else { + tableColumn.default = dbColumn["COLUMN_DEFAULT"] === "CURRENT_TIMESTAMP" ? dbColumn["COLUMN_DEFAULT"] : `'${dbColumn["COLUMN_DEFAULT"]}'`; + } + + if (dbColumn["EXTRA"].indexOf("on update") !== -1) { + tableColumn.onUpdate = dbColumn["EXTRA"].substring(dbColumn["EXTRA"].indexOf("on update") + 10); + } + + if (dbColumn["GENERATION_EXPRESSION"]) { + tableColumn.asExpression = dbColumn["GENERATION_EXPRESSION"]; + tableColumn.generatedType = dbColumn["EXTRA"].indexOf("VIRTUAL") !== -1 ? "VIRTUAL" : "STORED"; + } + + tableColumn.isUnique = !!columnUniqueIndex && !hasIgnoredIndex && !isConstraintComposite; + tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "YES"; + tableColumn.isPrimary = dbPrimaryKeys.some(dbPrimaryKey => { + return this.driver.buildTableName(dbPrimaryKey["TABLE_NAME"], undefined, dbPrimaryKey["TABLE_SCHEMA"]) === tableFullName && dbPrimaryKey["COLUMN_NAME"] === tableColumn.name; + }); + tableColumn.zerofill = dbColumn["COLUMN_TYPE"].indexOf("zerofill") !== -1; + tableColumn.unsigned = tableColumn.zerofill ? true : dbColumn["COLUMN_TYPE"].indexOf("unsigned") !== -1; + tableColumn.isGenerated = dbColumn["EXTRA"].indexOf("auto_increment") !== -1; + if (tableColumn.isGenerated) + tableColumn.generationStrategy = "increment"; + + tableColumn.comment = dbColumn["COLUMN_COMMENT"]; + if (dbColumn["CHARACTER_SET_NAME"]) + tableColumn.charset = dbColumn["CHARACTER_SET_NAME"] === defaultCharset ? undefined : dbColumn["CHARACTER_SET_NAME"]; + if (dbColumn["COLLATION_NAME"]) + tableColumn.collation = dbColumn["COLLATION_NAME"] === defaultCollation ? undefined : dbColumn["COLLATION_NAME"]; + + // check only columns that have length property + if (this.driver.withLengthColumnTypes.indexOf(tableColumn.type as ColumnType) !== -1 && dbColumn["CHARACTER_MAXIMUM_LENGTH"]) { + const length = dbColumn["CHARACTER_MAXIMUM_LENGTH"].toString(); + tableColumn.length = !this.isDefaultColumnLength(table, tableColumn, length) ? length : ""; + } + + if (tableColumn.type === "decimal" || tableColumn.type === "double" || tableColumn.type === "float") { + if (dbColumn["NUMERIC_PRECISION"] !== null && !this.isDefaultColumnPrecision(table, tableColumn, dbColumn["NUMERIC_PRECISION"])) + tableColumn.precision = parseInt(dbColumn["NUMERIC_PRECISION"]); + if (dbColumn["NUMERIC_SCALE"] !== null && !this.isDefaultColumnScale(table, tableColumn, dbColumn["NUMERIC_SCALE"])) + tableColumn.scale = parseInt(dbColumn["NUMERIC_SCALE"]); + } + + if (tableColumn.type === "enum" || tableColumn.type === "simple-enum") { + const colType = dbColumn["COLUMN_TYPE"]; + const items = colType.substring(colType.indexOf("(") + 1, colType.indexOf(")")).split(","); + tableColumn.enum = (items as string[]).map(item => { + return item.substring(1, item.length - 1); + }); + tableColumn.length = ""; + } + + if ((tableColumn.type === "datetime" || tableColumn.type === "time" || tableColumn.type === "timestamp") + && dbColumn["DATETIME_PRECISION"] !== null && dbColumn["DATETIME_PRECISION"] !== undefined + && !this.isDefaultColumnPrecision(table, tableColumn, parseInt(dbColumn["DATETIME_PRECISION"]))) { + tableColumn.precision = parseInt(dbColumn["DATETIME_PRECISION"]); + } + + return tableColumn; + }); + + // find foreign key constraints of table, group them by constraint name and build TableForeignKey. + const tableForeignKeyConstraints = OrmUtils.uniq(dbForeignKeys.filter(dbForeignKey => { + return this.driver.buildTableName(dbForeignKey["TABLE_NAME"], undefined, dbForeignKey["TABLE_SCHEMA"]) === tableFullName; + }), dbForeignKey => dbForeignKey["CONSTRAINT_NAME"]); + + table.foreignKeys = tableForeignKeyConstraints.map(dbForeignKey => { + const foreignKeys = dbForeignKeys.filter(dbFk => dbFk["CONSTRAINT_NAME"] === dbForeignKey["CONSTRAINT_NAME"]); + + // if referenced table located in currently used db, we don't need to concat db name to table name. + const database = dbForeignKey["REFERENCED_TABLE_SCHEMA"] === currentDatabase ? undefined : dbForeignKey["REFERENCED_TABLE_SCHEMA"]; + const referencedTableName = this.driver.buildTableName(dbForeignKey["REFERENCED_TABLE_NAME"], undefined, database); + + return new TableForeignKey({ + name: dbForeignKey["CONSTRAINT_NAME"], + columnNames: foreignKeys.map(dbFk => dbFk["COLUMN_NAME"]), + referencedTableName: referencedTableName, + referencedColumnNames: foreignKeys.map(dbFk => dbFk["REFERENCED_COLUMN_NAME"]), + onDelete: dbForeignKey["ON_DELETE"], + onUpdate: dbForeignKey["ON_UPDATE"] + }); + }); + + // find index constraints of table, group them by constraint name and build TableIndex. + const tableIndexConstraints = OrmUtils.uniq(dbIndices.filter(dbIndex => { + return this.driver.buildTableName(dbIndex["TABLE_NAME"], undefined, dbIndex["TABLE_SCHEMA"]) === tableFullName; + }), dbIndex => dbIndex["INDEX_NAME"]); + + table.indices = tableIndexConstraints.map(constraint => { + const indices = dbIndices.filter(index => { + return index["TABLE_SCHEMA"] === constraint["TABLE_SCHEMA"] + && index["TABLE_NAME"] === constraint["TABLE_NAME"] + && index["INDEX_NAME"] === constraint["INDEX_NAME"]; + }); + return new TableIndex({ + table: table, + name: constraint["INDEX_NAME"], + columnNames: indices.map(i => i["COLUMN_NAME"]), + isUnique: constraint["NON_UNIQUE"] === "0", + isSpatial: constraint["INDEX_TYPE"] === "SPATIAL", + isFulltext: constraint["INDEX_TYPE"] === "FULLTEXT" + }); + }); + + return table; + })); + } + + /** + * Builds create table sql + */ + protected createTableSql(table: Table, createForeignKeys?: boolean): Query { + const columnDefinitions = table.columns.map(column => this.buildCreateColumnSql(column, true)).join(", "); + let sql = `CREATE TABLE ${this.escapePath(table)} (${columnDefinitions}`; + + // we create unique indexes instead of unique constraints, because MySql does not have unique constraints. + // if we mark column as Unique, it means that we create UNIQUE INDEX. + table.columns + .filter(column => column.isUnique) + .forEach(column => { + const isUniqueIndexExist = table.indices.some(index => { + return index.columnNames.length === 1 && !!index.isUnique && index.columnNames.indexOf(column.name) !== -1; + }); + const isUniqueConstraintExist = table.uniques.some(unique => { + return unique.columnNames.length === 1 && unique.columnNames.indexOf(column.name) !== -1; + }); + if (!isUniqueIndexExist && !isUniqueConstraintExist) + table.indices.push(new TableIndex({ + name: this.connection.namingStrategy.uniqueConstraintName(table.name, [column.name]), + columnNames: [column.name], + isUnique: true + })); + }); + + // as MySql does not have unique constraints, we must create table indices from table uniques and mark them as unique. + if (table.uniques.length > 0) { + table.uniques.forEach(unique => { + const uniqueExist = table.indices.some(index => index.name === unique.name); + if (!uniqueExist) { + table.indices.push(new TableIndex({ + name: unique.name, + columnNames: unique.columnNames, + isUnique: true + })); + } + }); + } + + if (table.indices.length > 0) { + const indicesSql = table.indices.map(index => { + const columnNames = index.columnNames.map(columnName => `\`${columnName}\``).join(", "); + if (!index.name) + index.name = this.connection.namingStrategy.indexName(table.name, index.columnNames, index.where); + + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + + return `${indexType}INDEX \`${index.name}\` (${columnNames})`; + }).join(", "); + + sql += `, ${indicesSql}`; + } + + if (table.foreignKeys.length > 0 && createForeignKeys) { + const foreignKeysSql = table.foreignKeys.map(fk => { + const columnNames = fk.columnNames.map(columnName => `\`${columnName}\``).join(", "); + if (!fk.name) + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + const referencedColumnNames = fk.referencedColumnNames.map(columnName => `\`${columnName}\``).join(", "); + + let constraint = `CONSTRAINT \`${fk.name}\` FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; + if (fk.onDelete) + constraint += ` ON DELETE ${fk.onDelete}`; + if (fk.onUpdate) + constraint += ` ON UPDATE ${fk.onUpdate}`; + + return constraint; + }).join(", "); + + sql += `, ${foreignKeysSql}`; + } + + if (table.primaryColumns.length > 0) { + const columnNames = table.primaryColumns.map(column => `\`${column.name}\``).join(", "); + sql += `, PRIMARY KEY (${columnNames})`; + } + + sql += `) ENGINE=${table.engine || "InnoDB"}`; + + return new Query(sql); + } + + /** + * Builds drop table sql + */ + protected dropTableSql(tableOrName: Table|string): Query { + return new Query(`DROP TABLE ${this.escapePath(tableOrName)}`); + } + + protected createViewSql(view: View): Query { + if (typeof view.expression === "string") { + return new Query(`CREATE VIEW ${this.escapePath(view)} AS ${view.expression}`); + } else { + return new Query(`CREATE VIEW ${this.escapePath(view)} AS ${view.expression(this.connection).getQuery()}`); + } + } + + protected async insertViewDefinitionSql(view: View): Promise { + const currentDatabase = await this.getCurrentDatabase(); + const expression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery(); + const [query, parameters] = this.connection.createQueryBuilder() + .insert() + .into(this.getTypeormMetadataTableName()) + .values({ type: "VIEW", schema: currentDatabase, name: view.name, value: expression }) + .getQueryAndParameters(); + + return new Query(query, parameters); + } + + /** + * Builds drop view sql. + */ + protected dropViewSql(viewOrPath: View|string): Query { + return new Query(`DROP VIEW ${this.escapePath(viewOrPath)}`); + } + + /** + * Builds remove view sql. + */ + protected async deleteViewDefinitionSql(viewOrPath: View|string): Promise { + const currentDatabase = await this.getCurrentDatabase(); + const viewName = viewOrPath instanceof View ? viewOrPath.name : viewOrPath; + const qb = this.connection.createQueryBuilder(); + const [query, parameters] = qb.delete() + .from(this.getTypeormMetadataTableName()) + .where(`${qb.escape("type")} = 'VIEW'`) + .andWhere(`${qb.escape("schema")} = :schema`, { schema: currentDatabase }) + .andWhere(`${qb.escape("name")} = :name`, { name: viewName }) + .getQueryAndParameters(); + + return new Query(query, parameters); + } + + /** + * Builds create index sql. + */ + protected createIndexSql(table: Table, index: TableIndex): Query { + const columns = index.columnNames.map(columnName => `\`${columnName}\``).join(", "); + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + return new Query(`CREATE ${indexType}INDEX \`${index.name}\` ON ${this.escapePath(table)} (${columns})`); + } + + /** + * Builds drop index sql. + */ + protected dropIndexSql(table: Table, indexOrName: TableIndex|string): Query { + let indexName = indexOrName instanceof TableIndex ? indexOrName.name : indexOrName; + return new Query(`DROP INDEX \`${indexName}\` ON ${this.escapePath(table)}`); + } + + /** + * Builds create primary key sql. + */ + protected createPrimaryKeySql(table: Table, columnNames: string[]): Query { + const columnNamesString = columnNames.map(columnName => `\`${columnName}\``).join(", "); + return new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNamesString})`); + } + + /** + * Builds drop primary key sql. + */ + protected dropPrimaryKeySql(table: Table): Query { + return new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`); + } + + /** + * Builds create foreign key sql. + */ + protected createForeignKeySql(table: Table, foreignKey: TableForeignKey): Query { + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + let sql = `ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + sql += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + sql += ` ON UPDATE ${foreignKey.onUpdate}`; + + return new Query(sql); + } + + /** + * Builds drop foreign key sql. + */ + protected dropForeignKeySql(table: Table, foreignKeyOrName: TableForeignKey|string): Query { + const foreignKeyName = foreignKeyOrName instanceof TableForeignKey ? foreignKeyOrName.name : foreignKeyOrName; + return new Query(`ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${foreignKeyName}\``); + } + + protected parseTableName(target: Table|string) { + const tableName = target instanceof Table ? target.name : target; + return { + database: tableName.indexOf(".") !== -1 ? tableName.split(".")[0] : this.driver.database, + tableName: tableName.indexOf(".") !== -1 ? tableName.split(".")[1] : tableName + }; + } + + /** + * Escapes given table or view path. + */ + protected escapePath(target: Table|View|string, disableEscape?: boolean): string { + const tableName = target instanceof Table || target instanceof View ? target.name : target; + return tableName.split(".").map(i => disableEscape ? i : `\`${i}\``).join("."); + } + + /** + * Builds a part of query to create/change a column. + */ + protected buildCreateColumnSql(column: TableColumn, skipPrimary: boolean, skipName: boolean = false) { + let c = ""; + if (skipName) { + c = this.connection.driver.createFullType(column); + } else { + c = `\`${column.name}\` ${this.connection.driver.createFullType(column)}`; + } + if (column.asExpression) + c += ` AS (${column.asExpression}) ${column.generatedType ? column.generatedType : "VIRTUAL"}`; + + // if you specify ZEROFILL for a numeric column, MySQL automatically adds the UNSIGNED attribute to that column. + if (column.zerofill) { + c += " ZEROFILL"; + } else if (column.unsigned) { + c += " UNSIGNED"; + } + if (column.enum) + c += ` (${column.enum.map(value => "'" + value + "'").join(", ")})`; + if (column.charset) + c += ` CHARACTER SET "${column.charset}"`; + if (column.collation) + c += ` COLLATE "${column.collation}"`; + if (!column.isNullable) + c += " NOT NULL"; + if (column.isNullable) + c += " NULL"; + if (column.isPrimary && !skipPrimary) + c += " PRIMARY KEY"; + if (column.isGenerated && column.generationStrategy === "increment") // don't use skipPrimary here since updates can update already exist primary without auto inc. + c += " AUTO_INCREMENT"; + if (column.comment) + c += ` COMMENT '${column.comment}'`; + if (column.default !== undefined && column.default !== null) + c += ` DEFAULT ${column.default}`; + if (column.onUpdate) + c += ` ON UPDATE ${column.onUpdate}`; + + return c; + } + +} diff --git a/src/driver/cockroachdb/CockroachConnectionOptions.ts b/src/driver/cockroachdb/CockroachConnectionOptions.ts index 71e1d998f1..8dbd8fc151 100644 --- a/src/driver/cockroachdb/CockroachConnectionOptions.ts +++ b/src/driver/cockroachdb/CockroachConnectionOptions.ts @@ -33,4 +33,11 @@ export interface CockroachConnectionOptions extends BaseConnectionOptions, Cockr }; + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; + } diff --git a/src/driver/cockroachdb/CockroachDriver.ts b/src/driver/cockroachdb/CockroachDriver.ts index 31b0f298fd..188be7d8f9 100644 --- a/src/driver/cockroachdb/CockroachDriver.ts +++ b/src/driver/cockroachdb/CockroachDriver.ts @@ -710,11 +710,14 @@ export class CockroachDriver implements Driver { // create a connection pool const pool = new this.postgres.Pool(connectionOptions); const { logger } = this.connection; + + const poolErrorHandler = options.poolErrorHandler || ((error: any) => logger.log("warn", `Postgres pool raised an error. ${error}`)); + /* Attaching an error handler to pool errors is essential, as, otherwise, errors raised will go unhandled and cause the hosting app to crash. */ - pool.on("error", (error: any) => logger.log("warn", `Postgres pool raised an error. ${error}`)); + pool.on("error", poolErrorHandler); return new Promise((ok, fail) => { pool.connect((err: any, connection: any, release: Function) => { diff --git a/src/driver/cockroachdb/CockroachQueryRunner.ts b/src/driver/cockroachdb/CockroachQueryRunner.ts index 0e8468c4ce..10d6342bc1 100644 --- a/src/driver/cockroachdb/CockroachQueryRunner.ts +++ b/src/driver/cockroachdb/CockroachQueryRunner.ts @@ -505,7 +505,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner // rename foreign key constraints newTable.foreignKeys.forEach(foreignKey => { // build new constraint name - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -702,7 +702,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner // build new constraint name foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); foreignKey.columnNames.push(newColumn.name); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -1123,7 +1123,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner // new FK may be passed without name. In this case we generate FK name manually. if (!foreignKey.name) - foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); const up = this.createForeignKeySql(table, foreignKey); const down = this.dropForeignKeySql(table, foreignKey); @@ -1618,7 +1618,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", "); let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; diff --git a/src/driver/mongodb/MongoConnectionOptions.ts b/src/driver/mongodb/MongoConnectionOptions.ts index f8fe8e426e..845f3f718c 100644 --- a/src/driver/mongodb/MongoConnectionOptions.ts +++ b/src/driver/mongodb/MongoConnectionOptions.ts @@ -321,4 +321,10 @@ export interface MongoConnectionOptions extends BaseConnectionOptions { * Determines whether or not to use the new url parser. Default: false */ readonly useNewUrlParser?: boolean; + + /** + * Determines whether or not to use the new Server Discovery and Monitoring engine. Default: false + * https://github.com/mongodb/node-mongodb-native/releases/tag/v3.2.1 + */ + readonly useUnifiedTopology?: boolean; } diff --git a/src/driver/mongodb/MongoDriver.ts b/src/driver/mongodb/MongoDriver.ts index beb067380a..1de0187f2a 100644 --- a/src/driver/mongodb/MongoDriver.ts +++ b/src/driver/mongodb/MongoDriver.ts @@ -192,7 +192,8 @@ export class MongoDriver implements Driver { "auto_reconnect", "minSize", "monitorCommands", - "useNewUrlParser" + "useNewUrlParser", + "useUnifiedTopology" ]; // ------------------------------------------------------------------------- diff --git a/src/driver/mysql/MysqlDriver.ts b/src/driver/mysql/MysqlDriver.ts index 5c42251a55..bc1dda1241 100644 --- a/src/driver/mysql/MysqlDriver.ts +++ b/src/driver/mysql/MysqlDriver.ts @@ -120,6 +120,7 @@ export class MysqlDriver implements Driver { "longblob", "longtext", "enum", + "set", "binary", "varbinary", // json data type @@ -459,6 +460,9 @@ export class MysqlDriver implements Driver { } else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") { return "" + value; + + } else if (columnMetadata.type === "set") { + return DateUtils.simpleArrayToString(value); } return value; @@ -498,6 +502,8 @@ export class MysqlDriver implements Driver { && columnMetadata.enum.indexOf(parseInt(value)) >= 0) { // convert to number if that exists in possible enum options value = parseInt(value); + } else if (columnMetadata.type === "set") { + value = DateUtils.stringToSimpleArray(value); } if (columnMetadata.transformer) @@ -564,6 +570,10 @@ export class MysqlDriver implements Driver { return `'${defaultValue}'`; } + if ((columnMetadata.type === "set") && defaultValue !== undefined) { + return `'${DateUtils.simpleArrayToString(defaultValue)}'`; + } + if (typeof defaultValue === "number") { return "" + defaultValue; diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index 7589fdb0f9..1176e7eea5 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -408,7 +408,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { // build new constraint name const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries let up = `ALTER TABLE ${this.escapePath(newTable)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + @@ -599,7 +599,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { foreignKey.columnNames.push(newColumn.name); const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries let up = `ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + @@ -1004,7 +1004,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { // new FK may be passed without name. In this case we generate FK name manually. if (!foreignKey.name) - foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); const up = this.createForeignKeySql(table, foreignKey); const down = this.dropForeignKeySql(table, foreignKey); @@ -1333,7 +1333,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { tableColumn.scale = parseInt(dbColumn["NUMERIC_SCALE"]); } - if (tableColumn.type === "enum" || tableColumn.type === "simple-enum") { + if (tableColumn.type === "enum" || tableColumn.type === "simple-enum" || tableColumn.type === "set") { const colType = dbColumn["COLUMN_TYPE"]; const items = colType.substring(colType.indexOf("(") + 1, colType.indexOf(")")).split(","); tableColumn.enum = (items as string[]).map(item => { @@ -1462,7 +1462,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `\`${columnName}\``).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `\`${columnName}\``).join(", "); let constraint = `CONSTRAINT \`${fk.name}\` FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; @@ -1636,7 +1636,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { c += " UNSIGNED"; } if (column.enum) - c += ` (${column.enum.map(value => "'" + value + "'").join(", ")})`; + c += ` (${column.enum.map(value => "'" + value.replace("'", "''") + "'").join(", ")})`; if (column.charset) c += ` CHARACTER SET "${column.charset}"`; if (column.collation) diff --git a/src/driver/oracle/OracleQueryRunner.ts b/src/driver/oracle/OracleQueryRunner.ts index e7d99e4e16..ac0f34dc67 100644 --- a/src/driver/oracle/OracleQueryRunner.ts +++ b/src/driver/oracle/OracleQueryRunner.ts @@ -430,7 +430,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { // rename foreign key constraints newTable.foreignKeys.forEach(foreignKey => { // build new constraint name - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE "${newTable.name}" RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -613,7 +613,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { // build new constraint name foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); foreignKey.columnNames.push(newColumn.name); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE "${table.name}" RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -1004,7 +1004,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { // new FK may be passed without name. In this case we generate FK name manually. if (!foreignKey.name) - foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); const up = this.createForeignKeySql(table, foreignKey); const down = this.dropForeignKeySql(table, foreignKey); @@ -1351,7 +1351,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", "); let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES "${fk.referencedTableName}" (${referencedColumnNames})`; if (fk.onDelete && fk.onDelete !== "NO ACTION") // Oracle does not support NO ACTION, but we set NO ACTION by default in EntityMetadata @@ -1385,10 +1385,11 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { } protected createViewSql(view: View): Query { + const materializedClause = view.materialized ? "" : "MATERIALIZED "; if (typeof view.expression === "string") { - return new Query(`CREATE VIEW "${view.name}" AS ${view.expression}`); + return new Query(`CREATE ${materializedClause}VIEW "${view.name}" AS ${view.expression}`); } else { - return new Query(`CREATE VIEW "${view.name}" AS ${view.expression(this.connection).getQuery()}`); + return new Query(`CREATE ${materializedClause}VIEW "${view.name}" AS ${view.expression(this.connection).getQuery()}`); } } diff --git a/src/driver/postgres/PostgresConnectionOptions.ts b/src/driver/postgres/PostgresConnectionOptions.ts index dd49791a40..aa70411217 100644 --- a/src/driver/postgres/PostgresConnectionOptions.ts +++ b/src/driver/postgres/PostgresConnectionOptions.ts @@ -39,4 +39,11 @@ export interface PostgresConnectionOptions extends BaseConnectionOptions, Postgr * If uuid-ossp is selected, TypeORM will use the uuid_generate_v4() function from this extension. */ readonly uuidExtension?: "pgcrypto" | "uuid-ossp"; + + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; } diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 4195e4732d..0b25672b44 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -148,7 +148,8 @@ export class PostgresDriver implements Driver { "tstzrange", "daterange", "geometry", - "geography" + "geography", + "cube" ]; /** @@ -301,13 +302,16 @@ export class PostgresDriver implements Driver { const hasHstoreColumns = this.connection.entityMetadatas.some(metadata => { return metadata.columns.filter(column => column.type === "hstore").length > 0; }); + const hasCubeColumns = this.connection.entityMetadatas.some(metadata => { + return metadata.columns.filter(column => column.type === "cube").length > 0; + }); const hasGeometryColumns = this.connection.entityMetadatas.some(metadata => { return metadata.columns.filter(column => this.spatialTypes.indexOf(column.type) >= 0).length > 0; }); const hasExclusionConstraints = this.connection.entityMetadatas.some(metadata => { return metadata.exclusions.length > 0; }); - if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasExclusionConstraints) { + if (hasUuidColumns || hasCitextColumns || hasHstoreColumns || hasGeometryColumns || hasCubeColumns || hasExclusionConstraints) { await Promise.all([this.master, ...this.slaves].map(pool => { return new Promise((ok, fail) => { pool.connect(async (err: any, connection: any, release: Function) => { @@ -337,6 +341,12 @@ export class PostgresDriver implements Driver { } catch (_) { logger.log("warn", "At least one of the entities has a geometry column, but the 'postgis' extension cannot be installed automatically. Please install it manually using superuser rights"); } + if (hasCubeColumns) + try { + await this.executeQuery(connection, `CREATE EXTENSION IF NOT EXISTS "cube"`); + } catch (_) { + logger.log("warn", "At least one of the entities has a cube column, but the 'cube' extension cannot be installed automatically. Please install it manually using superuser rights"); + } if (hasExclusionConstraints) try { // The btree_gist extension provides operator support in PostgreSQL exclusion constraints @@ -414,9 +424,18 @@ export class PostgresDriver implements Driver { if (typeof value === "string") { return value; } else { - return Object.keys(value).map(key => { - return `"${key}"=>"${value[key]}"`; - }).join(", "); + // https://www.postgresql.org/docs/9.0/hstore.html + const quoteString = (value: unknown) => { + // If a string to be quoted is `null` or `undefined`, we return a literal unquoted NULL. + // This way, NULL values can be stored in the hstore object. + if (value === null || typeof value === "undefined") { + return "NULL"; + } + // Convert non-null values to string since HStore only stores strings anyway. + // To include a double quote or a backslash in a key or value, escape it with a backslash. + return `"${`${value}`.replace(/(?=["\\])/g, "\\")}"`; + }; + return Object.keys(value).map(key => quoteString(key) + "=>" + quoteString(value[key])).join(","); } } else if (columnMetadata.type === "simple-array") { @@ -425,6 +444,9 @@ export class PostgresDriver implements Driver { } else if (columnMetadata.type === "simple-json") { return DateUtils.simpleJsonToString(value); + } else if (columnMetadata.type === "cube") { + return `(${value.join(", ")})`; + } else if ( ( columnMetadata.type === "enum" @@ -463,13 +485,13 @@ export class PostgresDriver implements Driver { } else if (columnMetadata.type === "hstore") { if (columnMetadata.hstoreType === "object") { - const regexp = /"(.*?)"=>"(.*?[^\\"])"/gi; - const matchValue = value.match(regexp); + const unescapeString = (str: string) => str.replace(/\\./g, (m) => m[1]); + const regexp = /"([^"\\]*(?:\\.[^"\\]*)*)"=>(?:(NULL)|"([^"\\]*(?:\\.[^"\\]*)*)")(?:,|$)/g; const object: ObjectLiteral = {}; - let match; - while (match = regexp.exec(matchValue)) { - object[match[1].replace(`\\"`, `"`)] = match[2].replace(`\\"`, `"`); - } + `${value}`.replace(regexp, (_, key, nullValue, stringValue) => { + object[unescapeString(key)] = nullValue ? null : unescapeString(stringValue); + return ""; + }); return object; } else { @@ -482,6 +504,9 @@ export class PostgresDriver implements Driver { } else if (columnMetadata.type === "simple-json") { value = DateUtils.stringToSimpleJson(value); + } else if (columnMetadata.type === "cube") { + value = value.replace(/[\(\)\s]+/g, "").split(",").map(Number); + } else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-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) @@ -878,11 +903,14 @@ export class PostgresDriver implements Driver { // create a connection pool const pool = new this.postgres.Pool(connectionOptions); const { logger } = this.connection; + + const poolErrorHandler = options.poolErrorHandler || ((error: any) => logger.log("warn", `Postgres pool raised an error. ${error}`)); + /* Attaching an error handler to pool errors is essential, as, otherwise, errors raised will go unhandled and cause the hosting app to crash. */ - pool.on("error", (error: any) => logger.log("warn", `Postgres pool raised an error. ${error}`)); + pool.on("error", poolErrorHandler); return new Promise((ok, fail) => { pool.connect((err: any, connection: any, release: Function) => { diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 6b7219407f..53747e61bc 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -179,7 +179,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } else { switch (result.command) { case "DELETE": - // for DELETE query additionally return number of affected rows + case "UPDATE": + // for UPDATE and DELETE query additionally return number of affected rows ok([result.rows, result.rowCount]); break; default: @@ -469,7 +470,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // rename foreign key constraints newTable.foreignKeys.forEach(foreignKey => { // build new constraint name - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -686,7 +687,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // build new constraint name foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); foreignKey.columnNames.push(newColumn.name); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} RENAME CONSTRAINT "${foreignKey.name}" TO "${newForeignKeyName}"`)); @@ -1173,7 +1174,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // new FK may be passed without name. In this case we generate FK name manually. if (!foreignKey.name) - foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); const up = this.createForeignKeySql(table, foreignKey); const down = this.dropForeignKeySql(table, foreignKey); @@ -1701,7 +1702,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", "); let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; @@ -1838,7 +1839,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner protected createEnumTypeSql(table: Table, column: TableColumn, enumName?: string): Query { if (!enumName) enumName = this.buildEnumName(table, column); - const enumValues = column.enum!.map(value => `'${value}'`).join(", "); + const enumValues = column.enum!.map(value => `'${value.replace("'", "''")}'`).join(", "); return new Query(`CREATE TYPE ${enumName} AS ENUM(${enumValues})`); } diff --git a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts index 21f975d424..e7587ecb92 100644 --- a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts +++ b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts @@ -304,7 +304,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen // rename foreign key constraints newTable.foreignKeys.forEach(foreignKey => { - foreignKey.name = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); }); // rename indices @@ -384,7 +384,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen changedTable.findColumnForeignKeys(changedColumnSet.oldColumn).forEach(fk => { fk.columnNames.splice(fk.columnNames.indexOf(changedColumnSet.oldColumn.name), 1); fk.columnNames.push(changedColumnSet.newColumn.name); - fk.name = this.connection.namingStrategy.foreignKeyName(changedTable, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(changedTable, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); }); changedTable.findColumnIndices(changedColumnSet.oldColumn).forEach(index => { @@ -848,7 +848,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen const columnNames = ownForeignKeys.map(dbForeignKey => dbForeignKey["from"]); const referencedColumnNames = ownForeignKeys.map(dbForeignKey => dbForeignKey["to"]); // build foreign key name, because we can not get it directly. - const fkName = this.connection.namingStrategy.foreignKeyName(table, columnNames); + const fkName = this.connection.namingStrategy.foreignKeyName(table, columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); return new TableForeignKey({ name: fkName, @@ -975,7 +975,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", "); let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES "${fk.referencedTableName}" (${referencedColumnNames})`; diff --git a/src/driver/sqljs/SqljsConnectionOptions.ts b/src/driver/sqljs/SqljsConnectionOptions.ts index 19810cf545..feaadc1ea8 100644 --- a/src/driver/sqljs/SqljsConnectionOptions.ts +++ b/src/driver/sqljs/SqljsConnectionOptions.ts @@ -15,6 +15,11 @@ export interface SqljsConnectionOptions extends BaseConnectionOptions { */ readonly database?: Uint8Array; + /** + * Config that's used to initialize sql.js. + */ + readonly sqlJsConfig?: any; + /** * Enables the autoSave mechanism which either saves to location * or calls autoSaveCallback every time a change to the database is made. diff --git a/src/driver/sqljs/SqljsDriver.ts b/src/driver/sqljs/SqljsDriver.ts index 06060ad3c7..d1cf4994ff 100644 --- a/src/driver/sqljs/SqljsDriver.ts +++ b/src/driver/sqljs/SqljsDriver.ts @@ -248,7 +248,7 @@ export class SqljsDriver extends AbstractSqliteDriver { protected async createDatabaseConnectionWithImport(database?: Uint8Array): Promise { // sql.js < 1.0 exposes an object with a `Database` method. const isLegacyVersion = typeof this.sqlite.Database === "function"; - const sqlite = isLegacyVersion ? this.sqlite : await this.sqlite(); + const sqlite = isLegacyVersion ? this.sqlite : await this.sqlite(this.options.sqlJsConfig); if (database && database.length > 0) { this.databaseConnection = new sqlite.Database(database); } diff --git a/src/driver/sqlserver/SqlServerConnectionOptions.ts b/src/driver/sqlserver/SqlServerConnectionOptions.ts index e6bd2dac94..00d71eb401 100644 --- a/src/driver/sqlserver/SqlServerConnectionOptions.ts +++ b/src/driver/sqlserver/SqlServerConnectionOptions.ts @@ -106,6 +106,12 @@ export interface SqlServerConnectionOptions extends BaseConnectionOptions, SqlSe * to idle time. Supercedes softIdleTimeoutMillis Default: 30000 */ readonly idleTimeoutMillis?: number; + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly errorHandler?: (err: any) => any; }; /** @@ -271,4 +277,5 @@ export interface SqlServerConnectionOptions extends BaseConnectionOptions, SqlSe }; + } diff --git a/src/driver/sqlserver/SqlServerDriver.ts b/src/driver/sqlserver/SqlServerDriver.ts index a961623b9d..c06b911fe6 100644 --- a/src/driver/sqlserver/SqlServerDriver.ts +++ b/src/driver/sqlserver/SqlServerDriver.ts @@ -749,11 +749,13 @@ export class SqlServerDriver implements Driver { const pool = new this.mssql.ConnectionPool(connectionOptions); const { logger } = this.connection; + + const poolErrorHandler = (options.pool && options.pool.errorHandler) || ((error: any) => logger.log("warn", `MSSQL pool raised an error. ${error}`)); /* Attaching an error handler to pool errors is essential, as, otherwise, errors raised will go unhandled and cause the hosting app to crash. */ - pool.on("error", (error: any) => logger.log("warn", `MSSQL pool raised an error. ${error}`)); + pool.on("error", poolErrorHandler); const connection = pool.connect((err: any) => { if (err) return fail(err); diff --git a/src/driver/sqlserver/SqlServerQueryRunner.ts b/src/driver/sqlserver/SqlServerQueryRunner.ts index a9af96e463..9a3d96f0f9 100644 --- a/src/driver/sqlserver/SqlServerQueryRunner.ts +++ b/src/driver/sqlserver/SqlServerQueryRunner.ts @@ -622,7 +622,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner // rename foreign key constraints newTable.foreignKeys.forEach(foreignKey => { // build new constraint name - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`EXEC sp_rename "${this.buildForeignKeyName(foreignKey.name!, schemaName, dbName)}", "${newForeignKeyName}"`)); @@ -823,7 +823,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner // build new constraint name foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); foreignKey.columnNames.push(newColumn.name); - const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); // build queries upQueries.push(new Query(`EXEC sp_rename "${this.buildForeignKeyName(foreignKey.name!, schemaName, dbName)}", "${newForeignKeyName}"`)); @@ -1240,7 +1240,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner // new FK may be passed without name. In this case we generate FK name manually. if (!foreignKey.name) - foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames, foreignKey.referencedTableName, foreignKey.referencedColumnNames); const up = this.createForeignKeySql(table, foreignKey); const down = this.dropForeignKeySql(table, foreignKey); @@ -1816,7 +1816,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner const foreignKeysSql = table.foreignKeys.map(fk => { const columnNames = fk.columnNames.map(columnName => `"${columnName}"`).join(", "); if (!fk.name) - fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames, fk.referencedTableName, fk.referencedColumnNames); const referencedColumnNames = fk.referencedColumnNames.map(columnName => `"${columnName}"`).join(", "); let constraint = `CONSTRAINT "${fk.name}" FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; diff --git a/src/driver/types/ColumnTypes.ts b/src/driver/types/ColumnTypes.ts index 9eb1aab5b1..cae11f2467 100644 --- a/src/driver/types/ColumnTypes.ts +++ b/src/driver/types/ColumnTypes.ts @@ -161,6 +161,7 @@ export type SimpleColumnType = // other types |"enum" // mysql, postgres + |"set" // mysql |"cidr" // postgres |"inet" // postgres, cockroachdb |"macaddr"// postgres @@ -180,7 +181,8 @@ export type SimpleColumnType = |"urowid" // oracle |"uniqueidentifier" // mssql |"rowversion" // mssql - |"array"; // cockroachdb + |"array" // cockroachdb + |"cube"; // postgres /** * Any column type column can be. diff --git a/src/driver/types/DatabaseType.ts b/src/driver/types/DatabaseType.ts index a8c9f0a99f..83754c1a99 100644 --- a/src/driver/types/DatabaseType.ts +++ b/src/driver/types/DatabaseType.ts @@ -14,4 +14,5 @@ export type DatabaseType = "oracle"| "mssql"| "mongodb"| + "aurora-data-api"| "expo"; diff --git a/src/find-options/FindConditions.ts b/src/find-options/FindConditions.ts index c234391bfd..4765e97ee7 100644 --- a/src/find-options/FindConditions.ts +++ b/src/find-options/FindConditions.ts @@ -1,11 +1,8 @@ -import { FindOperator } from "./FindOperator"; +import {FindOperator} from "./FindOperator"; /** * Used for find operations. */ export type FindConditions = { - // @petter: https://github.com/typeorm/typeorm/issues/4427 - [P in keyof T]?: T[P] extends never - ? FindConditions | FindOperator> - : FindConditions | FindOperator>; + [P in keyof T]?: T[P] extends never ? FindConditions|FindOperator> : FindConditions|FindOperator>; }; diff --git a/src/index.ts b/src/index.ts index 39bfe77e26..20228058d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,9 +108,12 @@ export * from "./repository/TreeRepository"; export * from "./repository/MongoRepository"; export * from "./repository/RemoveOptions"; export * from "./repository/SaveOptions"; +export * from "./schema-builder/table/TableCheck"; export * from "./schema-builder/table/TableColumn"; +export * from "./schema-builder/table/TableExclusion"; export * from "./schema-builder/table/TableForeignKey"; export * from "./schema-builder/table/TableIndex"; +export * from "./schema-builder/table/TableUnique"; export * from "./schema-builder/table/Table"; export * from "./driver/mongodb/typings"; export * from "./driver/types/DatabaseType"; diff --git a/src/logger/AdvancedConsoleLogger.ts b/src/logger/AdvancedConsoleLogger.ts index 912d2b9da4..a974d633ef 100644 --- a/src/logger/AdvancedConsoleLogger.ts +++ b/src/logger/AdvancedConsoleLogger.ts @@ -74,11 +74,11 @@ export class AdvancedConsoleLogger implements Logger { switch (level) { case "log": if (this.options === "all" || (this.options instanceof Array && this.options.indexOf("log") !== -1)) - console.log(message); + PlatformTools.log(message); break; case "info": if (this.options === "all" || (this.options instanceof Array && this.options.indexOf("info") !== -1)) - console.info(message); + PlatformTools.logInfo("INFO:", message); break; case "warn": if (this.options === "all" || (this.options instanceof Array && this.options.indexOf("warn") !== -1)) diff --git a/src/metadata-args/TableMetadataArgs.ts b/src/metadata-args/TableMetadataArgs.ts index 880c02da2c..7cb02b1064 100644 --- a/src/metadata-args/TableMetadataArgs.ts +++ b/src/metadata-args/TableMetadataArgs.ts @@ -56,4 +56,10 @@ export interface TableMetadataArgs { */ expression?: string|((connection: Connection) => SelectQueryBuilder); + /** + * Indicates if view is materialized + */ + + materialized?: boolean; + } diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index 7335a45b4f..0d297fee00 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -839,8 +839,10 @@ export class EntityMetadata { */ protected buildTablePath(): string { let tablePath = this.tableName; - if (this.schema) + if (this.schema && ((this.connection.driver instanceof PostgresDriver) || (this.connection.driver instanceof SqlServerDriver))) { tablePath = this.schema + "." + tablePath; + } + if (this.database && !(this.connection.driver instanceof PostgresDriver)) { if (!this.schema && this.connection.driver instanceof SqlServerDriver) { tablePath = this.database + ".." + tablePath; diff --git a/src/metadata/ForeignKeyMetadata.ts b/src/metadata/ForeignKeyMetadata.ts index e72d56eb30..ece34699fc 100644 --- a/src/metadata/ForeignKeyMetadata.ts +++ b/src/metadata/ForeignKeyMetadata.ts @@ -106,7 +106,7 @@ export class ForeignKeyMetadata { this.columnNames = this.columns.map(column => column.databaseName); this.referencedColumnNames = this.referencedColumns.map(column => column.databaseName); this.referencedTablePath = this.referencedEntityMetadata.tablePath; - this.name = namingStrategy.foreignKeyName(this.entityMetadata.tablePath, this.columnNames); + this.name = namingStrategy.foreignKeyName(this.entityMetadata.tablePath, this.columnNames, this.referencedTablePath, this.referencedColumnNames); } } diff --git a/src/migration/MigrationExecutor.ts b/src/migration/MigrationExecutor.ts index ff2e81412f..981a8aaaa6 100644 --- a/src/migration/MigrationExecutor.ts +++ b/src/migration/MigrationExecutor.ts @@ -38,7 +38,6 @@ export class MigrationExecutor { constructor(protected connection: Connection, protected queryRunner?: QueryRunner) { - const options = this.connection.driver.options; this.migrationsTableName = connection.options.migrationsTableName || "migrations"; this.migrationsTable = this.connection.driver.buildTableName(this.migrationsTableName, options.schema, options.database); @@ -154,17 +153,28 @@ export class MigrationExecutor { .then(() => { // informative log about migration success successMigrations.push(migration); this.connection.logger.logSchemaBuild(`Migration ${migration.name} has been executed successfully.`); - }) - .then(() => { - if (pendingMigrations.length > 1) { - setImmediate(() => this.executePendingMigrations()); - } }); }); // commit transaction if we started it if (transactionStartedByUs) await queryRunner.commitTransaction(); + if (pendingMigrations.length > 1) { + const old = this.queryRunner; + this.queryRunner = queryRunner; + return new Promise((resolve, reject) => + setImmediate(() => this.executePendingMigrations().then( + (res) => { + this.queryRunner = old; + resolve(res); + }, + (err) => { + this.queryRunner = old; + reject(err); + }) + ) + ); + } } catch (err) { // rollback transaction if we started it if (transactionStartedByUs) { diff --git a/src/naming-strategy/DefaultNamingStrategy.ts b/src/naming-strategy/DefaultNamingStrategy.ts index 8880c9ef20..22bbf07f5f 100644 --- a/src/naming-strategy/DefaultNamingStrategy.ts +++ b/src/naming-strategy/DefaultNamingStrategy.ts @@ -78,7 +78,7 @@ export class DefaultNamingStrategy implements NamingStrategyInterface { return "DF_" + RandomGenerator.sha1(key).substr(0, 27); } - foreignKeyName(tableOrName: Table|string, columnNames: string[]): string { + foreignKeyName(tableOrName: Table|string, columnNames: string[], _referencedTablePath?: string, _referencedColumnNames?: string[]): string { // sort incoming column names to avoid issue when ["id", "name"] and ["name", "id"] arrays const clonedColumnNames = [...columnNames]; clonedColumnNames.sort(); diff --git a/src/naming-strategy/NamingStrategyInterface.ts b/src/naming-strategy/NamingStrategyInterface.ts index 77f8916a55..3f650e8c14 100644 --- a/src/naming-strategy/NamingStrategyInterface.ts +++ b/src/naming-strategy/NamingStrategyInterface.ts @@ -60,7 +60,7 @@ export interface NamingStrategyInterface { /** * Gets the name of the foreign key. */ - foreignKeyName(tableOrName: Table|string, columnNames: string[]): string; + foreignKeyName(tableOrName: Table|string, columnNames: string[], referencedTablePath?: string, referencedColumnNames?: string[]): string; /** * Gets the name of the index - simple and compose index. diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index a720c14621..f947830457 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -220,7 +220,7 @@ export class InsertQueryBuilder extends QueryBuilder { } /** - * Adds additional ON CONFLICT statement supported in postgres. + * Adds additional ON CONFLICT statement supported in postgres and cockroach. */ onConflict(statement: string): this { this.expressionMap.onConflict = statement; @@ -249,7 +249,7 @@ export class InsertQueryBuilder extends QueryBuilder { if (statement && statement.overwrite instanceof Array) { if (this.connection.driver instanceof MysqlDriver) { this.expressionMap.onUpdate.overwrite = statement.overwrite.map(column => `${column} = VALUES(${column})`).join(", "); - } else if (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof AbstractSqliteDriver) { + } else if (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof AbstractSqliteDriver || this.connection.driver instanceof CockroachDriver) { this.expressionMap.onUpdate.overwrite = statement.overwrite.map(column => `${column} = EXCLUDED.${column}`).join(", "); } } @@ -300,7 +300,7 @@ export class InsertQueryBuilder extends QueryBuilder { query += ` DEFAULT VALUES`; } } - if (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof AbstractSqliteDriver) { + if (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof AbstractSqliteDriver || this.connection.driver instanceof CockroachDriver) { query += `${this.expressionMap.onIgnore ? " ON CONFLICT DO NOTHING " : ""}`; query += `${this.expressionMap.onConflict ? " ON CONFLICT " + this.expressionMap.onConflict : ""}`; if (this.expressionMap.onUpdate) { diff --git a/src/query-builder/QueryExpressionMap.ts b/src/query-builder/QueryExpressionMap.ts index 929652de4e..c65d337c78 100644 --- a/src/query-builder/QueryExpressionMap.ts +++ b/src/query-builder/QueryExpressionMap.ts @@ -46,6 +46,11 @@ export class QueryExpressionMap { */ selects: SelectQuery[] = []; + /** + * Whether SELECT is DISTINCT. + */ + selectDistinct: boolean = false; + /** * FROM-s to be selected. */ diff --git a/src/query-builder/RelationRemover.ts b/src/query-builder/RelationRemover.ts index 8bbc7cce2e..41065f50d2 100644 --- a/src/query-builder/RelationRemover.ts +++ b/src/query-builder/RelationRemover.ts @@ -90,7 +90,7 @@ export class RelationRemover { }), ...junctionMetadata.inverseColumns.map((column, columnIndex) => { const parameterName = "secondValue_" + firstColumnValIndex + "_" + secondColumnValIndex + "_" + columnIndex; - parameters[parameterName] = firstColumnVal instanceof Object ? column.referencedColumn!.getEntityValue(secondColumnVal) : secondColumnVal; + parameters[parameterName] = secondColumnVal instanceof Object ? column.referencedColumn!.getEntityValue(secondColumnVal) : secondColumnVal; return `${column.databaseName} = :${parameterName}`; }) ].join(" AND "); @@ -108,4 +108,4 @@ export class RelationRemover { } } -} \ No newline at end of file +} diff --git a/src/query-builder/SelectQueryBuilder.ts b/src/query-builder/SelectQueryBuilder.ts index 07ca4e2327..3aac7587f4 100644 --- a/src/query-builder/SelectQueryBuilder.ts +++ b/src/query-builder/SelectQueryBuilder.ts @@ -158,6 +158,14 @@ export class SelectQueryBuilder extends QueryBuilder implements return this; } + /** + * Sets whether the selection is DISTINCT. + */ + distinct(distinct: boolean = true): this { + this.expressionMap.selectDistinct = distinct; + return this; + } + /** * Specifies FROM which entity's table select/update/delete will be executed. * Also sets a main string alias of the selection data. @@ -1400,8 +1408,9 @@ export class SelectQueryBuilder extends QueryBuilder implements return this.getTableName(alias.tablePath!) + " " + this.escape(alias.name); }); + const select = "SELECT " + (this.expressionMap.selectDistinct ? "DISTINCT " : ""); const selection = allSelects.map(select => select.selection + (select.aliasName ? " AS " + this.escape(select.aliasName) : "")).join(", "); - return "SELECT " + selection + " FROM " + froms.join(", ") + lock; + return select + selection + " FROM " + froms.join(", ") + lock; } /** diff --git a/src/query-builder/UpdateQueryBuilder.ts b/src/query-builder/UpdateQueryBuilder.ts index f034e41716..bc6ddb5f32 100644 --- a/src/query-builder/UpdateQueryBuilder.ts +++ b/src/query-builder/UpdateQueryBuilder.ts @@ -83,7 +83,16 @@ export class UpdateQueryBuilder extends QueryBuilder implements // execute update query const [sql, parameters] = this.getQueryAndParameters(); const updateResult = new UpdateResult(); - updateResult.raw = await queryRunner.query(sql, parameters); + const result = await queryRunner.query(sql, parameters); + + const driver = queryRunner.connection.driver; + if (driver instanceof PostgresDriver) { + updateResult.raw = result[0]; + updateResult.affected = result[1]; + } + else { + updateResult.raw = result; + } // if we are updating entities and entity updation is enabled we must update some of entity columns (like version, update date, etc.) if (this.expressionMap.updateEntity === true && diff --git a/src/query-builder/result/UpdateResult.ts b/src/query-builder/result/UpdateResult.ts index aa0e79e114..70f66d93ee 100644 --- a/src/query-builder/result/UpdateResult.ts +++ b/src/query-builder/result/UpdateResult.ts @@ -10,6 +10,12 @@ export class UpdateResult { */ raw: any; + /** + * Number of affected rows/documents + * Not all drivers support this + */ + affected?: number; + /** * Contains inserted entity id. * Has entity-like structure (not just column database name and values). @@ -22,4 +28,4 @@ export class UpdateResult { */ generatedMaps: ObjectLiteral[] = []; -} \ No newline at end of file +} diff --git a/src/repository/BaseEntity.ts b/src/repository/BaseEntity.ts index 78f24d291a..37d9694a74 100644 --- a/src/repository/BaseEntity.ts +++ b/src/repository/BaseEntity.ts @@ -46,15 +46,15 @@ export class BaseEntity { * Saves current entity in the database. * If entity does not exist in the database then inserts, otherwise updates. */ - save(): Promise { - return (this.constructor as any).getRepository().save(this); + save(options?: SaveOptions): Promise { + return (this.constructor as any).getRepository().save(this, options); } /** * Removes current entity from the database. */ - remove(): Promise { - return (this.constructor as any).getRepository().remove(this); + remove(options?: RemoveOptions): Promise { + return (this.constructor as any).getRepository().remove(this, options); } /** diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index 907d9278e8..ba2d2b28f6 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -104,6 +104,10 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { this.queryRunner = this.connection.createQueryRunner("master"); try { const tablePaths = this.entityToSyncMetadatas.map(metadata => metadata.tablePath); + // TODO: typeorm_metadata table needs only for Views for now. + // Remove condition or add new conditions if necessary (for CHECK constraints for example). + if (this.viewEntityToSyncMetadatas.length > 0) + await this.createTypeormMetadataTable(); await this.queryRunner.getTables(tablePaths); await this.queryRunner.getViews([]); this.queryRunner.enableSqlMemory(); @@ -139,7 +143,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { * Returns only entities that should be synced in the database. */ protected get viewEntityToSyncMetadatas(): EntityMetadata[] { - return this.connection.entityMetadatas.filter(metadata => metadata.tableType === "view"); + return this.connection.entityMetadatas.filter(metadata => metadata.tableType === "view" && metadata.synchronize); } /** diff --git a/src/schema-builder/options/ViewOptions.ts b/src/schema-builder/options/ViewOptions.ts index 3241da8961..eab47df77b 100644 --- a/src/schema-builder/options/ViewOptions.ts +++ b/src/schema-builder/options/ViewOptions.ts @@ -19,4 +19,9 @@ export interface ViewOptions { */ expression: string|((connection: Connection) => SelectQueryBuilder); + /** + * Indicates if view is materialized + */ + + materialized?: boolean; } diff --git a/src/schema-builder/view/View.ts b/src/schema-builder/view/View.ts index 866416a943..b2d94fe1cf 100644 --- a/src/schema-builder/view/View.ts +++ b/src/schema-builder/view/View.ts @@ -17,10 +17,16 @@ export class View { */ name: string; + + /** + * Indicates if view is materialized. + */ + materialized: boolean; + /** * View definition. */ - expression: string|((connection: Connection) => SelectQueryBuilder); + expression: string | ((connection: Connection) => SelectQueryBuilder); // ------------------------------------------------------------------------- // Constructor @@ -30,6 +36,7 @@ export class View { if (options) { this.name = options.name; this.expression = options.expression; + this.materialized = !!options.materialized; } } @@ -41,9 +48,10 @@ export class View { * Clones this table to a new table with all properties cloned. */ clone(): View { - return new View( { + return new View({ name: this.name, expression: this.expression, + materialized: this.materialized, }); } @@ -58,6 +66,7 @@ export class View { const options: ViewOptions = { name: driver.buildTableName(entityMetadata.tableName, entityMetadata.schema, entityMetadata.database), expression: entityMetadata.expression!, + materialized: false }; return new View(options); diff --git a/src/util/DateUtils.ts b/src/util/DateUtils.ts index 1f6b210ebb..9988c84c4f 100644 --- a/src/util/DateUtils.ts +++ b/src/util/DateUtils.ts @@ -170,7 +170,12 @@ export class DateUtils { } static stringToSimpleJson(value: any) { - return typeof value === "string" ? JSON.parse(value) : value; + try { + const simpleJSON = JSON.parse(value); + return (typeof simpleJSON === "object") ? simpleJSON : {}; + } catch (err) { + return {}; + } } static simpleEnumToString(value: any) { diff --git a/src/util/DirectoryExportedClassesLoader.ts b/src/util/DirectoryExportedClassesLoader.ts index 5d9c3056c2..9aea39c571 100644 --- a/src/util/DirectoryExportedClassesLoader.ts +++ b/src/util/DirectoryExportedClassesLoader.ts @@ -1,11 +1,14 @@ import {PlatformTools} from "../platform/PlatformTools"; import {EntitySchema} from "../index"; - +import {Logger} from "../logger/Logger"; /** * Loads all exported classes from the given directory. */ -export function importClassesFromDirectories(directories: string[], formats = [".js", ".ts"]): Function[] { +export function importClassesFromDirectories(logger: Logger, directories: string[], formats = [".js", ".ts"]): Function[] { + const logLevel = "info"; + const classesNotFoundMessage = "No classes were found using the provided glob pattern: "; + const classesFoundMessage = "All classes found using provided glob pattern"; function loadFileClasses(exported: any, allLoaded: Function[]) { if (typeof exported === "function" || exported instanceof EntitySchema) { allLoaded.push(exported); @@ -24,6 +27,11 @@ export function importClassesFromDirectories(directories: string[], formats = [" return allDirs.concat(PlatformTools.load("glob").sync(PlatformTools.pathNormalize(dir))); }, [] as string[]); + if (directories.length > 0 && allFiles.length === 0) { + logger.log(logLevel, `${classesNotFoundMessage} "${directories}"`); + } else if (allFiles.length > 0) { + logger.log(logLevel, `${classesFoundMessage} "${directories}" : "${allFiles}"`); + } const dirs = allFiles .filter(file => { const dtsExtension = file.substring(file.length - 5, file.length); diff --git a/test/functional/cube/postgres/cube-postgres.ts b/test/functional/cube/postgres/cube-postgres.ts new file mode 100644 index 0000000000..c36f7d74a4 --- /dev/null +++ b/test/functional/cube/postgres/cube-postgres.ts @@ -0,0 +1,116 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { Connection } from "../../../../src/connection/Connection"; +import { + closeTestingConnections, + createTestingConnections, + reloadTestingDatabases +} from "../../../utils/test-utils"; +import { Post } from "./entity/Post"; + +describe("cube-postgres", () => { + let connections: Connection[]; + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["postgres"] + }); + }); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should create correct schema with Postgres' cube type", () => + Promise.all( + connections.map(async connection => { + const queryRunner = connection.createQueryRunner(); + const schema = await queryRunner.getTable("post"); + await queryRunner.release(); + expect(schema).not.to.be.undefined; + const cubeColumn = schema!.columns.find( + tableColumn => + tableColumn.name === "color" && + tableColumn.type === "cube" + ); + expect(cubeColumn).to.not.be.undefined; + }) + )); + + it("should persist cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color); + }) + )); + + it("should update cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const color2 = [0, 255, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + + await postRepo.update( + { id: persistedPost.id }, + { color: color2 } + ); + + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color2); + }) + )); + + it("should re-save cube correctly", () => + Promise.all( + connections.map(async connection => { + const color = [255, 0, 0]; + const color2 = [0, 255, 0]; + const postRepo = connection.getRepository(Post); + const post = new Post(); + post.color = color; + const persistedPost = await postRepo.save(post); + + persistedPost.color = color2; + await postRepo.save(persistedPost); + + const foundPost = await postRepo.findOne(persistedPost.id); + expect(foundPost).to.exist; + expect(foundPost!.color).to.deep.equal(color2); + }) + )); + + it("should be able to order cube by euclidean distance", () => + Promise.all( + connections.map(async connection => { + const color1 = [255, 0, 0]; + const color2 = [255, 255, 0]; + const color3 = [255, 255, 255]; + + const post1 = new Post(); + post1.color = color1; + const post2 = new Post(); + post2.color = color2; + const post3 = new Post(); + post3.color = color3; + await connection.manager.save([post1, post2, post3]); + + const posts = await connection.manager + .createQueryBuilder(Post, "post") + .orderBy("color <-> '(0, 255, 0)'", "DESC") + .getMany(); + + const postIds = posts.map(post => post.id); + expect(postIds).to.deep.equal([post1.id, post3.id, post2.id]); + }) + )); +}); diff --git a/test/functional/cube/postgres/entity/Post.ts b/test/functional/cube/postgres/entity/Post.ts new file mode 100644 index 0000000000..d182b8b59b --- /dev/null +++ b/test/functional/cube/postgres/entity/Post.ts @@ -0,0 +1,15 @@ +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Column} from "../../../../../src/decorator/columns/Column"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column("cube", { + nullable: true + }) + color: number[]; +} diff --git a/test/functional/query-builder/select/query-builder-select.ts b/test/functional/query-builder/select/query-builder-select.ts index 725dcf9a54..6b1ba78e89 100644 --- a/test/functional/query-builder/select/query-builder-select.ts +++ b/test/functional/query-builder/select/query-builder-select.ts @@ -27,6 +27,21 @@ describe("query builder > select", () => { "FROM post post"); }))); + it("should append all entity mapped columns from main selection to SELECT DISTINCT statement", () => Promise.all(connections.map(async connection => { + const sql = connection.manager.createQueryBuilder(Post, "post") + .distinct() + .disableEscaping() + .getSql(); + + expect(sql).to.equal("SELECT DISTINCT post.id AS post_id, " + + "post.title AS post_title, " + + "post.description AS post_description, " + + "post.rating AS post_rating, " + + "post.version AS post_version, " + + "post.categoryId AS post_categoryId " + + "FROM post post"); + }))); + it("should append all entity mapped columns from both main selection and join selections to select statement", () => Promise.all(connections.map(async connection => { const sql = connection.createQueryBuilder(Post, "post") .leftJoinAndSelect("category", "category") diff --git a/test/functional/query-runner/rename-column.ts b/test/functional/query-runner/rename-column.ts index 9f73f180f4..58497967f9 100644 --- a/test/functional/query-runner/rename-column.ts +++ b/test/functional/query-runner/rename-column.ts @@ -186,7 +186,7 @@ describe("query runner > rename column", () => { await queryRunner.renameColumn(categoryTableName, "questionId", "questionId2"); table = await queryRunner.getTable(categoryTableName); - const newForeignKeyName = connection.namingStrategy.foreignKeyName(table!, ["questionId2"]); + const newForeignKeyName = connection.namingStrategy.foreignKeyName(table!, ["questionId2"], "question", ["id"]); table!.foreignKeys[0].name!.should.be.equal(newForeignKeyName); await queryRunner.executeMemoryDownSql(); diff --git a/test/functional/query-runner/rename-table.ts b/test/functional/query-runner/rename-table.ts index 065f93086f..f731a79eaf 100644 --- a/test/functional/query-runner/rename-table.ts +++ b/test/functional/query-runner/rename-table.ts @@ -172,7 +172,7 @@ describe("query runner > rename table", () => { await queryRunner.renameTable(categoryTableName, "renamedCategory"); table = await queryRunner.getTable(renamedCategoryTableName); - const newForeignKeyName = connection.namingStrategy.foreignKeyName(table!, ["questionId"]); + const newForeignKeyName = connection.namingStrategy.foreignKeyName(table!, ["questionId"], "question", ["id"]); table!.foreignKeys[0].name!.should.be.equal(newForeignKeyName); await queryRunner.executeMemoryDownSql(); diff --git a/test/github-issues/1308/entity/Author.ts b/test/github-issues/1308/entity/Author.ts new file mode 100644 index 0000000000..30a69da3eb --- /dev/null +++ b/test/github-issues/1308/entity/Author.ts @@ -0,0 +1,34 @@ +import {EntitySchemaOptions} from "../../../../src/entity-schema/EntitySchemaOptions"; +import {Post} from "./Post"; + +export class Author { + id: number; + + name: string; + + posts: Post[]; +} + +export const AuthorSchema: EntitySchemaOptions = { + name: "Author", + + target: Author, + + columns: { + id: { + primary: true, + type: Number + }, + + name: { + type: "varchar" + } + }, + + relations: { + posts: { + target: () => Post, + type: "one-to-many" + } + } +}; diff --git a/test/github-issues/1308/entity/Post.ts b/test/github-issues/1308/entity/Post.ts new file mode 100644 index 0000000000..3db6dddec2 --- /dev/null +++ b/test/github-issues/1308/entity/Post.ts @@ -0,0 +1,35 @@ +import {EntitySchemaOptions} from "../../../../src/entity-schema/EntitySchemaOptions"; +import {Author} from "./Author"; + +export class Post { + id: number; + + title: string; + + author: Author; +} + +export const PostSchema: EntitySchemaOptions = { + name: "Post", + + target: Post, + + columns: { + id: { + primary: true, + type: Number + }, + + title: { + type: "varchar" + } + }, + + relations: { + author: { + target: () => Author, + type: "many-to-one", + eager: true + } + } +}; diff --git a/test/github-issues/1308/issue-1308.ts b/test/github-issues/1308/issue-1308.ts new file mode 100644 index 0000000000..679b6de2c9 --- /dev/null +++ b/test/github-issues/1308/issue-1308.ts @@ -0,0 +1,49 @@ +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; +import { Connection } from "../../../src/connection/Connection"; +import { EntitySchema } from "../../../src"; +import { Author, AuthorSchema } from "./entity/Author"; +import { Post, PostSchema } from "./entity/Post"; + +describe("github issues > #1308 Raw Postgresql Update query result is always an empty array", () => { + let connections: Connection[]; + before( + async () => + (connections = await createTestingConnections({ + entities: [new EntitySchema(AuthorSchema), new EntitySchema(PostSchema)], + dropSchema: true, + enabledDrivers: ["postgres"], + })) + ); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + async function prepareData(connection: Connection) { + const author = new Author(); + author.id = 1; + author.name = "Jane Doe"; + await connection.manager.save(author); + } + + it("Update query returns the number of affected rows", () => + Promise.all( + connections.map(async connection => { + await prepareData(connection); + + const result1 = await connection.createQueryBuilder() + .update(Author) + .set({ name: "John Doe" }) + .where("name = :name", { name: "Jonas Doe" }) + .execute(); + + result1.affected!.should.be.eql(0); + + const result2 = await connection.createQueryBuilder() + .update(Author) + .set({ name: "John Doe" }) + .where("name = :name", { name: "Jane Doe" }) + .execute(); + + result2.affected!.should.be.eql(1); + }) + )); +}); diff --git a/test/github-issues/2632/entity/Category.ts b/test/github-issues/2632/entity/Category.ts new file mode 100644 index 0000000000..657daf5e52 --- /dev/null +++ b/test/github-issues/2632/entity/Category.ts @@ -0,0 +1,18 @@ +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../src/decorator/columns/Column"; +import {Post} from "./Post"; +import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany"; + +@Entity() +export class Category { + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @ManyToMany(type => Post, post => post.categories) + posts: Post[]; + +} diff --git a/test/github-issues/2632/entity/Post.ts b/test/github-issues/2632/entity/Post.ts new file mode 100644 index 0000000000..1f3c9c3946 --- /dev/null +++ b/test/github-issues/2632/entity/Post.ts @@ -0,0 +1,20 @@ +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../src/decorator/columns/Column"; +import {Category} from "./Category"; +import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany"; +import {JoinTable} from "../../../../src/decorator/relations/JoinTable"; + +@Entity() +export class Post { + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @ManyToMany(type => Category, category => category.posts) + @JoinTable() + categories: Category[]; + +} diff --git a/test/github-issues/2632/issue-2632.ts b/test/github-issues/2632/issue-2632.ts new file mode 100644 index 0000000000..fb655c97af --- /dev/null +++ b/test/github-issues/2632/issue-2632.ts @@ -0,0 +1,75 @@ +import "reflect-metadata"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {Post} from "./entity/Post"; +import {Category} from "./entity/Category"; +import {expect} from "chai"; + +describe("github issues > #2632 createQueryBuilder relation remove works only if using ID", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchema: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should add and remove relations of an entity if given a mix of ids and objects", () => Promise.all(connections.map(async connection => { + + const post1 = new Post(); + post1.title = "post #1"; + await connection.manager.save(post1); + + const post2 = new Post(); + post2.title = "post #2"; + await connection.manager.save(post2); + + const category1 = new Category(); + category1.title = "category #1"; + await connection.manager.save(category1); + + const category2 = new Category(); + category2.title = "category #2"; + await connection.manager.save(category2); + + await connection + .createQueryBuilder() + .relation(Post, "categories") + .of(post1) + .add(1); + + let loadedPost1 = await connection.manager.findOne(Post, 1, { relations: ["categories"] }); + expect(loadedPost1!.categories).to.deep.include({ id: 1, title: "category #1" }); + + await connection + .createQueryBuilder() + .relation(Post, "categories") + .of(post1) + .remove(1); + + loadedPost1 = await connection.manager.findOne(Post, 1, { relations: ["categories"] }); + expect(loadedPost1!.categories).to.be.eql([]); + + await connection + .createQueryBuilder() + .relation(Post, "categories") + .of(2) + .add(category2); + + let loadedPost2 = await connection.manager.findOne(Post, 2, { relations: ["categories"] }); + expect(loadedPost2!.categories).to.deep.include({ id: 2, title: "category #2" }); + + await connection + .createQueryBuilder() + .relation(Post, "categories") + .of(2) + .remove(category2); + + loadedPost1 = await connection.manager.findOne(Post, 2, { relations: ["categories"] }); + expect(loadedPost1!.categories).to.be.eql([]); + + }))); + +}); diff --git a/test/github-issues/2779/entity/Post.ts b/test/github-issues/2779/entity/Post.ts new file mode 100644 index 0000000000..1705b4afab --- /dev/null +++ b/test/github-issues/2779/entity/Post.ts @@ -0,0 +1,15 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "../../../../src"; +import { Role } from "../set"; + +@Entity("post") +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column("set", { + default: [Role.Admin, Role.Developer], + enum: Role + }) + roles: Role[]; +} \ No newline at end of file diff --git a/test/github-issues/2779/issue-2779.ts b/test/github-issues/2779/issue-2779.ts new file mode 100644 index 0000000000..fe0c82dc4d --- /dev/null +++ b/test/github-issues/2779/issue-2779.ts @@ -0,0 +1,44 @@ +import "reflect-metadata"; +import { Connection } from "../../../src/connection/Connection"; +import { closeTestingConnections, createTestingConnections } from "../../utils/test-utils"; +import { Post } from "./entity/Post"; +import { expect } from "chai"; +import { Role } from "./set"; + +describe("github issues > #2779 Could we add support for the MySQL/MariaDB SET data type?", () => { + + let connections: Connection[]; + before(async () => { + connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["mariadb", "mysql"], + schemaCreate: true, + dropSchema: true, + }); + }); + after(() => closeTestingConnections(connections)); + + it("should create column with SET datatype", () => Promise.all(connections.map(async connection => { + + const queryRunner = connection.createQueryRunner(); + const table = await queryRunner.getTable("post"); + table!.findColumnByName("roles")!.type.should.be.equal("set"); + await queryRunner.release(); + + }))); + + it("should persist and hydrate sets", () => Promise.all(connections.map(async connection => { + + const targetValue = [Role.Support, Role.Developer]; + + const post = new Post(); + post.roles = targetValue; + await connection.manager.save(post); + post.roles.should.be.deep.equal(targetValue); + + const loadedPost = await connection.manager.findOne(Post); + expect(loadedPost).not.to.be.undefined; + loadedPost!.roles.should.be.deep.equal(targetValue); + }))); + +}); diff --git a/test/github-issues/2779/set.ts b/test/github-issues/2779/set.ts new file mode 100644 index 0000000000..54880f5aa3 --- /dev/null +++ b/test/github-issues/2779/set.ts @@ -0,0 +1,5 @@ +export enum Role { + Admin = "Admin", + Support = "Support", + Developer = "Developer" +} \ No newline at end of file diff --git a/test/github-issues/3847/entity/Animal.ts b/test/github-issues/3847/entity/Animal.ts new file mode 100644 index 0000000000..adf55f281d --- /dev/null +++ b/test/github-issues/3847/entity/Animal.ts @@ -0,0 +1,17 @@ +import {Column, Entity, PrimaryGeneratedColumn} from "../../../../src/index"; +import {Category} from "./Category"; +import {ManyToOne} from "../../../../src/decorator/relations/ManyToOne"; + +@Entity() +export class Animal { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToOne(() => Category) + category: Category; + +} \ No newline at end of file diff --git a/test/github-issues/3847/entity/Category.ts b/test/github-issues/3847/entity/Category.ts new file mode 100644 index 0000000000..40bf5cdc19 --- /dev/null +++ b/test/github-issues/3847/entity/Category.ts @@ -0,0 +1,9 @@ +import {Entity, PrimaryGeneratedColumn} from "../../../../src/index"; + +@Entity() +export class Category { + + @PrimaryGeneratedColumn() + id: number; + +} \ No newline at end of file diff --git a/test/github-issues/3847/issue-3847.ts b/test/github-issues/3847/issue-3847.ts new file mode 100644 index 0000000000..d79a2be4b1 --- /dev/null +++ b/test/github-issues/3847/issue-3847.ts @@ -0,0 +1,30 @@ +import "reflect-metadata"; +import {expect} from "chai"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {Animal} from "./entity/Animal"; +import {NamingStrategyUnderTest} from "./naming/NamingStrategyUnderTest"; + + +describe("github issues > #3847 FEATURE REQUEST - Naming strategy foreign key override name", () => { + + let connections: Connection[]; + let namingStrategy = new NamingStrategyUnderTest(); + + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + namingStrategy + })); + beforeEach(() => { + return reloadTestingDatabases(connections); + }); + after(() => closeTestingConnections(connections)); + + it("NamingStrategyUnderTest#", () => Promise.all(connections.map(async connection => { + await connection.getRepository(Animal).find(); + + let metadata = connection.getMetadata(Animal); + + expect(metadata.foreignKeys[0].name).to.eq("fk_animal_category_categoryId"); + }))); +}); diff --git a/test/github-issues/3847/naming/NamingStrategyUnderTest.ts b/test/github-issues/3847/naming/NamingStrategyUnderTest.ts new file mode 100644 index 0000000000..e7e9ed3661 --- /dev/null +++ b/test/github-issues/3847/naming/NamingStrategyUnderTest.ts @@ -0,0 +1,16 @@ +import { DefaultNamingStrategy } from "../../../../src/naming-strategy/DefaultNamingStrategy"; +import { NamingStrategyInterface } from "../../../../src/naming-strategy/NamingStrategyInterface"; +import { Table } from "../../../../src"; + +export class NamingStrategyUnderTest extends DefaultNamingStrategy implements NamingStrategyInterface { + + foreignKeyName(tableOrName: Table|string, columnNames: string[], referencedTablePath?: string, referencedColumnNames?: string[]): string { + tableOrName = + typeof tableOrName === "string" ? tableOrName : tableOrName.name; + + return columnNames.reduce( + (name, column) => `${name}_${column}`, + `fk_${tableOrName}_${referencedTablePath}`, + ); + } +} \ No newline at end of file diff --git a/test/github-issues/4440/entity/Post.ts b/test/github-issues/4440/entity/Post.ts new file mode 100644 index 0000000000..5220e523cd --- /dev/null +++ b/test/github-issues/4440/entity/Post.ts @@ -0,0 +1,16 @@ +import { Column } from "../../../../src/decorator/columns/Column"; +import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn"; +import { Entity } from "../../../../src/decorator/entity/Entity"; + +@Entity() +export class Post { + @PrimaryColumn() + id: number; + + @Column({ + type: "simple-json", + nullable: true + }) + jsonField: any; + +} diff --git a/test/github-issues/4440/issue-4440.ts b/test/github-issues/4440/issue-4440.ts new file mode 100644 index 0000000000..e42ca2eb30 --- /dev/null +++ b/test/github-issues/4440/issue-4440.ts @@ -0,0 +1,43 @@ +import "reflect-metadata"; +import { Connection } from "../../../src/connection/Connection"; +import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; +import { Post } from "./entity/Post"; + +describe("github issues > #4440 simple-json column type throws error for string with no value", () => { + + let connections: Connection[]; + before(async () => { + connections = await createTestingConnections({ + entities: [Post], + schemaCreate: true, + dropSchema: true + }); + }); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should correctly add retrieve simple-json field with no value", () => + Promise.all(connections.map(async (connection) => { + const repo = connection.getRepository(Post); + const post = new Post(); + post.id = 1; + post.jsonField = ""; + await repo.save(post); + const postFound = await repo.findOne(1); + postFound!.id.should.eql(1); + postFound!.jsonField.should.eql({}); + }))); + + it("should correctly add retrieve simple-json field with some value", () => + Promise.all(connections.map(async (connection) => { + const repo = connection.getRepository(Post); + const post = new Post(); + post.id = 1; + post.jsonField = {"key": "value"}; + await repo.save(post); + const postFound = await repo.findOne(1); + postFound!.id.should.eql(1); + postFound!.jsonField.should.eql({"key": "value"}); + }))); + +}); diff --git a/test/github-issues/4513/entity/User.ts b/test/github-issues/4513/entity/User.ts new file mode 100644 index 0000000000..6df8684959 --- /dev/null +++ b/test/github-issues/4513/entity/User.ts @@ -0,0 +1,13 @@ +import { Entity, PrimaryColumn, Column } from "../../../../src"; + +@Entity() +export class User { + @PrimaryColumn() + name: string; + + @PrimaryColumn() + email: string; + + @Column() + age: number; +} \ No newline at end of file diff --git a/test/github-issues/4513/issue-4513.ts b/test/github-issues/4513/issue-4513.ts new file mode 100644 index 0000000000..f5d15cd96d --- /dev/null +++ b/test/github-issues/4513/issue-4513.ts @@ -0,0 +1,140 @@ +import "reflect-metadata"; +import { createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils"; +import { Connection } from "../../../src/connection/Connection"; +import { User } from "./entity/User"; + +describe("github issues > #4513 CockroachDB support for onConflict", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchema: true, + enabledDrivers: ["cockroachdb"] + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should insert if no conflict", () => Promise.all(connections.map(async connection => { + const user1 = new User(); + user1.name = "example"; + user1.email = "example@example.com"; + user1.age = 30; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user1) + .execute(); + + const user2 = new User(); + user2.name = "example2"; + user2.email = "example2@example.com"; + user2.age = 42; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user2) + .onConflict(`("name", "email") DO NOTHING`) + .execute(); + + await connection.manager.find(User).should.eventually.have.lengthOf(2); + }))); + + it("should update on conflict with do update", () => Promise.all(connections.map(async connection => { + const user1 = new User(); + user1.name = "example"; + user1.email = "example@example.com"; + user1.age = 30; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user1) + .execute(); + + const user2 = new User(); + user2.name = "example"; + user2.email = "example@example.com"; + user2.age = 42; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user2) + .onConflict(`("name", "email") DO UPDATE SET age = EXCLUDED.age`) + .execute(); + + await connection.manager.findOne(User, { name: "example", email: "example@example.com" }).should.eventually.be.eql({ + name: "example", + email: "example@example.com", + age: 42, + }); + }))); + + it("should not update on conflict with do nothing", () => Promise.all(connections.map(async connection => { + const user1 = new User(); + user1.name = "example"; + user1.email = "example@example.com"; + user1.age = 30; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user1) + .execute(); + + const user2 = new User(); + user2.name = "example"; + user2.email = "example@example.com"; + user2.age = 42; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user2) + .onConflict(`("name", "email") DO NOTHING`) + .execute(); + + await connection.manager.findOne(User, { name: "example", email: "example@example.com" }).should.eventually.be.eql({ + name: "example", + email: "example@example.com", + age: 30, + }); + }))); + + it("should update with orUpdate", () => Promise.all(connections.map(async connection => { + const user1 = new User(); + user1.name = "example"; + user1.email = "example@example.com"; + user1.age = 30; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user1) + .execute(); + + const user2 = new User(); + user2.name = "example"; + user2.email = "example@example.com"; + user2.age = 42; + + await connection.createQueryBuilder() + .insert() + .into(User) + .values(user2) + .orUpdate({ + conflict_target: ["name", "email"], + overwrite: ["age"], + }) + .execute(); + + await connection.manager.findOne(User, { name: "example", email: "example@example.com" }).should.eventually.be.eql({ + name: "example", + email: "example@example.com", + age: 42, + }); + }))); +}); \ No newline at end of file diff --git a/test/github-issues/4570/issue-4570.ts b/test/github-issues/4570/issue-4570.ts new file mode 100644 index 0000000000..e2ae7b3415 --- /dev/null +++ b/test/github-issues/4570/issue-4570.ts @@ -0,0 +1,19 @@ +import "reflect-metadata"; + +import {expect} from "chai"; +import {ColumnOptions, PrimaryColumn} from "../../../src"; + +describe("github issues > #4570 Fix PrimaryColumn decorator modifies passed option", () => { + it("should not modify passed options to PrimaryColumn", () => { + const options: ColumnOptions = {type: "varchar" }; + const clone = Object.assign({}, options); + + class Entity { + @PrimaryColumn(options) + pkey: string; + } + + expect(Entity).to.be; + expect(clone).to.be.eql(options); + }); +}); diff --git a/test/github-issues/4630/entity/User.ts b/test/github-issues/4630/entity/User.ts new file mode 100644 index 0000000000..82320febf5 --- /dev/null +++ b/test/github-issues/4630/entity/User.ts @@ -0,0 +1,15 @@ +import { Entity, Column, PrimaryGeneratedColumn } from "../../../../src"; + +export enum Realm { + Blackrock = "Blackrock", + KelThuzad = "Kel'Thuzad", +} + +@Entity() +export class User { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: "enum", enum: Realm }) + realm: Realm; +} diff --git a/test/github-issues/4630/issue-4630.ts b/test/github-issues/4630/issue-4630.ts new file mode 100644 index 0000000000..e2d18e65ab --- /dev/null +++ b/test/github-issues/4630/issue-4630.ts @@ -0,0 +1,32 @@ +import "reflect-metadata"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import { Realm } from "./entity/User"; +import {User} from "./entity/User"; + +describe("github issues > #4630 Enum string not escaping resulting in broken migrations.", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchema: true, + enabledDrivers: ["mysql", "postgres"] + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should support enums of strings with apostrophes in them", () => Promise.all(connections.map(async connection => { + const user = new User(); + user.realm = Realm.KelThuzad; + + await connection.manager.save(user); + + const users = await connection.manager.find(User); + + users.should.eql([{ + id: 1, + realm: "Kel'Thuzad" + }]); + }))); +}); diff --git a/test/github-issues/4719/entity/Post.ts b/test/github-issues/4719/entity/Post.ts new file mode 100644 index 0000000000..5f8319053f --- /dev/null +++ b/test/github-issues/4719/entity/Post.ts @@ -0,0 +1,12 @@ +import {Column, Entity, PrimaryGeneratedColumn, ObjectLiteral} from "../../../../src/index"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column("hstore", { hstoreType: "object" }) + hstoreObj: ObjectLiteral; + +} diff --git a/test/github-issues/4719/issue-4719.ts b/test/github-issues/4719/issue-4719.ts new file mode 100644 index 0000000000..6662e06ef4 --- /dev/null +++ b/test/github-issues/4719/issue-4719.ts @@ -0,0 +1,41 @@ +import "reflect-metadata"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {Post} from "./entity/Post"; + +describe("github issues > #4719 HStore with empty string values", () => { + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["postgres"] + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should handle HStore with empty string keys or values", () => Promise.all(connections.map(async connection => { + const queryRunner = connection.createQueryRunner(); + const postRepository = connection.getRepository(Post); + + const post = new Post(); + post.hstoreObj = {name: "Alice", surname: "A", age: 25, blank: "", "": "blank-key", "\"": "\"", foo: null}; + const {id} = await postRepository.save(post); + + const loadedPost = await postRepository.findOneOrFail(id); + loadedPost.hstoreObj.should.be.deep.equal( + { name: "Alice", surname: "A", age: "25", blank: "", "": "blank-key", "\"": "\"", foo: null }); + await queryRunner.release(); + }))); + + it("should not allow 'hstore injection'", () => Promise.all(connections.map(async connection => { + const queryRunner = connection.createQueryRunner(); + const postRepository = connection.getRepository(Post); + + const post = new Post(); + post.hstoreObj = { username: `", admin=>"1`, admin: "0" }; + const {id} = await postRepository.save(post); + + const loadedPost = await postRepository.findOneOrFail(id); + loadedPost.hstoreObj.should.be.deep.equal({ username: `", admin=>"1`, admin: "0" }); + await queryRunner.release(); + }))); +});