Merge branch 'master' into query-builder-refactoring

# Conflicts:
#	src/metadata/JoinColumnMetadata.ts
#	src/query-builder/QueryBuilder.ts
#	test/functional/connection/connection.ts
This commit is contained in:
Zotov Dmitry 2017-05-04 10:31:39 +05:00
commit 3a696bcec6
40 changed files with 737 additions and 134 deletions

View File

@ -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)

View File

@ -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/<num>/issue-<num>.ts` where
`<num>` 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 > #<issue number> <issue title>", () => {
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 <put a detailed description of what it should do here>", () => Promise.all(connections.map(async connection => {
// tests go here
})));
// you can add additional tests if needed
});
```
If you place entities in `./entity/<entity-name>.ts` relative to your `issue-<num>.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

201
README.md
View File

@ -1,23 +1,38 @@
# TypeORM
<div align="center">
<a href="https://typeorm.github.io/">
<img src="./resources/logo_big.png" width="492" height="228">
</a>
<br>
<br>
<a href="https://travis-ci.org/typeorm/typeorm">
<img src="https://travis-ci.org/typeorm/typeorm.svg?branch=master">
</a>
<a href="https://badge.fury.io/js/typeorm">
<img src="https://badge.fury.io/js/typeorm.svg">
</a>
<a href="https://david-dm.org/typeorm/typeorm">
<img src="https://david-dm.org/typeorm/typeorm.svg">
</a>
<a href="https://david-dm.org/typeorm/typeorm#info=devDependencies">
<img src="https://david-dm.org/typeorm/typeorm/dev-status.svg">
</a>
<a href="https://gitter.im/typeorm/typeorm?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge">
<img src="https://badges.gitter.im/typeorm/typeorm.svg">
</a>
<br>
<br>
</div>
[![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

View File

@ -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());

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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;
}

View File

@ -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;

View File

@ -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) {

View File

@ -30,4 +30,3 @@ export function CreateDateColumn(options?: ColumnOptions): Function {
getMetadataArgsStorage().columns.add(args);
};
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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.`);
}

View File

@ -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":

View File

@ -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];
}

View File

@ -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":

View File

@ -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":

View File

@ -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) + ")";

View File

@ -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":

View File

@ -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) + ")";

View File

@ -103,7 +103,9 @@ export class EntityManager extends BaseEntityManager {
if (target.length === 0)
return Promise.resolve(target);
return this.getRepository<Entity[]>(target[0].constructor).persist(entity as Entity[], options);
return Promise.all(target.map((t, i) => {
return this.getRepository<Entity>(t.constructor).persist((entity as Entity[])[i], options);
}));
} else {
return this.getRepository<Entity>(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<Entity[]>(target[0].constructor).remove(entity as Entity[], options);
return Promise.all(target.map((t, i) => {
return this.getRepository<Entity>(t.constructor).remove((entity as Entity[])[i], options);
}));
} else {
return this.getRepository<Entity>(target.constructor).remove(entity as Entity, options);
}

View File

@ -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<Entity>(entityClass: ObjectType<Entity>, connectionName: string): Repository<Entity>;
export function getRepository<Entity>(entityClass: ObjectType<Entity>, connectionName?: string): Repository<Entity>;
/**
* Gets repository for the given entity name.
*/
export function getRepository<Entity>(entityName: string, connectionName: string): Repository<Entity>;
export function getRepository<Entity>(entityName: string, connectionName?: string): Repository<Entity>;
/**
* Gets repository for the given entity class or name.
@ -270,12 +272,12 @@ export function getRepository<Entity>(entityClassOrName: ObjectType<Entity>|stri
/**
* Gets tree repository for the given entity class.
*/
export function getTreeRepository<Entity>(entityClass: ObjectType<Entity>, connectionName: string): TreeRepository<Entity>;
export function getTreeRepository<Entity>(entityClass: ObjectType<Entity>, connectionName?: string): TreeRepository<Entity>;
/**
* Gets tree repository for the given entity name.
*/
export function getTreeRepository<Entity>(entityName: string, connectionName: string): TreeRepository<Entity>;
export function getTreeRepository<Entity>(entityName: string, connectionName?: string): TreeRepository<Entity>;
/**
* Gets tree repository for the given entity class or name.
@ -287,12 +289,12 @@ export function getTreeRepository<Entity>(entityClassOrName: ObjectType<Entity>|
/**
* Gets mongodb repository for the given entity class.
*/
export function getMongoRepository<Entity>(entityClass: ObjectType<Entity>, connectionName: string): MongoRepository<Entity>;
export function getMongoRepository<Entity>(entityClass: ObjectType<Entity>, connectionName?: string): MongoRepository<Entity>;
/**
* Gets mongodb repository for the given entity name.
*/
export function getMongoRepository<Entity>(entityName: string, connectionName: string): MongoRepository<Entity>;
export function getMongoRepository<Entity>(entityName: string, connectionName?: string): MongoRepository<Entity>;
/**
* Gets mongodb repository for the given entity class or name.

View File

@ -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;
}
// ---------------------------------------------------------------------

View File

@ -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.`);

View File

@ -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.

View File

@ -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);
}));
});
});
});

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
})));
});

View File

@ -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[];
}

View File

@ -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[];
}

View File

@ -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;
})));
});

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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");
})));
});

View File

@ -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");
})));
});

View File

@ -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;
}
}