diff --git a/CHANGELOG.md b/CHANGELOG.md index 069c00ccc..15d0a86da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,14 @@ each for its own `findOne*` or `find*` methods * fixes [#285](https://github.com/typeorm/typeorm/issues/285) - issue when cli commands rise `CannotCloseNotConnectedError` * fixes [#309](https://github.com/typeorm/typeorm/issues/309) - issue when `andHaving` didn't work without calling `having` on `QueryBuilder` +# 0.0.10 + +* added `ObjectLiteral` and `ObjectType` into main exports +* fixed issue fixes [#345](https://github.com/typeorm/typeorm/issues/345). +* fixed issue with migration not saving into the database correctly. + Note its a breaking change if you have run migrations before and have records in the database table, + make sure to apply corresponding changes. More info in [#360](https://github.com/typeorm/typeorm/issues/360) issue. + # 0.0.9 (latest) * fixed bug with indices from columns are not being inherited from parent entity [#242](https://github.com/typeorm/typeorm/issues/242) diff --git a/DEVELOPER.md b/DEVELOPER.md index 7e2774ea4..45945675f 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -80,7 +80,46 @@ You can link (or simply copy/paste) this directory into your project and test Ty ## Running Tests Locally -Setup your environment configuration by copying `ormconfig.json.dist` into `ormconfig.json` and +It would be greatly appreciated if PRs that change code come with appropriate tests. + +To create a test for a specific issue opened on github, create a file: `test/github-issues//issue-.ts` where +`` is the corresponding github issue. For example, if you were creating a PR to fix github issue #363, you'd +create `test/github-issues/363/issue-363.ts`. + +Most tests will benefit from using this template as a starting point: + +```ts +import "reflect-metadata"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {expect} from "chai"; + +describe("github issues > # ", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("should ", () => Promise.all(connections.map(async connection => { + + // tests go here + + }))); + + // you can add additional tests if needed + +}); +``` + +If you place entities in `./entity/.ts` relative to your `issue-.ts` file, +they will automatically be loaded. + +To run the tests, setup your environment configuration by copying `ormconfig.json.dist` into `ormconfig.json` and replacing parameters with your own. Then run tests: @@ -92,6 +131,15 @@ npm test You should execute test suites before submitting a PR to github. All the tests are executed on our Continuous Integration infrastructure and a PR could only be merged once the tests pass. +>**Hint:** you can use the `--grep` flag to pass a Regex to `gulp-mocha`. Only the tests have have `describe`/`it` +>statements that match the Regex will be run. For example: +> +>```shell +>npm test -- --grep="github issues > #363" +>``` +> +>This is useful when trying to get a specific test or subset of tests to pass. + ## Using Docker To run your tests you need dbms installed on your machine. Alternatively, you can use docker diff --git a/README.md b/README.md index ade25e0e5..b38cf1d3f 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,38 @@ -# TypeORM + -[![Build Status](https://travis-ci.org/typeorm/typeorm.svg?branch=master)](https://travis-ci.org/typeorm/typeorm) -[![npm version](https://badge.fury.io/js/typeorm.svg)](https://badge.fury.io/js/typeorm) -[![Dependency Status](https://david-dm.org/typeorm/typeorm.svg)](https://david-dm.org/typeorm/typeorm) -[![devDependency Status](https://david-dm.org/typeorm/typeorm/dev-status.svg)](https://david-dm.org/typeorm/typeorm#info=devDependencies) -[![Join the chat at https://gitter.im/typeorm/typeorm](https://badges.gitter.im/typeorm/typeorm.svg)](https://gitter.im/typeorm/typeorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -[![TypeORM Logo](./resources/logo_big.png)](https://typeorm.github.io/) - -> Please support a project by simply putting a github star. -Share this library with friends on twitter and everywhere else you can. +> Please support a project by simply putting a Github star. +Share this library with friends on Twitter and everywhere else you can. > ORM is in active development, but main API is pretty stable. If you notice bug or have something not working please report an issue, we'll try to fix it as soon as possible. More documentation and features expected to be soon. Feel free to contribute. -> 0.0.9 is released! Most notable changes are in the [changelog](./CHANGELOG.md). +> For the latest release changes see [changelog](./CHANGELOG.md). -TypeORM is an [Object Relational Mapper](1) (ORM) for node.js written in +TypeORM is an [Object Relational Mapper](1) (ORM) for Node.js written in TypeScript that can be used with TypeScript or JavaScript (ES5, ES6, ES7). Its goal to always support latest JavaScript features and provide features that help you to develop any kind of applications that use database - from @@ -30,7 +45,7 @@ your objects * map your selections from tables to javascript objects and map table columns to javascript object's properties * create one-to-one, many-to-one, one-to-many, many-to-many relations between tables -* and much more ... +* and much more... TypeORM uses Data Mapper pattern, unlike all other JavaScript ORMs that currently exist, which means you can write loosely coupled, scalable, @@ -52,9 +67,9 @@ TypeORM is highly influenced by other ORMs, such as [Hibernate](http://hibernate `npm install reflect-metadata --save` - and use it somewhere in the global place of your app: + and import it somewhere in the global place of your app (for example in `app.ts`): - * `require("reflect-metadata")` in your app's entry point (for example `app.ts`) + `import "reflect-metadata";` 3. You may need to install node typings: @@ -66,7 +81,7 @@ TypeORM is highly influenced by other ORMs, such as [Hibernate](http://hibernate `npm install mysql --save` - * for **Postgres** + * for **PostgreSQL** `npm install pg --save` @@ -96,12 +111,12 @@ Also make sure you are using TypeScript compiler version > **2.1** and you have "experimentalDecorators": true, ``` -You'll also need to enable `es6` in the `lib` section of compiler options, or install `es6-shim` from `@typings`. +You'll also need to enable `es6` in the `lib` section of compiler options, or install `es6-shim` from `@types`. #### Node.js version -TypeORM was tested with Node.JS version 4 and above. -If you have errors during app bootstrap, try to upgrade your node.js version to the latest version. +TypeORM was tested with Node.js version 4 and above. +If you have errors during app bootstrap, try to upgrade your Node.js version to the latest version. #### Usage in the browser with WebSQL (experimental) @@ -129,7 +144,7 @@ export class Photo { ### Creating entity -Now lets make it entity: +Now let's make it entity: ```typescript import {Entity} from "typeorm"; @@ -214,7 +229,7 @@ export class Photo { ### Create auto-increment / generated / sequence / identity column -Now, lets say you want to make your id column to be auto-generated (this is known as auto-increment / sequence / generated identity column). +Now, let's say you want to make your id column to be auto-generated (this is known as auto-increment / sequence / generated identity column). To do that you need to change your column's type to integer and set a `{ generated: true }` in your primary column's options: ```typescript @@ -278,10 +293,10 @@ export class Photo { ### Custom column data types -Next step, lets fix our data types. By default, string is mapped to a varchar(255)-like type (depend of database type). +Next step, let's fix our data types. By default, string is mapped to a varchar(255)-like type (depend of database type). Number is mapped to a float/double-like type (depend of database type). We don't want all our columns to be limited varchars or excessive floats. -Lets setup correct data types: +Let's setup correct data types: ```typescript import {Entity, Column, PrimaryGeneratedColumn} from "typeorm"; @@ -313,7 +328,7 @@ export class Photo { ### Creating connection with the database -Now, when our entity is created, lets create `app.ts` file and setup our connection there: +Now, when our entity is created, let's create `app.ts` file and setup our connection there: ```typescript import "reflect-metadata"; @@ -334,11 +349,11 @@ createConnection({ ], autoSchemaSync: true, }).then(connection => { - // here you can start to work with your entities + // Here you can start to work with your entities }).catch(error => console.log(error)); ``` -We are using mysql in this example, but you can use any other database. +We are using MySQL in this example, but you can use any other database. To use another database simply change type in the driver options to the database type you are using: mysql, mariadb, postgres, sqlite, mssql or oracle. Also make sure to use your own host, port, username, password and database settings. @@ -371,7 +386,7 @@ createConnection({ ], autoSchemaSync: true, }).then(connection => { - // here you can start to work with your entities + // Here you can start to work with your entities }).catch(error => console.log(error)); ``` @@ -392,12 +407,11 @@ Now you if run your `app.ts`, connection with database will be initialized and d | isPublished | boolean | | +-------------+--------------+----------------------------+ ``` - -Now you can run your `app.ts`, connection with database will be initialized, and database table for your Photo will be created. + ### Creating and inserting photo into the database -Now lets create a new photo to save it in the database: +Now let's create a new photo to save it in the database: ```typescript import {createConnection} from "typeorm"; @@ -422,7 +436,7 @@ createConnection(/*...*/).then(connection => { ### Using async/await syntax -Lets use latest TypeScript advantages and use async/await syntax instead: +Let's use latest TypeScript advantages and use async/await syntax instead: ```typescript import {createConnection} from "typeorm"; @@ -448,7 +462,7 @@ createConnection(/*...*/).then(async connection => { We just created a new photo and saved it in the database. We used `EntityManager` to save it. Using entity managers you can manipulate any entity in your app. -Now lets load our saved entity: +Now let's load our saved entity: ```typescript import {createConnection} from "typeorm"; @@ -467,7 +481,7 @@ savedPhotos will be an array of Photo objects with the data loaded from the data ### Using Repositories -Now lets refactor our code and use `Repository` instead of EntityManager. +Now let's refactor our code and use `Repository` instead of EntityManager. Each entity has its own repository which handles all operations with its entity. When you deal with entities a lot, Repositories are more convenient to use then EntityManager: @@ -498,7 +512,7 @@ createConnection(/*...*/).then(async connection => { ### Loading photos from the database -Lets try more load operations using Repository: +Let's try more load operations using Repository: ```typescript import {createConnection} from "typeorm"; @@ -523,15 +537,15 @@ createConnection(/*...*/).then(async connection => { console.log("All published photos: ", allPublishedPhotos); let [allPhotos, photosCount] = await photoRepository.findAndCount(); - console.log("All photos: ", allPublishedPhotos); - console.log("Photos count: ", allPublishedPhotos); + console.log("All photos: ", allPhotos); + console.log("Photos count: ", photosCount); }).catch(error => console.log(error)); ``` ### Updating photo in the database -Now lets load a single photo from the database, update it and save it: +Now let's load a single photo from the database, update it and save it: ```typescript import {createConnection} from "typeorm"; @@ -553,7 +567,6 @@ Now photo with `id = 1` will be updated in the database. Now let's remove our photo from the database: - ```typescript import {createConnection} from "typeorm"; import {Photo} from "./entity/Photo"; @@ -569,10 +582,10 @@ createConnection(/*...*/).then(async connection => { Now photo with `id = 1` will be removed from the database. -### creating a one-to-one relation +### Creating a one-to-one relation -Lets create a one-to-one relation with another class. -Lets create a new class called PhotoMetadata.ts which will contain a PhotoMetadata class which supposed to contain our photo's additional meta-information: +Let's create a one-to-one relation with another class. +Let's create a new class called `PhotoMetadata.ts` which will contain a PhotoMetadata class which supposed to contain our photo's additional meta-information: ```typescript import {Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn} from "typeorm"; @@ -612,7 +625,7 @@ We can also write it as a `() => Photo`, but we use `type => Photo as convention Type variable itself does not contain anything. We also put `@JoinColumn` decorator, which indicates that this side of the relationship will be owning relationship. -Relations can be a uni-directional and bi-directional. +Relations can be a unidirectional and bidirectional. Only one side of relational can be owner. Using this decorator is required on owner side of the relationship. @@ -620,7 +633,7 @@ If you run the app you'll see a new generated table, and it will contain a colum ```shell +-------------+--------------+----------------------------+ -| photo | +| photometadata | +-------------+--------------+----------------------------+ | id | int(11) | PRIMARY KEY AUTO_INCREMENT | | height | int(11) | | @@ -632,9 +645,9 @@ If you run the app you'll see a new generated table, and it will contain a colum +-------------+--------------+----------------------------+ ``` -### persisting an object with one-to-one relation +### Persisting an object with one-to-one relation -Now lets save a photo, its metadata and attach them to each other. +Now let's save a photo, its metadata and attach them to each other. ```typescript import {createConnection} from "typeorm"; @@ -643,14 +656,14 @@ import {PhotoMetadata} from "./entity/PhotoMetadata"; createConnection(/*...*/).then(async connection => { - // create a photo + // Create a photo let photo = new Photo(); photo.name = "Me and Bears"; photo.description = "I am near polar bears"; photo.filename = "photo-with-bears.jpg" photo.isPublished = true; - // create a photo metadata + // Create a photo metadata let metadata = new PhotoMetadata(); metadata.height = 640; metadata.width = 480; @@ -659,29 +672,29 @@ createConnection(/*...*/).then(async connection => { metadata.orientation = "portait"; metadata.photo = photo; // this way we connect them - // get entity repositories + // Get entity repositories let photoRepository = connection.getRepository(Photo); let metadataRepository = connection.getRepository(PhotoMetadata); - // first we should persist a photo + // First we should persist a photo await photoRepository.persist(photo); - // photo is saved. Now we need to persist a photo metadata + // Photo is saved. Now we need to persist a photo metadata await metadataRepository.persist(metadata); - // done - console.log("metadata is saved, and relation between metadata and photo is created in the database too"); + // Done + console.log("Metadata is saved, and relation between metadata and photo is created in the database too"); }).catch(error => console.log(error)); ``` ### Adding inverse side of a relation -Relations can be a uni-directional and bi-directional. -Now, relation between PhotoMetadata and Photo is uni-directional. +Relations can be a unidirectional and bidirectional. +Now, relation between PhotoMetadata and Photo is unidirectional. Owner of the relation is PhotoMetadata and Photo doesn't know anything about PhotoMetadata. This makes complicated accessing a photo metadata from the photo objects. -To fix it we should add inverse relation and make relations between PhotoMetadata and Photo bi-directional. +To fix it we should add inverse relation and make relations between PhotoMetadata and Photo bidirectional. Let's modify our entities: ```typescript @@ -724,9 +737,9 @@ Owning side of relationship contain a column with a foreign key in the database. ### Loading object with their relations -Now lets load our photo, and its photo metadata in a single query. +Now let's load our photo, and its photo metadata in a single query. There are two ways to do it - one you can use `FindOptions`, second is to use QueryBuilder. -Lets use FindOptions first. +Let's use FindOptions first. `Repository.find` method allows you to specify object with FindOptions interface. Using this you can customize your query to perform more complex queries. @@ -759,7 +772,7 @@ We also used `innerJoinAndSelect` to inner and join and select the data from pho In `"photo.metadata"` "photo" is an alias you used, and "metadata" is a property name with relation of the object you are selecting. `"metadata"`: is a new alias to the data returned by join expression. -Lets use `QueryBuilder` for the same purpose. QueryBuilder allows to use more complex queries in an elegant way: +Let's use `QueryBuilder` for the same purpose. QueryBuilder allows to use more complex queries in an elegant way: ```typescript import {createConnection} from "typeorm"; @@ -778,7 +791,7 @@ createConnection(/*...*/).then(async connection => { }).catch(error => console.log(error)); ``` -### using cascade options to automatically save related objects +### Using cascade options to automatically save related objects We can setup cascade options in our relations, in the cases when we want our related object to be persisted whenever other object is saved. Let's change our photo's `@OneToOne` decorator a bit: @@ -807,14 +820,14 @@ Now we can simply persist a photo object, and metadata object will persist autom ```typescript createConnection(options).then(async connection => { - // create photo object + // Create photo object let photo = new Photo(); photo.name = "Me and Bears"; photo.description = "I am near polar bears"; photo.filename = "photo-with-bears.jpg" photo.isPublished = true; - // create photo metadata object + // Create photo metadata object let metadata = new PhotoMetadata(); metadata.height = 640; metadata.width = 480; @@ -824,10 +837,10 @@ createConnection(options).then(async connection => { photo.metadata = metadata; // this way we connect them - // get repository + // Get repository let photoRepository = connection.getRepository(Photo); - // first we should persist a photo + // Persisting a photo also persist the metadata await photoRepository.persist(photo); console.log("Photo is saved, photo metadata is saved too.") @@ -835,11 +848,11 @@ createConnection(options).then(async connection => { }).catch(error => console.log(error)); ``` -### creating a many-to-one / one-to-many relation +### Creating a many-to-one / one-to-many relation -Lets create a many-to-one / one-to-many relation. -Lets say a photo has one author, and each author can have many photos. -First, lets create Author class: +Let's create a many-to-one / one-to-many relation. +Let's say a photo has one author, and each author can have many photos. +First, let's create Author class: ```typescript import {Entity, Column, PrimaryGeneratedColumn, OneToMany, JoinColumn} from "typeorm"; @@ -854,7 +867,7 @@ export class Author { @Column() name: string; - @OneToMany(type => Photo, photo => photo.author) // note: we will create author property in the Photo class below + @OneToMany(type => Photo, photo => photo.author) // Note: we will create author property in the Photo class below photos: Photo[]; } ``` @@ -862,7 +875,7 @@ export class Author { Author contains an inverse side of a relationship. OneToMany is always an inverse side of relation, and it can't exist without ManyToOne of the other side of relationship. -Now lets add owner side of relationship into the Photo entity: +Now let's add owner side of relationship into the Photo entity: ```typescript import {Entity, Column, PrimaryGeneratedColumn, ManyToOne} from "typeorm"; @@ -909,11 +922,11 @@ It will also modify photo table - add a new column author and create a foreign k +-------------+--------------+----------------------------+ ``` -### creating a many-to-many relation +### Creating a many-to-many relation -Lets create a many-to-one / many-to-many relation. -Lets say a photo can be in many albums, and multiple can have many photos. -Lets create an `Album` class: +Let's create a many-to-one / many-to-many relation. +Let's say a photo can be in many albums, and multiple can have many photos. +Let's create an `Album` class: ```typescript import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable} from "typeorm"; @@ -927,30 +940,30 @@ export class Album { @Column() name: string; - @ManyToMany(type => Photo, photo => photo.albums, { // note: we will create "albums" property in the Photo class below - cascadeInsert: true, // allow to insert a new photo on album save - cascadeUpdate: true, // allow to update a photo on album save - cascadeRemove: true // allow to remove a photo on album remove + @ManyToMany(type => Photo, photo => photo.albums, { // Note: we will create "albums" property in the Photo class below + cascadeInsert: true, // Allow to insert a new photo on album save + cascadeUpdate: true, // Allow to update a photo on album save + cascadeRemove: true // Allow to remove a photo on album remove }) @JoinTable() - photos: Photo[] = []; // we initialize array for convinience here + photos: Photo[] = []; // We initialize array for convinience here } ``` `@JoinTable` is required to specify that this is owner side of the relationship. -Now lets add inverse side of our relation to the `Photo` class: +Now let's add inverse side of our relation to the `Photo` class: ```typescript export class Photo { /// ... other columns @ManyToMany(type => Album, album => album.photos, { - cascadeInsert: true, // allow to insert a new album on photo save - cascadeUpdate: true, // allow to update an album on photo save - cascadeRemove: true // allow to remove an album on photo remove + cascadeInsert: true, // Allow to insert a new album on photo save + cascadeUpdate: true, // Allow to update an album on photo save + cascadeRemove: true // Allow to remove an album on photo remove }) - albums: Album[] = []; // we initialize array for convinience here + albums: Album[] = []; // We initialize array for convinience here } ``` @@ -974,19 +987,19 @@ const options: CreateConnectionOptions = { }; ``` -Now lets insert albums and photos to our database: +Now let's insert albums and photos to our database: ```typescript let connection = await createConnection(options); -// create a few albums +// Create a few albums let album1 = new Album(); album1.name = "Bears"; let album2 = new Album(); album2.name = "Me"; -// create a few photos +// Create a few photos let photo1 = new Photo(); photo1.name = "Me and Bears"; photo1.description = "I am near polar bears"; @@ -999,23 +1012,23 @@ photo2.description = "I am near polar bears"; photo2.filename = "photo-with-bears.jpg"; photo2.albums.push(album2); -// get entity repository +// Get entity repository let photoRepository = connection.getRepository(Photo); -// first save a first photo -// we only save the photos, albums are persisted +// First save a first photo +// We only save the photos, albums are persisted // automatically because of cascade options await photoRepository.persist(photo1); -// second save a first photo +// Second save a first photo await photoRepository.persist(photo2); console.log("Both photos have been saved"); ``` -### using QueryBuilder +### Using QueryBuilder -You can use QueryBuilder to build even more complex queries. For example you can do this: +You can use QueryBuilder to build even more complex queries. For example, you can do this: ```typescript let photoRepository = connection.getRepository(Photo); @@ -1069,6 +1082,8 @@ There are few repositories which you can clone and start with: * [Example how to use TypeORM with JavaScript](https://github.com/typeorm/javascript-example) * [Example how to use TypeORM with JavaScript and Babel](https://github.com/typeorm/babel-example) * [Example how to use TypeORM with TypeScript and SystemJS in Browser](https://github.com/typeorm/browser-example) +* [Example how to use Express and TypeORM with TypeScript](https://github.com/typeorm/typescript-express-example) +* [Example how to use Koa and TypeORM with TypeScript](https://github.com/typeorm/typescript-koa-example) ## Extensions diff --git a/gulpfile.ts b/gulpfile.ts index 709fd2ad3..500ad6b16 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -19,6 +19,7 @@ const sourcemaps = require("gulp-sourcemaps"); const istanbul = require("gulp-istanbul"); const remapIstanbul = require("remap-istanbul/lib/gulpRemapIstanbul"); const ts = require("gulp-typescript"); +const args = require('yargs').argv; @Gulpclass() export class Gulpfile { @@ -297,6 +298,7 @@ export class Gulpfile { return gulp.src(["./build/compiled/test/**/*.js"]) .pipe(mocha({ bail: true, + grep: !!args.grep ? new RegExp(args.grep) : undefined, timeout: 15000 })) .pipe(istanbul.writeReports()); diff --git a/resources/logo_big.png b/resources/logo_big.png index b255eac89..1da97498a 100644 Binary files a/resources/logo_big.png and b/resources/logo_big.png differ diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index 279bb0329..e7d8f699d 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -310,7 +310,7 @@ export class Connection { * Imports naming strategies from the given paths (directories) and registers them in the current connection. */ importNamingStrategiesFromDirectories(paths: string[]): this { - this.importEntities(importClassesFromDirectories(paths)); + this.importNamingStrategies(importClassesFromDirectories(paths)); return this; } diff --git a/src/connection/ConnectionOptions.ts b/src/connection/ConnectionOptions.ts index 469e7e269..e2d52c3b3 100644 --- a/src/connection/ConnectionOptions.ts +++ b/src/connection/ConnectionOptions.ts @@ -80,12 +80,16 @@ export interface ConnectionOptions { * * Note that for MongoDB database it does not create schema, because MongoDB is schemaless. * Instead, it syncs just by creating indices. + * + * todo: rename it simply to synchronize: boolean ? */ readonly autoSchemaSync?: boolean; /** * Indicates if migrations should be auto run on every application launch. * Alternative to it, you can use CLI and run migration:create command. + * + * todo: rename it simply to runMigrations: boolean ? */ readonly autoMigrationsRun?: boolean; diff --git a/src/decorator/NamingStrategy.ts b/src/decorator/NamingStrategy.ts index 2a6741098..ac77cd8bc 100644 --- a/src/decorator/NamingStrategy.ts +++ b/src/decorator/NamingStrategy.ts @@ -3,6 +3,9 @@ import {NamingStrategyMetadataArgs} from "../metadata-args/NamingStrategyMetadat /** * Decorator registers a new naming strategy to be used in naming things. + * + * todo: deprecate using naming strategies this way. use it without decorators + * todo: but add multiple default naming strategies for use */ export function NamingStrategy(name?: string): Function { return function (target: Function) { diff --git a/src/decorator/columns/CreateDateColumn.ts b/src/decorator/columns/CreateDateColumn.ts index 870843fc6..657ebda66 100644 --- a/src/decorator/columns/CreateDateColumn.ts +++ b/src/decorator/columns/CreateDateColumn.ts @@ -30,4 +30,3 @@ export function CreateDateColumn(options?: ColumnOptions): Function { getMetadataArgsStorage().columns.add(args); }; } - diff --git a/src/decorator/columns/PrimaryGeneratedColumn.ts b/src/decorator/columns/PrimaryGeneratedColumn.ts index 778d0ab23..505c46bd4 100644 --- a/src/decorator/columns/PrimaryGeneratedColumn.ts +++ b/src/decorator/columns/PrimaryGeneratedColumn.ts @@ -1,5 +1,4 @@ import {ColumnOptions} from "../options/ColumnOptions"; -import {ColumnTypes} from "../../metadata/types/ColumnTypes"; import {getMetadataArgsStorage} from "../../index"; import {PrimaryColumnCannotBeNullableError} from "../error/PrimaryColumnCannotBeNullableError"; import {ColumnMetadataArgs} from "../../metadata-args/ColumnMetadataArgs"; diff --git a/src/decorator/options/ColumnOptions.ts b/src/decorator/options/ColumnOptions.ts index 08abd7aa9..427acfdff 100644 --- a/src/decorator/options/ColumnOptions.ts +++ b/src/decorator/options/ColumnOptions.ts @@ -80,4 +80,10 @@ export interface ColumnOptions { */ readonly localTimezone?: boolean; + /** + * Indicates if column's type will be set as a fixed-length data type. + * Works only with "string" columns. + */ + readonly fixedLength?: boolean; + } diff --git a/src/driver/mongodb/MongoQueryRunner.ts b/src/driver/mongodb/MongoQueryRunner.ts index 5bcdafbd3..27e40cc0c 100644 --- a/src/driver/mongodb/MongoQueryRunner.ts +++ b/src/driver/mongodb/MongoQueryRunner.ts @@ -643,7 +643,7 @@ export class MongoQueryRunner implements QueryRunner { /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { throw new Error(`Schema update queries are not supported by MongoDB driver.`); } diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index 9bcc8fed8..eb7dbb7a0 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -684,11 +684,15 @@ export class MysqlQueryRunner implements QueryRunner { /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": - return "varchar(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + if (typeOptions.fixedLength) { + return "char(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } else { + return "varchar(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } case "text": return "text"; case "boolean": diff --git a/src/driver/oracle/OracleDriver.ts b/src/driver/oracle/OracleDriver.ts index f5d46a6cb..58b3b6bdb 100644 --- a/src/driver/oracle/OracleDriver.ts +++ b/src/driver/oracle/OracleDriver.ts @@ -196,7 +196,7 @@ export class OracleDriver implements Driver { const keys = Object.keys(parameters).map(parameter => "(:" + parameter + "\\b)").join("|"); sql = sql.replace(new RegExp(keys, "g"), (key: string) => { escapedParameters.push(parameters[key.substr(1)]); - return ":" + key; + return key; }); // todo: make replace only in value statements, otherwise problems return [sql, escapedParameters]; } diff --git a/src/driver/oracle/OracleQueryRunner.ts b/src/driver/oracle/OracleQueryRunner.ts index 6db611a71..ab197dc15 100644 --- a/src/driver/oracle/OracleQueryRunner.ts +++ b/src/driver/oracle/OracleQueryRunner.ts @@ -742,10 +742,14 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": - return "varchar2(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + if (typeOptions.fixedLength) { + return "char(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } else { + return "varchar2(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } case "text": return "clob"; case "boolean": diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index a2f8333c4..b213c950b 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -258,15 +258,15 @@ export class PostgresQueryRunner implements QueryRunner { const tableNamesString = tableNames.map(name => "'" + name + "'").join(", "); const tablesSql = `SELECT * FROM information_schema.tables WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}' AND table_name IN (${tableNamesString})`; const columnsSql = `SELECT * FROM information_schema.columns WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}'`; - const indicesSql = `SELECT t.relname AS table_name, i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a + const indicesSql = `SELECT t.relname AS table_name, i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_namespace ns WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid -AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname IN (${tableNamesString}) ORDER BY t.relname, i.relname`; - const foreignKeysSql = `SELECT table_name, constraint_name FROM information_schema.table_constraints WHERE table_catalog = '${this.dbName}' AND constraint_type = 'FOREIGN KEY'`; - const uniqueKeysSql = `SELECT * FROM information_schema.table_constraints WHERE table_catalog = '${this.dbName}' AND constraint_type = 'UNIQUE'`; +AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname IN (${tableNamesString}) AND t.relnamespace = ns.OID AND ns.nspname ='${this.schemaName}' ORDER BY t.relname, i.relname`; + const foreignKeysSql = `SELECT table_name, constraint_name FROM information_schema.table_constraints WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}' AND constraint_type = 'FOREIGN KEY'`; + const uniqueKeysSql = `SELECT * FROM information_schema.table_constraints WHERE table_catalog = '${this.dbName}' AND table_schema = '${this.schemaName}' AND constraint_type = 'UNIQUE'`; const primaryKeysSql = `SELECT c.column_name, tc.table_name, tc.constraint_name FROM information_schema.table_constraints tc JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name) JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name -where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`; +where constraint_type = 'PRIMARY KEY' AND c.table_schema = '${this.schemaName}' and tc.table_catalog = '${this.dbName}'`; const [dbTables, dbColumns, dbIndices, dbForeignKeys, dbUniqueKeys, primaryKeys]: ObjectLiteral[][] = await Promise.all([ this.query(tablesSql), this.query(columnsSql), @@ -744,10 +744,14 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`; /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }): string { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": - return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + if (typeOptions.fixedLength) { + return "character(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } else { + return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } case "text": return "text"; case "boolean": diff --git a/src/driver/sqlite/SqliteQueryRunner.ts b/src/driver/sqlite/SqliteQueryRunner.ts index de84679d2..c73e54f92 100644 --- a/src/driver/sqlite/SqliteQueryRunner.ts +++ b/src/driver/sqlite/SqliteQueryRunner.ts @@ -727,7 +727,7 @@ export class SqliteQueryRunner implements QueryRunner { /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")"; diff --git a/src/driver/sqlserver/SqlServerQueryRunner.ts b/src/driver/sqlserver/SqlServerQueryRunner.ts index f9242338d..f37168f0a 100644 --- a/src/driver/sqlserver/SqlServerQueryRunner.ts +++ b/src/driver/sqlserver/SqlServerQueryRunner.ts @@ -791,10 +791,14 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": - return "nvarchar(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + if (typeOptions.fixedLength) { + return "nchar(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } else { + return "nvarchar(" + (typeOptions.length ? typeOptions.length : 255) + ")"; + } case "text": return "ntext"; case "boolean": diff --git a/src/driver/websql/WebsqlQueryRunner.ts b/src/driver/websql/WebsqlQueryRunner.ts index 47e63a1aa..51d482e47 100644 --- a/src/driver/websql/WebsqlQueryRunner.ts +++ b/src/driver/websql/WebsqlQueryRunner.ts @@ -737,7 +737,7 @@ export class WebsqlQueryRunner implements QueryRunner { /** * Creates a database type from a given column metadata. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }) { + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string { switch (typeOptions.type) { case "string": return "character varying(" + (typeOptions.length ? typeOptions.length : 255) + ")"; diff --git a/src/entity-manager/EntityManager.ts b/src/entity-manager/EntityManager.ts index 25e55c86b..7a08ad70f 100644 --- a/src/entity-manager/EntityManager.ts +++ b/src/entity-manager/EntityManager.ts @@ -103,7 +103,9 @@ export class EntityManager extends BaseEntityManager { if (target.length === 0) return Promise.resolve(target); - return this.getRepository(target[0].constructor).persist(entity as Entity[], options); + return Promise.all(target.map((t, i) => { + return this.getRepository(t.constructor).persist((entity as Entity[])[i], options); + })); } else { return this.getRepository(target.constructor).persist(entity as Entity, options); } @@ -178,7 +180,9 @@ export class EntityManager extends BaseEntityManager { } else { // todo: throw exception if constructor in target is not set if (target instanceof Array) { - return this.getRepository(target[0].constructor).remove(entity as Entity[], options); + return Promise.all(target.map((t, i) => { + return this.getRepository(t.constructor).remove((entity as Entity[])[i], options); + })); } else { return this.getRepository(target.constructor).remove(entity as Entity, options); } diff --git a/src/index.ts b/src/index.ts index 27df8120b..b7eeb6850 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,8 @@ import {MongoRepository} from "./repository/MongoRepository"; // ------------------------------------------------------------------------- export * from "./container"; +export * from "./common/ObjectType"; +export * from "./common/ObjectLiteral"; export * from "./decorator/columns/Column"; export * from "./decorator/columns/CreateDateColumn"; export * from "./decorator/columns/DiscriminatorColumn"; @@ -253,12 +255,12 @@ export function getEntityManager(connectionName: string = "default"): EntityMana /** * Gets repository for the given entity class. */ -export function getRepository(entityClass: ObjectType, connectionName: string): Repository; +export function getRepository(entityClass: ObjectType, connectionName?: string): Repository; /** * Gets repository for the given entity name. */ -export function getRepository(entityName: string, connectionName: string): Repository; +export function getRepository(entityName: string, connectionName?: string): Repository; /** * Gets repository for the given entity class or name. @@ -270,12 +272,12 @@ export function getRepository(entityClassOrName: ObjectType|stri /** * Gets tree repository for the given entity class. */ -export function getTreeRepository(entityClass: ObjectType, connectionName: string): TreeRepository; +export function getTreeRepository(entityClass: ObjectType, connectionName?: string): TreeRepository; /** * Gets tree repository for the given entity name. */ -export function getTreeRepository(entityName: string, connectionName: string): TreeRepository; +export function getTreeRepository(entityName: string, connectionName?: string): TreeRepository; /** * Gets tree repository for the given entity class or name. @@ -287,12 +289,12 @@ export function getTreeRepository(entityClassOrName: ObjectType| /** * Gets mongodb repository for the given entity class. */ -export function getMongoRepository(entityClass: ObjectType, connectionName: string): MongoRepository; +export function getMongoRepository(entityClass: ObjectType, connectionName?: string): MongoRepository; /** * Gets mongodb repository for the given entity name. */ -export function getMongoRepository(entityName: string, connectionName: string): MongoRepository; +export function getMongoRepository(entityName: string, connectionName?: string): MongoRepository; /** * Gets mongodb repository for the given entity class or name. diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index 42dfba243..40169870c 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -128,6 +128,12 @@ export class ColumnMetadata { */ readonly localTimezone?: boolean; + /** + * Indicates if column's type will be set as a fixed-length data type. + * Works only with "string" columns. + */ + readonly fixedLength?: boolean; + // --------------------------------------------------------------------- // Private Properties // --------------------------------------------------------------------- @@ -176,6 +182,8 @@ export class ColumnMetadata { this.timezone = args.options.timezone; if (args.options.localTimezone) this.localTimezone = args.options.localTimezone; + if (args.options.fixedLength) + this.fixedLength = args.options.fixedLength; } // --------------------------------------------------------------------- diff --git a/src/migration/MigrationExecutor.ts b/src/migration/MigrationExecutor.ts index e5da48d7b..6f0080587 100644 --- a/src/migration/MigrationExecutor.ts +++ b/src/migration/MigrationExecutor.ts @@ -223,7 +223,7 @@ export class MigrationExecutor { protected getMigrations(): Migration[] { const migrations = this.connection.getMigrations().map(migration => { const migrationClassName = (migration.constructor as any).name; - const migrationTimestamp = parseInt(migrationClassName.substr(-10)); + const migrationTimestamp = parseInt(migrationClassName.substr(-13)); if (!migrationTimestamp) throw new Error(`Migration class name should contain a class name at the end of the file. ${migrationClassName} migration name is wrong.`); diff --git a/src/query-runner/QueryRunner.ts b/src/query-runner/QueryRunner.ts index c86463f77..4a41ebdaf 100644 --- a/src/query-runner/QueryRunner.ts +++ b/src/query-runner/QueryRunner.ts @@ -79,7 +79,7 @@ export interface QueryRunner { /** * Converts a column type of the metadata to the database column's type. */ - normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean }): any; + normalizeType(typeOptions: { type: ColumnType, length?: string|number, precision?: number, scale?: number, timezone?: boolean, fixedLength?: boolean }): string; /** * Checks if "DEFAULT" values in the column metadata and in the database schema are equal. diff --git a/test/functional/connection/connection.ts b/test/functional/connection/connection.ts index 9d7711c83..9a8f0cbcc 100644 --- a/test/functional/connection/connection.ts +++ b/test/functional/connection/connection.ts @@ -1,10 +1,16 @@ import "reflect-metadata"; import {expect} from "chai"; import {Post} from "./entity/Post"; +import {Guest as GuestV1} from "./entity/v1/Guest"; +import {Comment as CommentV1} from "./entity/v1/Comment"; +import {Guest as GuestV2} from "./entity/v2/Guest"; +import {Comment as CommentV2} from "./entity/v2/Comment"; import {View} from "./entity/View"; import {Category} from "./entity/Category"; import {closeTestingConnections, createTestingConnections, setupSingleTestingConnection} from "../../utils/test-utils"; +import {createConnection} from "../../../src/index"; import {Connection} from "../../../src/connection/Connection"; +import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; import {Repository} from "../../../src/repository/Repository"; import {TreeRepository} from "../../../src/repository/TreeRepository"; import {getConnectionManager} from "../../../src/index"; @@ -381,33 +387,89 @@ describe("Connection", () => { }); + describe("Different names of the same content of the schema", () => { + + let connections: Connection[]; + beforeEach(async () => { + const [connection1] = await createTestingConnections({ + name: "test", + enabledDrivers: ["postgres"], + entities: [CommentV1, GuestV1], + schemaName: "test-schema", + dropSchemaOnConnection: true, + }); + const [connection2] = await createTestingConnections({ + name: "another", + enabledDrivers: ["postgres"], + entities: [CommentV1, GuestV1], + schemaName: "another-schema", + dropSchemaOnConnection: true + }); + connections = [connection1, connection2]; + }); + after(() => closeTestingConnections(connections)); + it("should not interfere with each other", async () => { + await Promise.all(connections.map(c => c.syncSchema())); + await closeTestingConnections(connections); + const [connection1] = await createTestingConnections({ + name: "test", + enabledDrivers: ["postgres"], + entities: [CommentV2, GuestV2], + schemaName: "test-schema", + dropSchemaOnConnection: false, + schemaCreate: true + }); + const [connection2] = await createTestingConnections({ + name: "another", + enabledDrivers: ["postgres"], + entities: [CommentV2, GuestV2], + schemaName: "another-schema", + dropSchemaOnConnection: false, + schemaCreate: true + }); + connections = [connection1, connection2]; + }); + }); + describe("Can change postgres default schema name", () => { let connections: Connection[]; beforeEach(async () => { - connections = await createTestingConnections({ + const [connection1] = await createTestingConnections({ + name: "test", enabledDrivers: ["postgres"], - entities: [Post], + entities: [CommentV1, GuestV1], schemaName: "test-schema", + dropSchemaOnConnection: true, + }); + const [connection2] = await createTestingConnections({ + name: "another", + enabledDrivers: ["postgres"], + entities: [CommentV1, GuestV1], + schemaName: "another-schema", dropSchemaOnConnection: true }); + connections = [connection1, connection2]; }); afterEach(() => closeTestingConnections(connections)); + it("schema name can be set", () => { return Promise.all(connections.map(async connection => { await connection.syncSchema(true); + const schemaName = (connection.driver as PostgresDriver).schemaName; + const comment = new CommentV1(); + comment.title = "Change SchemaName"; + comment.context = `To ${schemaName}`; - const post = new Post(); - post.title = "ChangeSchemaName"; - - const PostRepo = connection.getRepository(Post); - await PostRepo.persist(post); + const commentRepo = connection.getRepository(CommentV1); + await commentRepo.persist(comment); const query = await connection.driver.createQueryRunner(); - const rows = await query.query(`select * from "test-schema"."post" where id = $1`, [post.id]); - expect(rows[0]["title"]).to.be.eq(post.title); + const rows = await query.query(`select * from "${schemaName}"."comment" where id = $1`, [comment.id]); + expect(rows[0]["context"]).to.be.eq(comment.context); })); }); + }); - + }); diff --git a/test/functional/connection/entity/v1/Comment.ts b/test/functional/connection/entity/v1/Comment.ts new file mode 100644 index 000000000..5d6c5d8f9 --- /dev/null +++ b/test/functional/connection/entity/v1/Comment.ts @@ -0,0 +1,32 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne"; +import {OneToMany} from "../../../../../src/decorator/relations/OneToMany"; +// import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn"; +import {Index} from "../../../../../src/decorator/Index"; +import {Guest} from "./Guest"; + +@Entity() +@Index("author_and_title_unique", ["author", "title"], { unique: true }) +export class Comment { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + @Index() + title: string; + + @Column() + context: string; + + @OneToMany(type => Comment, comment => comment.relay) + reference?: Comment; + + @ManyToOne(type => Comment, comment => comment.reference) + relay?: Comment; + + @ManyToOne(type => Guest, guest => guest.comments) + author: Guest; +} \ No newline at end of file diff --git a/test/functional/connection/entity/v1/Guest.ts b/test/functional/connection/entity/v1/Guest.ts new file mode 100644 index 000000000..da3debacd --- /dev/null +++ b/test/functional/connection/entity/v1/Guest.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 {OneToMany} from "../../../../../src/decorator/relations/OneToMany"; +import {Comment} from "./Comment"; + +@Entity() +export class Guest { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + username: string; + + @OneToMany(type => Comment, comment => comment.author) + comments: Comment[]; +} \ No newline at end of file diff --git a/test/functional/connection/entity/v2/Comment.ts b/test/functional/connection/entity/v2/Comment.ts new file mode 100644 index 000000000..c2a755a03 --- /dev/null +++ b/test/functional/connection/entity/v2/Comment.ts @@ -0,0 +1,24 @@ +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne"; +import {Index} from "../../../../../src/decorator/Index"; +import {Guest} from "./Guest"; + +@Entity() +@Index("author_and_title_unique_rename", ["author", "title", "context"], { unique: true }) +export class Comment { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + @Index() + title: string; + + @Column() + context: string; + + @ManyToOne(type => Guest, guest => guest.comments) + author: Guest; +} \ No newline at end of file diff --git a/test/functional/connection/entity/v2/Guest.ts b/test/functional/connection/entity/v2/Guest.ts new file mode 100644 index 000000000..da3debacd --- /dev/null +++ b/test/functional/connection/entity/v2/Guest.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 {OneToMany} from "../../../../../src/decorator/relations/OneToMany"; +import {Comment} from "./Comment"; + +@Entity() +export class Guest { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + username: string; + + @OneToMany(type => Comment, comment => comment.author) + comments: Comment[]; +} \ No newline at end of file diff --git a/test/github-issues/341/entity/Category.ts b/test/github-issues/341/entity/Category.ts new file mode 100644 index 000000000..c7bffbe21 --- /dev/null +++ b/test/github-issues/341/entity/Category.ts @@ -0,0 +1,19 @@ +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 {OneToOne} from "../../../../src/decorator/relations/OneToOne"; + +@Entity() +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + name: string; + + @OneToOne(type => Post, post => post.category) + post: Post; + +} \ No newline at end of file diff --git a/test/github-issues/341/entity/Post.ts b/test/github-issues/341/entity/Post.ts new file mode 100644 index 000000000..c0a4e0157 --- /dev/null +++ b/test/github-issues/341/entity/Post.ts @@ -0,0 +1,24 @@ +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 {OneToOne} from "../../../../src/decorator/relations/OneToOne"; +import {JoinColumn} from "../../../../src/decorator/relations/JoinColumn"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @Column({ nullable: true }) + categoryName: string; + + @OneToOne(type => Category, category => category.post) + @JoinColumn({ name: "categoryName", referencedColumnName: "name" }) + category: Category; + +} \ No newline at end of file diff --git a/test/github-issues/341/issue-341.ts b/test/github-issues/341/issue-341.ts new file mode 100644 index 000000000..954c40bdb --- /dev/null +++ b/test/github-issues/341/issue-341.ts @@ -0,0 +1,41 @@ +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 > OneToOne relation with referencedColumnName does not work", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("custom join column name and referencedColumnName", () => Promise.all(connections.map(async connection => { + + const category = new Category(); + category.name = "category #1"; + await connection.entityManager.persist(category); + + const post = new Post(); + post.title = "post #1"; + post.category = category; + await connection.entityManager.persist(post); + + const loadedPost = await connection + .entityManager + .createQueryBuilder(Post, "post") + .leftJoinAndSelect("post.category", "category") + .getOne(); + + expect(loadedPost).not.to.be.empty; + expect(loadedPost!.category).not.to.be.empty; + + }))); + +}); diff --git a/test/github-issues/345/entity/Category.ts b/test/github-issues/345/entity/Category.ts new file mode 100644 index 000000000..6dd5d42ea --- /dev/null +++ b/test/github-issues/345/entity/Category.ts @@ -0,0 +1,21 @@ +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"; +import {JoinTable} from "../../../../src/decorator/relations/JoinTable"; + +@Entity() +export class Category { + + @PrimaryGeneratedColumn() + category_id: number; + + @Column() + name: string; + + @ManyToMany(() => Post, post => post.categories) + @JoinTable() + posts: Post[]; + +} \ No newline at end of file diff --git a/test/github-issues/345/entity/Post.ts b/test/github-issues/345/entity/Post.ts new file mode 100644 index 000000000..4979b0c15 --- /dev/null +++ b/test/github-issues/345/entity/Post.ts @@ -0,0 +1,21 @@ +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"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @ManyToMany(() => Category, category => category.posts, { + cascadeInsert: true + }) + categories: Category[]; + +} \ No newline at end of file diff --git a/test/github-issues/345/issue-345.ts b/test/github-issues/345/issue-345.ts new file mode 100644 index 000000000..b05f5306f --- /dev/null +++ b/test/github-issues/345/issue-345.ts @@ -0,0 +1,45 @@ +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 > Join query on ManyToMany relations not working", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("embedded with custom column name should persist and load without errors", () => Promise.all(connections.map(async connection => { + + for (let i = 0; i < 20; i++) { + const category = new Category(); + category.name = "Category #" + i; + await connection.entityManager.persist(category); + } + + const post = new Post(); + post.title = "SuperRace"; + post.categories = [new Category()]; + post.categories[0].name = "SuperCategory"; + await connection.entityManager.persist(post); + + const loadedPost = await connection + .entityManager + .createQueryBuilder(Post, "post") + .leftJoinAndSelect("post.categories", "category") + .where("category.category_id IN (:ids)", { ids: [21] }) + .getOne(); + + expect(loadedPost).not.to.be.empty; + expect(loadedPost!.categories).not.to.be.empty; + + }))); + +}); diff --git a/test/github-issues/363/entity/Car.ts b/test/github-issues/363/entity/Car.ts new file mode 100644 index 000000000..9cee8660e --- /dev/null +++ b/test/github-issues/363/entity/Car.ts @@ -0,0 +1,12 @@ +import { Entity } from "../../../../src/decorator/entity/Entity"; +import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import { Column } from "../../../../src/decorator/columns/Column"; + +@Entity() +export class Car { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; +} \ No newline at end of file diff --git a/test/github-issues/363/entity/Fruit.ts b/test/github-issues/363/entity/Fruit.ts new file mode 100644 index 000000000..738df1f7f --- /dev/null +++ b/test/github-issues/363/entity/Fruit.ts @@ -0,0 +1,12 @@ +import { Entity } from "../../../../src/decorator/entity/Entity"; +import { PrimaryGeneratedColumn } from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import { Column } from "../../../../src/decorator/columns/Column"; + +@Entity() +export class Fruit { + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; +} \ No newline at end of file diff --git a/test/github-issues/363/issue-363.ts b/test/github-issues/363/issue-363.ts new file mode 100644 index 000000000..884938203 --- /dev/null +++ b/test/github-issues/363/issue-363.ts @@ -0,0 +1,76 @@ +import "reflect-metadata"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {expect} from "chai"; +import {Car} from "./entity/Car"; +import {Fruit} from "./entity/Fruit"; + +describe("github issues > #363 Can't save 2 unrelated entity types in a single persist call", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("entityManager should allow you to save unrelated entities with one persist call", () => Promise.all(connections.map(async connection => { + + const car = new Car(); + car.name = "Ferrari"; + + const fruit = new Fruit(); + fruit.name = "Banana"; + + const [savedCar, savedFruit] = await connection.entityManager.persist([car, fruit]); + + expect(savedFruit).to.have.property("name", "Banana"); + expect(savedFruit).to.be.instanceof(Fruit); + + expect(savedCar).to.have.property("name", "Ferrari"); + expect(savedCar).to.be.instanceof(Car); + + const cars = await connection.entityManager.find(Car); + + // before the changes in this PR, all the tests before this one actually passed + expect(cars).to.length(1); + expect(cars[0]).to.have.property("name", "Ferrari"); + + const fruits = await connection.entityManager.find(Fruit); + + expect(fruits).to.length(1); + expect(fruits[0]).to.have.property("name", "Banana"); + + }))); + + it("entityManager should allow you to delete unrelated entities with one remove call", () => Promise.all(connections.map(async connection => { + + const fruit = new Fruit(); + fruit.name = "Banana"; + + const fruit2 = new Fruit(); + fruit2.name = "Apple"; + + const [savedFruit] = await connection.entityManager.persist([fruit, fruit2]); + + const car = new Car(); + car.name = "Ferrari"; + + const savedCar = await connection.entityManager.persist(car); + + await connection.entityManager.remove([savedCar, savedFruit]); + + const cars = await connection.entityManager.find(Car); + + expect(cars).to.length(0); + + const fruits = await connection.entityManager.find(Fruit); + + expect(fruits).to.length(1); + expect(fruits[0]).to.have.property("name", "Apple"); + + }))); + +}); diff --git a/test/other-issues/column-getters/column-getters.ts b/test/other-issues/column-getters/column-getters.ts new file mode 100644 index 000000000..68f273873 --- /dev/null +++ b/test/other-issues/column-getters/column-getters.ts @@ -0,0 +1,39 @@ +import "reflect-metadata"; +import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src/connection/Connection"; +import {Post} from "./entity/Post"; +import {expect} from "chai"; + +describe("other issues > column with getter / setter should work", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [__dirname + "/entity/*{.js,.ts}"], + schemaCreate: true, + dropSchemaOnConnection: true, + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("getters and setters should work correctly", () => Promise.all(connections.map(async connection => { + + const post = new Post(); + post.title = "Super title"; + post.text = "About this post"; + await connection.entityManager.persist(post); + + const loadedPost = await connection + .entityManager + .createQueryBuilder(Post, "post") + .where("post.id = :id", { id: 1 }) + .getOne(); + + expect(loadedPost).not.to.be.empty; + expect(loadedPost!.title).not.to.be.empty; + expect(loadedPost!.text).not.to.be.empty; + loadedPost!.title.should.be.equal("Super title"); + loadedPost!.text.should.be.equal("About this post"); + + }))); + +}); diff --git a/test/other-issues/column-getters/entity/Post.ts b/test/other-issues/column-getters/entity/Post.ts new file mode 100644 index 000000000..9699b4892 --- /dev/null +++ b/test/other-issues/column-getters/entity/Post.ts @@ -0,0 +1,25 @@ +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../src/decorator/columns/Column"; + +@Entity() +export class Post { + + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: "title" }) + private _title: string; + + @Column() + text: string; + + set title(title: string) { + this._title = title; + } + + get title(): string { + return this._title; + } + +} \ No newline at end of file