From c0c8dff3ec0b6c9bcb8fd5b9857262cf762a54b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20H=C3=A4ggholm?= Date: Sat, 14 Sep 2019 17:54:22 -0700 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 7623e890609610c1ea8cc6f98f70556267d8a8c6 Merge: 038c2edd 13ac2224 Author: Petter Häggholm Date: Sat Sep 14 17:54:03 2019 -0700 bugfix commit 038c2edd31def7860e46e8f3c92cfc88d7e66c61 Author: Petter Häggholm Date: Sat Sep 14 14:22:35 2019 -0700 fix locking commit 13ac22242ad1e2a46cc3aa6af870f4b1cfae1af3 Author: Umed Khudoiberdiev Date: Fri Sep 13 23:20:36 2019 +0500 updated changelog commit ae1f131a4fe3fd6eacc703420d314cce413b5d11 Author: Umed Khudoiberdiev Date: Fri Sep 13 21:22:40 2019 +0500 version bump commit d8f1c81dcfed33bf73493fccd162dff70f2679f9 Author: Roman <52884896+roman-acumen@users.noreply.github.com> Date: Fri Sep 13 18:12:23 2019 +0300 fix: sqlite connections don't ignore the schema property (#4599) commit 0094f61d63d116ae33e7c1fdd01b14819a56f81f Author: Dennie de Lange Date: Fri Sep 13 16:58:20 2019 +0200 feat: add referenced table metadata to NamingStrategy to resolve foreign key name (#4274) Feature allows to use foreignkey metadata to generate database foreignkey names. Closes #3847 and #1355 commit 3abe5b9508c96c77f5e1b4cfbcfeb0c5f41e9f4c Author: Toby Hinloopen Date: Fri Sep 13 16:04:12 2019 +0200 fix: "hstore injection" & properly handle NULL, empty string, backslashes & quotes in hstore key/value pairs (#4720) * Improve HStore object support * Add hstore-injection test commit 644c21b4c0f7978b97b397067b813a63289db0b9 Author: Muma David Bwalya Date: Fri Sep 13 12:32:22 2019 +0200 docs: explicitly defining the optional nature of the ManyToOne and OneToMany relationship when viewed from the child entity (#4722) * Update many-to-one-one-to-many-relations.md Explicitly defining the optional nature of the ManyToOne and OneToMany relationship when viewed from the child entity. * Update docs/many-to-one-one-to-many-relations.md Punctuation and more accurate semantics. Co-Authored-By: Toby Hinloopen commit c52b3d225c1b4a621f3239f5e3babd4a70ccc0a5 Author: Evgeniy Date: Fri Sep 13 13:04:11 2019 +0300 fix: views generating broken Migrations (#4726) MigrationGenerateCommand didn't apply parameters to query and information about ViewTables couldn't be inserted into typeorm_metadata table. Also added code that creates typeorm_metadata table if ViewTables exists Fixed issue #4123 commit 1d73a90c0339c46cdb917494b01956bfb133569d Author: Ian Mobley Date: Fri Sep 13 00:22:17 2019 -0700 fix: createQueryBuilder relation remove works only if using ID (#2632) (#4734) commit 81f4b43f3e8c6a1dfe0b64a0b5f7c5874a24b6f7 Author: Alex Howard Date: Fri Sep 13 03:16:07 2019 -0400 docs: added missing comma in relations.md (#4739) commit 7808bba416c0b1cf9850194499e8c8d3502657b7 Author: Junggun Lim Date: Thu Sep 5 12:44:25 2019 -0700 feat: UpdateResult returns affected rows in postgresql (#4432) * Added 'affected' field in UpdateResult as well as in DeleteResult. * PostgresQueryRunner returns the number of affected rows properly * UpdateQueryBuilder retrieves the affected rows returned by PostgresQueryRunner and sets the added 'affected' field of UpdateResult properly. Closes: #1308 commit 7a0beedc365ab5090d08e1482abcdbc45e558933 Author: Mophy Xiong Date: Fri Sep 6 03:33:39 2019 +0800 fix: the excessive stack depth comparing types `FindConditions` and `FindConditions` problem (#4470) commit dacac83d53aac952bb49ffa281a4c5b3b5abf3d8 Author: Michał Wadas Date: Thu Sep 5 21:28:56 2019 +0200 feat: add materialized View support for Postgres (#4478) feat: add option to synchronize or not to synchronize ViewEntity Fixes #4317 Fixes #3996 commit db8074aa35a57fad35ec74c8572e19286fd52e73 Author: Nicolas Hervé Date: Thu Sep 5 19:42:52 2019 +0200 feat: add support for ON CONFLICT for cockroach (#4518) Closes: #4513 commit 19e21795d0450bc96c84cf4a5b88d73546b0007b Author: hauau Date: Fri Sep 6 00:38:54 2019 +0700 feat: add `set` datatype support for MySQL/MariaDB (#4538) Set possible values defined using existing enum column option. Sets are implemented as arrays. Closes: #2779 commit 5c311ed8d46552a0543953eabc6f98a2d5875a28 Author: David Chen <42685381+DavidChen-minted@users.noreply.github.com> Date: Thu Sep 5 08:50:10 2019 -0700 feat: add options to input init config for sql.js (#4560) * add options to input init config for sql.js * update changelog * updated connection-options docs commit 3cf470d92a543830569dd4a6bd62882ec22cc656 Author: Aviad Hadad Date: Thu Sep 5 18:46:19 2019 +0300 fix: change PrimaryColumn decorator to clone passed options (#4571) Closes: #4570 commit 9e3d664748dd9cd9956022b699a41856d360fd73 Author: Mike Guida Date: Thu Sep 5 08:38:41 2019 -0700 docs: clarify title of configuration sources (#4592) commit 587d5344a121f447a7cd50807dc28424075de241 Author: Thomas Gieling Date: Thu Sep 5 17:37:43 2019 +0200 use remove in stead of delete (#4574) the `remove` method requires the entity itself as input. To remove a record based on it's id, you need to use `delete` commit a925be97a6e21c58b2941ba5e11be9eeb91ac30c Author: Michał Wadas Date: Thu Sep 5 16:05:23 2019 +0200 feat: add postgres pool error handler (#4474) Add option to customize pool error handling. Users can decide to log these errors with higher level (eg. error), crash application or reconnect. commit f65ecc7fbfd243ad8a90e8a5025f73f3a180cc1b Author: JB Reefer Date: Thu Sep 5 09:10:10 2019 -0400 docs: grammar and wording fixes in active-record-data-mapper.md (#4615) Grammar and wording fixes in bottom paragraph commit 445c740bea176299b4c532f5a65382dc802ce6e3 Author: Duckie <7842848+duckies@users.noreply.github.com> Date: Thu Sep 5 08:59:50 2019 -0400 fix: apostrophe in Postgres enum strings breaks query (#4631) * Patch to allow apostrophes in postgres enum string. * Make linter happy * Testing fix for MySQL * Limit to postgres and mysql, fix test description * Lets not be greedy. commit c1406bb1fc521e66449870c4c97f7ae95307fd61 Author: kevindashgit Date: Thu Sep 5 05:30:24 2019 -0700 docs: documentation for showMigrations() (#4644) commit 2478198642fe099758688e44360c2b7b11993580 Author: azxj <31400828+azxj@users.noreply.github.com> Date: Thu Sep 5 20:29:23 2019 +0800 docs: remove duplicate segment in entities.md (#4638) Remove duplicate segment. commit 5bd29d58c3422beab0018f973bd59e1f7695c7d2 Author: QoVoQ <674263588@qq.com> Date: Thu Sep 5 20:27:30 2019 +0800 docs: update many-to-many-relations.md, make it easier to understand (#4680) * Update many-to-many-relations.md Make the example of the last section `many-to-many relations with custom properties` more clear and general. * Update many-to-many-relations.md commit 92e42701eeb9d1891bb6b9c82a5bf20cb605afbf Author: Coroliov Oleg <1880059+ruscon@users.noreply.github.com> Date: Thu Sep 5 15:25:06 2019 +0300 feat: add mongodb `useUnifiedTopology` config parameter (#4684) commit 690e6f59deb46e118678f9cfad5b767ede5702b0 Author: CavidM Date: Thu Sep 5 05:18:03 2019 -0700 fix: "database" option error in driver when use "url" option for connection commit e589fda2474dbd5547e9982f4aa4c66b27847c10 Author: Michael Dzjaparidze Date: Tue Sep 3 15:05:58 2019 +0200 feat: export additional schema builder classes (#4325) commit 3951b58218552f422c6fd936f509f5685347deee Author: David Podhola Date: Sat Aug 31 23:39:04 2019 +0200 docs: fix missing async (#4458) In the second example the lambda is correctly started with an `async`. It is missing in the first example. commit d4e2443875abeee2bb0584492266398f4f9a72c3 Author: Kateile Date: Sat Aug 31 21:26:44 2019 +0300 docs: update typeorm-model-shim.js comment link (#4540) This link http://webpack.github.io/docs/configuration.html#resolve no longer exist commit 4c2bffc7bade17c9e27c3876669d112593b314f1 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Aug 30 23:43:04 2019 +0200 chore: bump lodash from 4.17.11 to 4.17.15 (#4671) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15) Signed-off-by: dependabot[bot] commit 10bac1fb9b644253d27b3f3ebaa9c4ee476a722f Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Aug 30 23:35:40 2019 +0200 build: bump mixin-deep from 1.3.1 to 1.3.2 (#4648) Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2. - [Release notes](https://github.com/jonschlinkert/mixin-deep/releases) - [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2) Signed-off-by: dependabot[bot] commit e81a77abc09c9e4d954690eef2c6afea9e2371a2 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Aug 30 23:33:36 2019 +0200 chore: bump lodash.template from 4.4.0 to 4.5.0 (#4416) Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0) Signed-off-by: dependabot[bot] commit 00d46e1ef0096ed5932fae8fcb3f62dabfeacaeb Author: JB Reefer Date: Sat Aug 24 05:08:26 2019 -0400 docs: Update using-cli.md (#4618) Fix typo commit d1594f5d41a35bac7bb99d4f00e76c2fb670ee17 Author: Abhijeet Chakraborty <16278759+abhijeet1403@users.noreply.github.com> Date: Wed Aug 14 14:01:48 2019 +0530 fix: resolve issue with conversion string to simple-json (#4476) Closes: #4440 commit c321562c9863aebf9c7132046f7de164ba529847 Author: Arseny Yankovsky Date: Tue Aug 13 10:12:25 2019 +0200 feat: Aurora Data API (#4375) * Initial POC Implementation * Initial POC Implementation * Implemented an interface transformation between typeorm and data api so most of the queries should work, added some tests * Fixed lint errors * Fixed a regex and added some tests on query transformation * Move out to a separate repo * Bumped aurora driver version to latest * Bumped aurora driver version to latest * Delegate transactions to the driver * Delegate transactions to the driver * WIP * WIP * Bump the aurora driver version * Bump the aurora driver version * Fixed aurora driver version * removed unused entity commit b4e9cf06830a85679f143f398e0ea950b87cdf1e Author: Marcos Date: Wed Aug 7 11:35:11 2019 +0200 docs: update typo in separating-entity-definition.md Typo in line 190: "Categeory" changed to "Category" commit 9930283f157a0cd24c71aec28939473f5dd2f886 Author: Charlie Cruzan <35579283+cruzach@users.noreply.github.com> Date: Wed Aug 7 05:33:42 2019 -0400 docs: update outdated Expo SQLite link * update Expo SQLite link * docs: fix expo-example repo link commit 5e00e81626c41e0445b46922fb74903e5f790cd5 Author: Humberto Villalpando <43765965+betov18x@users.noreply.github.com> Date: Wed Jul 31 06:47:36 2019 -0500 chore: Update README.md Added some links for nativescript examples with vue and angular commit 10a5182aeb79db4ef7efb343b76e597e31695707 Author: markamPL Date: Wed Jul 24 09:00:47 2019 +0200 docs: update typo in using-ormconfig.md Typo in .env configuration: .*js changed to *.js commit b6d62788ea681c7e049c2efe11c92b356da8f203 Author: Liau Jian Jie Date: Sun Jul 21 22:10:53 2019 +0800 feat: add multi-dimensional cube support for PostgreSQL (#4378) commit e12479ed43c4c5361de8d4b9c55120e2aa9ccb22 Author: Abdoulaye K. Traoré Date: Sat Jul 13 18:57:16 2019 +0200 feat: log files loaded from glob patterns (#4346) This new feature logs the files that are loaded using the glob patterns to aid in debugging. Closes: #4162 commit c8dbf099ba991c25d236790ab6fa903cfdc0b57c Author: Mike Harris Date: Wed Jul 10 09:17:31 2019 -0400 docs: Fix Typo (#4412) commit a858de1f195d41aaffdeb8c10faa538dc1a15f5c Author: Vlad Poluch Date: Mon Jul 1 16:01:09 2019 +0200 Revert "docs: fix typographical error in faq (#4321)" (#4380) This reverts commit 117185b5e5d7a4672af6ba1f1341de98fa8d1fef. commit 39a8e344c58dafd6db9dc5d149608292903b67e9 Author: Benjamin Dobell Date: Mon Jul 1 01:18:55 2019 +1000 feat: Added support for DISTINCT queries (#4109) commit c8a9ea083cc4ea24e76685617e0b5ef846c27e1d Author: Max Sommer Date: Sun Jun 30 17:12:07 2019 +0200 docs: add explanation ManyToMany with custom properties (#4308) * Add explanation ManyToMany with custom properties Since I myself ran into the issue and had to rewrite my code based on this solution I hope it may be helpful for others as well 😊 * Update explanation with more relateable example Update explanation with abstract example to be more relateable Add cross link to faq * Update phrasing, unnecessary explanation and code Remove unnecessary explanation upfront mentioning FAQ Update phrasing of main explanatory paragraph to be more precise Update code example to work correctly commit 79bf9f7013d1c4248398812b659393b10d62b124 Author: Kononnable Date: Sun Jun 30 17:11:06 2019 +0200 build: node version upgrade in travis (#4312) * upgrading node CI tested versions * sqlite3 version upgrade commit a6d7ba27efffd88acbe43096f5259f0d3b16498b Author: Leonardo Falk Date: Sun Jun 30 12:07:19 2019 -0300 fix: add SaveOptions and RemoveOptions into ActiveRecord (#4318) * Adding SaveOptions to BaseEntity#save * Adding RemoveOptions to BaseEntity#remove commit 684ffd196d713bec52d00968762d433407c2b37b Merge: 6429ccd5 6a1206e8 Author: Umed Khudoiberdiev Date: Sun Jun 30 14:39:21 2019 +0300 Merge pull request #4376 from typeorm/revert-4306-fix-4291 Revert "fix: improve sql.js v1.0 support in browser environment" commit 6a1206e815bac6c60677665e59d0b89d7327d349 Author: Umed Khudoiberdiev Date: Sun Jun 30 14:38:54 2019 +0300 Revert "fix: improve sql.js v1.0 support in browser environment" commit 6429ccd5117e4237bf579df9373fe26a0b771277 Merge: 117185b5 ed87e343 Author: Umed Khudoiberdiev Date: Sun Jun 30 14:00:46 2019 +0300 Merge pull request #4306 from michaelbromley/fix-4291 fix: improve sql.js v1.0 support in browser environment commit 117185b5e5d7a4672af6ba1f1341de98fa8d1fef Author: SriNath <12288245+SrChip15@users.noreply.github.com> Date: Wed Jun 19 09:13:47 2019 -0400 docs: fix typographical error in faq (#4321) change "typedi" to "typed" in how to use TypeORM with a dependency injection tool question commit ed87e3438da6d77b86dab32876ecd0a49c71800f Author: Michael Bromley Date: Tue Jun 18 10:25:35 2019 +0200 Add more documentation on using sql.js in the browser commit f7bcd8faa7fda5779e199885f835a5e57db2a015 Author: Michael Bromley Date: Tue Jun 18 10:05:04 2019 +0200 Fix sql.js v1.0 support in browser --- .travis.yml | 7 +- CHANGELOG.md | 38 +- README.md | 2 + docs/active-record-data-mapper.md | 6 +- docs/connection-options.md | 6 + docs/entities.md | 50 +- docs/example-with-express.md | 2 +- docs/faq.md | 2 +- docs/many-to-many-relations.md | 47 + docs/many-to-one-one-to-many-relations.md | 2 +- docs/relations.md | 2 +- docs/select-query-builder.md | 2 +- docs/separating-entity-definition.md | 2 +- docs/supported-platforms.md | 2 +- docs/transactions.md | 4 +- docs/troubleshooting.md | 16 + docs/using-cli.md | 4 +- docs/using-ormconfig.md | 4 +- docs/zh_CN/connection-options.md | 2 + docs/zh_CN/entities.md | 43 - docs/zh_CN/troubleshooting.md | 16 + extra/typeorm-model-shim.js | 4 +- ormconfig.circleci.json | 3 +- ormconfig.json.dist | 3 +- ormconfig.travis.json | 3 +- package-lock.json | 166 +- package.json | 9 +- src/commands/MigrationGenerateCommand.ts | 8 +- src/connection/Connection.ts | 4 +- src/connection/ConnectionMetadataBuilder.ts | 6 +- src/connection/ConnectionOptions.ts | 3 + src/decorator/columns/Column.ts | 6 + src/decorator/columns/PrimaryColumn.ts | 2 +- src/decorator/entity-view/ViewEntity.ts | 2 + src/decorator/options/ViewEntityOptions.ts | 13 + src/driver/DriverFactory.ts | 3 + .../AuroraDataApiConnection.ts | 20 + ...roraDataApiConnectionCredentialsOptions.ts | 43 + .../AuroraDataApiConnectionOptions.ts | 23 + .../aurora-data-api/AuroraDataApiDriver.ts | 828 +++++++++ .../AuroraDataApiQueryRunner.ts | 1614 +++++++++++++++++ .../cockroachdb/CockroachConnectionOptions.ts | 7 + src/driver/cockroachdb/CockroachDriver.ts | 5 +- .../cockroachdb/CockroachQueryRunner.ts | 8 +- src/driver/mongodb/MongoConnectionOptions.ts | 6 + src/driver/mongodb/MongoDriver.ts | 3 +- src/driver/mysql/MysqlDriver.ts | 10 + src/driver/mysql/MysqlQueryRunner.ts | 12 +- src/driver/oracle/OracleQueryRunner.ts | 13 +- .../postgres/PostgresConnectionOptions.ts | 7 + src/driver/postgres/PostgresDriver.ts | 52 +- src/driver/postgres/PostgresQueryRunner.ts | 13 +- .../AbstractSqliteQueryRunner.ts | 8 +- src/driver/sqljs/SqljsConnectionOptions.ts | 5 + src/driver/sqljs/SqljsDriver.ts | 2 +- .../sqlserver/SqlServerConnectionOptions.ts | 7 + src/driver/sqlserver/SqlServerDriver.ts | 4 +- src/driver/sqlserver/SqlServerQueryRunner.ts | 8 +- src/driver/types/ColumnTypes.ts | 4 +- src/driver/types/DatabaseType.ts | 1 + src/find-options/FindConditions.ts | 7 +- src/index.ts | 3 + src/logger/AdvancedConsoleLogger.ts | 4 +- src/metadata-args/TableMetadataArgs.ts | 6 + src/metadata/EntityMetadata.ts | 4 +- src/metadata/ForeignKeyMetadata.ts | 2 +- src/migration/MigrationExecutor.ts | 22 +- src/naming-strategy/DefaultNamingStrategy.ts | 2 +- .../NamingStrategyInterface.ts | 2 +- src/query-builder/InsertQueryBuilder.ts | 6 +- src/query-builder/QueryExpressionMap.ts | 5 + src/query-builder/RelationRemover.ts | 4 +- src/query-builder/SelectQueryBuilder.ts | 11 +- src/query-builder/UpdateQueryBuilder.ts | 11 +- src/query-builder/result/UpdateResult.ts | 8 +- src/repository/BaseEntity.ts | 8 +- src/schema-builder/RdbmsSchemaBuilder.ts | 6 +- src/schema-builder/options/ViewOptions.ts | 5 + src/schema-builder/view/View.ts | 13 +- src/util/DateUtils.ts | 7 +- src/util/DirectoryExportedClassesLoader.ts | 12 +- .../functional/cube/postgres/cube-postgres.ts | 116 ++ test/functional/cube/postgres/entity/Post.ts | 15 + .../select/query-builder-select.ts | 15 + test/functional/query-runner/rename-column.ts | 2 +- test/functional/query-runner/rename-table.ts | 2 +- test/github-issues/1308/entity/Author.ts | 34 + test/github-issues/1308/entity/Post.ts | 35 + test/github-issues/1308/issue-1308.ts | 49 + test/github-issues/2632/entity/Category.ts | 18 + test/github-issues/2632/entity/Post.ts | 20 + test/github-issues/2632/issue-2632.ts | 75 + test/github-issues/2779/entity/Post.ts | 15 + test/github-issues/2779/issue-2779.ts | 44 + test/github-issues/2779/set.ts | 5 + test/github-issues/3847/entity/Animal.ts | 17 + test/github-issues/3847/entity/Category.ts | 9 + test/github-issues/3847/issue-3847.ts | 30 + .../3847/naming/NamingStrategyUnderTest.ts | 16 + test/github-issues/4440/entity/Post.ts | 16 + test/github-issues/4440/issue-4440.ts | 43 + test/github-issues/4513/entity/User.ts | 13 + test/github-issues/4513/issue-4513.ts | 140 ++ test/github-issues/4570/issue-4570.ts | 19 + test/github-issues/4630/entity/User.ts | 15 + test/github-issues/4630/issue-4630.ts | 32 + test/github-issues/4719/entity/Post.ts | 12 + test/github-issues/4719/issue-4719.ts | 41 + 108 files changed, 3944 insertions(+), 249 deletions(-) create mode 100644 docs/troubleshooting.md create mode 100644 docs/zh_CN/troubleshooting.md create mode 100644 src/driver/aurora-data-api/AuroraDataApiConnection.ts create mode 100644 src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts create mode 100644 src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts create mode 100644 src/driver/aurora-data-api/AuroraDataApiDriver.ts create mode 100644 src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts create mode 100644 test/functional/cube/postgres/cube-postgres.ts create mode 100644 test/functional/cube/postgres/entity/Post.ts create mode 100644 test/github-issues/1308/entity/Author.ts create mode 100644 test/github-issues/1308/entity/Post.ts create mode 100644 test/github-issues/1308/issue-1308.ts create mode 100644 test/github-issues/2632/entity/Category.ts create mode 100644 test/github-issues/2632/entity/Post.ts create mode 100644 test/github-issues/2632/issue-2632.ts create mode 100644 test/github-issues/2779/entity/Post.ts create mode 100644 test/github-issues/2779/issue-2779.ts create mode 100644 test/github-issues/2779/set.ts create mode 100644 test/github-issues/3847/entity/Animal.ts create mode 100644 test/github-issues/3847/entity/Category.ts create mode 100644 test/github-issues/3847/issue-3847.ts create mode 100644 test/github-issues/3847/naming/NamingStrategyUnderTest.ts create mode 100644 test/github-issues/4440/entity/Post.ts create mode 100644 test/github-issues/4440/issue-4440.ts create mode 100644 test/github-issues/4513/entity/User.ts create mode 100644 test/github-issues/4513/issue-4513.ts create mode 100644 test/github-issues/4570/issue-4570.ts create mode 100644 test/github-issues/4630/entity/User.ts create mode 100644 test/github-issues/4630/issue-4630.ts create mode 100644 test/github-issues/4719/entity/Post.ts create mode 100644 test/github-issues/4719/issue-4719.ts 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(); + }))); +});