Merge pull request #73 from typeorm/persistance-refactoring

Persistance refactoring
This commit is contained in:
Umed Khudoiberdiev 2016-12-06 13:43:27 +05:00 committed by GitHub
commit 95164dcbbd
221 changed files with 9346 additions and 3074 deletions

View File

@ -1,4 +1,29 @@
# 0.0.2 (in development)
# 0.0.3
* completely refactored persistence mechanism:
* added experimental support of `{ nullable: true }` in relations
* cascade operations should work better now
* optimized all queries
* entities with recursive entities should be persisted correctly now
* now `undefined` properties are skipped in the persistence operation, as well as `undefined` relations.
* added platforms abstractions to allow typeorm to work on multiple platforms
* added experimental support of typeorm in the browser
* breaking changes in `QueryBuilder`:
* `getSingleResult()` renamed to `getOne()`
* `getResults()` renamed to `getMany()`
* `getResultsAndCount()` renamed to `getManyAndCount()`
* in the innerJoin*/leftJoin* methods now no need to specify `ON`
* in the innerJoin*/leftJoin* methods no longer supports parameters, use `addParameters` or `setParameter` instead.
* `setParameters` is now works just like `addParameters` (because previous behaviour confused users),
`addParameters` now is deprecated
* `getOne` returns `Promise<Entity|undefined>`
* breaking changes in `Repository` and `EntityManager`:
* `findOne` and `findOneById` now return `Promise<Entity|undefined>` instead of `Promise<Entity>`
* now typeorm is compiled into `ES5` instead of `ES6` - this allows to run it on older versions of node.js
* fixed multiple issues with dates and utc-related stuff
* multiple bugfixes
# 0.0.2
* lot of API refactorings
* complete support TypeScript 2
@ -9,4 +34,4 @@
# 0.0.1
* first stable version, works with TypeScript 1.x
* first stable version, works with TypeScript 1.x

View File

@ -9,9 +9,11 @@ Share this library with friends on twitter and everywhere else you can.
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.3 is released! Most notable changes are in the [changelog](./CHANGELOG.md).
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
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
small applications with a few tables to large scale enterprise applications.
TypeORM helps you to:
@ -31,7 +33,8 @@ maintainable applications with less problems.
The benefit of using TypeORM for the programmer is the ability to focus on
the business logic and worry about persistence only as a secondary problem.
TypeORM is highly influenced by other ORMs, such as [Hibernate](http://hibernate.org/orm/) and [Doctrine](http://www.doctrine-project.org/).
TypeORM is highly influenced by other ORMs, such as [Hibernate](http://hibernate.org/orm/),
[Doctrine](http://www.doctrine-project.org/) and [Entity Framework](https://www.asp.net/entity-framework).
## Installation
@ -65,12 +68,35 @@ TypeORM is highly influenced by other ORMs, such as [Hibernate](http://hibernate
`npm install mssql --save`
* for **Oracle**
* for **Oracle** (experimental)
`npm install oracledb --save`
Install only one of them, depend on which database you use.
To make oracle driver to work you need to follow installation instructions from
[their](https://github.com/oracle/node-oracledb) site.
#### TypeScript configuration
Also make sure you are using TypeScript compiler version > **2.1** and you have enabled following settings in `tsconfig.json`:
```json
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
```
#### Node.js version
TypeORM was tested with Node.JS version 6 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
TypeORM works in the browser and has experimental support of WebSQL.
If you want to use TypeORM in the browser then you need to `npm i typeorm-browser` instead of `typeorm`.
More information about it in [this page](https://typeorm.github.io/usage-in-browser.html).
Also take a look on [this sample](https://github.com/typeorm/browser-example).
## Quick Start
@ -297,7 +323,7 @@ createConnection({
autoSchemaSync: true,
}).then(connection => {
// 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.
@ -334,7 +360,7 @@ createConnection({
autoSchemaSync: true,
}).then(connection => {
// here you can start to work with your entities
});
}).catch(error => console.log(error));
```
### Run the application
@ -379,7 +405,7 @@ createConnection(/*...*/).then(connection => {
console.log("Photo has been saved");
});
});
}).catch(error => console.log(error));
```
### Using async/await syntax
@ -402,7 +428,7 @@ createConnection(/*...*/).then(async connection => {
await connection.entityManager.persist(photo);
console.log("Photo has been saved");
});
}).catch(error => console.log(error));
```
### Using Entity Manager
@ -422,7 +448,7 @@ createConnection(/*...*/).then(async connection => {
let savedPhotos = await connection.entityManager.find(Photo);
console.log("All photos from the db: ", savedPhotos);
});
}).catch(error => console.log(error));
```
savedPhotos will be an array of Photo objects with the data loaded from the database.
@ -455,7 +481,7 @@ createConnection(/*...*/).then(async connection => {
let savedPhotos = await photoRepository.find();
console.log("All photos from the db: ", savedPhotos);
});
}).catch(error => console.log(error));
```
### Loading photos from the database
@ -488,7 +514,7 @@ createConnection(/*...*/).then(async connection => {
console.log("All photos: ", allPublishedPhotos);
console.log("Photos count: ", allPublishedPhotos);
});
}).catch(error => console.log(error));
```
### Updating photo in the database
@ -506,7 +532,7 @@ createConnection(/*...*/).then(async connection => {
photoToUpdate.name = "Me, my friends and polar bears";
await photoRepository.persist(photoToUpdate);
});
}).catch(error => console.log(error));
```
Now photo with `id = 1` will be updated in the database.
@ -526,7 +552,7 @@ createConnection(/*...*/).then(async connection => {
let photoToRemove = await photoRepository.findOneById(1);
await photoRepository.remove(photoToRemove);
});
}).catch(error => console.log(error));
```
Now photo with `id = 1` will be removed from the database.
@ -633,7 +659,8 @@ createConnection(/*...*/).then(async connection => {
// 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
@ -707,7 +734,8 @@ createConnection(/*...*/).then(async connection => {
}
});
});
}).catch(error => console.log(error));
```
Here photos will contain array of photos from the database, and each photo will contain its photo metadata.
@ -731,10 +759,11 @@ createConnection(/*...*/).then(async connection => {
/*...*/
let photoRepository = connection.getRepository(Photo);
let photos = await photoRepository.createQueryBuilder("photo")
.innerJoinAndSelect("photo.metadata")
.getResults();
.innerJoinAndSelect("photo.metadata", "metadata")
.getMany();
});
}).catch(error => console.log(error));
```
### using cascade options to automatically save related objects
@ -780,7 +809,8 @@ createConnection(options).then(async connection => {
metadata.compressed = true;
metadata.comment = "cybershoot";
metadata.orientation = "portait";
metadata.photo = photo; // this way we connect them
photo.metadata = metadata; // this way we connect them
// get repository
let photoRepository = connection.getRepository(Photo);
@ -789,7 +819,8 @@ createConnection(options).then(async connection => {
await photoRepository.persist(photo);
console.log("Photo is saved, photo metadata is saved too.")
});
}).catch(error => console.log(error));
```
### creating a many-to-one / one-to-many relation
@ -931,7 +962,7 @@ const options: CreateConnectionOptions = {
};
```
Now lets insert author and photo to our database:
Now lets insert albums and photos to our database:
```typescript
let connection = await createConnection(options);
@ -947,18 +978,20 @@ album2.name = "Me";
let photo1 = new Photo();
photo1.name = "Me and Bears";
photo1.description = "I am near polar bears";
photo1.filename = "photo-with-bears.jpg"
photo1.filename = "photo-with-bears.jpg";
photo1.albums.push(album1);
let photo2 = new Photo();
photo2.name = "Me and Bears";
photo2.description = "I am near polar bears";
photo2.filename = "photo-with-bears.jpg"
photo2.filename = "photo-with-bears.jpg";
photo2.albums.push(album2);
// get entity repository
let photoRepository = connection.getRepository(Photo);
// first save a first photo
// we only save a photos, albums are persisted
// we only save the photos, albums are persisted
// automatically because of cascade options
await photoRepository.persist(photo1);
@ -976,15 +1009,15 @@ You can use QueryBuilder to build even more complex queries. For example you can
let photoRepository = connection.getRepository(Photo);
let photos = await photoRepository
.createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.
.innerJoinAndSelect("photo.metadata")
.leftJoinAndSelect("photo.albums")
.innerJoinAndSelect("photo.metadata", "metadata")
.leftJoinAndSelect("photo.albums", "albums")
.where("photo.isPublished=true")
.andWhere("(photo.name=:photoName OR photo.name=:bearName)")
.orderBy("photo.id", "DESC")
.setFirstResult(5)
.setMaxResults(10)
.setParameters({ photoName: "My", bearName: "Mishka" })
.getResults();
.getMany();
```
This query builder will select you all photos that are published and whose name is "My" or "Mishka",
@ -1010,6 +1043,7 @@ You'll use query builder in your application a lot. Learn more about QueryBuilde
* [Subscribers and entity listeners](https://typeorm.github.io/subscribers-and-entity-listeners.html)
* [Using service container](https://typeorm.github.io/using-service-container.html)
* [Decorators Reference](https://typeorm.github.io/decorators-reference.html)
* [Usage in the browser](https://typeorm.github.io/usage-in-browser.html)
## Samples

1015
README_BROWSER.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,14 +4,17 @@ const gulp = require("gulp");
const del = require("del");
const shell = require("gulp-shell");
const replace = require("gulp-replace");
const rename = require("gulp-rename");
const file = require("gulp-file");
const uglify = require("gulp-uglify");
const mocha = require("gulp-mocha");
const chai = require("chai");
const tslint = require("gulp-tslint");
const stylish = require("tslint-stylish");
const ts = require("gulp-typescript");
const sourcemaps = require("gulp-sourcemaps");
const istanbul = require("gulp-istanbul");
const remapIstanbul = require("remap-istanbul/lib/gulpRemapIstanbul");
const ts = require("gulp-typescript");
@Gulpclass()
export class Gulpfile {
@ -38,14 +41,132 @@ export class Gulpfile {
}
// -------------------------------------------------------------------------
// Packaging and Publishing tasks
// Build and packaging for browser
// -------------------------------------------------------------------------
/**
* Copies all source files into destination folder in a correct structure.
*/
@Task()
browserCopySources() {
return gulp.src([
"./src/**/*.ts",
"!./src/commands/*.ts",
"!./src/cli.ts",
"!./src/typeorm.ts",
"!./src/decorators-shim.ts",
"!./src/platform/PlatformTools.ts",
"!./src/platform/BrowserPlatformTools.ts"
])
.pipe(gulp.dest("./build/browser/typeorm"));
}
/**
* Creates special main file for browser build.
*/
@Task()
browserCopyMainBrowserFile() {
return gulp.src("./package.json", { read: false })
.pipe(file("typeorm.ts", `export * from "./typeorm/index";`))
.pipe(gulp.dest("./build/browser"));
}
/**
* Replaces PlatformTools with browser-specific implementation called BrowserPlatformTools.
*/
@Task()
browserCopyPlatformTools() {
return gulp.src("./src/platform/BrowserPlatformTools.ts")
.pipe(rename("PlatformTools.ts"))
.pipe(gulp.dest("./build/browser/typeorm/platform"));
}
/**
* Runs files compilation of browser-specific source code.
*/
@MergedTask()
browserCompile() {
const tsProject = ts.createProject("tsconfig.json", {
outFile: "typeorm-browser.js",
module: "system",
typescript: require("typescript")
});
const tsResult = gulp.src(["./build/browser/**/*.ts", "./node_modules/@types/**/*.ts"])
.pipe(sourcemaps.init())
.pipe(tsProject());
return [
tsResult.dts.pipe(gulp.dest("./build/browser-package")),
tsResult.js
.pipe(sourcemaps.write(".", { sourceRoot: "", includeContent: true }))
.pipe(gulp.dest("./build/browser-package"))
];
}
/**
* Uglifys all code.
*/
@Task()
browserUglify() {
return gulp.src("./build/browser-package/*.js")
.pipe(uglify())
.pipe(rename("typeorm-browser.min.js"))
.pipe(gulp.dest("./build/browser-package"));
}
/**
* Copies README_BROWSER.md into README.md for the typeorm-browser package.
*/
@Task()
browserCopyReadmeFile() {
return gulp.src("./README_BROWSER.md")
.pipe(rename("README.md"))
.pipe(gulp.dest("./build/browser-package"));
}
/**
* Copies package_browser.json into package.json for the typeorm-browser package.
*/
@Task()
browserCopyPackageJsonFile() {
return gulp.src("./package_browser.json")
.pipe(rename("package.json"))
.pipe(replace("\"private\": true,", "\"private\": false,"))
.pipe(gulp.dest("./build/browser-package"));
}
/**
* Runs all tasks for the browser build and package.
*/
@SequenceTask()
browserPackage() {
return [
["browserCopySources", "browserCopyMainBrowserFile", "browserCopyPlatformTools"],
"browserCompile",
["browserCopyReadmeFile", "browserUglify", "browserCopyPackageJsonFile"]
];
}
/**
* Publishes a browser package.
*/
@Task()
browserPublish() {
return gulp.src("*.js", { read: false })
.pipe(shell([
"cd ./build/browser-package && npm publish"
]));
}
// -------------------------------------------------------------------------
// Main Packaging and Publishing tasks
// -------------------------------------------------------------------------
/**
* Publishes a package to npm from ./build/package directory.
*/
@Task()
npmPublish() {
nodePublish() {
return gulp.src("*.js", { read: false })
.pipe(shell([
"cd ./build/package && npm publish"
@ -60,7 +181,7 @@ export class Gulpfile {
const tsProject = ts.createProject("tsconfig.json", { typescript: require("typescript") });
const tsResult = gulp.src(["./src/**/*.ts", "./node_modules/@types/**/*.ts"])
.pipe(sourcemaps.init())
.pipe(ts(tsProject));
.pipe(tsProject());
return [
tsResult.dts.pipe(gulp.dest("./build/package")),
@ -79,6 +200,17 @@ export class Gulpfile {
.pipe(gulp.dest("./build/package"));
}
/**
* Removes /// <reference from compiled sources.
*/
@Task()
packageReplaceReferences() {
return gulp.src("./build/package/**/*.d.ts")
.pipe(replace(`/// <reference types="node" />`, ""))
.pipe(replace(`/// <reference types="chai" />`, ""))
.pipe(gulp.dest("./build/package"));
}
/**
* Moves all compiled files to the final package directory.
*/
@ -100,23 +232,15 @@ export class Gulpfile {
}
/**
* This task will replace all typescript code blocks in the README (since npm does not support typescript syntax
* highlighting) and copy this README file into the package folder.
* Creates a package that can be published to npm.
*/
@Task()
packageReadmeFile() {
return gulp.src("./README.md")
.pipe(replace(/```typescript([\s\S]*?)```/g, "```javascript$1```"))
.pipe(gulp.dest("./build/package"));
}
/**
* This task will copy typings.json file to the build package.
*/
@Task()
copyTypingsFile() {
return gulp.src("./typings.json")
.pipe(gulp.dest("./build/package"));
@SequenceTask()
nodePackage() {
return [
"packageCompile",
"packageMoveCompiledFiles",
["packageClearCompileDirectory", "packageReplaceReferences", "packagePreparePackageFile"],
];
}
/**
@ -126,10 +250,7 @@ export class Gulpfile {
package() {
return [
"clean",
"packageCompile",
"packageMoveCompiledFiles",
"packageClearCompileDirectory",
["packagePreparePackageFile", "packageReadmeFile", "copyTypingsFile"]
["nodePackage", "browserPackage"]
];
}
@ -138,7 +259,7 @@ export class Gulpfile {
*/
@SequenceTask()
publish() {
return ["package", "npmPublish"];
return ["package", "nodePublish", "browserPublish"];
}
// -------------------------------------------------------------------------
@ -195,7 +316,7 @@ export class Gulpfile {
*/
@SequenceTask()
tests() {
return [/*"compile", */"tslint", "coveragePost", "coverageRemap"];
return ["compile", "tslint", "coveragePost", "coverageRemap"];
}
}

View File

@ -1,8 +1,8 @@
{
"name": "typeorm",
"private": true,
"version": "0.0.2",
"description": "Data-mapper ORM for Typescript",
"version": "0.0.3",
"description": "Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases.",
"license": "MIT",
"readmeFilename": "README.md",
"author": {
@ -21,56 +21,64 @@
"typescript",
"typescript-orm",
"mysql",
"mysql-orm"
"mysql-orm",
"postgresql",
"postgresql-orm",
"mariadb",
"mariadb-orm",
"sqlite",
"sqlite-orm",
"sql-server",
"sql-server-orm",
"oracle",
"oracle-orm",
"websql",
"websql-orm"
],
"devDependencies": {
"@types/chai": "^3.4.30",
"@types/chai-as-promised": "0.0.29",
"@types/mocha": "^2.2.32",
"@types/mocha": "^2.2.33",
"@types/promises-a-plus": "0.0.27",
"@types/sinon": "^1.16.30",
"@types/sinon": "^1.16.32",
"chai": "^3.4.1",
"chai-as-promised": "^5.3.0",
"chai-as-promised": "^6.0.0",
"del": "^2.2.2",
"gulp": "^3.9.1",
"gulp-file": "^0.3.0",
"gulp-istanbul": "^1.1.1",
"gulp-mocha": "^3.0.1",
"gulp-rename": "^1.2.2",
"gulp-replace": "^0.5.4",
"gulp-shell": "^0.5.1",
"gulp-sourcemaps": "^1.6.0",
"gulp-tslint": "^6.1.1",
"gulp-typescript": "^2.14.0",
"gulp-sourcemaps": "^1.9.1",
"gulp-tslint": "^7.0.1",
"gulp-typescript": "^3.1.3",
"gulp-uglify": "^2.0.0",
"gulpclass": "0.1.1",
"mariasql": "^0.2.6",
"mocha": "^3.0.2",
"mocha": "^3.2.0",
"mssql": "^3.3.0",
"mysql": "^2.11.1",
"mysql2": "^1.1.0",
"mysql": "^2.12.0",
"mysql2": "^1.1.2",
"oracledb": "^1.11.0",
"pg": "^6.1.0",
"remap-istanbul": "^0.6.4",
"remap-istanbul": "^0.7.0",
"sinon": "^1.17.6",
"sinon-chai": "^2.8.0",
"sqlite3": "^3.1.4",
"ts-node": "^1.3.0",
"tslint": "^3.15.0-dev.0",
"sqlite3": "^3.1.8",
"ts-node": "^1.7.0",
"tslint": "^4.0.2",
"tslint-stylish": "^2.1.0-beta",
"typescript": "^2.1.0-dev.20160919"
"typescript": "^2.1.1"
},
"dependencies": {
"@types/lodash": "4.14.35",
"@types/node": "^6.0.39",
"@types/node": "^6.0.51",
"app-root-path": "^2.0.1",
"glob": "^7.1.0",
"lodash": "^4.16.1",
"moment": "^2.15.0",
"path": "^0.12.7",
"glob": "^7.1.1",
"reflect-metadata": "^0.1.8",
"require-all": "^2.0.0",
"sha1": "^1.1.1",
"url": "^0.11.0",
"yargonaut": "^1.1.2",
"yargs": "^5.0.0"
"yargs": "^6.5.0"
},
"scripts": {
"test": "node_modules/.bin/gulp tests"

45
package_browser.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "typeorm-browser",
"private": true,
"version": "0.0.3",
"description": "Data-Mapper ORM for TypeScript, ES7, ES6, ES5. Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, WebSQL databases.",
"license": "MIT",
"readmeFilename": "README.md",
"author": {
"name": "Umed Khudoiberdiev",
"email": "pleerock.me@gmail.com"
},
"repository": {
"type": "git",
"url": "https://github.com/typeorm/typeorm.git"
},
"bugs": {
"url": "https://github.com/typeorm/typeorm/issues"
},
"tags": [
"orm",
"typescript",
"typescript-orm",
"mysql",
"mysql-orm",
"postgresql",
"postgresql-orm",
"mariadb",
"mariadb-orm",
"sqlite",
"sqlite-orm",
"sql-server",
"sql-server-orm",
"oracle",
"oracle-orm",
"websql",
"websql-orm"
],
"devDependencies": {
},
"dependencies": {
},
"scripts": {
"test": "node_modules/.bin/gulp tests"
}
}

View File

@ -1,17 +1,17 @@
import * as _ from "lodash";
import {NamingStrategyInterface} from "../../../src/naming-strategy/NamingStrategyInterface";
import {NamingStrategy} from "../../../src/decorator/NamingStrategy";
import {DefaultNamingStrategy} from "../../../src/naming-strategy/DefaultNamingStrategy";
import {snakeCase} from "../../../src/util/StringUtils";
@NamingStrategy("custom_strategy")
export class CustomNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
tableName(className: string, customName: string): string {
return customName ? customName : _.snakeCase(className);
return customName ? customName : snakeCase(className);
}
columnName(propertyName: string, customName: string): string {
return customName ? customName : _.snakeCase(propertyName);
return customName ? customName : snakeCase(propertyName);
}
columnNameCustomized(customName: string): string {
@ -19,7 +19,7 @@ export class CustomNamingStrategy extends DefaultNamingStrategy implements Namin
}
relationName(propertyName: string): string {
return _.snakeCase(propertyName);
return snakeCase(propertyName);
}
}

View File

@ -46,7 +46,7 @@ createConnection(options).then(connection => {
.createQueryBuilder("post")
.where("post.title=:keyword")
.setParameter("keyword", "hello")
.getResults();
.getMany();
})
.then(post => {
console.log("Loaded post: ", post);

View File

@ -50,7 +50,7 @@ export class Post {
cascadeRemove: true
})
@JoinColumn()
metadata: PostMetadata|undefined;
metadata: PostMetadata|null;
// post has relation with details. full cascades here
@OneToOne(type => PostInformation, information => information.post, {

View File

@ -60,15 +60,15 @@ createConnection(options).then(connection => {
return postRepository
.createQueryBuilder("post")
.leftJoinAndMapMany("post.superCategories", "post.categories", "categories")
.leftJoinAndMapOne("post.author", Author, "author", "ON", "author.id=post.authorId")
.getResults();
.leftJoinAndMapOne("post.author", Author, "author", "author.id=post.authorId")
.getMany();
}).then(posts => {
console.log("Loaded posts: ", posts);
return entityManager
.createQueryBuilder(Author, "author")
.getResults();
.getMany();
}).then(authors => {
console.log("Loaded authors: ", authors);

View File

@ -51,7 +51,7 @@ createConnection(options).then(connection => {
.leftJoin("post.categories", "categories")
.leftJoin("categories.author", "author")
.where("post.id=1")
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log("loadedPosts: ", loadedPost);
@ -71,7 +71,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("post.author", "author")
.leftJoinAndSelect("post.categories", "categories")
.where("post.id=:id", { id: post.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log(loadedPost);
@ -88,7 +88,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("post.author", "author")
.leftJoinAndSelect("post.categories", "categories")
.where("post.id=:id", { id: post.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log(loadedPost);
@ -102,7 +102,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("post.author", "author")
.leftJoinAndSelect("post.categories", "categories")
.where("post.id=:id", { id: post.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log(loadedPost);
@ -116,7 +116,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("post.author", "author")
.leftJoinAndSelect("post.categories", "categories")
.where("post.id=:id", { id: post.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log(loadedPost);

View File

@ -48,7 +48,7 @@ createConnection(options).then(async connection => {
console.log("employee has been updated: ", employee);
console.log("now loading the employee: ");
const loadedEmployee = await employeeRepository.findOneById(1);
const loadedEmployee = (await employeeRepository.findOneById(1))!;
console.log("loaded employee: ", loadedEmployee);
loadedEmployee.firstName = "dima";

View File

@ -45,7 +45,7 @@ export class Post {
@ManyToOne(type => PostMetadata, metadata => metadata.posts, {
cascadeRemove: true
})
metadata: PostMetadata|undefined;
metadata: PostMetadata|null;
// post has relation with details. full cascades here
@ManyToOne(type => PostInformation, information => information.posts, {

View File

@ -27,6 +27,6 @@ export class PostDetails {
cascadeUpdate: true,
cascadeRemove: true
})
posts: Post[] = [];
posts: Post[];
}

View File

@ -26,7 +26,7 @@ export class Post {
cascadeRemove: true
})
@JoinTable()
categories: PostCategory[] = [];
categories: PostCategory[];
// post has relation with details. cascade inserts here means if new PostDetails instance will be set to this
// relation it will be inserted automatically to the db when you save this Post entity
@ -34,7 +34,7 @@ export class Post {
cascadeInsert: true
})
@JoinTable()
details: PostDetails[] = [];
details: PostDetails[];
// post has relation with details. cascade update here means if new PostDetail instance will be set to this relation
// it will be inserted automatically to the db when you save this Post entity
@ -42,7 +42,7 @@ export class Post {
cascadeUpdate: true
})
@JoinTable()
images: PostImage[] = [];
images: PostImage[];
// post has relation with details. cascade update here means if new PostDetail instance will be set to this relation
// it will be inserted automatically to the db when you save this Post entity
@ -50,7 +50,7 @@ export class Post {
cascadeRemove: true
})
@JoinTable()
metadatas: PostMetadata[] = [];
metadatas: PostMetadata[];
// post has relation with details. full cascades here
@ManyToMany(type => PostInformation, information => information.posts, {
@ -59,11 +59,11 @@ export class Post {
cascadeRemove: true
})
@JoinTable()
informations: PostInformation[] = [];
informations: PostInformation[];
// post has relation with details. not cascades here. means cannot be persisted, updated or removed
@ManyToMany(type => PostAuthor, author => author.posts)
@JoinTable()
authors: PostAuthor[] = [];
authors: PostAuthor[];
}

View File

@ -27,6 +27,6 @@ export class PostDetails {
cascadeUpdate: true,
cascadeRemove: true
})
posts: Post[] = [];
posts: Post[];
}

View File

@ -53,7 +53,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("p.author", "author")
.leftJoinAndSelect("p.categories", "categories")
.where("p.id = :id", { id: loadedPost.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log("---------------------------");

View File

@ -52,7 +52,7 @@ createConnection(options).then(connection => {
Promise.all(posts.map(post => postRepository.persist(post)))
.then(savedPosts => {
console.log("Posts has been saved. Lets try to load some posts");
return qb.getResults();
return qb.getMany();
})
.then(loadedPost => {
console.log("post loaded: ", loadedPost);

View File

@ -52,7 +52,7 @@ createConnection(options).then(connection => {
.leftJoinAndSelect("p.author", "author")
.leftJoinAndSelect("p.categories", "categories")
.where("p.id = :id", { id: loadedPost.id })
.getSingleResult();
.getOne();
})
.then(loadedPost => {
console.log("load finished. Now lets update entity");

View File

@ -1,4 +1,6 @@
import {createConnection} from "../index";
import {QueryRunner} from "../query-runner/QueryRunner";
import {Connection} from "../connection/Connection";
/**
* Executes an sql query on the given connection.
@ -17,18 +19,24 @@ export class QueryCommand {
}
async handler(argv: any) {
let connection: Connection|undefined = undefined,
queryRunner: QueryRunner|undefined = undefined;
try {
process.env.SKIP_SCHEMA_CREATION = true;
const connectionName = "default" || argv.connection;
const connection = await createConnection(connectionName);
const queryRunner = await connection.driver.createQueryRunner();
connection = await createConnection("default" || argv.connection);
queryRunner = await connection.driver.createQueryRunner();
const queryResult = await queryRunner.query(argv._[1]);
console.log("Query executed. Result: ", queryResult);
await queryRunner.release();
await connection.close();
} catch (err) {
console.log(err);
console.error(err);
throw err;
} finally {
if (queryRunner)
await queryRunner.release();
if (connection)
await connection.close();
}
}
}

View File

@ -1,4 +1,5 @@
import {createConnections, createConnection} from "../index";
import {Connection} from "../connection/Connection";
/**
* Drops all tables of the database from the given connection.
@ -17,21 +18,29 @@ export class SchemaDropCommand {
}
async handler(argv: any) {
let connection: Connection|undefined = undefined, connections: Connection[] = [];
try {
process.env.LOGGER_CLI_SCHEMA_SYNC = true;
process.env.SKIP_SCHEMA_CREATION = true;
if (argv.connection) {
const connection = await createConnection(argv.connection);
connection = await createConnection(argv.connection);
await connection.dropDatabase();
await connection.close();
} else {
const connections = await createConnections();
connections = await createConnections();
await Promise.all(connections.map(connection => connection.dropDatabase()));
await Promise.all(connections.map(connection => connection.close()));
}
} catch (err) {
console.log(err);
console.error(err);
throw err;
} finally {
if (connection)
await connection.close();
await Promise.all(connections.map(connection => connection.close()));
}
}
}

View File

@ -1,4 +1,5 @@
import {createConnections, createConnection} from "../index";
import {Connection} from "../connection/Connection";
/**
* Synchronizes database schema with entities.
@ -17,21 +18,29 @@ export class SchemaSyncCommand {
}
async handler(argv: any) {
let connection: Connection|undefined = undefined, connections: Connection[] = [];
try {
process.env.LOGGER_CLI_SCHEMA_SYNC = true;
process.env.SKIP_SCHEMA_CREATION = true;
if (argv.connection) {
const connection = await createConnection(argv.connection);
connection = await createConnection(argv.connection);
await connection.syncSchema(false);
await connection.close();
} else {
const connections = await createConnections();
connections = await createConnections();
await Promise.all(connections.map(connection => connection.syncSchema(false)));
await Promise.all(connections.map(connection => connection.close()));
}
} catch (err) {
console.log(err);
console.error(err);
throw err;
} finally {
if (connection)
await connection.close();
await Promise.all(connections.map(connection => connection.close()));
}
}
}

View File

@ -9,8 +9,6 @@ import {importClassesFromDirectories, importJsonsFromDirectories} from "../util/
import {getMetadataArgsStorage, getFromContainer} from "../index";
import {EntityMetadataBuilder} from "../metadata-builder/EntityMetadataBuilder";
import {DefaultNamingStrategy} from "../naming-strategy/DefaultNamingStrategy";
import {EntityMetadataCollection} from "../metadata-args/collection/EntityMetadataCollection";
import {NoConnectionForRepositoryError} from "./error/NoConnectionForRepositoryError";
import {CannotImportAlreadyConnectedError} from "./error/CannotImportAlreadyConnectedError";
import {CannotCloseNotConnectedError} from "./error/CannotCloseNotConnectedError";
import {CannotConnectAlreadyConnectedError} from "./error/CannotConnectAlreadyConnectedError";
@ -22,7 +20,6 @@ import {EntitySchema} from "../entity-schema/EntitySchema";
import {CannotSyncNotConnectedError} from "./error/CannotSyncNotConnectedError";
import {CannotUseNamingStrategyNotConnectedError} from "./error/CannotUseNamingStrategyNotConnectedError";
import {Broadcaster} from "../subscriber/Broadcaster";
import {CannotGetEntityManagerNotConnectedError} from "./error/CannotGetEntityManagerNotConnectedError";
import {LazyRelationsWrapper} from "../lazy-loading/LazyRelationsWrapper";
import {SpecificRepository} from "../repository/SpecificRepository";
import {RepositoryAggregator} from "../repository/RepositoryAggregator";
@ -30,6 +27,7 @@ import {EntityMetadata} from "../metadata/EntityMetadata";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {Logger} from "../logger/Logger";
import {QueryRunnerProvider} from "../query-runner/QueryRunnerProvider";
import {EntityMetadataNotFound} from "../metadata-args/error/EntityMetadataNotFound";
/**
* Connection is a single database connection to a specific database of a database management system.
@ -59,7 +57,7 @@ export class Connection {
/**
* All entity metadatas that are registered for this connection.
*/
public readonly entityMetadatas = new EntityMetadataCollection();
public readonly entityMetadatas: EntityMetadata[] = [];
/**
* Used to broadcast connection events.
@ -147,9 +145,9 @@ export class Connection {
* Gets entity manager that allows to perform repository operations with any entity in this connection.
*/
get entityManager() {
if (!this.isConnected)
throw new CannotGetEntityManagerNotConnectedError(this.name);
// if (!this.isConnected)
// throw new CannotGetEntityManagerNotConnectedError(this.name);
return this._entityManager;
}
@ -167,12 +165,21 @@ export class Connection {
// connect to the database via its driver
await this.driver.connect();
// build all metadatas registered in the current connection
this.buildMetadatas();
// set connected status for the current connection
this._isConnected = true;
// build all metadatas registered in the current connection
try {
this.buildMetadatas();
} catch (error) {
// if for some reason build metadata fail (for example validation error during entity metadata check)
// connection needs to be closed
await this.close();
throw error;
}
return this;
}
@ -194,17 +201,7 @@ export class Connection {
*/
async dropDatabase(): Promise<void> {
const queryRunner = await this.driver.createQueryRunner();
await queryRunner.beginTransaction();
try {
await queryRunner.clearDatabase();
await queryRunner.commitTransaction();
await queryRunner.release();
} catch (error) {
await queryRunner.rollbackTransaction();
await queryRunner.release();
throw error;
}
await queryRunner.clearDatabase();
}
/**
@ -326,18 +323,27 @@ export class Connection {
/**
* Gets the entity metadata of the given entity class.
*/
getMetadata(entity: Function): EntityMetadata;
getMetadata(target: Function): EntityMetadata;
/**
* Gets the entity metadata of the given entity name.
*/
getMetadata(entity: string): EntityMetadata;
getMetadata(target: string): EntityMetadata;
/**
* Gets the entity metadata of the given entity class or schema name.
*/
getMetadata(target: Function|string): EntityMetadata;
/**
Gets entity metadata for the given entity class or schema name.
*/
getMetadata(entity: Function|string): EntityMetadata {
return this.entityMetadatas.findByTarget(entity);
getMetadata(target: Function|string): EntityMetadata {
const metadata = this.entityMetadatas.find(metadata => metadata.target === target || (typeof target === "string" && metadata.targetName === target));
if (!metadata)
throw new EntityMetadataNotFound(target);
return metadata;
}
/**
@ -350,6 +356,11 @@ export class Connection {
*/
getRepository<Entity>(entityName: string): Repository<Entity>;
/**
* Gets repository for the given entity name.
*/
getRepository<Entity>(entityClassOrName: ObjectType<Entity>|string): Repository<Entity>;
/**
* Gets repository for the given entity class or name.
*/
@ -395,6 +406,12 @@ export class Connection {
*/
getSpecificRepository<Entity>(entityName: string): SpecificRepository<Entity>;
/**
* Gets specific repository for the given entity class or name.
* SpecificRepository is a special repository that contains specific and non standard repository methods.
*/
getSpecificRepository<Entity>(entityClassOrName: ObjectType<Entity>|string): SpecificRepository<Entity>;
/**
* Gets specific repository for the given entity class or name.
* SpecificRepository is a special repository that contains specific and non standard repository methods.
@ -421,13 +438,13 @@ export class Connection {
* Finds repository aggregator of the given entity class or name.
*/
protected findRepositoryAggregator(entityClassOrName: ObjectType<any>|string): RepositoryAggregator {
if (!this.isConnected)
throw new NoConnectionForRepositoryError(this.name);
// if (!this.isConnected)
// throw new NoConnectionForRepositoryError(this.name);
if (!this.entityMetadatas.hasTarget(entityClassOrName))
if (!this.entityMetadatas.find(metadata => metadata.target === entityClassOrName || (typeof entityClassOrName === "string" && metadata.targetName === entityClassOrName)))
throw new RepositoryNotFoundError(this.name, entityClassOrName);
const metadata = this.entityMetadatas.findByTarget(entityClassOrName);
const metadata = this.getMetadata(entityClassOrName);
const repositoryAggregator = this.repositoryAggregators.find(repositoryAggregate => repositoryAggregate.metadata === metadata);
if (!repositoryAggregator)
throw new RepositoryNotFoundError(this.name, entityClassOrName);
@ -453,6 +470,7 @@ export class Connection {
getMetadataArgsStorage()
.entitySubscribers
.filterByTargets(this.subscriberClasses)
.toArray()
.map(metadata => getFromContainer(metadata.target))
.forEach(subscriber => this.entitySubscribers.push(subscriber));
}
@ -462,9 +480,10 @@ export class Connection {
getMetadataArgsStorage()
.entityListeners
.filterByTargets(this.entityClasses)
.toArray()
.forEach(metadata => this.entityListeners.push(new EntityListenerMetadata(metadata)));
}
// build entity metadatas from metadata args storage (collected from decorators)
if (this.entityClasses && this.entityClasses.length) {
getFromContainer(EntityMetadataBuilder)
@ -490,15 +509,16 @@ export class Connection {
* Creates a naming strategy to be used for this connection.
*/
protected createNamingStrategy(): NamingStrategyInterface {
// if naming strategies are not loaded, or used naming strategy is not set then use default naming strategy
if (!this.namingStrategyClasses || !this.namingStrategyClasses.length || !this.usedNamingStrategy)
return getFromContainer(DefaultNamingStrategy);
// try to find used naming strategy in the list of loaded naming strategies
const namingMetadata = getMetadataArgsStorage()
.namingStrategies
.filterByTargets(this.namingStrategyClasses)
.toArray()
.find(strategy => {
if (typeof this.usedNamingStrategy === "string") {
return strategy.name === this.usedNamingStrategy;
@ -506,7 +526,7 @@ export class Connection {
return strategy.target === this.usedNamingStrategy;
}
});
// throw an error if not found
if (!namingMetadata)
throw new NamingStrategyNotFoundError(this.usedNamingStrategy, this.name);
@ -526,7 +546,7 @@ export class Connection {
* Creates a new entity broadcaster using in this connection.
*/
protected createBroadcaster() {
return new Broadcaster(this.entityMetadatas, this.entitySubscribers, this.entityListeners);
return new Broadcaster(this, this.entitySubscribers, this.entityListeners);
}
/**

View File

@ -1,4 +1,3 @@
import * as fs from "fs";
import {Connection} from "./Connection";
import {ConnectionNotFoundError} from "./error/ConnectionNotFoundError";
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
@ -14,6 +13,8 @@ import {OracleDriver} from "../driver/oracle/OracleDriver";
import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
import {OrmUtils} from "../util/OrmUtils";
import {CannotDetermineConnectionOptionsError} from "./error/CannotDetermineConnectionOptionsError";
import {PlatformTools} from "../platform/PlatformTools";
import {WebsqlDriver} from "../driver/websql/WebsqlDriver";
/**
* ConnectionManager is used to store and manage all these different connections.
@ -29,7 +30,7 @@ export class ConnectionManager {
* List of connections registered in this connection manager.
*/
protected connections: Connection[] = [];
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -118,7 +119,7 @@ export class ConnectionManager {
* it will try to create connection from environment variables.
* There are several environment variables you can set:
*
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "mysql2", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_URL - database connection url. Should be a string.
* - TYPEORM_HOST - database host. Should be a string.
* - TYPEORM_PORT - database access port. Should be a number.
@ -186,7 +187,7 @@ export class ConnectionManager {
* it will try to create connection from environment variables.
* There are several environment variables you can set:
*
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "mysql2", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_URL - database connection url. Should be a string.
* - TYPEORM_HOST - database host. Should be a string.
* - TYPEORM_PORT - database access port. Should be a number.
@ -254,18 +255,18 @@ export class ConnectionManager {
* Checks if ormconfig.json exists.
*/
protected hasOrmConfigurationFile(): boolean {
const path = require("app-root-path").path + "/ormconfig.json";
if (!fs.existsSync(path))
const path = PlatformTools.load("app-root-path").path + "/ormconfig.json";
if (!PlatformTools.fileExist(path))
return false;
const configuration: ConnectionOptions[]|ConnectionOptions = require(path);
const configuration: ConnectionOptions[]|ConnectionOptions = PlatformTools.load(path);
if (configuration instanceof Array) {
return configuration
.filter(options => !options.environment || options.environment === process.env.NODE_ENV)
.filter(options => !options.environment || options.environment === PlatformTools.getEnvVariable("NODE_ENV"))
.length > 0;
} else if (configuration instanceof Object) {
if (configuration.environment && configuration.environment !== process.env.NODE_ENV)
if (configuration.environment && configuration.environment !== PlatformTools.getEnvVariable("NODE_ENV"))
return false;
return Object.keys(configuration).length > 0;
@ -278,14 +279,14 @@ export class ConnectionManager {
* Checks if there is a default connection in the ormconfig.json file.
*/
protected hasDefaultConfigurationInConfigurationFile(): boolean {
const path = require("app-root-path").path + "/ormconfig.json";
if (!fs.existsSync(path))
const path = PlatformTools.load("app-root-path").path + "/ormconfig.json";
if (!PlatformTools.fileExist(path))
return false;
const configuration: ConnectionOptions[]|ConnectionOptions = require(path);
const configuration: ConnectionOptions[]|ConnectionOptions = PlatformTools.load(path);
if (configuration instanceof Array) {
return !!configuration
.filter(options => !options.environment || options.environment === process.env.NODE_ENV)
.filter(options => !options.environment || options.environment === PlatformTools.getEnvVariable("NODE_ENV"))
.find(config => !!config.name || config.name === "default");
} else if (configuration instanceof Object) {
@ -293,7 +294,7 @@ export class ConnectionManager {
configuration.name !== "default")
return false;
if (configuration.environment && configuration.environment !== process.env.NODE_ENV)
if (configuration.environment && configuration.environment !== PlatformTools.getEnvVariable("NODE_ENV"))
return false;
return true;
@ -306,7 +307,7 @@ export class ConnectionManager {
* Checks if environment variables contains connection options.
*/
protected hasDefaultConfigurationInEnvironmentVariables(): boolean {
return !!process.env.TYPEORM_DRIVER_TYPE;
return !!PlatformTools.getEnvVariable("TYPEORM_DRIVER_TYPE");
}
/**
@ -315,28 +316,28 @@ export class ConnectionManager {
protected async createFromEnvAndConnect(): Promise<Connection> {
return this.createAndConnectByConnectionOptions({
driver: {
type: process.env.TYPEORM_DRIVER_TYPE,
url: process.env.TYPEORM_URL,
host: process.env.TYPEORM_HOST,
port: process.env.TYPEORM_PORT,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
sid: process.env.TYPEORM_SID,
storage: process.env.TYPEORM_STORAGE,
usePool: process.env.TYPEORM_USE_POOL !== undefined ? OrmUtils.toBoolean(process.env.TYPEORM_USE_POOL) : undefined, // special check for defined is required here
extra: process.env.TYPEORM_DRIVER_EXTRA ? JSON.parse(process.env.TYPEORM_DRIVER_EXTRA) : undefined
type: PlatformTools.getEnvVariable("TYPEORM_DRIVER_TYPE"),
url: PlatformTools.getEnvVariable("TYPEORM_URL"),
host: PlatformTools.getEnvVariable("TYPEORM_HOST"),
port: PlatformTools.getEnvVariable("TYPEORM_PORT"),
username: PlatformTools.getEnvVariable("TYPEORM_USERNAME"),
password: PlatformTools.getEnvVariable("TYPEORM_PASSWORD"),
database: PlatformTools.getEnvVariable("TYPEORM_DATABASE"),
sid: PlatformTools.getEnvVariable("TYPEORM_SID"),
storage: PlatformTools.getEnvVariable("TYPEORM_STORAGE"),
usePool: PlatformTools.getEnvVariable("TYPEORM_USE_POOL") !== undefined ? OrmUtils.toBoolean(PlatformTools.getEnvVariable("TYPEORM_USE_POOL")) : undefined, // special check for defined is required here
extra: PlatformTools.getEnvVariable("TYPEORM_DRIVER_EXTRA") ? JSON.parse(PlatformTools.getEnvVariable("TYPEORM_DRIVER_EXTRA")) : undefined
},
autoSchemaSync: OrmUtils.toBoolean(process.env.TYPEORM_AUTO_SCHEMA_SYNC),
entities: process.env.TYPEORM_ENTITIES ? process.env.TYPEORM_ENTITIES.split(",") : [],
subscribers: process.env.TYPEORM_SUBSCRIBERS ? process.env.TYPEORM_SUBSCRIBERS.split(",") : [],
entitySchemas: process.env.TYPEORM_ENTITY_SCHEMAS ? process.env.TYPEORM_ENTITY_SCHEMAS.split(",") : [],
namingStrategies: process.env.TYPEORM_NAMING_STRATEGIES ? process.env.TYPEORM_NAMING_STRATEGIES.split(",") : [],
usedNamingStrategy: process.env.TYPEORM_USED_NAMING_STRATEGY,
autoSchemaSync: OrmUtils.toBoolean(PlatformTools.getEnvVariable("TYPEORM_AUTO_SCHEMA_SYNC")),
entities: PlatformTools.getEnvVariable("TYPEORM_ENTITIES") ? PlatformTools.getEnvVariable("TYPEORM_ENTITIES").split(",") : [],
subscribers: PlatformTools.getEnvVariable("TYPEORM_SUBSCRIBERS") ? PlatformTools.getEnvVariable("TYPEORM_SUBSCRIBERS").split(",") : [],
entitySchemas: PlatformTools.getEnvVariable("TYPEORM_ENTITY_SCHEMAS") ? PlatformTools.getEnvVariable("TYPEORM_ENTITY_SCHEMAS").split(",") : [],
namingStrategies: PlatformTools.getEnvVariable("TYPEORM_NAMING_STRATEGIES") ? PlatformTools.getEnvVariable("TYPEORM_NAMING_STRATEGIES").split(",") : [],
usedNamingStrategy: PlatformTools.getEnvVariable("TYPEORM_USED_NAMING_STRATEGY"),
logging: {
logQueries: OrmUtils.toBoolean(process.env.TYPEORM_LOGGING_QUERIES),
logFailedQueryError: OrmUtils.toBoolean(process.env.TYPEORM_LOGGING_FAILED_QUERIES),
logOnlyFailedQueries: OrmUtils.toBoolean(process.env.TYPEORM_LOGGING_ONLY_FAILED_QUERIES),
logQueries: OrmUtils.toBoolean(PlatformTools.getEnvVariable("TYPEORM_LOGGING_QUERIES")),
logFailedQueryError: OrmUtils.toBoolean(PlatformTools.getEnvVariable("TYPEORM_LOGGING_FAILED_QUERIES")),
logOnlyFailedQueries: OrmUtils.toBoolean(PlatformTools.getEnvVariable("TYPEORM_LOGGING_ONLY_FAILED_QUERIES")),
}
});
}
@ -349,12 +350,12 @@ export class ConnectionManager {
* If path is not given, then ormconfig.json file will be searched near node_modules directory.
*/
protected async createFromConfigAndConnectToAll(path?: string): Promise<Connection[]> {
const optionsArray: ConnectionOptions[] = require(path || (require("app-root-path").path + "/ormconfig.json"));
const optionsArray: ConnectionOptions[] = PlatformTools.load(path || (PlatformTools.load("app-root-path").path + "/ormconfig.json"));
if (!optionsArray)
throw new Error(`Configuration ${path || "ormconfig.json"} was not found. Add connection configuration inside ormconfig.json file.`);
const promises = optionsArray
.filter(options => !options.environment || options.environment === process.env.NODE_ENV) // skip connection creation if environment is set in the options, and its not equal to the value in the NODE_ENV variable
.filter(options => !options.environment || options.environment === PlatformTools.getEnvVariable("NODE_ENV")) // skip connection creation if environment is set in the options, and its not equal to the value in the NODE_ENV variable
.map(options => this.createAndConnectByConnectionOptions(options));
return Promise.all(promises);
@ -367,15 +368,15 @@ export class ConnectionManager {
* If path is not given, then ormconfig.json file will be searched near node_modules directory.
*/
protected async createFromConfigAndConnect(connectionName: string, path?: string): Promise<Connection> {
const optionsArray: ConnectionOptions[] = require(path || (require("app-root-path").path + "/ormconfig.json"));
const optionsArray: ConnectionOptions[] = PlatformTools.load(path || (PlatformTools.load("app-root-path").path + "/ormconfig.json"));
if (!optionsArray)
throw new Error(`Configuration ${path || "ormconfig.json"} was not found. Add connection configuration inside ormconfig.json file.`);
const environmentLessOptions = optionsArray.filter(options => (options.name || "default") === connectionName);
const options = environmentLessOptions.filter(options => !options.environment || options.environment === process.env.NODE_ENV); // skip connection creation if environment is set in the options, and its not equal to the value in the NODE_ENV variable
const options = environmentLessOptions.filter(options => !options.environment || options.environment === PlatformTools.getEnvVariable("NODE_ENV")); // skip connection creation if environment is set in the options, and its not equal to the value in the NODE_ENV variable
if (!options.length)
throw new Error(`Connection "${connectionName}" ${process.env.NODE_ENV ? "for the environment " + process.env.NODE_ENV + " " : ""}was not found in the json configuration file.` +
throw new Error(`Connection "${connectionName}" ${PlatformTools.getEnvVariable("NODE_ENV") ? "for the environment " + PlatformTools.getEnvVariable("NODE_ENV") + " " : ""}was not found in the json configuration file.` +
(environmentLessOptions.length ? ` However there are such configurations for other environments: ${environmentLessOptions.map(options => options.environment).join(", ")}.` : ""));
return this.createAndConnectByConnectionOptions(options[0]);
@ -391,11 +392,11 @@ export class ConnectionManager {
await connection.connect();
// if option is set - drop schema once connection is done
if (options.dropSchemaOnConnection && !process.env.SKIP_SCHEMA_CREATION)
if (options.dropSchemaOnConnection && !PlatformTools.getEnvVariable("SKIP_SCHEMA_CREATION"))
await connection.dropDatabase();
// if option is set - automatically synchronize a schema
if (options.autoSchemaSync && !process.env.SKIP_SCHEMA_CREATION)
if (options.autoSchemaSync && !PlatformTools.getEnvVariable("SKIP_SCHEMA_CREATION"))
await connection.syncSchema();
return connection;
@ -417,9 +418,7 @@ export class ConnectionManager {
protected createDriver(options: DriverOptions, logger: Logger): Driver {
switch (options.type) {
case "mysql":
return new MysqlDriver(options, logger, undefined, "mysql");
case "mysql2":
return new MysqlDriver(options, logger, undefined, "mysql2");
return new MysqlDriver(options, logger, undefined);
case "postgres":
return new PostgresDriver(options, logger);
case "mariadb":
@ -430,6 +429,8 @@ export class ConnectionManager {
return new OracleDriver(options, logger);
case "mssql":
return new SqlServerDriver(options, logger);
case "websql":
return new WebsqlDriver(options, logger);
default:
throw new MissingDriverError(options.type);
}

View File

@ -8,9 +8,9 @@ export class CannotDetermineConnectionOptionsError extends Error {
constructor() {
super();
this.message = `Cannot create connection, because connection options are missing. ` +
`You either need to explicitly pass connection options, either create a ormconfig.json with with connection options ` +
`and "default" connection name, either to set proper environment variables. Also, if you are using environment-specific ` +
`configurations in your ormconfig.json make sure your are running under correct NODE_ENV.`;
`You either need to explicitly pass connection options, either create a ormconfig.json with with connection options ` +
`and "default" connection name, either to set proper environment variables. Also, if you are using environment-specific ` +
`configurations in your ormconfig.json make sure your are running under correct NODE_ENV.`;
this.stack = new Error().stack;
}

View File

@ -6,7 +6,7 @@ export class MissingDriverError extends Error {
constructor(driverType: string) {
super();
this.message = `Wrong driver ${driverType} given. Supported drivers are: "mysql", "mysql2", "postgres", "mssql", "oracle", "mariadb", "sqlite".`;
this.message = `Wrong driver ${driverType} given. Supported drivers are: "mysql", "postgres", "mssql", "oracle", "mariadb", "sqlite".`;
this.stack = new Error().stack;
}

View File

@ -7,7 +7,7 @@ export class NoConnectionForRepositoryError extends Error {
constructor(connectionName: string) {
super();
this.message = `Cannot get a Repository for "${connectionName} connection, because connection with the database ` +
`is not established yet. Call connection#connect method to establish connection.`;
`is not established yet. Call connection#connect method to establish connection.`;
this.stack = new Error().stack;
}

View File

@ -7,7 +7,7 @@ export class RepositoryNotFoundError extends Error {
constructor(connectionName: string, entityClass: Function|string) {
super();
const targetName = typeof entityClass === "function" && (<any> entityClass).name ? (<any> entityClass).name : entityClass;
this.message = `No repository for "${targetName}" was found. Looks like this entity is not registered in ` +
this.message = `No repository for "${targetName}" was found. Looks like this entity is not registered in ` +
`current "${connectionName}" connection?`;
this.stack = new Error().stack;
}

View File

@ -21,6 +21,7 @@ export interface UseContainerOptions {
*/
export const defaultContainer: { get<T>(someClass: { new (...args: any[]): T }|Function): T } = new (class {
private instances: { type: Function, object: any }[] = [];
get<T>(someClass: { new (...args: any[]): T }): T {
let instance = this.instances.find(instance => instance.type === someClass);
if (!instance) {

View File

@ -6,7 +6,7 @@ import {ColumnType, ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ColumnMetadataArgs} from "../../metadata-args/ColumnMetadataArgs";
/**
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* Column decorator is used to mark a specific class property as a table column. Only properties decorated with this
* decorator will be persisted to the database when entity be saved.
*/
export function Column(): Function;
@ -41,7 +41,7 @@ export function Column(typeOrOptions?: ColumnType|ColumnOptions, options?: Colum
options = <ColumnOptions> typeOrOptions;
}
return function (object: Object, propertyName: string) {
// todo: need to store not string type, but original type instead? (like in relation metadata)
const reflectedType = ColumnTypes.typeToString((Reflect as any).getMetadata("design:type", object, propertyName));
@ -51,7 +51,7 @@ export function Column(typeOrOptions?: ColumnType|ColumnOptions, options?: Colum
// if column options are not given then create a new empty options
if (!options) options = {} as ColumnOptions;
// check if there is no type in column options then set type from first function argument, or guessed one
if (!options.type)
options = Object.assign({ type: type } as ColumnOptions, options);

View File

@ -44,7 +44,7 @@ export function PrimaryColumn(typeOrOptions?: ColumnType|ColumnOptions, options?
// check if there is no type in column options then set type from first function argument, or guessed one
if (!options.type)
options = Object.assign({ type: type } as ColumnOptions, options);
options = Object.assign({type: type} as ColumnOptions, options);
// if we still don't have a type then we need to give error to user that type is required
if (!options.type)

View File

@ -20,7 +20,7 @@ export function PrimaryGeneratedColumn(options?: ColumnOptions): Function {
// check if there is no type in column options then set the int type - by default for auto generated column
if (!options.type)
options = Object.assign({ type: "int" } as ColumnOptions, options);
options = Object.assign({type: "int"} as ColumnOptions, options);
// check if column is not nullable, because we cannot allow a primary key to be nullable
if (options.nullable)

View File

@ -19,7 +19,7 @@ export interface ColumnOptions {
* Column type's length. Used only on some column types.
* For example type = "string" and length = "100" means that ORM will create a column with type varchar(100).
*/
readonly length?: string;
readonly length?: string|number;
/**
* Indicates if this column is PRIMARY.
@ -72,5 +72,19 @@ export interface ColumnOptions {
* Note that timezone option is not supported by all databases (only postgres for now).
*/
readonly timezone?: boolean;
/**
* Indicates if date object must be stored in given date's timezone.
* By default date is saved in UTC timezone.
* Works only with "datetime" columns.
*/
readonly storeInLocalTimezone?: boolean;
/**
* Indicates if date object must be loaded and set to the Date object in local timezone.
* By default date is loaded in UTC timezone.
* Works only with "datetime" columns.
*/
readonly loadInLocalTimezone?: boolean;
}

View File

@ -21,5 +21,5 @@ export interface TableOptions {
* Specifies if this table will be skipped during schema synchronization.
*/
readonly skipSchemaSync?: boolean;
}

View File

@ -37,7 +37,7 @@ export function ManyToMany<T>(typeFunction: (type?: any) => ObjectType<T>,
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
const isLazy = reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise";

View File

@ -6,7 +6,7 @@ import {RelationMetadataArgs} from "../../metadata-args/RelationMetadataArgs";
/**
* Many-to-one relation allows to create type of relation when Entity1 can have single instance of Entity2, but
* Entity2 can have a multiple instances of Entity1. Entity1 is an owner of the relationship, and storages Entity2 id
* Entity2 can have a multiple instances of Entity1. Entity1 is an owner of the relationship, and storages Entity2 id
* on its own side.
*/
export function ManyToOne<T>(typeFunction: (type?: any) => ObjectType<T>, options?: RelationOptions): Function;
@ -26,8 +26,8 @@ export function ManyToOne<T>(typeFunction: (type?: any) => ObjectType<T>,
* on its own side.
*/
export function ManyToOne<T>(typeFunction: (type?: any) => ObjectType<T>,
inverseSideOrOptions?: string|((object: T) => any)|RelationOptions,
options?: RelationOptions): Function {
inverseSideOrOptions?: string|((object: T) => any)|RelationOptions,
options?: RelationOptions): Function {
let inverseSideProperty: string|((object: T) => any);
if (typeof inverseSideOrOptions === "object") {
options = <RelationOptions> inverseSideOrOptions;

View File

@ -7,7 +7,7 @@ import {RelationMetadataArgs} from "../../metadata-args/RelationMetadataArgs";
// todo: make decorators which use inverse side string separate
/**
* One-to-many relation allows to create type of relation when Entity2 can have multiple instances of Entity1.
* One-to-many relation allows to create type of relation when Entity2 can have multiple instances of Entity1.
* Entity1 have only one Entity2. Entity1 is an owner of the relationship, and storages Entity2 id on its own side.
*/
// export function OneToMany<T>(typeFunction: (type?: any) => ConstructorFunction<T>, options?: RelationOptions): Function;
@ -33,7 +33,7 @@ export function OneToMany<T>(typeFunction: (type?: any) => ObjectType<T>,
} else {
inverseSideProperty = <string|((object: T) => any)> inverseSideOrOptions;
}
// todo: for OneToMany having inverse side is required because otherwise its not possible to do anything (selections/persisment)
// todo: validate it somehow?

View File

@ -12,7 +12,7 @@ export function TreeChildren(options?: RelationOptions): Function {
const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
const isLazy = reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise";
// add one-to-many relation for this
const args: RelationMetadataArgs = {
isTreeChildren: true,

View File

@ -12,7 +12,7 @@ export function TreeParent(options?: RelationOptions): Function {
const reflectedType = (Reflect as any).getMetadata("design:type", object, propertyName);
const isLazy = reflectedType && typeof reflectedType.name === "string" && reflectedType.name.toLowerCase() === "promise";
const args: RelationMetadataArgs = {
isTreeParent: true,
target: object.constructor,

207
src/decorators-shim.ts Normal file
View File

@ -0,0 +1,207 @@
// columns
/* export */
function Column(typeOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function CreateDateColumn(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function DiscriminatorColumn(discriminatorOptions: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function PrimaryColumn(typeOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function PrimaryGeneratedColumn(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function UpdateDateColumn(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function VersionColumn(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
// listeners
/* export */
function AfterInsert(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function AfterLoad(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function AfterRemove(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function AfterUpdate(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function BeforeInsert(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function BeforeRemove(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function BeforeUpdate(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function EventSubscriber(): Function {
return function (object: Object, propertyName: string) {
};
}
// relations
/* export */
function JoinColumn(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function JoinTable(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function ManyToMany<T>(typeFunction: any, inverseSideOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function ManyToOne<T>(typeFunction: any, inverseSideOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function OneToMany<T>(typeFunction: any, inverseSideOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function OneToOne<T>(typeFunction: any, inverseSideOrOptions?: any, options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function RelationCount<T>(relation: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function RelationId<T>(relation: any): Function {
return function (object: Object, propertyName: string) {
};
}
// tables
/* export */
function AbstractTable(): Function {
return function (object: Object) {
};
}
/* export */
function ClassTableChild(tableName?: any, options?: any): Function {
return function (object: Object) {
};
}
/* export */
function ClosureTable(name?: any, options?: any): Function {
return function (object: Object) {
};
}
/* export */
function EmbeddableTable(): Function {
return function (object: Object) {
};
}
/* export */
function SingleTableChild(): Function {
return function (object: Object) {
};
}
/* export */
function Table(name?: any, options?: any): Function {
return function (object: Object) {
};
}
/* export */
function TableInheritance(type?: any): Function {
return function (object: Object) {
};
}
// tree
/* export */
function TreeChildren(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function TreeLevelColumn(): Function {
return function (object: Object, propertyName: string) {
};
}
/* export */
function TreeParent(options?: any): Function {
return function (object: Object, propertyName: string) {
};
}

View File

@ -2,7 +2,6 @@ import {DriverOptions} from "./DriverOptions";
import {QueryRunner} from "../query-runner/QueryRunner";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {ColumnType} from "../metadata/types/ColumnTypes";
/**
* Driver organizes TypeORM communication with specific database management system.
@ -65,7 +64,7 @@ export interface Driver {
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
prepareHydratedValue(value: any, type: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type.

View File

@ -6,7 +6,7 @@ export interface DriverOptions {
/**
* Database type. This value is required.
*/
readonly type: "mysql"|"mysql2"|"postgres"|"mariadb"|"sqlite"|"oracle"|"mssql";
readonly type: "mysql"|"postgres"|"mariadb"|"sqlite"|"oracle"|"mssql"|"websql";
/**
* Connection url to where perform connection to.

View File

@ -50,16 +50,21 @@ export class DriverUtils {
* Extracts connection data from the connection url.
*/
private static parseConnectionUrl(url: string) {
const urlParser = require("url");
const params = urlParser.parse(url);
const auth = params.auth.split(":");
const firstSlashes = url.indexOf("//");
const preBase = url.substr(firstSlashes + 2);
const secondSlash = preBase.indexOf("/");
const base = (secondSlash !== -1) ? preBase.substr(0, secondSlash) : preBase;
const afterBase = (secondSlash !== -1) ? preBase.substr(secondSlash) + 1 : undefined;
const [usernameAndPassword, hostAndPort] = base.split("@");
const [username, password] = usernameAndPassword.split(":");
const [host, port] = hostAndPort.split(":");
return {
host: params.hostname,
username: auth[0],
password: auth[1],
port: parseInt(params.port),
database: params.pathname.split("/")[1]
host: host,
username: username,
password: password,
port: port ? parseInt(port) : undefined,
database: afterBase ? afterBase.split("/")[0] : undefined
};
}

View File

@ -3,16 +3,16 @@ import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError";
import {DriverOptions} from "../DriverOptions";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {DriverUtils} from "../DriverUtils";
import {Logger} from "../../logger/Logger";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {MysqlQueryRunner} from "./MysqlQueryRunner";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import * as moment from "moment";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {PlatformTools} from "../../platform/PlatformTools";
/**
* Organizes communication with MySQL DBMS.
@ -57,21 +57,15 @@ export class MysqlDriver implements Driver {
*/
protected logger: Logger;
/**
* Driver type's version. node-mysql and mysql2 are supported.
*/
protected version: "mysql"|"mysql2" = "mysql";
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(options: DriverOptions, logger: Logger, mysql?: any, mysqlVersion: "mysql"|"mysql2" = "mysql") {
constructor(options: DriverOptions, logger: Logger, mysql?: any) {
this.options = DriverUtils.buildDriverOptions(options);
this.logger = logger;
this.mysql = mysql;
this.version = mysqlVersion;
// validate options to make sure everything is set
if (!this.options.host)
@ -130,7 +124,7 @@ export class MysqlDriver implements Driver {
*/
disconnect(): Promise<void> {
if (!this.databaseConnection && !this.pool)
throw new ConnectionIsNotSetError(this.version);
throw new ConnectionIsNotSetError("mysql");
return new Promise<void>((ok, fail) => {
const handler = (err: any) => err ? fail(err) : ok();
@ -155,7 +149,7 @@ export class MysqlDriver implements Driver {
*/
async createQueryRunner(): Promise<QueryRunner> {
if (!this.databaseConnection && !this.pool)
return Promise.reject(new ConnectionIsNotSetError(this.version));
return Promise.reject(new ConnectionIsNotSetError("mysql"));
const databaseConnection = await this.retrieveDatabaseConnection();
return new MysqlQueryRunner(databaseConnection, this, this.logger);
@ -212,72 +206,60 @@ export class MysqlDriver implements Driver {
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
if (moment(value).isValid())
return moment(value).format("YYYY-MM-DD");
else return "0000-00-00";
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
if (moment(value).isValid())
return moment(value).format("HH:mm:ss");
else return "00:00:00";
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
if (moment(value).isValid())
return moment(value).format("YYYY-MM-DD HH:mm:ss");
else return "0000-00-00 00:00:00";
if (columnMetadata.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
@ -324,20 +306,23 @@ export class MysqlDriver implements Driver {
if (this.databaseConnection)
return Promise.resolve(this.databaseConnection);
throw new ConnectionIsNotSetError(this.version);
throw new ConnectionIsNotSetError("mysql");
}
/**
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.mysql = require(this.version);
this.mysql = PlatformTools.load("mysql"); // try to load first supported package
} catch (e) {
throw new DriverPackageNotInstalledError("Mysql", this.version);
try {
this.mysql = PlatformTools.load("mysql2"); // try to load second supported package
} catch (e) {
throw new DriverPackageNotInstalledError("Mysql", "mysql");
}
}
}

View File

@ -64,14 +64,26 @@ export class MysqlQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`;
const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS query FROM information_schema.tables WHERE table_schema = '${this.dbName}'`;
const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`;
await this.beginTransaction();
try {
const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`;
const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS query FROM information_schema.tables WHERE table_schema = '${this.dbName}'`;
const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`;
await this.query(disableForeignKeysCheckQuery);
const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery);
await Promise.all(dropQueries.map(query => this.query(query["query"])));
await this.query(enableForeignKeysCheckQuery);
await this.query(disableForeignKeysCheckQuery);
const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery);
await Promise.all(dropQueries.map(query => this.query(query["query"])));
await this.query(enableForeignKeysCheckQuery);
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
}
}
/**
@ -179,13 +191,24 @@ export class MysqlQueryRunner implements QueryRunner {
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
const parameters = Object.keys(conditions).map(key => conditions[key]);
await this.query(sql, parameters);
}
@ -272,7 +295,7 @@ export class MysqlQueryRunner implements QueryRunner {
// create index schemas from the loaded indices
tableSchema.indices = dbIndices
.filter(dbIndex => {
return dbIndex["TABLE_NAME"] === tableSchema.name &&
return dbIndex["TABLE_NAME"] === tableSchema.name &&
(!tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["INDEX_NAME"])) &&
(!tableSchema.primaryKeys.find(primaryKey => primaryKey.name === dbIndex["INDEX_NAME"]));
})
@ -388,7 +411,7 @@ export class MysqlQueryRunner implements QueryRunner {
const promises = foreignKeys.map(foreignKey => {
const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(",");
let sql = `ALTER TABLE ${dbTable.name} ADD CONSTRAINT \`${foreignKey.name}\` ` +
let sql = `ALTER TABLE ${dbTable.name} ADD CONSTRAINT \`${foreignKey.name}\` ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES \`${foreignKey.referencedTableName}\`(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
@ -522,7 +545,7 @@ export class MysqlQueryRunner implements QueryRunner {
if (column.comment)
c += " COMMENT '" + column.comment + "'";
if (column.default)
c += " DEFAULT " + column.default + "";
c += " DEFAULT '" + column.default + "'";
return c;
}

View File

@ -3,16 +3,16 @@ import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError";
import {DriverOptions} from "../DriverOptions";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {DriverUtils} from "../DriverUtils";
import {Logger} from "../../logger/Logger";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {OracleQueryRunner} from "./OracleQueryRunner";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import * as moment from "moment";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {PlatformTools} from "../../platform/PlatformTools";
/**
* Organizes communication with Oracle DBMS.
@ -217,66 +217,60 @@ export class OracleDriver implements Driver {
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
return moment(value).format("HH:mm:ss");
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD HH:mm:ss");
if (columnMetadata.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
@ -336,12 +330,10 @@ export class OracleDriver implements Driver {
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.oracle = require("oracledb");
} catch (e) {
this.oracle = PlatformTools.load("oracledb");
} catch (e) { // todo: better error for browser env
throw new DriverPackageNotInstalledError("Oracle", "oracledb");
}
}

View File

@ -64,14 +64,27 @@ export class OracleQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`;
const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS query FROM information_schema.tables WHERE table_schema = '${this.dbName}'`;
const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`;
await this.beginTransaction();
try {
const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`;
const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS query FROM information_schema.tables WHERE table_schema = '${this.dbName}'`;
const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`;
await this.query(disableForeignKeysCheckQuery);
const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery);
await Promise.all(dropQueries.map(query => this.query(query["query"])));
await this.query(enableForeignKeysCheckQuery);
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
}
await this.query(disableForeignKeysCheckQuery);
const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery);
await Promise.all(dropQueries.map(query => this.query(query["query"])));
await this.query(enableForeignKeysCheckQuery);
}
/**
@ -159,7 +172,10 @@ export class OracleQueryRunner implements QueryRunner {
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map(key => ":" + key).join(", ");
const parameters = keys.map(key => keyValues[key]);
const insertSql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`;
const insertSql = columns.length > 0
? `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`
: `INSERT INTO ${this.driver.escapeTableName(tableName)} DEFAULT VALUES`;
if (generatedColumn) {
const sql2 = `declare lastId number; begin ${insertSql} returning "id" into lastId; dbms_output.enable; dbms_output.put_line(lastId); dbms_output.get_line(:ln, :st); end;`;
const saveResult = await this.query(sql2, parameters.concat([
@ -191,13 +207,24 @@ export class OracleQueryRunner implements QueryRunner {
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
const parameters = Object.keys(conditions).map(key => conditions[key]);
await this.query(sql, parameters);
}
@ -587,6 +614,8 @@ AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner ORDE
c += " GENERATED BY DEFAULT ON NULL AS IDENTITY";
// if (column.comment) // todo: less priority, fix it later
// c += " COMMENT '" + column.comment + "'";
if (column.default)
c += " DEFAULT '" + column.default + "'";
return c;
}

View File

@ -4,15 +4,15 @@ import {DriverOptions} from "../DriverOptions";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {DriverUtils} from "../DriverUtils";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {Logger} from "../../logger/Logger";
import * as moment from "moment";
import {PostgresQueryRunner} from "./PostgresQueryRunner";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {PlatformTools} from "../../platform/PlatformTools";
// todo(tests):
// check connection with url
@ -179,67 +179,65 @@ export class PostgresDriver implements Driver {
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (column.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
return moment(value).format("HH:mm:ss");
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD HH:mm:ss");
if (column.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
// pg(pg-types) have done JSON.parse conversion
// https://github.com/brianc/node-pg-types/blob/ed2d0e36e33217b34530727a98d20b325389e73a/lib/textParsers.js#L170
// pg(pg-types) have done JSON.parse conversion
// https://github.com/brianc/node-pg-types/blob/ed2d0e36e33217b34530727a98d20b325389e73a/lib/textParsers.js#L170
return value;
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
if (typeof value === "string") {
return JSON.parse(value);
} else {
return value;
}
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
@ -255,7 +253,7 @@ export class PostgresDriver implements Driver {
const builtParameters: any[] = [];
const keys = Object.keys(parameters).map(parameter => "(:" + parameter + "\\b)").join("|");
sql = sql.replace(new RegExp(keys, "g"), (key: string): string => {
sql = sql.replace(new RegExp(keys, "g"), (key: string): string => {
const value = parameters[key.substr(1)];
if (value instanceof Array) {
return value.map((v: any) => {
@ -340,12 +338,10 @@ export class PostgresDriver implements Driver {
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.postgres = require("pg");
} catch (e) {
this.postgres = PlatformTools.load("pg");
} catch (e) { // todo: better error for browser env
throw new DriverPackageNotInstalledError("Postgres", "pg");
}
}

View File

@ -63,9 +63,21 @@ export class PostgresQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const selectDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || tablename || '" CASCADE;' as query FROM pg_tables WHERE schemaname = 'public'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
await this.beginTransaction();
try {
const selectDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || tablename || '" CASCADE;' as query FROM pg_tables WHERE schemaname = 'public'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
}
}
/**
@ -147,7 +159,9 @@ export class PostgresQueryRunner implements QueryRunner {
const keys = Object.keys(keyValues);
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "$" + (index + 1)).join(",");
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ generatedColumn ? " RETURNING " + this.driver.escapeColumnName(generatedColumn.name) : "" }`;
const sql = columns.length > 0
? `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ generatedColumn ? " RETURNING " + this.driver.escapeColumnName(generatedColumn.name) : "" }`
: `INSERT INTO ${this.driver.escapeTableName(tableName)} DEFAULT VALUES ${ generatedColumn ? " RETURNING " + this.driver.escapeColumnName(generatedColumn.name) : "" }`;
const parameters = keys.map(key => keyValues[key]);
const result: ObjectLiteral[] = await this.query(sql, parameters);
if (generatedColumn)
@ -175,14 +189,25 @@ export class PostgresQueryRunner implements QueryRunner {
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const parameters = Object.keys(conditions).map(key => conditions[key]);
const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`;
await this.query(query, parameters);
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
await this.query(sql, parameters);
}
/**
@ -287,7 +312,7 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
// create index schemas from the loaded indices
tableSchema.indices = dbIndices
.filter(dbIndex => {
return dbIndex["table_name"] === tableSchema.name &&
return dbIndex["table_name"] === tableSchema.name &&
(!tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["index_name"])) &&
(!tableSchema.primaryKeys.find(primaryKey => primaryKey.name === dbIndex["index_name"])) &&
(!dbUniqueKeys.find(key => key["constraint_name"] === dbIndex["index_name"]));
@ -582,6 +607,8 @@ where constraint_type = 'PRIMARY KEY' and tc.table_catalog = '${this.dbName}'`;
c += " NOT NULL";
if (column.isGenerated)
c += " PRIMARY KEY";
if (column.default)
c += " DEFAULT '" + column.default + "'";
return c;
}

View File

@ -4,14 +4,14 @@ import {DriverOptions} from "../DriverOptions";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {Logger} from "../../logger/Logger";
import * as moment from "moment";
import {SqliteQueryRunner} from "./SqliteQueryRunner";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {PlatformTools} from "../../platform/PlatformTools";
/**
* Organizes communication with sqlite DBMS.
@ -83,7 +83,12 @@ export class SqliteDriver implements Driver {
connection: connection,
isTransactionActive: false
};
ok();
// we need to enable foreign keys in sqlite to make sure all foreign key related features
// working properly. this also makes onDelete to work with sqlite.
connection.run(`PRAGMA foreign_keys = ON;`, (err: any, result: any) => {
ok();
});
});
});
}
@ -125,66 +130,60 @@ export class SqliteDriver implements Driver {
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
return moment(value).format("HH:mm:ss");
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD HH:mm:ss");
if (columnMetadata.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
@ -200,7 +199,7 @@ export class SqliteDriver implements Driver {
const builtParameters: any[] = [];
const keys = Object.keys(parameters).map(parameter => "(:" + parameter + "\\b)").join("|");
sql = sql.replace(new RegExp(keys, "g"), (key: string): string => {
sql = sql.replace(new RegExp(keys, "g"), (key: string): string => {
const value = parameters[key.substr(1)];
if (value instanceof Array) {
return value.map((v: any) => {
@ -256,12 +255,10 @@ export class SqliteDriver implements Driver {
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.sqlite = require("sqlite3").verbose();
} catch (e) {
this.sqlite = PlatformTools.load("sqlite3").verbose();
} catch (e) { // todo: better error for browser env
throw new DriverPackageNotInstalledError("SQLite", "sqlite3");
}
}

View File

@ -66,9 +66,22 @@ export class SqliteQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const selectDropsQuery = `select 'drop table ' || name || ';' as query from sqlite_master where type = 'table' and name != 'sqlite_sequence'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
await this.query(`PRAGMA foreign_keys = OFF;`);
await this.beginTransaction();
try {
const selectDropsQuery = `select 'drop table ' || name || ';' as query from sqlite_master where type = 'table' and name != 'sqlite_sequence'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
await this.query(`PRAGMA foreign_keys = ON;`);
}
}
/**
@ -153,11 +166,11 @@ export class SqliteQueryRunner implements QueryRunner {
this.logger.logQuery(sql, parameters);
return new Promise<any[]>((ok, fail) => {
const _this = this;
const __this = this;
this.databaseConnection.connection.run(sql, parameters, function (err: any): void {
if (err) {
_this.logger.logFailedQuery(sql, parameters);
_this.logger.logQueryError(err);
__this.logger.logFailedQuery(sql, parameters);
__this.logger.logQueryError(err);
fail(err);
} else {
if (generatedColumn)
@ -188,14 +201,25 @@ export class SqliteQueryRunner implements QueryRunner {
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const parameters = Object.keys(conditions).map(key => conditions[key]);
const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`;
await this.query(query, parameters);
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
await this.query(sql, parameters);
}
/**
@ -303,7 +327,7 @@ export class SqliteQueryRunner implements QueryRunner {
// create index schemas from the loaded indices
const indicesPromises = dbIndices
.filter(dbIndex => {
return dbIndex["origin"] !== "pk" &&
return dbIndex["origin"] !== "pk" &&
(!tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["name"])) &&
(!tableSchema.primaryKeys.find(primaryKey => primaryKey.name === dbIndex["name"]));
})
@ -533,6 +557,8 @@ export class SqliteQueryRunner implements QueryRunner {
c += " UNIQUE";
if (column.isGenerated === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.
c += " PRIMARY KEY AUTOINCREMENT";
if (column.default)
c += " DEFAULT '" + column.default + "'";
return c;
}
@ -549,6 +575,7 @@ export class SqliteQueryRunner implements QueryRunner {
const columnNames = foreignKey.columnNames.map(name => `"${name}"`).join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(name => `"${name}"`).join(", ");
sql1 += `, FOREIGN KEY(${columnNames}) REFERENCES "${foreignKey.referencedTableName}"(${referencedColumnNames})`;
if (foreignKey.onDelete) sql1 += " ON DELETE " + foreignKey.onDelete;
});
const primaryKeyColumns = tableSchema.columns.filter(column => column.isPrimary && !column.isGenerated);

View File

@ -3,16 +3,16 @@ import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError";
import {DriverOptions} from "../DriverOptions";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError";
import {DriverPackageLoadError} from "../error/DriverPackageLoadError";
import {DriverUtils} from "../DriverUtils";
import {Logger} from "../../logger/Logger";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {SqlServerQueryRunner} from "./SqlServerQueryRunner";
import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes";
import * as moment from "moment";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {PlatformTools} from "../../platform/PlatformTools";
/**
* Organizes communication with SQL Server DBMS.
@ -201,66 +201,60 @@ export class SqlServerDriver implements Driver {
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, column: ColumnMetadata): any {
switch (column.type) {
preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return moment(value).format("YYYY-MM-DD");
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
return moment(value).format("HH:mm:ss");
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
return moment(value).format("YYYY-MM-DD HH:mm:ss");
if (columnMetadata.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as any[])
.map(i => String(i))
.join(",");
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column metadata.
*/
prepareHydratedValue(value: any, type: ColumnType): any;
/**
* Prepares given value to a value to be persisted, based on its column type.
*/
prepareHydratedValue(value: any, column: ColumnMetadata): any;
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any {
const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType;
switch (type) {
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
case ColumnTypes.DATE:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD").toDate();
case ColumnTypes.TIME:
return moment(value, "HH:mm:ss").toDate();
case ColumnTypes.DATETIME:
if (value instanceof Date)
return value;
return moment(value, "YYYY-MM-DD HH:mm:ss").toDate();
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return (value as string).split(",");
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
@ -321,12 +315,10 @@ export class SqlServerDriver implements Driver {
* If driver dependency is not given explicitly, then try to load it via "require".
*/
protected loadDependencies(): void {
if (!require)
throw new DriverPackageLoadError();
try {
this.mssql = require("mssql");
} catch (e) {
this.mssql = PlatformTools.load("mssql");
} catch (e) { // todo: better error for browser env
throw new DriverPackageNotInstalledError("SQL Server", "mssql");
}
}

View File

@ -64,20 +64,32 @@ export class SqlServerQueryRunner implements QueryRunner {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const allTablesSql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'`;
const allTablesResults: ObjectLiteral[] = await this.query(allTablesSql);
const tableNames = allTablesResults.map(result => result["TABLE_NAME"]);
await Promise.all(tableNames.map(async tableName => {
const dropForeignKeySql = `SELECT 'ALTER TABLE ' + OBJECT_SCHEMA_NAME(parent_object_id) + '.[' + OBJECT_NAME(parent_object_id) + '] DROP CONSTRAINT ' + name as query FROM sys.foreign_keys WHERE referenced_object_id = object_id('${tableName}')`;
const dropFkQueries: ObjectLiteral[] = await this.query(dropForeignKeySql);
return Promise.all(dropFkQueries.map(result => result["query"]).map(dropQuery => {
return this.query(dropQuery);
await this.beginTransaction();
try {
const allTablesSql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'`;
const allTablesResults: ObjectLiteral[] = await this.query(allTablesSql);
const tableNames = allTablesResults.map(result => result["TABLE_NAME"]);
await Promise.all(tableNames.map(async tableName => {
const dropForeignKeySql = `SELECT 'ALTER TABLE ' + OBJECT_SCHEMA_NAME(parent_object_id) + '.[' + OBJECT_NAME(parent_object_id) + '] DROP CONSTRAINT ' + name as query FROM sys.foreign_keys WHERE referenced_object_id = object_id('${tableName}')`;
const dropFkQueries: ObjectLiteral[] = await this.query(dropForeignKeySql);
return Promise.all(dropFkQueries.map(result => result["query"]).map(dropQuery => {
return this.query(dropQuery);
}));
}));
}));
await Promise.all(tableNames.map(tableName => {
const dropTableSql = `DROP TABLE "${tableName}"`;
return this.query(dropTableSql);
}));
await Promise.all(tableNames.map(tableName => {
const dropTableSql = `DROP TABLE "${tableName}"`;
return this.query(dropTableSql);
}));
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
}
// const selectDropsQuery = `SELECT 'DROP TABLE "' + TABLE_NAME + '"' as query FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';`;
// const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
@ -212,7 +224,11 @@ export class SqlServerQueryRunner implements QueryRunner {
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "@" + index).join(",");
const parameters = keys.map(key => keyValues[key]);
const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) ${ generatedColumn ? "OUTPUT INSERTED." + generatedColumn.name + " " : "" }VALUES (${values})`;
const sql = columns.length > 0
? `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) ${ generatedColumn ? "OUTPUT INSERTED." + generatedColumn.name + " " : "" }VALUES (${values})`
: `INSERT INTO ${this.driver.escapeTableName(tableName)} DEFAULT ${ generatedColumn ? "OUTPUT INSERTED." + generatedColumn.name + " " : "" }VALUES`;
const result = await this.query(sql, parameters);
return generatedColumn ? result instanceof Array ? result[0][generatedColumn.name] : result[generatedColumn.name] : undefined;
}
@ -236,13 +252,24 @@ export class SqlServerQueryRunner implements QueryRunner {
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void> {
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = this.parametrize(conditions).join(" AND ");
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
const parameters = Object.keys(conditions).map(key => conditions[key]);
await this.query(sql, parameters);
}
@ -491,7 +518,7 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C
const promises = foreignKeys.map(foreignKey => {
const columnNames = foreignKey.columnNames.map(column => `"` + column + `"`).join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `"` + column + `"`).join(",");
let sql = `ALTER TABLE "${dbTable.name}" ADD CONSTRAINT "${foreignKey.name}" ` +
let sql = `ALTER TABLE "${dbTable.name}" ADD CONSTRAINT "${foreignKey.name}" ` +
`FOREIGN KEY (${columnNames}) ` +
`REFERENCES "${foreignKey.referencedTableName}"(${referencedColumnNames})`;
if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete;
@ -621,6 +648,8 @@ WHERE columnUsages.TABLE_CATALOG = '${this.dbName}' AND tableConstraints.TABLE_C
// c += " PRIMARY KEY";
if (column.comment)
c += " COMMENT '" + column.comment + "'";
if (column.default)
c += " DEFAULT '" + column.default + "'";
return c;
}

View File

@ -0,0 +1,243 @@
import {Driver} from "../Driver";
import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError";
import {DriverOptions} from "../DriverOptions";
import {DatabaseConnection} from "../DatabaseConnection";
import {DriverUtils} from "../DriverUtils";
import {Logger} from "../../logger/Logger";
import {QueryRunner} from "../../query-runner/QueryRunner";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {DriverOptionNotSetError} from "../error/DriverOptionNotSetError";
import {DataTransformationUtils} from "../../util/DataTransformationUtils";
import {WebsqlQueryRunner} from "./WebsqlQueryRunner";
/**
* Organizes communication with WebSQL in the browser.
*/
export class WebsqlDriver implements Driver {
// -------------------------------------------------------------------------
// Public Properties
// -------------------------------------------------------------------------
/**
* Driver connection options.
*/
readonly options: DriverOptions;
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* Connection to database.
*/
protected databaseConnection: DatabaseConnection|undefined;
/**
* Logger used go log queries and errors.
*/
protected logger: Logger;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(options: DriverOptions, logger: Logger) {
this.options = DriverUtils.buildDriverOptions(options);
this.logger = logger;
// validate options to make sure everything is set
// if (!this.options.host)
// throw new DriverOptionNotSetError("host");
// if (!this.options.username)
// throw new DriverOptionNotSetError("username");
if (!this.options.database)
throw new DriverOptionNotSetError("database");
// todo: what about extra options: version, description, size
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Performs connection to the database.
* Based on pooling options, it can either create connection immediately,
* either create a pool and create connection when needed.
*/
connect(): Promise<void> {
// build connection options for the driver
const options = Object.assign({}, {
database: this.options.database,
}, this.options.extra || {});
return new Promise<void>((ok, fail) => {
const connection = (window as any).openDatabase(
options.database,
options.version,
options.description,
options.size,
);
this.databaseConnection = {
id: 1,
connection: connection,
isTransactionActive: false
};
ok();
});
}
/**
* Closes connection with the database.
*/
disconnect(): Promise<void> {
if (!this.databaseConnection)
throw new ConnectionIsNotSetError("websql");
return new Promise<void>((ok, fail) => {
// const handler = (err: any) => err ? fail(err) : ok();
// todo: find out how to close connection
ok();
});
}
/**
* Creates a query runner used for common queries.
*/
async createQueryRunner(): Promise<QueryRunner> {
if (!this.databaseConnection)
return Promise.reject(new ConnectionIsNotSetError("websql"));
const databaseConnection = await this.retrieveDatabaseConnection();
return new WebsqlQueryRunner(databaseConnection, this, this.logger);
}
/**
* Access to the native implementation of the database.
*/
nativeInterface() {
return {
connection: this.databaseConnection ? this.databaseConnection.connection : undefined
};
}
/**
* Replaces parameters in the given sql with special escaping character
* and an array of parameter names to be passed to a query.
*/
escapeQueryWithParameters(sql: string, parameters: ObjectLiteral): [string, any[]] {
if (!parameters || !Object.keys(parameters).length)
return [sql, []];
const escapedParameters: any[] = [];
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 "?";
}); // todo: make replace only in value statements, otherwise problems
return [sql, escapedParameters];
}
/**
* Escapes a column name.
*/
escapeColumnName(columnName: string): string {
return columnName; // "`" + columnName + "`";
}
/**
* Escapes an alias.
*/
escapeAliasName(aliasName: string): string {
return aliasName; // "`" + aliasName + "`";
}
/**
* Escapes a table name.
*/
escapeTableName(tableName: string): string {
return tableName; // "`" + tableName + "`";
}
/**
* Prepares given value to a value to be persisted, based on its column type and metadata.
*/
preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any {
if (value === null || value === undefined)
return null;
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value === true ? 1 : 0;
case ColumnTypes.DATE:
return DataTransformationUtils.mixedDateToDateString(value);
case ColumnTypes.TIME:
return DataTransformationUtils.mixedDateToTimeString(value);
case ColumnTypes.DATETIME:
if (columnMetadata.storeInLocalTimezone) {
return DataTransformationUtils.mixedDateToDatetimeString(value);
} else {
return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
}
case ColumnTypes.JSON:
return JSON.stringify(value);
case ColumnTypes.SIMPLE_ARRAY:
return DataTransformationUtils.simpleArrayToString(value);
}
return value;
}
/**
* Prepares given value to a value to be persisted, based on its column type or metadata.
*/
prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any {
switch (columnMetadata.type) {
case ColumnTypes.BOOLEAN:
return value ? true : false;
// case ColumnTypes.DATETIME:
// if (value instanceof Date)
// return value;
//
// if (columnMetadata.loadInLocalTimezone) {
// return DataTransformationUtils.mixedDateToDatetimeString(value);
// } else {
// return DataTransformationUtils.mixedDateToUtcDatetimeString(value);
// }
case ColumnTypes.JSON:
return JSON.parse(value);
case ColumnTypes.SIMPLE_ARRAY:
return DataTransformationUtils.stringToSimpleArray(value);
}
return value;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Retrieves a new database connection.
* If pooling is enabled then connection from the pool will be retrieved.
* Otherwise active connection will be returned.
*/
protected retrieveDatabaseConnection(): Promise<DatabaseConnection> {
if (this.databaseConnection)
return Promise.resolve(this.databaseConnection);
throw new ConnectionIsNotSetError("websql");
}
}

View File

@ -0,0 +1,622 @@
import {QueryRunner} from "../../query-runner/QueryRunner";
import {DatabaseConnection} from "../DatabaseConnection";
import {ObjectLiteral} from "../../common/ObjectLiteral";
import {TransactionAlreadyStartedError} from "../error/TransactionAlreadyStartedError";
import {TransactionNotStartedError} from "../error/TransactionNotStartedError";
import {Logger} from "../../logger/Logger";
import {DataTypeNotSupportedByDriverError} from "../error/DataTypeNotSupportedByDriverError";
import {ColumnSchema} from "../../schema-builder/schema/ColumnSchema";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
import {TableSchema} from "../../schema-builder/schema/TableSchema";
import {ForeignKeySchema} from "../../schema-builder/schema/ForeignKeySchema";
import {PrimaryKeySchema} from "../../schema-builder/schema/PrimaryKeySchema";
import {IndexSchema} from "../../schema-builder/schema/IndexSchema";
import {QueryRunnerAlreadyReleasedError} from "../../query-runner/error/QueryRunnerAlreadyReleasedError";
import {NamingStrategyInterface} from "../../naming-strategy/NamingStrategyInterface";
import {WebsqlDriver} from "./WebsqlDriver";
/**
* Runs queries on a single websql database connection.
*/
export class WebsqlQueryRunner implements QueryRunner {
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
/**
* Indicates if connection for this query runner is released.
* Once its released, query runner cannot run queries anymore.
*/
protected isReleased = false;
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected databaseConnection: DatabaseConnection,
protected driver: WebsqlDriver,
protected logger: Logger) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Releases database connection. This is needed when using connection pooling.
* If connection is not from a pool, it should not be released.
* You cannot use this class's methods after its released.
*/
release(): Promise<void> {
if (this.databaseConnection.releaseCallback) {
this.isReleased = true;
return this.databaseConnection.releaseCallback();
}
return Promise.resolve();
}
/**
* Removes all tables from the currently connected database.
*/
async clearDatabase(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// await this.query(`PRAGMA foreign_keys = OFF;`);
await this.beginTransaction();
try {
const selectDropsQuery = `select 'drop table ' || name || ';' as query from sqlite_master where type = 'table' and name != 'sqlite_sequence'`;
const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery);
await Promise.all(dropQueries.map(q => this.query(q["query"])));
await this.commitTransaction();
} catch (error) {
await this.rollbackTransaction();
throw error;
} finally {
await this.release();
// await this.query(`PRAGMA foreign_keys = ON;`);
}
}
/**
* Starts transaction.
*/
async beginTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (this.databaseConnection.isTransactionActive)
throw new TransactionAlreadyStartedError();
this.databaseConnection.isTransactionActive = true;
// await this.query("BEGIN TRANSACTION");
}
/**
* Commits transaction.
*/
async commitTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
// await this.query("COMMIT");
this.databaseConnection.isTransactionActive = false;
}
/**
* Rollbacks transaction.
*/
async rollbackTransaction(): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
if (!this.databaseConnection.isTransactionActive)
throw new TransactionNotStartedError();
// await this.query("ROLLBACK");
this.databaseConnection.isTransactionActive = false;
}
/**
* Checks if transaction is in progress.
*/
isTransactionActive(): boolean {
return this.databaseConnection.isTransactionActive;
}
/**
* Executes a given SQL query.
*/
query(query: string, parameters?: any[]): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
this.logger.logQuery(query, parameters);
return new Promise((ok, fail) => {
const db = this.databaseConnection.connection;
// todo: check if transaction is not active
db.transaction((tx: any) => {
tx.executeSql(query, parameters, (tx: any, result: any) => {
const rows = Object
.keys(result.rows)
.filter(key => key !== "length")
.map(key => result.rows[key]);
ok(rows);
}, (tx: any, err: any) => {
this.logger.logFailedQuery(query, parameters);
this.logger.logQueryError(err);
return fail(err);
});
});
});
}
/**
* Insert a new row into given table.
*/
async insert(tableName: string, keyValues: ObjectLiteral, generatedColumn?: ColumnMetadata): Promise<any> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const keys = Object.keys(keyValues);
const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", ");
const values = keys.map((key, index) => "$" + (index + 1)).join(",");
const sql = columns.length > 0 ? (`INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`) : `INSERT INTO ${this.driver.escapeTableName(tableName)} DEFAULT VALUES`;
const parameters = keys.map(key => keyValues[key]);
this.logger.logQuery(sql, parameters);
return new Promise<any[]>((ok, fail) => {
const db = this.databaseConnection.connection;
// todo: check if transaction is not active
db.transaction((tx: any) => {
tx.executeSql(sql, parameters, (tx: any, result: any) => {
if (generatedColumn)
return ok(result["insertId"]);
ok();
}, (tx: any, err: any) => {
this.logger.logFailedQuery(sql, parameters);
this.logger.logQueryError(err);
return fail(err);
});
});
});
}
/**
* Updates rows that match given conditions in the given table.
*/
async update(tableName: string, valuesMap: ObjectLiteral, conditions: ObjectLiteral): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const updateValues = this.parametrize(valuesMap).join(", ");
const conditionString = this.parametrize(conditions, Object.keys(valuesMap).length).join(" AND ");
const query = `UPDATE ${this.driver.escapeTableName(tableName)} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`;
const updateParams = Object.keys(valuesMap).map(key => valuesMap[key]);
const conditionParams = Object.keys(conditions).map(key => conditions[key]);
const allParameters = updateParams.concat(conditionParams);
await this.query(query, allParameters);
}
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, condition: string, parameters?: any[]): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral): Promise<void>;
/**
* Deletes from the given table by a given conditions.
*/
async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND ");
const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters;
const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`;
await this.query(sql, parameters);
}
/**
* Inserts rows into closure table.
*/
async insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise<number> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
let sql = "";
if (hasLevel) {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant, level) ` +
`SELECT ancestor, ${newEntityId}, level + 1 FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}, 1`;
} else {
sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant) ` +
`SELECT ancestor, ${newEntityId} FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}`;
}
await this.query(sql);
const results: ObjectLiteral[] = await this.query(`SELECT MAX(level) as level FROM ${tableName} WHERE descendant = ${parentId}`);
return results && results[0] && results[0]["level"] ? parseInt(results[0]["level"]) + 1 : 1;
}
/**
* Loads all tables (with given names) from the database and creates a TableSchema from them.
*/
async loadSchemaTables(tableNames: string[], namingStrategy: NamingStrategyInterface): Promise<TableSchema[]> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// if no tables given then no need to proceed
if (!tableNames || !tableNames.length)
return [];
// load tables, columns, indices and foreign keys
const dbTables: ObjectLiteral[] = await this.query(`SELECT * FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence'`);
// if tables were not found in the db, no need to proceed
if (!dbTables || !dbTables.length)
return [];
// create table schemas for loaded tables
return Promise.all(dbTables.map(async dbTable => {
const tableSchema = new TableSchema(dbTable["name"]);
// load columns and indices
/*const [dbColumns, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([
this.query(`PRAGMA table_info("${dbTable["name"]}")`),
this.query(`PRAGMA index_list("${dbTable["name"]}")`),
this.query(`PRAGMA foreign_key_list("${dbTable["name"]}")`),
]);
// find column name with auto increment
let autoIncrementColumnName: string|undefined = undefined;
const tableSql: string = dbTable["sql"];
if (tableSql.indexOf("AUTOINCREMENT") !== -1) {
autoIncrementColumnName = tableSql.substr(0, tableSql.indexOf("AUTOINCREMENT"));
const comma = autoIncrementColumnName.lastIndexOf(",");
const bracket = autoIncrementColumnName.lastIndexOf("(");
if (comma !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(comma);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
} else if (bracket !== -1) {
autoIncrementColumnName = autoIncrementColumnName.substr(bracket);
autoIncrementColumnName = autoIncrementColumnName.substr(0, autoIncrementColumnName.lastIndexOf("\""));
autoIncrementColumnName = autoIncrementColumnName.substr(autoIncrementColumnName.indexOf("\"") + 1);
}
}
// create column schemas from the loaded columns
tableSchema.columns = dbColumns.map(dbColumn => {
const columnSchema = new ColumnSchema();
columnSchema.name = dbColumn["name"];
columnSchema.type = dbColumn["type"].toLowerCase();
columnSchema.default = dbColumn["dflt_value"] !== null && dbColumn["dflt_value"] !== undefined ? dbColumn["dflt_value"] : undefined;
columnSchema.isNullable = dbColumn["notnull"] === 0;
columnSchema.isPrimary = dbColumn["pk"] === 1;
columnSchema.comment = ""; // todo later
columnSchema.isGenerated = autoIncrementColumnName === dbColumn["name"];
const columnForeignKeys = dbForeignKeys
.filter(foreignKey => foreignKey["from"] === dbColumn["name"])
.map(foreignKey => {
const keyName = namingStrategy.foreignKeyName(dbTable["name"], [foreignKey["from"]], foreignKey["table"], [foreignKey["to"]]);
return new ForeignKeySchema(keyName, [foreignKey["from"]], [foreignKey["to"]], foreignKey["table"], foreignKey["on_delete"]); // todo: how sqlite return from and to when they are arrays? (multiple column foreign keys)
});
tableSchema.addForeignKeys(columnForeignKeys);
return columnSchema;
});
// create primary key schema
await Promise.all(dbIndices
.filter(index => index["origin"] === "pk")
.map(async index => {
const indexInfos: ObjectLiteral[] = await this.query(`PRAGMA index_info("${index["name"]}")`);
const indexColumns = indexInfos.map(indexInfo => indexInfo["name"]);
indexColumns.forEach(indexColumn => {
tableSchema.primaryKeys.push(new PrimaryKeySchema(index["name"], indexColumn));
});
}));
// create index schemas from the loaded indices
const indicesPromises = dbIndices
.filter(dbIndex => {
return dbIndex["origin"] !== "pk" &&
(!tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["name"])) &&
(!tableSchema.primaryKeys.find(primaryKey => primaryKey.name === dbIndex["name"]));
})
.map(dbIndex => dbIndex["name"])
.filter((value, index, self) => self.indexOf(value) === index) // unqiue
.map(async dbIndexName => {
const dbIndex = dbIndices.find(dbIndex => dbIndex["name"] === dbIndexName);
const indexInfos: ObjectLiteral[] = await this.query(`PRAGMA index_info("${dbIndex!["name"]}")`);
const indexColumns = indexInfos.map(indexInfo => indexInfo["name"]);
// check if db index is generated by sqlite itself and has special use case
if (dbIndex!["name"].substr(0, "sqlite_autoindex".length) === "sqlite_autoindex") {
if (dbIndex!["unique"] === 1) { // this means we have a special index generated for a column
// so we find and update the column
indexColumns.forEach(columnName => {
const column = tableSchema.columns.find(column => column.name === columnName);
if (column)
column.isUnique = true;
});
}
return Promise.resolve(undefined);
} else {
return new IndexSchema(dbTable["name"], dbIndex!["name"], indexColumns, dbIndex!["unique"] === "1");
}
});
const indices = await Promise.all(indicesPromises);
tableSchema.indices = indices.filter(index => !!index) as IndexSchema[];*/
return tableSchema;
}));
}
/**
* Creates a new table from the given table metadata and column metadatas.
*/
async createTable(table: TableSchema): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
// skip columns with foreign keys, we will add them later
const columnDefinitions = table.columns.map(column => this.buildCreateColumnSql(column)).join(", ");
let sql = `CREATE TABLE "${table.name}" (${columnDefinitions}`;
const primaryKeyColumns = table.columns.filter(column => column.isPrimary && !column.isGenerated);
if (primaryKeyColumns.length > 0)
sql += `, PRIMARY KEY(${primaryKeyColumns.map(column => `${column.name}`).join(", ")})`; // for some reason column escaping here generates a wrong schema
sql += `)`;
await this.query(sql);
}
/**
* Creates a new column from the column metadata in the table.
*/
async createColumns(tableSchema: TableSchema, columns: ColumnSchema[]): Promise<void> { // todo: remove column metadata returning
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
await this.recreateTable(tableSchema);
}
/**
* Changes a column in the table.
* Changed column looses all its keys in the db.
*/
async changeColumns(tableSchema: TableSchema, changedColumns: { newColumn: ColumnSchema, oldColumn: ColumnSchema }[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
return this.recreateTable(tableSchema);
}
/**
* Drops the columns in the table.
*/
async dropColumns(tableSchema: TableSchema, columns: ColumnSchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeColumns(columns);
return this.recreateTable(newTable);
}
/**
* Updates table's primary keys.
*/
async updatePrimaryKeys(dbTable: TableSchema): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
return this.recreateTable(dbTable);
}
/**
* Creates a new foreign keys.
*/
async createForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.addForeignKeys(foreignKeys);
return this.recreateTable(newTable);
}
/**
* Drops a foreign keys from the table.
*/
async dropForeignKeys(tableSchema: TableSchema, foreignKeys: ForeignKeySchema[]): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const newTable = tableSchema.clone();
newTable.removeForeignKeys(foreignKeys);
return this.recreateTable(newTable);
}
/**
* Creates a new index.
*/
async createIndex(index: IndexSchema): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const columnNames = index.columnNames.map(columnName => `"${columnName}"`).join(",");
const sql = `CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${index.name}" ON "${index.tableName}"(${columnNames})`;
await this.query(sql);
}
/**
* Drops an index from the table.
*/
async dropIndex(tableName: string, indexName: string, isGenerated: boolean = false): Promise<void> {
if (this.isReleased)
throw new QueryRunnerAlreadyReleasedError();
const sql = `DROP INDEX "${indexName}"`;
await this.query(sql);
}
/**
* Creates a database type from a given column metadata.
*/
normalizeType(column: ColumnMetadata) {
switch (column.normalizedDataType) {
case "string":
return "character varying(" + (column.length ? column.length : 255) + ")";
case "text":
return "text";
case "boolean":
return "boolean";
case "integer":
case "int":
return "integer";
case "smallint":
return "smallint";
case "bigint":
return "bigint";
case "float":
return "real";
case "double":
case "number":
return "double precision";
case "decimal":
if (column.precision && column.scale) {
return `decimal(${column.precision},${column.scale})`;
} else if (column.scale) {
return `decimal(${column.scale})`;
} else if (column.precision) {
return `decimal(${column.precision})`;
} else {
return "decimal";
}
case "date":
return "date";
case "time":
if (column.timezone) {
return "time with time zone";
} else {
return "time without time zone";
}
case "datetime":
if (column.timezone) {
return "timestamp with time zone";
} else {
return "timestamp without time zone";
}
case "json":
return "json";
case "simple_array":
return column.length ? "character varying(" + column.length + ")" : "text";
}
throw new DataTypeNotSupportedByDriverError(column.type, "SQLite");
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Parametrizes given object of values. Used to create column=value queries.
*/
protected parametrize(objectLiteral: ObjectLiteral, startIndex: number = 0): string[] {
return Object.keys(objectLiteral).map((key, index) => this.driver.escapeColumnName(key) + "=$" + (startIndex + index + 1));
}
/**
* Builds a query for create column.
*/
protected buildCreateColumnSql(column: ColumnSchema): string {
let c = "\"" + column.name + "\"";
if (column instanceof ColumnMetadata) {
c += " " + this.normalizeType(column);
} else {
c += " " + column.type;
}
if (column.isNullable !== true)
c += " NOT NULL";
if (column.isUnique === true)
c += " UNIQUE";
if (column.isGenerated === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.
c += " PRIMARY KEY AUTOINCREMENT";
if (column.default)
c += " DEFAULT '" + column.default + "'";
return c;
}
protected async recreateTable(tableSchema: TableSchema): Promise<void> {
// const withoutForeignKeyColumns = columns.filter(column => column.foreignKeys.length === 0);
// const createForeignKeys = options && options.createForeignKeys;
const columnDefinitions = tableSchema.columns.map(dbColumn => this.buildCreateColumnSql(dbColumn)).join(", ");
const columnNames = tableSchema.columns.map(column => `"${column.name}"`).join(", ");
let sql1 = `CREATE TABLE "temporary_${tableSchema.name}" (${columnDefinitions}`;
// if (options && options.createForeignKeys) {
tableSchema.foreignKeys.forEach(foreignKey => {
const columnNames = foreignKey.columnNames.map(name => `"${name}"`).join(", ");
const referencedColumnNames = foreignKey.referencedColumnNames.map(name => `"${name}"`).join(", ");
sql1 += `, FOREIGN KEY(${columnNames}) REFERENCES "${foreignKey.referencedTableName}"(${referencedColumnNames})`;
if (foreignKey.onDelete) sql1 += " ON DELETE " + foreignKey.onDelete;
});
const primaryKeyColumns = tableSchema.columns.filter(column => column.isPrimary && !column.isGenerated);
if (primaryKeyColumns.length > 0)
sql1 += `, PRIMARY KEY(${primaryKeyColumns.map(column => `${column.name}`).join(", ")})`; // for some reason column escaping here generate a wrong schema
sql1 += ")";
// todo: need also create uniques and indices?
// recreate a table with a temporary name
await this.query(sql1);
// migrate all data from the table into temporary table
const sql2 = `INSERT INTO "temporary_${tableSchema.name}" SELECT ${columnNames} FROM "${tableSchema.name}"`;
await this.query(sql2);
// drop old table
const sql3 = `DROP TABLE "${tableSchema.name}"`;
await this.query(sql3);
// rename temporary table
const sql4 = `ALTER TABLE "temporary_${tableSchema.name}" RENAME TO "${tableSchema.name}"`;
await this.query(sql4);
// also re-create indices
const indexPromises = tableSchema.indices.map(index => this.createIndex(index));
// const uniquePromises = tableSchema.uniqueKeys.map(key => this.createIndex(key));
await Promise.all(indexPromises/*.concat(uniquePromises)*/);
}
}

View File

@ -162,8 +162,8 @@ export abstract class BaseEntityManager {
/**
* Creates a new query builder that can be used to build an sql query.
*/
createQueryBuilder<Entity>(entityClass: ObjectType<Entity>, alias: string): QueryBuilder<Entity> {
return this.getRepository(entityClass).createQueryBuilder(alias);
createQueryBuilder<Entity>(entityClass: ObjectType<Entity>|Function|string, alias: string): QueryBuilder<Entity> {
return this.getRepository(entityClass as any).createQueryBuilder(alias);
}
/**
@ -239,7 +239,7 @@ export abstract class BaseEntityManager {
if (this.queryRunnerProvider && this.queryRunnerProvider.isReleased)
throw new QueryRunnerProviderAlreadyReleasedError();
const metadata = this.connection.entityMetadatas.findByTarget(entityClassOrName);
const metadata = this.connection.getMetadata(entityClassOrName);
let repositoryAggregator = this.repositoryAggregators.find(repositoryAggregate => repositoryAggregate.metadata === metadata);
if (!repositoryAggregator) {
repositoryAggregator = new RepositoryAggregator(

View File

@ -70,6 +70,9 @@ export class EntityManager extends BaseEntityManager {
return this.getRepository<Entity|Entity[]>(target).persist(entity);
} else {
if (target instanceof Array) {
if (target.length === 0)
return Promise.resolve(target);
return this.getRepository<Entity[]>(target[0].constructor).persist(entity as Entity[]);
} else {
return this.getRepository<Entity>(target.constructor).persist(entity as Entity);
@ -124,7 +127,7 @@ export class EntityManager extends BaseEntityManager {
}
}
/**
* Finds entities that match given conditions.
*/
@ -151,10 +154,10 @@ export class EntityManager extends BaseEntityManager {
find<Entity>(entityClass: ObjectType<Entity>, conditionsOrFindOptions?: ObjectLiteral|FindOptions, options?: FindOptions): Promise<Entity[]> {
if (conditionsOrFindOptions && options) {
return this.getRepository(entityClass).find(conditionsOrFindOptions, options);
} else if (conditionsOrFindOptions) {
return this.getRepository(entityClass).find(conditionsOrFindOptions);
} else {
return this.getRepository(entityClass).find();
}
@ -208,27 +211,27 @@ export class EntityManager extends BaseEntityManager {
/**
* Finds first entity that matches given conditions.
*/
findOne<Entity>(entityClass: ObjectType<Entity>): Promise<Entity>;
findOne<Entity>(entityClass: ObjectType<Entity>): Promise<Entity|undefined>;
/**
* Finds first entity that matches given conditions.
*/
findOne<Entity>(entityClass: ObjectType<Entity>, conditions: ObjectLiteral): Promise<Entity>;
findOne<Entity>(entityClass: ObjectType<Entity>, conditions: ObjectLiteral): Promise<Entity|undefined>;
/**
* Finds first entity that matches given conditions.
*/
findOne<Entity>(entityClass: ObjectType<Entity>, options: FindOptions): Promise<Entity>;
findOne<Entity>(entityClass: ObjectType<Entity>, options: FindOptions): Promise<Entity|undefined>;
/**
* Finds first entity that matches given conditions.
*/
findOne<Entity>(entityClass: ObjectType<Entity>, conditions: ObjectLiteral, options: FindOptions): Promise<Entity>;
findOne<Entity>(entityClass: ObjectType<Entity>, conditions: ObjectLiteral, options: FindOptions): Promise<Entity|undefined>;
/**
* Finds first entity that matches given conditions.
*/
findOne<Entity>(entityClass: ObjectType<Entity>, conditionsOrFindOptions?: ObjectLiteral|FindOptions, options?: FindOptions): Promise<Entity> {
findOne<Entity>(entityClass: ObjectType<Entity>, conditionsOrFindOptions?: ObjectLiteral|FindOptions, options?: FindOptions): Promise<Entity|undefined> {
if (conditionsOrFindOptions && options) {
return this.getRepository(entityClass).findOne(conditionsOrFindOptions, options);
@ -240,10 +243,18 @@ export class EntityManager extends BaseEntityManager {
}
}
/**
* Finds entities with ids.
* Optionally find options can be applied.
*/
findByIds<Entity>(entityClass: ObjectType<Entity>, ids: any[], options?: FindOptions): Promise<Entity[]> {
return this.getRepository(entityClass).findByIds(ids, options);
}
/**
* Finds entity with given id.
*/
findOneById<Entity>(entityClass: ObjectType<Entity>, id: any, options?: FindOptions): Promise<Entity> {
findOneById<Entity>(entityClass: ObjectType<Entity>, id: any, options?: FindOptions): Promise<Entity|undefined> {
return this.getRepository(entityClass).findOneById(id, options);
}

View File

@ -7,8 +7,8 @@ export class NoNeedToReleaseEntityManagerError extends Error {
constructor() {
super();
this.message = `Entity manager is not using single database connection and cannot be released. ` +
`Only entity managers created by connection#createEntityManagerWithSingleDatabaseConnection ` +
`methods have a single database connection and they should be released.`;
`Only entity managers created by connection#createEntityManagerWithSingleDatabaseConnection ` +
`methods have a single database connection and they should be released.`;
this.stack = new Error().stack;
}

View File

@ -16,7 +16,7 @@ export interface EntitySchema {
* Target bind to this entity schema. Optional.
*/
target?: Function;
/**
* Entity name.
*/
@ -143,7 +143,7 @@ export interface EntitySchema {
* Column collation. Note that not all databases support it.
*/
collation?: string; // todo: looks like this is not used
};
};
@ -188,12 +188,12 @@ export interface EntitySchema {
* First column of the join table.
*/
joinColumn?: JoinColumnOptions;
/**
* Second (inverse) column of the join table.
*/
inverseJoinColumn?: JoinColumnOptions;
};
/**
@ -257,7 +257,7 @@ export interface EntitySchema {
* Database cascade action on delete.
*/
onDelete?: OnDeleteType;
};
};

View File

@ -3,7 +3,7 @@ import {ObjectLiteral} from "../common/ObjectLiteral";
/**
* Options to be passed to find methods.
*
*
* Example:
* const options: FindOptions = {
* alias: "photo",
@ -90,7 +90,7 @@ export interface FindOptions {
having?: string;
/**
* WHERE conditions. Key-value object pair, where each key is a column name and value is a column value.
* WHERE conditions. Key-value object pair, where each key is a column name and value is a column value.
* "AND" is applied between all parameters.
*/
whereConditions?: ObjectLiteral;
@ -115,7 +115,7 @@ export interface FindOptions {
* Array of columns to LEFT JOIN.
*/
leftJoinAndSelect?: { [key: string]: string };
/**
* Array of columns to INNER JOIN.
*/
@ -135,5 +135,10 @@ export interface FindOptions {
* Parameters used in the WHERE and HAVING expressions.
*/
parameters?: Object;
/**
* Indicates if query builder should add virtual columns to the entity too.
*/
enabledOptions?: ("RELATION_ID_VALUES")[];
}

View File

@ -1,4 +1,3 @@
import {FindOptions} from "./FindOptions";
import {QueryBuilder} from "../query-builder/QueryBuilder";
@ -27,7 +26,8 @@ export class FindOptionsUtils {
!!possibleOptions.innerJoinAndSelect ||
!!possibleOptions.leftJoin ||
!!possibleOptions.innerJoin ||
!!possibleOptions.parameters
!!possibleOptions.parameters ||
!!possibleOptions.enabledOptions
);
}
@ -35,7 +35,7 @@ export class FindOptionsUtils {
* Applies give find options to the given query builder.
*/
static applyOptionsToQueryBuilder(qb: QueryBuilder<any>, options: FindOptions): QueryBuilder<any> {
if (options.limit)
qb.setLimit(options.limit);
if (options.offset)
@ -48,47 +48,47 @@ export class FindOptionsUtils {
qb.where(options.where);
if (options.having)
qb.having(options.having);
if (options.whereConditions) {
Object.keys(options.whereConditions).forEach(key => {
const name = key.indexOf(".") === -1 ? options.alias + "." + key : key;
qb.andWhere(name + "=:" + key);
});
qb.addParameters(options.whereConditions);
qb.setParameters(options.whereConditions);
}
if (options.havingConditions) {
Object.keys(options.havingConditions).forEach(key => {
const name = key.indexOf(".") === -1 ? options.alias + "." + key : key;
qb.andHaving(name + "=:" + key);
});
qb.addParameters(options.havingConditions);
qb.setParameters(options.havingConditions);
}
if (options.orderBy)
Object.keys(options.orderBy).forEach(columnName => qb.addOrderBy(columnName, options.orderBy![columnName]));
if (options.groupBy)
options.groupBy.forEach(groupBy => qb.addGroupBy(groupBy));
if (options.leftJoin)
Object.keys(options.leftJoin).forEach(key => {
if (options.leftJoin) // this check because of tsc bug
qb.leftJoin(options.leftJoin[key], key);
});
if (options.innerJoin)
Object.keys(options.innerJoin).forEach(key => {
if (options.innerJoin) // this check because of tsc bug
qb.innerJoin(options.innerJoin[key], key);
});
if (options.leftJoinAndSelect)
Object.keys(options.leftJoinAndSelect).forEach(key => {
if (options.leftJoinAndSelect) // this check because of tsc bug
qb.leftJoinAndSelect(options.leftJoinAndSelect[key], key);
});
if (options.innerJoinAndSelect)
Object.keys(options.innerJoinAndSelect).forEach(key => {
if (options.innerJoinAndSelect) // this check because of tsc bug
@ -96,9 +96,15 @@ export class FindOptionsUtils {
});
if (options.parameters)
qb.addParameters(options.parameters);
qb.setParameters(options.parameters);
if (options.enabledOptions) {
options.enabledOptions.forEach(option => {
qb.enableOption(option);
});
}
return qb;
}
}

View File

@ -1,6 +1,5 @@
/*!
*/
import {DriverOptions} from "./driver/DriverOptions";
import {ConnectionManager} from "./connection/ConnectionManager";
import {Connection} from "./connection/Connection";
@ -113,7 +112,7 @@ export function getConnectionManager(): ConnectionManager {
* it will try to create connection from environment variables.
* There are several environment variables you can set:
*
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "mysql2", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_URL - database connection url. Should be a string.
* - TYPEORM_HOST - database host. Should be a string.
* - TYPEORM_PORT - database access port. Should be a number.
@ -164,7 +163,7 @@ export function createConnection(optionsOrConnectionNameFromConfig?: ConnectionO
* it will try to create connection from environment variables.
* There are several environment variables you can set:
*
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "mysql2", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_DRIVER_TYPE - driver type. Can be "mysql", "postgres", "mariadb", "sqlite", "oracle" or "mssql".
* - TYPEORM_URL - database connection url. Should be a string.
* - TYPEORM_HOST - database host. Should be a string.
* - TYPEORM_PORT - database access port. Should be a number.

View File

@ -2,76 +2,106 @@ import {RelationMetadata} from "../metadata/RelationMetadata";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {Connection} from "../connection/Connection";
/**
* This class wraps entities and provides functions there to lazily load its relations.
*/
export class LazyRelationsWrapper {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(private connection: Connection) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
wrap(object: Object, relation: RelationMetadata) {
const connection = this.connection;
const index = "__" + relation.propertyName + "__";
const loadIndex = "__load_" + relation.propertyName + "__";
const resolveIndex = "__has_" + relation.propertyName + "__";
const promiseIndex = "__promise__" + relation.propertyName + "__";
const resolveIndex = "__has__" + relation.propertyName + "__";
Object.defineProperty(object, relation.propertyName, {
get: function() {
if (this[resolveIndex] === true)
return Promise.resolve(this[index]);
if (this[loadIndex])
return this[loadIndex];
if (this[promiseIndex])
return this[promiseIndex];
// create shortcuts for better readability
const escapeAlias = (alias: string) => connection.driver.escapeAliasName(alias);
const escapeColumn = (column: string) => connection.driver.escapeColumnName(column);
const qb = new QueryBuilder(connection);
if (relation.isManyToMany || relation.isOneToMany) {
if (relation.isManyToMany) {
if (relation.hasInverseSide) { // if we don't have inverse side then we can't select and join by relation from inverse side
qb.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName)
.innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName);
} else {
qb.select(relation.propertyName)
.from(relation.type, relation.propertyName)
.innerJoin(relation.junctionEntityMetadata.table.name, relation.junctionEntityMetadata.name, "ON",
`${relation.junctionEntityMetadata.name}.${relation.name}=:${relation.propertyName}Id`)
.setParameter(relation.propertyName + "Id", this[relation.referencedColumnName]);
}
qb.select(relation.propertyName)
.from(relation.type, relation.propertyName)
.innerJoin(relation.junctionEntityMetadata.table.name, relation.junctionEntityMetadata.name,
`${escapeAlias(relation.junctionEntityMetadata.name)}.${escapeColumn(relation.name)}=:${relation.propertyName}Id`)
.setParameter(relation.propertyName + "Id", this[relation.referencedColumnName]);
this[loadIndex] = qb.getResults().then(results => {
this[promiseIndex] = qb.getMany().then(results => {
this[index] = results;
this[resolveIndex] = true;
delete this[loadIndex];
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
return this[loadIndex];
return this[promiseIndex];
} else if (relation.isOneToMany) {
qb.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName)
.innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName)
.andWhereInIds([relation.entityMetadata.getEntityIdMixedMap(this)]);
this[promiseIndex] = qb.getMany().then(results => {
this[index] = results;
this[resolveIndex] = true;
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
return this[promiseIndex];
} else {
if (relation.hasInverseSide) {
qb.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName)
.innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName);
.innerJoin(`${relation.propertyName}.${relation.inverseRelation.propertyName}`, relation.entityMetadata.targetName)
.andWhereInIds([relation.entityMetadata.getEntityIdMixedMap(this)]);
} else {
// (ow) post.category<=>category.post
// loaded: category from post
// example: SELECT category.id AS category_id, category.name AS category_name FROM category category
// INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
qb.select(relation.propertyName) // category
.from(relation.type, relation.propertyName) // Category, category
.innerJoin(relation.entityMetadata.target as Function, relation.entityMetadata.name, "ON",
`${relation.entityMetadata.name}.${relation.propertyName}=:${relation.propertyName}Id`) // Post, post, post.category = categoryId
.setParameter(relation.propertyName + "Id", this[relation.referencedColumnName]);
.innerJoin(relation.entityMetadata.target as Function, relation.entityMetadata.name,
`${escapeAlias(relation.entityMetadata.name)}.${escapeColumn(relation.propertyName)}=${escapeAlias(relation.propertyName)}.${escapeColumn(relation.referencedColumn.propertyName)}`)
.andWhereInIds([relation.entityMetadata.getEntityIdMixedMap(this)]);
}
// console.log(qb.getSql());
this[loadIndex] = qb.getSingleResult().then(result => {
this[promiseIndex] = qb.getOne().then(result => {
this[index] = result;
this[resolveIndex] = true;
delete this[loadIndex];
delete this[promiseIndex];
return this[index];
}).catch(err => {
throw err;
});
return this[loadIndex];
return this[promiseIndex];
}
},
set: function(promise: Promise<any>) {
@ -88,5 +118,5 @@ export class LazyRelationsWrapper {
configurable: true
});
}
}

View File

@ -1,4 +1,5 @@
import {LoggerOptions} from "./LoggerOptions";
import {PlatformTools} from "../platform/PlatformTools";
/**
* Performs logging of the events in TypeORM.
@ -21,8 +22,8 @@ export class Logger {
*/
logQuery(query: string, parameters?: any[]) {
if (this.options.logQueries ||
process.env.LOGGER_CLI_SCHEMA_SYNC)
this.log("log", `executing query: ${query}${parameters && parameters.length ? " -- PARAMETERS: " + JSON.stringify(parameters) : ""}`);
PlatformTools.getEnvVariable("LOGGER_CLI_SCHEMA_SYNC"))
this.log("log", `executing query: ${query}${parameters && parameters.length ? " -- PARAMETERS: " + this.stringifyParams(parameters) : ""}`);
}
/**
@ -31,8 +32,8 @@ export class Logger {
logFailedQuery(query: string, parameters?: any[]) {
if (this.options.logQueries ||
this.options.logOnlyFailedQueries ||
process.env.LOGGER_CLI_SCHEMA_SYNC)
this.log("error", `query failed: ${query}${parameters && parameters.length ? " -- PARAMETERS: " + JSON.stringify(parameters) : ""}`);
PlatformTools.getEnvVariable("LOGGER_CLI_SCHEMA_SYNC"))
this.log("error", `query failed: ${query}${parameters && parameters.length ? " -- PARAMETERS: " + this.stringifyParams(parameters) : ""}`);
}
/**
@ -40,7 +41,7 @@ export class Logger {
*/
logQueryError(error: any) {
if (this.options.logFailedQueryError ||
process.env.LOGGER_CLI_SCHEMA_SYNC)
PlatformTools.getEnvVariable("LOGGER_CLI_SCHEMA_SYNC"))
this.log("error", "error during executing query:" + error);
}
@ -49,7 +50,7 @@ export class Logger {
*/
logSchemaBuild(message: string) {
if (this.options.logSchemaCreation ||
process.env.LOGGER_CLI_SCHEMA_SYNC)
PlatformTools.getEnvVariable("LOGGER_CLI_SCHEMA_SYNC"))
this.log("info", message);
}
@ -80,4 +81,21 @@ export class Logger {
}
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Converts parameters to a string.
* Sometimes parameters can have circular objects and therefor we are handle this case too.
*/
protected stringifyParams(parameters: any[]) {
try {
return JSON.stringify(parameters);
} catch (error) { // most probably circular objects in parameters
return parameters;
}
}
}

View File

@ -18,14 +18,14 @@ export interface ColumnMetadataArgs {
/**
* Class's property type (reflected) to which column is applied.
*
*
* todo: check when this is not set, because for the entity schemas we don't set it.
*/
readonly propertyType?: string;
/**
* Column mode in which column will work.
*
*
* todo: find name better then "mode".
*/
readonly mode: ColumnMode;
@ -34,5 +34,5 @@ export interface ColumnMetadataArgs {
* Extra column options.
*/
readonly options: ColumnOptions;
}

View File

@ -12,5 +12,5 @@ export interface DiscriminatorValueMetadataArgs {
* Discriminator value.
*/
readonly value: any;
}

View File

@ -17,5 +17,5 @@ export interface EmbeddedMetadataArgs {
* Type of the class to be embedded.
*/
readonly type: ((type?: any) => Function);
}

View File

@ -19,5 +19,5 @@ export interface EntityListenerMetadataArgs {
* The type of the listener.
*/
readonly type: EventListenerType;
}

View File

@ -11,6 +11,7 @@ import {EmbeddedMetadata} from "../metadata/EmbeddedMetadata";
*/
export interface EntityMetadataArgs {
readonly junction: boolean;
readonly target: Function|string;
readonly tablesPrefix?: string;
readonly inheritanceType?: "single-table"|"class-table";
@ -22,5 +23,5 @@ export interface EntityMetadataArgs {
readonly indexMetadatas?: IndexMetadata[];
readonly foreignKeyMetadatas?: ForeignKeyMetadata[];
readonly embeddedMetadatas?: EmbeddedMetadata[];
}

View File

@ -1,4 +1,3 @@
/**
* Arguments for EntitySubscriberMetadata class.
*/
@ -8,5 +7,5 @@ export interface EntitySubscriberMetadataArgs {
* Class to which subscriber is applied.
*/
readonly target: Function;
}

View File

@ -1,4 +1,3 @@
/**
* Arguments for IndexMetadata class.
*/
@ -23,5 +22,5 @@ export interface IndexMetadataArgs {
* Indicates if index must be unique or not.
*/
readonly unique: boolean;
}

View File

@ -12,5 +12,5 @@ export interface InheritanceMetadataArgs {
* Inheritance type.
*/
readonly type: "single-table"|"class-table";
}

View File

@ -22,5 +22,5 @@ export interface JoinColumnMetadataArgs {
* Name of the column in the entity to which this column is referenced.
*/
readonly referencedColumnName?: string;
}

View File

@ -30,5 +30,5 @@ export interface JoinTableMetadataArgs {
* Second (inverse) column of the join table.
*/
readonly inverseJoinColumn?: JoinColumnOptions;
}

View File

@ -56,7 +56,7 @@ export class MetadataArgsStorage {
const allTableMetadataArgs = classes ? this.tables.filterByTargets(classes) : this.tables;
const tableMetadatas = allTableMetadataArgs.filter(table => table.type === "regular" || table.type === "closure" || table.type === "class-table-child");
return tableMetadatas.map(tableMetadata => {
return tableMetadatas.toArray().map(tableMetadata => {
return this.mergeWithAbstract(allTableMetadataArgs, tableMetadata);
});
}
@ -68,7 +68,7 @@ export class MetadataArgsStorage {
const tables = classes ? this.tables.filterByTargets(classes) : this.tables;
const embeddableTableMetadatas = tables.filter(table => table.type === "embeddable");
return embeddableTableMetadatas.map(embeddableTableMetadata => {
return embeddableTableMetadatas.toArray().map(embeddableTableMetadata => {
return this.mergeWithEmbeddable(embeddableTableMetadatas, embeddableTableMetadata);
});
}
@ -79,8 +79,8 @@ export class MetadataArgsStorage {
/**
*/
private mergeWithAbstract(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
table: TableMetadataArgs) {
protected mergeWithAbstract(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
table: TableMetadataArgs) {
const indices = this.indices.filterByTarget(table.target);
const columns = this.columns.filterByTarget(table.target);
@ -92,21 +92,21 @@ export class MetadataArgsStorage {
const relationIds = this.relationIds.filterByTarget(table.target);
const embeddeds = this.embeddeds.filterByTarget(table.target);
const inheritances = this.inheritances.filterByTarget(table.target);
const inheritance = (inheritances.length > 0) ? inheritances[0] : undefined;
const inheritance = (inheritances.length > 0) ? inheritances.toArray()[0] : undefined;
const discriminatorValues: DiscriminatorValueMetadataArgs[] = [];
// find parent if this table is class-table-child
let parent: TableMetadataArgs|undefined = undefined;
// merge metadata from abstract tables
allTableMetadatas.forEach(inheritedTable => {
allTableMetadatas.toArray().forEach(inheritedTable => {
if (table.type === "single-table-child") return;
if (!table.target || !inheritedTable.target) return;
if (!(table.target instanceof Function) || !(inheritedTable.target instanceof Function)) return;
if (!this.isInherited(table.target, inheritedTable.target)) return;
// check if inheritedTable is a class with class table inheritance - then we don't need to merge its columns, relations, etc. things
if (!!this.inheritances.filterByTarget(inheritedTable.target).find(inheritance => inheritance.type === "class-table")) {
if (!!this.inheritances.filterByTarget(inheritedTable.target).toArray().find(inheritance => inheritance.type === "class-table")) {
parent = inheritedTable;
return;
}
@ -114,36 +114,44 @@ export class MetadataArgsStorage {
const metadatasFromAbstract = this.mergeWithAbstract(allTableMetadatas, inheritedTable);
metadatasFromAbstract.columns
.filterRepeatedMetadatas(columns)
.forEach(metadata => columns.push(metadata));
.filterRepeatedMetadatas(columns.toArray())
.toArray()
.forEach(metadata => columns.add(metadata));
metadatasFromAbstract.relations
.filterRepeatedMetadatas(relations)
.forEach(metadata => relations.push(metadata));
.filterRepeatedMetadatas(relations.toArray())
.toArray()
.forEach(metadata => relations.add(metadata));
metadatasFromAbstract.joinColumns
.filterRepeatedMetadatas(joinColumns)
.forEach(metadata => joinColumns.push(metadata));
.filterRepeatedMetadatas(joinColumns.toArray())
.toArray()
.forEach(metadata => joinColumns.add(metadata));
metadatasFromAbstract.joinTables
.filterRepeatedMetadatas(joinTables)
.forEach(metadata => joinTables.push(metadata));
.filterRepeatedMetadatas(joinTables.toArray())
.toArray()
.forEach(metadata => joinTables.add(metadata));
metadatasFromAbstract.entityListeners
.filterRepeatedMetadatas(entityListeners)
.forEach(metadata => entityListeners.push(metadata));
.filterRepeatedMetadatas(entityListeners.toArray())
.toArray()
.forEach(metadata => entityListeners.add(metadata));
metadatasFromAbstract.relationCounts
.filterRepeatedMetadatas(relationCounts)
.forEach(metadata => relationCounts.push(metadata));
.filterRepeatedMetadatas(relationCounts.toArray())
.toArray()
.forEach(metadata => relationCounts.add(metadata));
metadatasFromAbstract.relationIds
.filterRepeatedMetadatas(relationIds)
.forEach(metadata => relationIds.push(metadata));
.filterRepeatedMetadatas(relationIds.toArray())
.toArray()
.forEach(metadata => relationIds.add(metadata));
metadatasFromAbstract.embeddeds
.filterRepeatedMetadatas(embeddeds)
.forEach(metadata => embeddeds.push(metadata));
.filterRepeatedMetadatas(embeddeds.toArray())
.toArray()
.forEach(metadata => embeddeds.add(metadata));
});
@ -151,7 +159,7 @@ export class MetadataArgsStorage {
const children: TableMetadataArgs[] = [];
if (inheritance && inheritance.type === "single-table") {
allTableMetadatas.forEach(childTable => {
allTableMetadatas.toArray().forEach(childTable => {
if (childTable.type !== "single-table-child") return;
if (!childTable.target || !table.target) return;
if (!(childTable.target instanceof Function) || !(table.target instanceof Function)) return;
@ -160,6 +168,7 @@ export class MetadataArgsStorage {
children.push(childTable);
this.discriminatorValues
.filterByTarget(childTable.target)
.toArray()
.forEach(metadata => discriminatorValues.push(metadata));
// for single table inheritance we also merge all columns, relation, etc. into same table
@ -167,36 +176,44 @@ export class MetadataArgsStorage {
const metadatasFromAbstract = this.mergeWithAbstract(allTableMetadatas, childTable);
metadatasFromAbstract.columns
.filterRepeatedMetadatas(columns)
.forEach(metadata => columns.push(metadata));
.filterRepeatedMetadatas(columns.toArray())
.toArray()
.forEach(metadata => columns.add(metadata));
metadatasFromAbstract.relations
.filterRepeatedMetadatas(relations)
.forEach(metadata => relations.push(metadata));
.filterRepeatedMetadatas(relations.toArray())
.toArray()
.forEach(metadata => relations.add(metadata));
metadatasFromAbstract.joinColumns
.filterRepeatedMetadatas(joinColumns)
.forEach(metadata => joinColumns.push(metadata));
.filterRepeatedMetadatas(joinColumns.toArray())
.toArray()
.forEach(metadata => joinColumns.add(metadata));
metadatasFromAbstract.joinTables
.filterRepeatedMetadatas(joinTables)
.forEach(metadata => joinTables.push(metadata));
.filterRepeatedMetadatas(joinTables.toArray())
.toArray()
.forEach(metadata => joinTables.add(metadata));
metadatasFromAbstract.entityListeners
.filterRepeatedMetadatas(entityListeners)
.forEach(metadata => entityListeners.push(metadata));
.filterRepeatedMetadatas(entityListeners.toArray())
.toArray()
.forEach(metadata => entityListeners.add(metadata));
metadatasFromAbstract.relationCounts
.filterRepeatedMetadatas(relationCounts)
.forEach(metadata => relationCounts.push(metadata));
.filterRepeatedMetadatas(relationCounts.toArray())
.toArray()
.forEach(metadata => relationCounts.add(metadata));
metadatasFromAbstract.relationIds
.filterRepeatedMetadatas(relationIds)
.forEach(metadata => relationIds.push(metadata));
.filterRepeatedMetadatas(relationIds.toArray())
.toArray()
.forEach(metadata => relationIds.add(metadata));
metadatasFromAbstract.embeddeds
.filterRepeatedMetadatas(embeddeds)
.forEach(metadata => embeddeds.push(metadata));
.filterRepeatedMetadatas(embeddeds.toArray())
.toArray()
.forEach(metadata => embeddeds.add(metadata));
metadatasFromAbstract.children
.forEach(metadata => children.push(metadata));
@ -221,11 +238,11 @@ export class MetadataArgsStorage {
discriminatorValues: discriminatorValues
};
}
/**
*/
private mergeWithEmbeddable(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
tableMetadata: TableMetadataArgs) {
protected mergeWithEmbeddable(allTableMetadatas: TargetMetadataArgsCollection<TableMetadataArgs>,
tableMetadata: TableMetadataArgs) {
const columns = this.columns.filterByTarget(tableMetadata.target);
allTableMetadatas
@ -234,12 +251,14 @@ export class MetadataArgsStorage {
if (!(tableMetadata.target instanceof Function) || !(metadata.target instanceof Function)) return false;
return this.isInherited(tableMetadata.target, metadata.target); // todo: fix it for entity schema
})
.toArray()
.forEach(parentMetadata => {
const metadatasFromParents = this.mergeWithEmbeddable(allTableMetadatas, parentMetadata);
metadatasFromParents.columns
.filterRepeatedMetadatas(columns)
.forEach(metadata => columns.push(metadata));
.filterRepeatedMetadatas(columns.toArray())
.toArray()
.forEach(metadata => columns.add(metadata));
});
return {
@ -251,12 +270,11 @@ export class MetadataArgsStorage {
/**
* Checks if this table is inherited from another table.
*/
private isInherited(target1: Function, target2: Function) {
protected isInherited(target1: Function, target2: Function) {
// we cannot use instanceOf in this method, because we need order of inherited tables, to ensure that
// properties get inherited in a right order. To achieve it we can only check a first parent of the class
// return this.target.prototype instanceof anotherTable.target;
return Object.getPrototypeOf(target1.prototype).constructor === target2;
}
}

View File

@ -12,5 +12,5 @@ export interface NamingStrategyMetadataArgs {
* Strategy name.
*/
readonly name: string;
}

View File

@ -17,5 +17,5 @@ export interface RelationCountMetadataArgs {
* Target's relation which it should count.
*/
readonly relation: string|((object: any) => any);
}

View File

@ -17,5 +17,5 @@ export interface RelationIdMetadataArgs {
* Target's relation which it should count.
*/
readonly relation: string|((object: any) => any);
}

View File

@ -30,7 +30,7 @@ export interface RelationMetadataArgs {
/**
* Original (reflected) class's property type.
*
*
* todo: this can be empty for relations from entity schemas.
*/
readonly propertyType?: any;
@ -65,5 +65,5 @@ export interface RelationMetadataArgs {
* Indicates if this is a children (can be only one-to-many relation) relation in the tree tables.
*/
readonly isTreeChildren?: boolean;
}

View File

@ -38,5 +38,5 @@ export interface TableMetadataArgs {
* Whether table must be synced during schema build or not
*/
readonly skipSchemaSync?: boolean;
}

View File

@ -1,43 +0,0 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {EntityMetadataNotFound} from "../error/EntityMetadataNotFound";
/**
* Array for the entity metadatas.
*/
export class EntityMetadataCollection extends Array<EntityMetadata> {
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
hasTarget(target: Function|string) {
return !!this.find(metadata => metadata.target === target || (typeof target === "string" && metadata.targetName === target));
}
findByTarget(target: Function|string): EntityMetadata {
const metadata = this.find(metadata => metadata.target === target || (typeof target === "string" && metadata.targetName === target));
if (!metadata)
throw new EntityMetadataNotFound(target);
return metadata;
}
findByName(name: string) {
const metadata = this.find(metadata => metadata.name === name);
if (!metadata)
throw new EntityMetadataNotFound(name);
return metadata;
}
filter(callbackfn: (value: EntityMetadata, index?: number, array?: Array<EntityMetadata>) => any, thisArg?: any): EntityMetadataCollection {
thisArg = thisArg || void 0;
return this.reduce(function(out: EntityMetadataCollection, val: EntityMetadata, index: number, array: Array<EntityMetadata>) {
if (callbackfn.call(thisArg, val, index, array)) {
out.push(val);
}
return out;
}, new EntityMetadataCollection());
}
}

View File

@ -1,6 +1,6 @@
import {TargetMetadataArgsCollection} from "./TargetMetadataArgsCollection";
export class PropertyMetadataArgsCollection<T extends { target?: Function|string, propertyName?: string }> extends TargetMetadataArgsCollection<T> {
export class PropertyMetadataArgsCollection<T extends { target?: Function|string, propertyName?: string }> extends TargetMetadataArgsCollection<T> {
// -------------------------------------------------------------------------
// Public Methods
@ -13,7 +13,7 @@ export class PropertyMetadataArgsCollection<T extends { target?: Function|string
}
findByProperty(propertyName: string) {
return this.find(item => item.propertyName === propertyName);
return this.items.find(item => item.propertyName === propertyName);
}
hasWithProperty(propertyName: string) {

View File

@ -1,24 +1,37 @@
import {MetadataAlreadyExistsError} from "../../metadata-builder/error/MetadataAlreadyExistsError";
export class TargetMetadataArgsCollection<T extends { target?: Function|string }> extends Array<T> {
export class TargetMetadataArgsCollection<T extends { target?: Function|string }> {
// -------------------------------------------------------------------------
// Protected Properties
// -------------------------------------------------------------------------
protected items: T[] = [];
// -------------------------------------------------------------------------
// Public Properties
// -------------------------------------------------------------------------
get length() {
return this.items.length;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): this {
const collection = new (<any> this.constructor)();
super.filter(callbackfn)
.forEach(metadata => collection.add(metadata));
this.items.filter(callbackfn).forEach(metadata => collection.add(metadata));
return collection;
}
filterByTarget(cls?: Function|string): this {
// if no class specified then simply return empty collection
if (!cls)
return new (<any> this.constructor)();
return this.filterByTargets([cls]);
}
@ -38,15 +51,19 @@ export class TargetMetadataArgsCollection<T extends { target?: Function|string }
throw new MetadataAlreadyExistsError((<any> metadata.constructor).name, metadata.target);
}
this.push(metadata);
this.items.push(metadata);
}
toArray() {
return this.items.map(item => item);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private hasWithTarget(constructor: Function): boolean {
return !!this.find(metadata => metadata.target === constructor);
return !!this.items.find(metadata => metadata.target === constructor);
}
}

View File

@ -23,7 +23,7 @@ export interface ClosureJunctionEntityMetadataBuilderArgs {
* Helps to create EntityMetadatas for junction tables for closure tables.
*/
export class ClosureJunctionEntityMetadataBuilder {
build(driver: Driver, lazyRelationsWrapper: LazyRelationsWrapper, args: ClosureJunctionEntityMetadataBuilderArgs) {
const columns = [
@ -37,7 +37,7 @@ export class ClosureJunctionEntityMetadataBuilder {
type: args.primaryColumn.type,
name: "ancestor"
}
}),
}),
new ColumnMetadata(<ColumnMetadataArgs> {
target: "__virtual__",
propertyName: "__virtual__",
@ -50,7 +50,7 @@ export class ClosureJunctionEntityMetadataBuilder {
}
})
];
if (args.hasTreeLevelColumn) {
columns.push(new ColumnMetadata(<ColumnMetadataArgs> {
target: "__virtual__",
@ -71,6 +71,7 @@ export class ClosureJunctionEntityMetadataBuilder {
});
return new EntityMetadata({
junction: true,
target: "__virtual__",
tablesPrefix: driver.options.tablesPrefix,
namingStrategy: args.namingStrategy,
@ -82,5 +83,5 @@ export class ClosureJunctionEntityMetadataBuilder {
]
}, lazyRelationsWrapper);
}
}

View File

@ -34,7 +34,7 @@ export class EntityMetadataBuilder {
// todo: check if multiple tree parent metadatas in validator
// todo: tree decorators can be used only on closure table (validation)
// todo: throw error if parent tree metadata was not specified in a closure table
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
@ -47,7 +47,7 @@ export class EntityMetadataBuilder {
// extract into separate class?
schemas.forEach(schema => {
// add table metadata args from the schema
const tableSchema = schema.table || {} as any;
const table: TableMetadataArgs = {
@ -58,7 +58,7 @@ export class EntityMetadataBuilder {
orderBy: tableSchema.orderBy
};
metadataArgsStorage.tables.add(table);
// add columns metadata args from the schema
Object.keys(schema.columns).forEach(columnName => {
const columnSchema = schema.columns[columnName];
@ -73,7 +73,7 @@ export class EntityMetadataBuilder {
mode = "treeChildrenCount";
if (columnSchema.treeLevel)
mode = "treeLevel";
const column: ColumnMetadataArgs = {
target: schema.target || schema.name,
mode: mode,
@ -93,10 +93,10 @@ export class EntityMetadataBuilder {
scale: columnSchema.scale
}
};
metadataArgsStorage.columns.add(column);
});
// add relation metadata args from the schema
if (schema.relations) {
Object.keys(schema.relations).forEach(relationName => {
@ -129,7 +129,7 @@ export class EntityMetadataBuilder {
target: schema.target || schema.name,
propertyName: relationName
};
metadataArgsStorage.joinColumns.push(joinColumn);
metadataArgsStorage.joinColumns.add(joinColumn);
} else {
const joinColumn: JoinColumnMetadataArgs = {
target: schema.target || schema.name,
@ -137,7 +137,7 @@ export class EntityMetadataBuilder {
name: relationSchema.joinColumn.name,
referencedColumnName: relationSchema.joinColumn.referencedColumnName
};
metadataArgsStorage.joinColumns.push(joinColumn);
metadataArgsStorage.joinColumns.add(joinColumn);
}
}
@ -148,7 +148,7 @@ export class EntityMetadataBuilder {
target: schema.target || schema.name,
propertyName: relationName
};
metadataArgsStorage.joinTables.push(joinTable);
metadataArgsStorage.joinTables.add(joinTable);
} else {
const joinTable: JoinTableMetadataArgs = {
target: schema.target || schema.name,
@ -157,13 +157,13 @@ export class EntityMetadataBuilder {
joinColumn: relationSchema.joinTable.joinColumn,
inverseJoinColumn: relationSchema.joinTable.inverseJoinColumn
};
metadataArgsStorage.joinTables.push(joinTable);
metadataArgsStorage.joinTables.add(joinTable);
}
}
});
}
});
return this.build(driver, lazyRelationsWrapper, metadataArgsStorage, namingStrategy);
}
@ -191,43 +191,43 @@ export class EntityMetadataBuilder {
const allMergedArgs = metadataArgsStorage.getMergedTableMetadatas(entityClasses);
allMergedArgs.forEach(mergedArgs => {
const tables = [mergedArgs.table].concat(mergedArgs.children);
tables.forEach(tableArgs => {
// find embeddable tables for embeddeds registered in this table and create EmbeddedMetadatas from them
const embeddeds: EmbeddedMetadata[] = [];
mergedArgs.embeddeds.forEach(embedded => {
mergedArgs.embeddeds.toArray().forEach(embedded => {
const embeddableTable = embeddableMergedArgs.find(embeddedMergedArgs => embeddedMergedArgs.table.target === embedded.type());
if (embeddableTable) {
const table = new TableMetadata(embeddableTable.table);
const columns = embeddableTable.columns.map(args => new ColumnMetadata(args));
const columns = embeddableTable.columns.toArray().map(args => new ColumnMetadata(args));
embeddeds.push(new EmbeddedMetadata(embedded.type(), embedded.propertyName, table, columns));
}
});
// create metadatas from args
const argsForTable = mergedArgs.inheritance && mergedArgs.inheritance.type === "single-table" ? mergedArgs.table : tableArgs;
const table = new TableMetadata(argsForTable);
const columns = mergedArgs.columns.map(args => {
const columns = mergedArgs.columns.toArray().map(args => {
// if column's target is a child table then this column should have all nullable columns
if (mergedArgs.inheritance &&
mergedArgs.inheritance.type === "single-table" &&
args.target !== mergedArgs.table.target &&
!!mergedArgs.children.find(childTable => childTable.target === args.target)) {
args.target !== mergedArgs.table.target && !!mergedArgs.children.find(childTable => childTable.target === args.target)) {
args.options.nullable = true;
}
return new ColumnMetadata(args);
});
const relations = mergedArgs.relations.map(args => new RelationMetadata(args));
const indices = mergedArgs.indices.map(args => new IndexMetadata(args));
const relations = mergedArgs.relations.toArray().map(args => new RelationMetadata(args));
const indices = mergedArgs.indices.toArray().map(args => new IndexMetadata(args));
const discriminatorValueArgs = mergedArgs.discriminatorValues.find(discriminatorValueArgs => {
return discriminatorValueArgs.target === tableArgs.target;
});
// create a new entity metadata
const entityMetadata = new EntityMetadata({
junction: false,
target: tableArgs.target,
tablesPrefix: driver.options.tablesPrefix,
namingStrategy: namingStrategy,
@ -274,7 +274,7 @@ export class EntityMetadataBuilder {
// save relation id-s data
entityMetadata.relations.forEach(relation => {
const relationIdMetadata = mergedArgs.relationIds.find(relationId => {
const relationIdMetadata = mergedArgs.relationIds.toArray().find(relationId => {
if (relationId.relation instanceof Function)
return relation.propertyName === relationId.relation(entityMetadata.createPropertiesMap());
@ -290,7 +290,7 @@ export class EntityMetadataBuilder {
// save relation counter-s data
entityMetadata.relations.forEach(relation => {
const relationCountMetadata = mergedArgs.relationCounts.find(relationCount => {
const relationCountMetadata = mergedArgs.relationCounts.toArray().find(relationCount => {
if (relationCount.relation instanceof Function)
return relation.propertyName === relationCount.relation(entityMetadata.createPropertiesMap());
@ -318,7 +318,7 @@ export class EntityMetadataBuilder {
const inverseEntityMetadata = entityMetadatas.find(m => m.target === relation.type || (typeof relation.type === "string" && m.targetName === relation.type));
if (!inverseEntityMetadata)
throw new Error("Entity metadata for " + entityMetadata.name + "#" + relation.propertyName + " was not found.");
relation.inverseEntityMetadata = inverseEntityMetadata;
});
});
@ -335,9 +335,6 @@ export class EntityMetadataBuilder {
}
});
// check for errors in a built metadata schema (we need to check after relationEntityMetadata is set)
getFromContainer(EntityMetadataValidator).validateMany(entityMetadatas);
// generate columns and foreign keys for tables with relations
entityMetadatas.forEach(metadata => {
metadata.relationsWithJoinColumns.forEach(relation => {
@ -357,6 +354,7 @@ export class EntityMetadataBuilder {
primary: relation.isPrimary
}
});
relationalColumn.relationMetadata = relation;
metadata.addColumn(relationalColumn);
}
@ -379,7 +377,7 @@ export class EntityMetadataBuilder {
if (metadata.primaryColumns.length > 1)
throw new Error(`Cannot use given entity ${metadata.name} as a closure table, because it have multiple primary keys. Entities with multiple primary keys are not supported in closure tables.`);
const closureJunctionEntityMetadata = getFromContainer(ClosureJunctionEntityMetadataBuilder).build(driver, lazyRelationsWrapper, {
namingStrategy: namingStrategy,
table: metadata.table,
@ -389,7 +387,7 @@ export class EntityMetadataBuilder {
metadata.closureJunctionTable = closureJunctionEntityMetadata;
entityMetadatas.push(closureJunctionEntityMetadata);
});
// generate junction tables for all many-to-many tables
entityMetadatas.forEach(metadata => {
metadata.ownerManyToManyRelations.forEach(relation => {
@ -461,7 +459,10 @@ export class EntityMetadataBuilder {
metadata.foreignKeys.push(foreignKey);
});
// check for errors in a built metadata schema (we need to check after relationEntityMetadata is set)
getFromContainer(EntityMetadataValidator).validateMany(entityMetadatas);
return entityMetadatas;
}
}

View File

@ -1,7 +0,0 @@
export class EntityMetadataFactory {
createEntityMetadataBuilder() {
}
}

View File

@ -6,6 +6,8 @@ import {MissingJoinColumnError} from "./error/MissingJoinColumnError";
import {MissingJoinTableError} from "./error/MissingJoinTableError";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {MissingPrimaryColumnError} from "./error/MissingPrimaryColumnError";
import {CircularRelationsError} from "./error/CircularRelationsError";
import {DepGraph} from "../util/DepGraph";
/// todo: add check if there are multiple tables with the same name
/// todo: add checks when generated column / table names are too long for the specific driver
@ -24,6 +26,7 @@ export class EntityMetadataValidator {
*/
validateMany(entityMetadatas: EntityMetadata[]) {
entityMetadatas.forEach(entityMetadata => this.validate(entityMetadata, entityMetadatas));
this.validateDependencies(entityMetadatas);
}
/**
@ -32,7 +35,7 @@ export class EntityMetadataValidator {
validate(entityMetadata: EntityMetadata, allEntityMetadatas: EntityMetadata[]) {
// check if table metadata has an id
if (!entityMetadata.table.isClassTableChild && !entityMetadata.primaryColumns.length)
if (!entityMetadata.table.isClassTableChild && !entityMetadata.primaryColumns.length && !entityMetadata.junction)
throw new MissingPrimaryColumnError(entityMetadata);
// validate if table is using inheritance it has a discriminator
@ -78,6 +81,10 @@ export class EntityMetadataValidator {
if (relation.hasInverseSide && relation.inverseRelation.joinColumn && relation.isOneToOne)
throw new UsingJoinColumnOnlyOnOneSideAllowedError(entityMetadata, relation);
// check if join column really has referenced column
if (relation.joinColumn && !relation.joinColumn.referencedColumn)
throw new Error(`Join column does not have referenced column set`);
}
// if its a one-to-one relation and JoinColumn is missing on both sides of the relation
@ -89,13 +96,40 @@ export class EntityMetadataValidator {
// or its one-side relation without JoinTable we should give an error
if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable))
throw new MissingJoinTableError(entityMetadata, relation);
// todo: validate if its one-to-one and side which does not have join column MUST have inverse side
// todo: validate if its many-to-many and side which does not have join table MUST have inverse side
// todo: if there is a relation, and inverse side is specified only on one side, shall we give error
// todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion".
// todo: add validation if there two entities with the same target, and show error message with description of the problem (maybe file was renamed/moved but left in output directory)
// todo: check if there are multiple columns on the same column applied.
});
}
/**
* Validates dependencies of the entity metadatas.
*/
protected validateDependencies(entityMetadatas: EntityMetadata[]) {
const graph = new DepGraph();
entityMetadatas.forEach(entityMetadata => {
graph.addNode(entityMetadata.name);
});
entityMetadatas.forEach(entityMetadata => {
entityMetadata.relationsWithJoinColumns
.filter(relation => !relation.isNullable)
.forEach(relation => {
graph.addDependency(entityMetadata.name, relation.inverseEntityMetadata.name);
});
});
try {
graph.overallOrder();
} catch (err) {
throw new CircularRelationsError(err.toString().replace("Error: Dependency Cycle Found: ", ""));
}
}
}

View File

@ -23,12 +23,12 @@ export interface JunctionEntityMetadataBuilderArgs {
* Helps to create EntityMetadatas for junction tables.
*/
export class JunctionEntityMetadataBuilder {
build(driver: Driver, lazyRelationsWrapper: LazyRelationsWrapper, args: JunctionEntityMetadataBuilderArgs) {
const column1 = args.joinTable.referencedColumn;
const column2 = args.joinTable.inverseReferencedColumn;
const tableMetadata = new TableMetadata({
target: "",
name: args.joinTable.name,
@ -61,14 +61,15 @@ export class JunctionEntityMetadataBuilder {
primary: true
}
});
const entityMetadata = new EntityMetadata({
junction: true,
target: "__virtual__",
tablesPrefix: driver.options.tablesPrefix,
namingStrategy: args.namingStrategy,
tableMetadata: tableMetadata,
columnMetadatas: [
junctionColumn1,
junctionColumn1,
junctionColumn2
],
foreignKeyMetadatas: [
@ -86,5 +87,5 @@ export class JunctionEntityMetadataBuilder {
return entityMetadata;
}
}

View File

@ -0,0 +1,11 @@
/**
* Thrown when circular relations detected with nullable set to false.
*/
export class CircularRelationsError extends Error {
name = "CircularRelationsError";
constructor(path: string) {
super(`Circular relations detected: ${path}. To resolve this issue you need to set nullable: false somewhere in this dependency structure.`);
}
}

View File

@ -10,10 +10,10 @@ export class MissingJoinColumnError extends Error {
super();
if (relation.hasInverseSide) {
this.message = `JoinColumn is missing on both sides of ${entityMetadata.name}#${relation.propertyName} and ` +
`${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} one-to-one relationship. ` +
`${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} one-to-one relationship. ` +
`You need to put JoinColumn decorator on one of the sides.`;
} else {
this.message = `JoinColumn is missing on ${entityMetadata.name}#${relation.propertyName} one-to-one relationship. ` +
this.message = `JoinColumn is missing on ${entityMetadata.name}#${relation.propertyName} one-to-one relationship. ` +
`You need to put JoinColumn decorator on it.`;
}
}

View File

@ -10,11 +10,11 @@ export class MissingJoinTableError extends Error {
super();
if (relation.hasInverseSide) {
this.message = `JoinTable is missing on both sides of ${entityMetadata.name}#${relation.propertyName} and ` +
`${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} many-to-many relationship. ` +
this.message = `JoinTable is missing on both sides of ${entityMetadata.name}#${relation.propertyName} and ` +
`${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} many-to-many relationship. ` +
`You need to put decorator decorator on one of the sides.`;
} else {
this.message = `JoinTable is missing on ${entityMetadata.name}#${relation.propertyName} many-to-many relationship. ` +
this.message = `JoinTable is missing on ${entityMetadata.name}#${relation.propertyName} many-to-many relationship. ` +
`You need to put JoinTable decorator on it.`;
}
}

View File

@ -7,7 +7,7 @@ export class MissingPrimaryColumnError extends Error {
constructor(entityMetadata: EntityMetadata) {
super();
this.message = `Entity "${entityMetadata.name}" does not have a primary column. Primary column is required to ` +
this.message = `Entity "${entityMetadata.name}" does not have a primary column. Primary column is required to ` +
`have in all your entities. Use @PrimaryColumn decorator to add a primary column to your entity.`;
}

View File

@ -8,7 +8,7 @@ export class UsingJoinColumnIsNotAllowedError extends Error {
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinColumn on ${entityMetadata.name}#${relation.propertyName} is wrong. ` +
this.message = `Using JoinColumn on ${entityMetadata.name}#${relation.propertyName} is wrong. ` +
`You can use JoinColumn only on one-to-one and many-to-one relations.`;
}

View File

@ -8,8 +8,8 @@ export class UsingJoinColumnOnlyOnOneSideAllowedError extends Error {
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinColumn is allowed only on one side of the one-to-one relationship. ` +
`Both ${entityMetadata.name}#${relation.propertyName} and ${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} ` +
this.message = `Using JoinColumn is allowed only on one side of the one-to-one relationship. ` +
`Both ${entityMetadata.name}#${relation.propertyName} and ${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} ` +
`has JoinTable decorators. Choose one of them and left JoinTable decorator only on it.`;
}

View File

@ -8,8 +8,8 @@ export class UsingJoinTableIsNotAllowedError extends Error {
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinTable on ${entityMetadata.name}#${relation.propertyName} is wrong. ` +
`${entityMetadata.name}#${relation.propertyName} has ${relation.relationType} relation, ` +
this.message = `Using JoinTable on ${entityMetadata.name}#${relation.propertyName} is wrong. ` +
`${entityMetadata.name}#${relation.propertyName} has ${relation.relationType} relation, ` +
`however you can use JoinTable only on many-to-many relations.`;
}

View File

@ -8,8 +8,8 @@ export class UsingJoinTableOnlyOnOneSideAllowedError extends Error {
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinTable is allowed only on one side of the many-to-many relationship. ` +
`Both ${entityMetadata.name}#${relation.propertyName} and ${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} ` +
this.message = `Using JoinTable is allowed only on one side of the many-to-many relationship. ` +
`Both ${entityMetadata.name}#${relation.propertyName} and ${relation.inverseEntityMetadata.name}#${relation.inverseRelation.propertyName} ` +
`has JoinTable decorators. Choose one of them and left JoinColumn decorator only on it.`;
}

View File

@ -2,10 +2,11 @@ import {ColumnMetadataArgs} from "../metadata-args/ColumnMetadataArgs";
import {ColumnType} from "./types/ColumnTypes";
import {EntityMetadata} from "./EntityMetadata";
import {EmbeddedMetadata} from "./EmbeddedMetadata";
import {RelationMetadata} from "./RelationMetadata";
/**
* Kinda type of the column. Not a type in the database, but locally used type to determine what kind of column
* we are working with.
* we are working with.
* For example, "primary" means that it will be a primary column, or "createDate" means that it will create a create
* date column.
*/
@ -30,6 +31,11 @@ export class ColumnMetadata {
*/
embeddedMetadata: EmbeddedMetadata;
/**
* If this column is foreign key of some relation then this relation's metadata will be here.
*/
relationMetadata: RelationMetadata;
// ---------------------------------------------------------------------
// Public Readonly Properties
// ---------------------------------------------------------------------
@ -77,7 +83,7 @@ export class ColumnMetadata {
/**
* Indicates if value in the database should be unique or not.
*/
readonly isUnique: boolean= false;
readonly isUnique: boolean = false;
/**
* Indicates if column can contain nulls or not.
@ -113,6 +119,20 @@ export class ColumnMetadata {
*/
readonly timezone: boolean;
/**
* Indicates if date object must be stored in given date's timezone.
* By default date is saved in UTC timezone.
* Works only with "datetime" columns.
*/
readonly storeInLocalTimezone?: boolean;
/**
* Indicates if date object must be loaded and set to the Date object in local timezone.
* By default date is loaded in UTC timezone.
* Works only with "datetime" columns.
*/
readonly loadInLocalTimezone?: boolean;
// ---------------------------------------------------------------------
// Private Properties
// ---------------------------------------------------------------------
@ -140,7 +160,7 @@ export class ColumnMetadata {
this.type = args.options.type;
if (args.options.length)
this.length = args.options.length;
this.length = String(args.options.length);
if (args.options.primary)
this.isPrimary = args.options.primary;
if (args.options.generated)
@ -159,6 +179,10 @@ export class ColumnMetadata {
this.precision = args.options.precision;
if (args.options.timezone)
this.timezone = args.options.timezone;
if (args.options.storeInLocalTimezone)
this.storeInLocalTimezone = args.options.storeInLocalTimezone;
if (args.options.loadInLocalTimezone)
this.loadInLocalTimezone = args.options.loadInLocalTimezone;
}
// ---------------------------------------------------------------------
@ -179,7 +203,7 @@ export class ColumnMetadata {
* Column name in the database.
*/
get name(): string {
// if this column is embedded's column then apply different entity
if (this.embeddedMetadata)
return this.embeddedMetadata.entityMetadata.namingStrategy.embeddedColumnName(this.embeddedMetadata.propertyName, this.propertyName, this._name);
@ -187,7 +211,7 @@ export class ColumnMetadata {
// if there is a naming strategy then use it to normalize propertyName as column name
if (this.entityMetadata)
return this.entityMetadata.namingStrategy.columnName(this.propertyName, this._name);
throw new Error(`Column ${this._name ? this._name + " " : ""}is not attached to any entity or embedded.`);
}
@ -246,10 +270,10 @@ export class ColumnMetadata {
get embeddedProperty() {
if (!this.embeddedMetadata)
throw new Error(`This column${ this._name ? this._name + " " : "" } is not in embedded entity.`);
return this.embeddedMetadata.propertyName;
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
@ -257,13 +281,13 @@ export class ColumnMetadata {
hasEntityValue(entity: any) {
if (!entity)
return false;
if (this.isInEmbedded) {
return entity[this.embeddedProperty] !== undefined &&
entity[this.embeddedProperty] !== null &&
entity[this.embeddedProperty][this.propertyName] !== undefined &&
return entity[this.embeddedProperty] !== undefined &&
entity[this.embeddedProperty] !== null &&
entity[this.embeddedProperty][this.propertyName] !== undefined &&
entity[this.embeddedProperty][this.propertyName] !== null;
} else {
return entity[this.propertyName] !== undefined &&
entity[this.propertyName] !== null;

View File

@ -53,7 +53,7 @@ export class EmbeddedMetadata {
column.embeddedMetadata = this;
});
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------

View File

@ -30,7 +30,7 @@ export class EntityMetadata {
* Parent's entity metadata. Used in inheritance patterns.
*/
parentEntityMetadata: EntityMetadata;
// -------------------------------------------------------------------------
// Public Readonly Properties
// -------------------------------------------------------------------------
@ -46,6 +46,11 @@ export class EntityMetadata {
*/
readonly target: Function|string;
/**
* Indicates if this entity metadata of a junction table, or not.
*/
readonly junction: boolean;
/**
* Entity's table metadata.
*/
@ -104,6 +109,7 @@ export class EntityMetadata {
constructor(args: EntityMetadataArgs,
private lazyRelationsWrapper: LazyRelationsWrapper) {
this.target = args.target;
this.junction = args.junction;
this.tablesPrefix = args.tablesPrefix;
this.namingStrategy = args.namingStrategy;
this.table = args.tableMetadata;
@ -136,7 +142,7 @@ export class EntityMetadata {
get name(): string {
if (!this.table)
throw new Error("No table target set to the entity metadata.");
return this.targetName ? this.targetName : this.table.name;
}
@ -183,7 +189,7 @@ export class EntityMetadata {
if (this.target instanceof Function)
return (<any> this.target).name;
return "";
}
@ -243,13 +249,6 @@ export class EntityMetadata {
return this.primaryColumns[0];
}
/**
* Checks if entity has any primary columns.
get hasPrimaryColumns(): ColumnMetadata[] {
}*/
/**
* Gets the primary columns.
*/
@ -303,7 +302,7 @@ export class EntityMetadata {
const column = this._columns.find(column => column.mode === "createDate");
if (!column)
throw new Error(`CreateDateColumn was not found in entity ${this.name}`);
return column;
}
@ -339,7 +338,7 @@ export class EntityMetadata {
const column = this._columns.find(column => column.mode === "version");
if (!column)
throw new Error(`VersionColumn was not found in entity ${this.name}`);
return column;
}
@ -537,6 +536,9 @@ export class EntityMetadata {
return typeof nameOrFn === "string" ? nameOrFn : nameOrFn(this.createPropertiesMap());
}
/**
* todo: undefined entities should not go there
*/
getEntityIdMap(entity: any): ObjectLiteral|undefined {
if (!entity)
return undefined;
@ -544,22 +546,142 @@ export class EntityMetadata {
const map: ObjectLiteral = {};
if (this.parentEntityMetadata) {
this.primaryColumnsWithParentIdColumns.forEach(column => {
map[column.propertyName] = entity[column.propertyName];
const entityValue = entity[column.propertyName];
if (entityValue === null || entityValue === undefined)
return;
// if entity id is a relation, then extract referenced column from that relation
const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName);
if (columnRelation && columnRelation.joinColumn) {
map[column.propertyName] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
map[column.propertyName] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
} else {
map[column.propertyName] = entityValue;
}
});
} else {
this.primaryColumns.forEach(column => {
map[column.propertyName] = entity[column.propertyName];
const entityValue = entity[column.propertyName];
if (entityValue === null || entityValue === undefined)
return;
// if entity id is a relation, then extract referenced column from that relation
const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName);
if (columnRelation && columnRelation.joinColumn) {
map[column.propertyName] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
map[column.propertyName] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
} else {
map[column.propertyName] = entityValue;
}
});
}
const hasAllIds = this.primaryColumns.every(primaryColumn => {
return map[primaryColumn.propertyName] !== undefined && map[primaryColumn.propertyName] !== null;
return Object.keys(map).length > 0 ? map : undefined;
}
/**
* Same as getEntityIdMap, but instead of id column property names it returns database column names.
*/
getDatabaseEntityIdMap(entity: ObjectLiteral): ObjectLiteral|undefined {
const map: ObjectLiteral = {};
if (this.parentEntityMetadata) {
this.primaryColumnsWithParentIdColumns.forEach(column => {
const entityValue = entity[column.propertyName];
if (entityValue === null || entityValue === undefined)
return;
// if entity id is a relation, then extract referenced column from that relation
const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName);
if (columnRelation && columnRelation.joinColumn) {
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
} else {
map[column.name] = entityValue;
}
});
} else {
this.primaryColumns.forEach(column => {
const entityValue = entity[column.propertyName];
if (entityValue === null || entityValue === undefined)
return;
// if entity id is a relation, then extract referenced column from that relation
const columnRelation = this.relations.find(relation => relation.propertyName === column.propertyName);
if (columnRelation && columnRelation.joinColumn) {
map[column.name] = entityValue[columnRelation.joinColumn.referencedColumn.propertyName];
} else if (columnRelation && columnRelation.inverseRelation.joinColumn) {
map[column.name] = entityValue[columnRelation.inverseRelation.joinColumn.referencedColumn.propertyName];
} else {
map[column.name] = entityValue;
}
});
}
const hasAllIds = Object.keys(map).every(key => {
return map[key] !== undefined && map[key] !== null;
});
return hasAllIds ? map : undefined;
}
/**
* Same as `getEntityIdMap` but the key of the map will be the column names instead of the property names.
createSimpleIdMap(id: any): ObjectLiteral {
const map: ObjectLiteral = {};
if (this.parentEntityMetadata) {
this.primaryColumnsWithParentIdColumns.forEach(column => {
map[column.propertyName] = id;
});
} else {
this.primaryColumns.forEach(column => {
map[column.propertyName] = id;
});
}
return map;
} */
/**
* Same as createSimpleIdMap, but instead of id column property names it returns database column names.
createSimpleDatabaseIdMap(id: any): ObjectLiteral {
const map: ObjectLiteral = {};
if (this.parentEntityMetadata) {
this.primaryColumnsWithParentIdColumns.forEach(column => {
map[column.name] = id;
});
} else {
this.primaryColumns.forEach(column => {
map[column.name] = id;
});
}
return map;
}*/
/**
* todo: undefined entities should not go there??
* todo: shouldnt be entity ObjectLiteral here?
*/
getEntityIdMixedMap(entity: any): any {
if (!entity)
return undefined;
if (this.hasMultiplePrimaryKeys) {
return this.getEntityIdMap(entity);
} else {
return entity[this.firstPrimaryColumn.propertyName]; // todo: what about parent primary column?
}
}
/**
* Same as `getEntityIdMap` but the key of the map will be the column names instead of the property names.
*/
getEntityIdColumnMap(entity: any): ObjectLiteral|undefined {
return this.transformIdMapToColumnNames(this.getEntityIdMap(entity));
@ -582,7 +704,7 @@ export class EntityMetadata {
getColumnByPropertyName(propertyName: string) {
return this._columns.find(column => column.propertyName === propertyName);
}
/**
* Checks if column with the given property name exist.
*/
@ -611,7 +733,7 @@ export class EntityMetadata {
const relation = this.relations.find(relation => relation.propertyName === propertyName);
if (!relation)
throw new Error(`Relation with property name ${propertyName} in ${this.name} entity was not found.`);
return relation;
}
@ -632,7 +754,7 @@ export class EntityMetadata {
return relation;
}
addColumn(column: ColumnMetadata) {
this._columns.push(column);
column.entityMetadata = this;
@ -682,29 +804,82 @@ export class EntityMetadata {
return this.compareIds(firstEntityIds, secondEntityIds);
}
compareIds(firstIds: ObjectLiteral|undefined, secondIds: ObjectLiteral|undefined): boolean {
if (!firstIds || !secondIds)
compareIds(firstId: ObjectLiteral|undefined, secondId: ObjectLiteral|undefined): boolean {
if (firstId === undefined || firstId === null || secondId === undefined || secondId === null)
return false;
return Object.keys(firstIds).every(key => {
return firstIds[key] === secondIds[key];
return Object.keys(firstId).every(key => {
return firstId[key] === secondId[key];
});
}
/**
* Compares two entity ids.
* If any of the given value is empty then it will return false.
*/
compareEntityMixedIds(firstId: any, secondId: any): boolean {
if (firstId === undefined || firstId === null || secondId === undefined || secondId === null)
return false;
if (this.hasMultiplePrimaryKeys) {
return Object.keys(firstId).every(key => {
return firstId[key] === secondId[key];
});
} else {
return firstId === secondId;
}
}
/**
* Iterates throw entity and finds and extracts all values from relations in the entity.
* If relation value is an array its being flattened.
*/
extractRelationValuesFromEntity(entity: ObjectLiteral, relations: RelationMetadata[]): [RelationMetadata, any][] {
const relationsAndValues: [RelationMetadata, any][] = [];
extractRelationValuesFromEntity(entity: ObjectLiteral, relations: RelationMetadata[]): [RelationMetadata, any, EntityMetadata][] {
const relationsAndValues: [RelationMetadata, any, EntityMetadata][] = [];
relations.forEach(relation => {
const value = relation.getEntityValue(entity);
if (value instanceof Array) {
value.forEach(subValue => relationsAndValues.push([relation, subValue]));
value.forEach(subValue => relationsAndValues.push([relation, subValue, relation.inverseEntityMetadata]));
} else if (value) {
relationsAndValues.push([relation, value]);
relationsAndValues.push([relation, value, relation.inverseEntityMetadata]);
}
});
return relationsAndValues;
}
/**
* Checks if given entity has an id.
*/
hasId(entity: ObjectLiteral): boolean {
// if (this.metadata.parentEntityMetadata) {
// return this.metadata.parentEntityMetadata.parentIdColumns.every(parentIdColumn => {
// const columnName = parentIdColumn.propertyName;
// return !!entity &&
// entity.hasOwnProperty(columnName) &&
// entity[columnName] !== null &&
// entity[columnName] !== undefined &&
// entity[columnName] !== "";
// });
// } else {
return this.primaryColumns.every(primaryColumn => {
const columnName = primaryColumn.propertyName;
return !!entity &&
entity.hasOwnProperty(columnName) &&
entity[columnName] !== null &&
entity[columnName] !== undefined &&
entity[columnName] !== "";
});
// }
}
/**
* Checks if there any non-nullable column exist in this entity.
*/
get hasNonNullableColumns(): boolean {
return this.relationsWithJoinColumns.some(relation => !relation.isNullable || relation.isPrimary);
// return this.relationsWithJoinColumns.some(relation => relation.isNullable || relation.isPrimary);
}
}

View File

@ -13,5 +13,5 @@ export class EntitySubscriberMetadata {
constructor(args: EntitySubscriberMetadataArgs) {
this.target = args.target;
}
}

Some files were not shown because too many files have changed in this diff Show More