mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
first commit
This commit is contained in:
commit
4309b8d810
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
build/
|
||||
node_modules/
|
||||
typings/
|
||||
npm-debug.log
|
||||
14
README.md
Normal file
14
README.md
Normal file
@ -0,0 +1,14 @@
|
||||
# TypeORM
|
||||
|
||||
ORM that works in Typescript.
|
||||
|
||||
## Usage
|
||||
|
||||
ORM development is in progress. Readme and documentations expected to be soon.
|
||||
|
||||
## Samples
|
||||
|
||||
Take a look on samples in [./sample](https://github.com/pleerock/typeorm/tree/master/sample) for more examples of
|
||||
usages.
|
||||
|
||||
## Todos
|
||||
1
gulpfile.js
Normal file
1
gulpfile.js
Normal file
@ -0,0 +1 @@
|
||||
eval(require("typescript").transpile(require("fs").readFileSync("./gulpfile.ts").toString()));
|
||||
158
gulpfile.ts
Normal file
158
gulpfile.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import {Gulpclass, Task, SequenceTask} from "gulpclass/Decorators";
|
||||
|
||||
const gulp = require("gulp");
|
||||
const del = require("del");
|
||||
const shell = require("gulp-shell");
|
||||
const replace = require("gulp-replace");
|
||||
const mocha = require("gulp-mocha");
|
||||
const chai = require("chai");
|
||||
const tslint = require("gulp-tslint");
|
||||
const stylish = require("tslint-stylish");
|
||||
|
||||
@Gulpclass()
|
||||
export class Gulpfile {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// General tasks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cleans build folder.
|
||||
*/
|
||||
@Task()
|
||||
clean(cb: Function) {
|
||||
return del(["./build/**"], cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs typescript files compilation.
|
||||
*/
|
||||
@Task()
|
||||
compile() {
|
||||
return gulp.src("*.js", { read: false })
|
||||
.pipe(shell(["tsc"]));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Packaging and Publishing tasks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Publishes a package to npm from ./build/package directory.
|
||||
*/
|
||||
@Task()
|
||||
npmPublish() {
|
||||
return gulp.src("*.js", { read: false })
|
||||
.pipe(shell([
|
||||
"cd ./build/package && npm publish"
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all files that will be in a package.
|
||||
*/
|
||||
@Task()
|
||||
packageFiles() {
|
||||
return gulp.src("./build/es5/src/**/*")
|
||||
.pipe(gulp.dest("./build/package"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the "private" state of the packaged package.json file to public.
|
||||
*/
|
||||
@Task()
|
||||
packagePreparePackageFile() {
|
||||
return gulp.src("./package.json")
|
||||
.pipe(replace("\"private\": true,", "\"private\": false,"))
|
||||
.pipe(gulp.dest("./build/package"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@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"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a package that can be published to npm.
|
||||
*/
|
||||
@SequenceTask()
|
||||
package() {
|
||||
return [
|
||||
"clean",
|
||||
"compile",
|
||||
["packageFiles", "packagePreparePackageFile", "packageReadmeFile", "copyTypingsFile"]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a package and publishes it to npm.
|
||||
*/
|
||||
@SequenceTask()
|
||||
publish() {
|
||||
return ["package", "npmPublish"];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Run tests tasks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Runs ts linting to validate source code.
|
||||
*/
|
||||
@Task()
|
||||
tslint() {
|
||||
return gulp.src(["./src/**/*.ts", "./test/**/*.ts", "./sample/**/*.ts"])
|
||||
.pipe(tslint())
|
||||
.pipe(tslint.report(stylish, {
|
||||
emitError: true,
|
||||
sort: true,
|
||||
bell: true
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs integration tests.
|
||||
*/
|
||||
@Task()
|
||||
integration() {
|
||||
chai.should();
|
||||
chai.use(require("sinon-chai"));
|
||||
return gulp.src("./build/es5/test/integration/**/*.js")
|
||||
.pipe(mocha());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs unit-tests.
|
||||
*/
|
||||
@Task()
|
||||
unit() {
|
||||
chai.should();
|
||||
chai.use(require("sinon-chai"));
|
||||
return gulp.src("./build/es5/test/unit/**/*.js")
|
||||
.pipe(mocha());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the code and runs tests.
|
||||
*/
|
||||
@SequenceTask()
|
||||
tests() {
|
||||
return ["compile", "tslint", "unit", "integration"];
|
||||
}
|
||||
|
||||
}
|
||||
56
package.json
Normal file
56
package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "typeodm",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"description": "ODM for MongoDB used Typescript",
|
||||
"license": "Apache-2.0",
|
||||
"readmeFilename": "README.md",
|
||||
"author": {
|
||||
"name": "Umed Khudoiberdiev",
|
||||
"email": "zarrhost@gmail.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PLEEROCK/typeodm.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/PLEEROCK/typeodm/issues"
|
||||
},
|
||||
"tags": [
|
||||
"odm",
|
||||
"typescript",
|
||||
"typescript-odm",
|
||||
"mongodb",
|
||||
"mongodb-odm"
|
||||
],
|
||||
"devDependencies": {
|
||||
"chai": "^3.4.1",
|
||||
"del": "^2.2.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-mocha": "^2.2.0",
|
||||
"gulp-replace": "^0.5.4",
|
||||
"gulp-shell": "^0.5.1",
|
||||
"gulp-tslint": "^4.3.1",
|
||||
"gulpclass": "0.1.0",
|
||||
"mocha": "^2.3.2",
|
||||
"mysql": "^2.10.2",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"tslint": "^3.3.0",
|
||||
"tslint-stylish": "^2.1.0-beta",
|
||||
"typescript": "^1.8.0",
|
||||
"typings": "^0.6.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs": "^0.0.2",
|
||||
"mongodb": ">=2.0.0",
|
||||
"path": "^0.11.14",
|
||||
"reflect-metadata": "^0.1.3",
|
||||
"sha1": "^1.1.1",
|
||||
"typedi": "~0.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "./node_modules/.bin/gulp package",
|
||||
"test": "node_modules/.bin/mocha -w"
|
||||
}
|
||||
}
|
||||
29
sample/sample1-simple-entity/app.ts
Normal file
29
sample/sample1-simple-entity/app.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {TypeORM} from "../../src/TypeORM";
|
||||
import {Post} from "./entity/Post";
|
||||
import {ConnectionOptions} from "../../src/connection/ConnectionOptions";
|
||||
|
||||
// first create a connection
|
||||
let options: ConnectionOptions = {
|
||||
host: "192.168.99.100",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "admin",
|
||||
database: "test",
|
||||
autoSchemaCreate: true
|
||||
};
|
||||
|
||||
TypeORM.createMysqlConnection(options, [Post]).then(connection => {
|
||||
|
||||
let post = new Post();
|
||||
post.text = "Hello how are you?";
|
||||
post.title = "hello";
|
||||
|
||||
// finally save it
|
||||
let postRepository = connection.getRepository<Post>(Post);
|
||||
|
||||
postRepository
|
||||
.persist(post)
|
||||
.then(post => console.log("Post has been saved"))
|
||||
.catch(error => console.log("Cannot save. Error: ", error));
|
||||
|
||||
}, error => console.log("Cannot connect: ", error));
|
||||
16
sample/sample1-simple-entity/entity/Post.ts
Normal file
16
sample/sample1-simple-entity/entity/Post.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
|
||||
@Table("sample1_post")
|
||||
export class Post {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
}
|
||||
39
sample/sample2-one-to-one/app.ts
Normal file
39
sample/sample2-one-to-one/app.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {TypeORM} from "../../src/TypeORM";
|
||||
import {Post} from "./entity/Post";
|
||||
import {PostDetails} from "./entity/PostDetails";
|
||||
import {Image} from "./entity/Image";
|
||||
|
||||
// first create a connection
|
||||
let options = {
|
||||
host: "192.168.99.100",
|
||||
port: 3306,
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
autoSchemaCreate: true
|
||||
};
|
||||
|
||||
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image]).then(connection => {
|
||||
|
||||
let postRepository = connection.getRepository<Post>(Post);
|
||||
return postRepository.findById(1).then(post => {
|
||||
console.log(post);
|
||||
}, err => console.log(err));
|
||||
|
||||
return;
|
||||
|
||||
let details = new PostDetails();
|
||||
details.comment = "This is post about hello";
|
||||
details.meta = "about-hello";
|
||||
|
||||
const post = new Post();
|
||||
post.text = "Hello how are you?";
|
||||
post.title = "hello";
|
||||
post.details = details;
|
||||
|
||||
postRepository
|
||||
.persist(post)
|
||||
.then(post => console.log("Post has been saved"))
|
||||
.catch(error => console.log("Cannot save. Error: ", error));
|
||||
|
||||
}, error => console.log("Cannot connect: ", error));
|
||||
20
sample/sample2-one-to-one/entity/Image.ts
Normal file
20
sample/sample2-one-to-one/entity/Image.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {ManyToOne, OneToMany} from "../../../src/decorator/Relations";
|
||||
import {Post} from "./Post";
|
||||
|
||||
@Table("sample2_image")
|
||||
export class Image {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ManyToOne<Post>(() => Post, post => post.images, {
|
||||
isAlwaysLeftJoin: true
|
||||
})
|
||||
post: Post;
|
||||
|
||||
}
|
||||
33
sample/sample2-one-to-one/entity/Post.ts
Normal file
33
sample/sample2-one-to-one/entity/Post.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {OneToOne, OneToMany} from "../../../src/decorator/Relations";
|
||||
import {PostDetails} from "./PostDetails";
|
||||
import {Image} from "./Image";
|
||||
|
||||
@Table("sample2_post")
|
||||
export class Post {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
isNullable: false
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column({
|
||||
isNullable: false
|
||||
})
|
||||
text: string;
|
||||
|
||||
@OneToOne<PostDetails>(true, () => PostDetails, details => details.post, {
|
||||
isAlwaysInnerJoin: true
|
||||
})
|
||||
details: PostDetails;
|
||||
|
||||
@OneToMany<Image>(() => Image, image => image.post, {
|
||||
isAlwaysLeftJoin: true
|
||||
})
|
||||
images: Image[];
|
||||
|
||||
}
|
||||
21
sample/sample2-one-to-one/entity/PostDetails.ts
Normal file
21
sample/sample2-one-to-one/entity/PostDetails.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {OneToOne} from "../../../src/decorator/Relations";
|
||||
import {Post} from "./Post";
|
||||
|
||||
@Table("sample2_post_details")
|
||||
export class PostDetails {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
meta: string;
|
||||
|
||||
@Column()
|
||||
comment: string;
|
||||
|
||||
@OneToOne<Post>(false, () => Post, post => post.details)
|
||||
post: Post;
|
||||
|
||||
}
|
||||
35
sample/sample3-many-to-one/app.ts
Normal file
35
sample/sample3-many-to-one/app.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {TypeORM} from "../../src/TypeORM";
|
||||
import {Post} from "./entity/Post";
|
||||
import {Comment} from "./entity/Comment";
|
||||
|
||||
// first create a connection
|
||||
let options = {
|
||||
host: "192.168.99.100",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "admin",
|
||||
database: "test",
|
||||
autoSchemaCreate: true
|
||||
};
|
||||
|
||||
TypeORM.createMysqlConnection(options, [Post, Comment]).then(connection => {
|
||||
|
||||
let comment1 = new Comment();
|
||||
comment1.text = "Hello world";
|
||||
let comment2 = new Comment();
|
||||
comment2.text = "Bye world";
|
||||
|
||||
let post = new Post();
|
||||
post.text = "Hello how are you?";
|
||||
post.title = "hello";
|
||||
post.comments = [comment1, comment2];
|
||||
|
||||
// finally save it
|
||||
let postRepository = connection.getRepository<Post>(Post);
|
||||
|
||||
postRepository
|
||||
.persist(post)
|
||||
.then(post => console.log("Post has been saved"))
|
||||
.catch(error => console.log("Cannot save. Error: ", error));
|
||||
|
||||
}, error => console.log("Cannot connect: ", error));
|
||||
18
sample/sample3-many-to-one/entity/Comment.ts
Normal file
18
sample/sample3-many-to-one/entity/Comment.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {ManyToOne} from "../../../src/decorator/Relations";
|
||||
import {Post} from "./Post";
|
||||
|
||||
@Table("sample3-comment")
|
||||
export class Comment {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@ManyToOne<Post>(_ => Post, post => post.comments)
|
||||
post: Post;
|
||||
|
||||
}
|
||||
21
sample/sample3-many-to-one/entity/Post.ts
Normal file
21
sample/sample3-many-to-one/entity/Post.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {OneToMany} from "../../../src/decorator/Relations";
|
||||
import {Comment} from "./Comment";
|
||||
|
||||
@Table("sample3-post")
|
||||
export class Post {
|
||||
|
||||
@PrimaryColumn("int", { isAutoIncrement: true })
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@OneToMany<Comment>(_ => Comment, comment => comment.post)
|
||||
comments: Comment[];
|
||||
|
||||
}
|
||||
36
sample/sample4-many-to-many/app.ts
Normal file
36
sample/sample4-many-to-many/app.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {TypeORM} from "../../src/TypeORM";
|
||||
import {Post} from "./entity/Post";
|
||||
import {Category} from "./entity/Category";
|
||||
|
||||
// first create a connection
|
||||
let options = {
|
||||
host: "192.168.99.100",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "admin",
|
||||
database: "test",
|
||||
autoSchemaCreate: true
|
||||
};
|
||||
|
||||
TypeORM.createMysqlConnection(options, [Post, Category]).then(connection => {
|
||||
|
||||
let category1 = new Category();
|
||||
category1.name = "People";
|
||||
|
||||
let category2 = new Category();
|
||||
category2.name = "Human";
|
||||
|
||||
let post = new Post();
|
||||
post.text = "Hello how are you?";
|
||||
post.title = "hello";
|
||||
post.categories = [category1, category2];
|
||||
|
||||
// finally save it
|
||||
let postRepository = connection.getRepository<Post>(Post);
|
||||
|
||||
postRepository
|
||||
.persist(post)
|
||||
.then(post => console.log("Post has been saved"))
|
||||
.catch(error => console.log("Cannot save. Error: ", error));
|
||||
|
||||
}, error => console.log("Cannot connect: ", error));
|
||||
18
sample/sample4-many-to-many/entity/Category.ts
Normal file
18
sample/sample4-many-to-many/entity/Category.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {ManyToMany} from "../../../src/decorator/Relations";
|
||||
import {Post} from "./Post";
|
||||
|
||||
@Table("sample4-category")
|
||||
export class Category {
|
||||
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ManyToMany<Post>(false, _ => Post, post => post.categories)
|
||||
posts: Post[];
|
||||
|
||||
}
|
||||
21
sample/sample4-many-to-many/entity/Post.ts
Normal file
21
sample/sample4-many-to-many/entity/Post.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
|
||||
import {Table} from "../../../src/decorator/Tables";
|
||||
import {ManyToMany} from "../../../src/decorator/Relations";
|
||||
import {Category} from "./Category";
|
||||
|
||||
@Table("sample4-post")
|
||||
export class Post {
|
||||
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
text: string;
|
||||
|
||||
@ManyToMany<Category>(true, _ => Category, category => category.posts)
|
||||
categories: Category[];
|
||||
|
||||
}
|
||||
45
src/TypeORM.ts
Normal file
45
src/TypeORM.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {ConnectionOptions} from "./connection/ConnectionOptions";
|
||||
import {ConnectionManager} from "./connection/ConnectionManager";
|
||||
import {Connection} from "./connection/Connection";
|
||||
import {MysqlDriver} from "./driver/MysqlDriver";
|
||||
//import * as mysql from "mysql";
|
||||
let mysql = require("mysql");
|
||||
|
||||
/**
|
||||
* Provides quick functions for easy-way of performing some commonly making operations.
|
||||
*/
|
||||
export class TypeORM {
|
||||
|
||||
/**
|
||||
* Global connection manager.
|
||||
*/
|
||||
static connectionManager = new ConnectionManager();
|
||||
|
||||
/**
|
||||
* Creates a new connection to mongodb. Imports documents and subscribers from the given directories.
|
||||
*/
|
||||
static createMysqlConnection(options: string, documentDirectories: string[]|Function[], subscriberDirectories?: string[]): Promise<Connection>;
|
||||
static createMysqlConnection(options: ConnectionOptions, documentDirectories: string[]|Function[], subscriberDirectories?: string[]): Promise<Connection>;
|
||||
static createMysqlConnection(configuration: string|ConnectionOptions, documentDirectories: string[]|Function[], subscriberDirectories?: string[]): Promise<Connection> {
|
||||
if (typeof configuration === "string") {
|
||||
configuration = { url: <string> configuration };
|
||||
}
|
||||
|
||||
this.connectionManager.addConnection(new MysqlDriver(mysql));
|
||||
|
||||
if (documentDirectories && documentDirectories.length > 0) {
|
||||
if (typeof documentDirectories[0] === "string") {
|
||||
this.connectionManager.importEntitiesFromDirectories(<string[]> documentDirectories);
|
||||
} else {
|
||||
this.connectionManager.importEntities(<Function[]> documentDirectories);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscriberDirectories && subscriberDirectories.length > 0)
|
||||
this.connectionManager.importSubscribersFromDirectories(subscriberDirectories);
|
||||
|
||||
const connection = this.connectionManager.getConnection();
|
||||
return connection.connect(<ConnectionOptions> configuration).then(() => connection);
|
||||
}
|
||||
|
||||
}
|
||||
168
src/connection/Connection.ts
Normal file
168
src/connection/Connection.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import {Driver} from "../driver/Driver";
|
||||
import {ConnectionOptions} from "./ConnectionOptions";
|
||||
import {Repository} from "../repository/Repository";
|
||||
import {OrmSubscriber} from "../subscriber/OrmSubscriber";
|
||||
import {OrmBroadcaster} from "../subscriber/OrmBroadcaster";
|
||||
import {RepositoryNotFoundError} from "./error/RepositoryNotFoundError";
|
||||
import {BroadcasterNotFoundError} from "./error/BroadcasterNotFoundError";
|
||||
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
|
||||
import {SchemaCreator} from "../schema-creator/SchemaCreator";
|
||||
|
||||
/**
|
||||
* A single connection instance to the database. Each connection has its own repositories, subscribers and metadatas.
|
||||
*/
|
||||
export class Connection {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private _name: string;
|
||||
private _driver: Driver;
|
||||
private _metadatas: EntityMetadata[] = [];
|
||||
private _subscribers: OrmSubscriber<any>[] = [];
|
||||
private _broadcasters: OrmBroadcaster<any>[] = [];
|
||||
private _repositories: Repository<any>[] = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(name: string, driver: Driver) {
|
||||
this._name = name;
|
||||
this._driver = driver;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Getter / Setter Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The name of the connection.
|
||||
*/
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database driver used by this connection.
|
||||
*/
|
||||
get driver(): Driver {
|
||||
return this._driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* All subscribers that are registered for this connection.
|
||||
*/
|
||||
get subscribers(): OrmSubscriber<any>[] {
|
||||
return this._subscribers;
|
||||
}
|
||||
|
||||
/**
|
||||
* All broadcasters that are registered for this connection.
|
||||
*/
|
||||
get broadcasters(): OrmBroadcaster<any>[] {
|
||||
return this._broadcasters;
|
||||
}
|
||||
|
||||
/**
|
||||
* All metadatas that are registered for this connection.
|
||||
*/
|
||||
get metadatas(): EntityMetadata[] {
|
||||
return this._metadatas;
|
||||
}
|
||||
|
||||
/**
|
||||
* All repositories that are registered for this connection.
|
||||
*/
|
||||
get repositories(): Repository<any>[] {
|
||||
return this._repositories;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Performs connection to the database.
|
||||
*/
|
||||
connect(options: ConnectionOptions): Promise<void> {
|
||||
const schemaCreator = new SchemaCreator(this);
|
||||
return this._driver.connect(options)
|
||||
.then(() => {
|
||||
if (options.autoSchemaCreate === true)
|
||||
return schemaCreator.create();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this connection.
|
||||
*/
|
||||
close(): Promise<void> {
|
||||
return this._driver.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new entity metadatas.
|
||||
*/
|
||||
addMetadatas(metadatas: EntityMetadata[]) {
|
||||
this._metadatas = this._metadatas.concat(metadatas);
|
||||
this._broadcasters = this._broadcasters.concat(metadatas.map(metadata => this.createBroadcasterForMetadata(metadata)));
|
||||
this._repositories = this._repositories.concat(metadatas.map(metadata => this.createRepositoryForMetadata(metadata)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds subscribers to this connection.
|
||||
*/
|
||||
addSubscribers(subscribers: OrmSubscriber<any>[]) {
|
||||
this._subscribers = this._subscribers.concat(subscribers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets repository for the given entity class.
|
||||
*/
|
||||
getRepository<Entity>(entityClass: Function): Repository<Entity> {
|
||||
const metadata = this.getMetadata(entityClass);
|
||||
const repository = this.repositories.find(repository => repository.metadata === metadata);
|
||||
if (!repository)
|
||||
throw new RepositoryNotFoundError(entityClass);
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metadata for the given entity class.
|
||||
*/
|
||||
getMetadata(entityClass: Function): EntityMetadata {
|
||||
const metadata = this.metadatas.find(metadata => metadata.target === entityClass);
|
||||
// todo:
|
||||
// if (!metadata)
|
||||
// throw new MetadataNotFoundError(entityClass);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the broadcaster for the given entity class.
|
||||
*/
|
||||
getBroadcaster<Entity>(entityClass: Function): OrmBroadcaster<Entity> {
|
||||
let metadata = this.broadcasters.find(broadcaster => broadcaster.entityClass === entityClass);
|
||||
if (!metadata)
|
||||
throw new BroadcasterNotFoundError(entityClass);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private createBroadcasterForMetadata(metadata: EntityMetadata): OrmBroadcaster<any> {
|
||||
return new OrmBroadcaster<any>(this.subscribers, metadata.target);
|
||||
}
|
||||
|
||||
private createRepositoryForMetadata(metadata: EntityMetadata): Repository<any> {
|
||||
return new Repository<any>(this, metadata, this.getBroadcaster(metadata.target));
|
||||
}
|
||||
|
||||
}
|
||||
141
src/connection/ConnectionManager.ts
Normal file
141
src/connection/ConnectionManager.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import {Connection} from "./Connection";
|
||||
import {OrmUtils} from "../util/OrmUtils";
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {Driver} from "../driver/Driver";
|
||||
import {DefaultNamingStrategy} from "../naming-strategy/DefaultNamingStrategy";
|
||||
import {ConnectionNotFoundError} from "./error/ConnectionNotFoundError";
|
||||
import {EntityMetadataBuilder} from "../metadata-builder/EntityMetadataBuilder";
|
||||
|
||||
/**
|
||||
* Connection manager holds all connections made to the databases.
|
||||
*/
|
||||
export class ConnectionManager {
|
||||
|
||||
// todo: add support for importing entities and subscribers from subdirectories, make support of glob patterns
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connections: Connection[] = [];
|
||||
private entityMetadataBuilder: EntityMetadataBuilder;
|
||||
private _container: { get(someClass: any): any };
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(entityMetadataBuilder?: EntityMetadataBuilder) {
|
||||
if (!entityMetadataBuilder)
|
||||
entityMetadataBuilder = new EntityMetadataBuilder(defaultMetadataStorage, new DefaultNamingStrategy());
|
||||
|
||||
this.entityMetadataBuilder = entityMetadataBuilder;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sets a container that can be used in your custom subscribers. This allows you to inject services in your
|
||||
* classes.
|
||||
*/
|
||||
set container(container: { get(someClass: any): any }) {
|
||||
this._container = container;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates and adds a new connection with given driver.
|
||||
*/
|
||||
addConnection(driver: Driver): void;
|
||||
addConnection(name: string, driver: Driver): void;
|
||||
addConnection(name: any, driver?: Driver): void {
|
||||
if (typeof name === "object") {
|
||||
driver = <Driver> name;
|
||||
name = "default";
|
||||
}
|
||||
this.connections.push(new Connection(name, driver));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the specific connection.
|
||||
*/
|
||||
getConnection(name: string = "default"): Connection {
|
||||
const foundConnection = this.connections.find(connection => connection.name === name);
|
||||
if (!foundConnection)
|
||||
throw new ConnectionNotFoundError(name);
|
||||
|
||||
return foundConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports entities to the given connection.
|
||||
*/
|
||||
importEntities(entities: Function[]): void;
|
||||
importEntities(connectionName: string, entities: Function[]): void;
|
||||
importEntities(connectionNameOrEntities: string|Function[], entities?: Function[]): void {
|
||||
let connectionName = "default";
|
||||
if (typeof connectionNameOrEntities === "string") {
|
||||
connectionName = <string> connectionNameOrEntities;
|
||||
} else {
|
||||
entities = <Function[]> connectionNameOrEntities;
|
||||
}
|
||||
|
||||
let metadatas = this.entityMetadataBuilder.build(entities);
|
||||
if (metadatas.length > 0)
|
||||
this.getConnection(connectionName).addMetadatas(metadatas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports entities from the given paths.
|
||||
*/
|
||||
importEntitiesFromDirectories(paths: string[]): void;
|
||||
importEntitiesFromDirectories(connectionName: string, paths: string[]): void;
|
||||
importEntitiesFromDirectories(connectionNameOrPaths: string|string[], paths?: string[]): void {
|
||||
let connectionName = "default";
|
||||
if (typeof connectionNameOrPaths === "string") {
|
||||
connectionName = <string> connectionNameOrPaths;
|
||||
} else {
|
||||
paths = <string[]> connectionNameOrPaths;
|
||||
}
|
||||
|
||||
let entitiesInFiles = OrmUtils.requireAll(paths);
|
||||
let allEntities = entitiesInFiles.reduce((allEntities, entities) => {
|
||||
return allEntities.concat(Object.keys(entities).map(key => entities[key]));
|
||||
}, []);
|
||||
|
||||
this.importEntities(connectionName, allEntities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports subscribers from the given paths.
|
||||
*/
|
||||
importSubscribersFromDirectories(paths: string[]): void;
|
||||
importSubscribersFromDirectories(connectionName: string, paths: string[]): void;
|
||||
importSubscribersFromDirectories(connectionName: any, paths?: string[]): void {
|
||||
if (typeof connectionName === "object") {
|
||||
paths = connectionName;
|
||||
connectionName = "default";
|
||||
}
|
||||
|
||||
const subscribersInFiles = OrmUtils.requireAll(paths);
|
||||
const allSubscriberClasses = subscribersInFiles.reduce((all, subscriberInFile) => {
|
||||
return all.concat(Object.keys(subscriberInFile).map(key => subscriberInFile[key]));
|
||||
}, []);
|
||||
|
||||
const subscribers = defaultMetadataStorage
|
||||
.ormEventSubscriberMetadatas
|
||||
.filter(metadata => allSubscriberClasses.indexOf(metadata.constructor) !== -1)
|
||||
.map(metadata => {
|
||||
let constructor: any = metadata.constructor;
|
||||
return this._container ? this._container.get(constructor) : new constructor();
|
||||
});
|
||||
if (subscribers.length > 0)
|
||||
this.getConnection(connectionName).addSubscribers(subscribers);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/connection/ConnectionOptions.ts
Normal file
16
src/connection/ConnectionOptions.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Connection options passed to the document.
|
||||
*/
|
||||
export interface ConnectionOptions {
|
||||
|
||||
/**
|
||||
* Url to where perform connection.
|
||||
*/
|
||||
url?: string;
|
||||
host?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: string;
|
||||
autoSchemaCreate?: boolean;
|
||||
|
||||
}
|
||||
9
src/connection/error/BroadcasterNotFoundError.ts
Normal file
9
src/connection/error/BroadcasterNotFoundError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class BroadcasterNotFoundError extends Error {
|
||||
name = "BroadcasterNotFoundError";
|
||||
|
||||
constructor(documentClassOrName: string|Function) {
|
||||
super();
|
||||
this.message = `No broadcaster for ${documentClassOrName} has been found!`;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/connection/error/ConnectionNotFoundError.ts
Normal file
9
src/connection/error/ConnectionNotFoundError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class ConnectionNotFoundError extends Error {
|
||||
name = "ConnectionNotFoundError";
|
||||
|
||||
constructor(name: string) {
|
||||
super();
|
||||
this.message = `No connection ${name} found.`;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/connection/error/RepositoryNotFoundError.ts
Normal file
9
src/connection/error/RepositoryNotFoundError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class RepositoryNotFoundError extends Error {
|
||||
name = "RepositoryNotFoundError";
|
||||
|
||||
constructor(entityClass: Function) {
|
||||
super();
|
||||
this.message = `No repository for ${entityClass} has been found!`;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/connection/error/SchemaNotFoundError.ts
Normal file
9
src/connection/error/SchemaNotFoundError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class SchemaNotFoundError extends Error {
|
||||
name = "SchemaNotFoundError";
|
||||
|
||||
constructor(documentClassOrName: string|Function) {
|
||||
super();
|
||||
this.message = `No schema for ${documentClassOrName} has been found!`;
|
||||
}
|
||||
|
||||
}
|
||||
72
src/decorator/Columns.ts
Normal file
72
src/decorator/Columns.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import "reflect-metadata";
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
|
||||
import {ColumnOptions} from "../metadata-builder/options/ColumnOptions";
|
||||
|
||||
/**
|
||||
* Column decorator is used to mark a specific class property as a table column. Only table columns will be
|
||||
* persisted to the database when document is being saved.
|
||||
*/
|
||||
export function Column(options?: ColumnOptions): Function;
|
||||
export function Column(type?: string, options?: ColumnOptions): Function;
|
||||
export function Column(typeOrOptions?: string|ColumnOptions, options?: ColumnOptions): Function {
|
||||
let type: string;
|
||||
if (typeof typeOrOptions === "string") {
|
||||
type = <string> typeOrOptions;
|
||||
} else {
|
||||
options = <ColumnOptions> typeOrOptions;
|
||||
}
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
if (!type)
|
||||
type = Reflect.getMetadata("design:type", object, propertyName);
|
||||
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
if (!options.type)
|
||||
options.type = type;
|
||||
|
||||
if (options.isAutoIncrement)
|
||||
throw new Error(`Column for property ${propertyName} in ${(<any>object.constructor).name} cannot have auto increment. To have this ability you need to use @PrimaryColumn decorator.`);
|
||||
|
||||
// todo: need proper type validation here
|
||||
|
||||
const metadata = new ColumnMetadata(object.constructor, propertyName, false, false, false, options);
|
||||
defaultMetadataStorage.addColumnMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Column decorator is used to mark a specific class property as a table column. Only table columns will be
|
||||
* persisted to the database when document is being saved.
|
||||
*/
|
||||
export function PrimaryColumn(options?: ColumnOptions): Function;
|
||||
export function PrimaryColumn(type?: string, options?: ColumnOptions): Function;
|
||||
export function PrimaryColumn(typeOrOptions?: string|ColumnOptions, options?: ColumnOptions): Function {
|
||||
let type: string;
|
||||
if (typeof typeOrOptions === "string") {
|
||||
type = <string> typeOrOptions;
|
||||
} else {
|
||||
options = <ColumnOptions> typeOrOptions;
|
||||
}
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
if (!type)
|
||||
type = Reflect.getMetadata("design:type", object, propertyName);
|
||||
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
if (!options.type)
|
||||
options.type = type;
|
||||
|
||||
if (options.isNullable)
|
||||
throw new Error(`Primary column for property ${propertyName} in ${(<any>object.constructor).name} cannot be nullable. Its not allowed for primary keys. Please remove isNullable option.`);
|
||||
|
||||
// todo: need proper type validation here
|
||||
|
||||
const metadata = new ColumnMetadata(object.constructor, propertyName, true, false, false, options);
|
||||
defaultMetadataStorage.addColumnMetadata(metadata);
|
||||
};
|
||||
}
|
||||
35
src/decorator/Decorators.ts
Normal file
35
src/decorator/Decorators.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {ConnectionManager} from "../connection/ConnectionManager";
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {OrmEventSubscriberMetadata} from "../metadata-builder/metadata/OrmEventSubscriberMetadata";
|
||||
|
||||
/**
|
||||
* Subscribers that gonna listen to odm events must be annotated with this annotation.
|
||||
*/
|
||||
export function OdmEventSubscriber() {
|
||||
return function (target: Function) {
|
||||
const metadata = new OrmEventSubscriberMetadata(target);
|
||||
defaultMetadataStorage.addOrmEventSubscriberMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
export function OrmRepository(className: Function, connectionName?: string): Function {
|
||||
return function(target: Function, key: string, index: number) {
|
||||
|
||||
let container: any;
|
||||
try {
|
||||
container = require("typedi/Container").Container;
|
||||
} catch (err) {
|
||||
throw new Error("OdmRepository cannot be used because typedi extension is not installed.");
|
||||
}
|
||||
|
||||
container.registerParamHandler({
|
||||
type: target,
|
||||
index: index,
|
||||
getValue: () => {
|
||||
const connectionManager: ConnectionManager = container.get(ConnectionManager);
|
||||
const connection = connectionManager.getConnection(connectionName);
|
||||
return connection.getRepository(className);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
23
src/decorator/Indices.ts
Normal file
23
src/decorator/Indices.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {IndexMetadata} from "../metadata-builder/metadata/IndexMetadata";
|
||||
import {CompoundIndexMetadata} from "../metadata-builder/metadata/CompoundIndexMetadata";
|
||||
|
||||
/**
|
||||
* Fields that needs to be indexed must be marked with this annotation.
|
||||
*/
|
||||
export function Index() {
|
||||
return function (object: Object, propertyName: string) {
|
||||
const metadata = new IndexMetadata(object.constructor, propertyName);
|
||||
defaultMetadataStorage.addIndexMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compound indexes must be set on document classes and must specify fields to be indexed.
|
||||
*/
|
||||
export function CompoundIndex(fields: string[]) {
|
||||
return function (cls: Function) {
|
||||
const metadata = new CompoundIndexMetadata(cls, fields);
|
||||
defaultMetadataStorage.addCompoundIndexMetadata(metadata);
|
||||
};
|
||||
}
|
||||
106
src/decorator/Relations.ts
Normal file
106
src/decorator/Relations.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {RelationTypeInFunction, PropertyTypeInFunction, RelationMetadata} from "../metadata-builder/metadata/RelationMetadata";
|
||||
import {RelationOptions} from "../metadata-builder/options/RelationOptions";
|
||||
import {RelationTypes} from "../metadata-builder/types/RelationTypes";
|
||||
|
||||
export function OneToOne<T>(isOwning: boolean, typeFunction: RelationTypeInFunction, options?: RelationOptions): Function;
|
||||
export function OneToOne<T>(isOwning: boolean, typeFunction: RelationTypeInFunction, inverseSide?: PropertyTypeInFunction<T>, options?: RelationOptions): Function;
|
||||
export function OneToOne<T>(isOwning: boolean, typeFunction: RelationTypeInFunction,
|
||||
inverseSideOrOptions: PropertyTypeInFunction<T>|RelationOptions,
|
||||
options?: RelationOptions): Function {
|
||||
let inverseSideProperty: PropertyTypeInFunction<T>;
|
||||
if (typeof inverseSideOrOptions === "object") {
|
||||
options = <RelationOptions> inverseSideOrOptions;
|
||||
} else {
|
||||
inverseSideProperty = <PropertyTypeInFunction<T>> inverseSideOrOptions;
|
||||
}
|
||||
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
// todo: type in function validation, inverse side function validation
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
const metadata = new RelationMetadata(
|
||||
object.constructor, propertyName, RelationTypes.ONE_TO_ONE, typeFunction, inverseSideProperty, isOwning, options
|
||||
);
|
||||
defaultMetadataStorage.addRelationMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
export function OneToMany<T>(typeFunction: RelationTypeInFunction, options?: RelationOptions): Function;
|
||||
export function OneToMany<T>(typeFunction: RelationTypeInFunction, inverseSide?: PropertyTypeInFunction<T>, options?: RelationOptions): Function;
|
||||
export function OneToMany<T>(typeFunction: RelationTypeInFunction,
|
||||
inverseSideOrOptions: PropertyTypeInFunction<T>|RelationOptions,
|
||||
options?: RelationOptions): Function {
|
||||
let inverseSide: PropertyTypeInFunction<T>;
|
||||
if (typeof inverseSideOrOptions === "object") {
|
||||
options = <RelationOptions> inverseSideOrOptions;
|
||||
} else {
|
||||
inverseSide = <PropertyTypeInFunction<T>> inverseSideOrOptions;
|
||||
}
|
||||
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
// todo: type in function validation, inverse side function validation
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
const metadata = new RelationMetadata(
|
||||
object.constructor, propertyName, RelationTypes.ONE_TO_MANY, typeFunction, inverseSide, false, options
|
||||
);
|
||||
defaultMetadataStorage.addRelationMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
export function ManyToOne<T>(typeFunction: RelationTypeInFunction, options?: RelationOptions): Function;
|
||||
export function ManyToOne<T>(typeFunction: RelationTypeInFunction, inverseSide?: PropertyTypeInFunction<T>, options?: RelationOptions): Function;
|
||||
export function ManyToOne<T>(typeFunction: RelationTypeInFunction,
|
||||
inverseSideOrOptions: PropertyTypeInFunction<T>|RelationOptions,
|
||||
options?: RelationOptions): Function {
|
||||
let inverseSide: PropertyTypeInFunction<T>;
|
||||
if (typeof inverseSideOrOptions === "object") {
|
||||
options = <RelationOptions> inverseSideOrOptions;
|
||||
} else {
|
||||
inverseSide = <PropertyTypeInFunction<T>> inverseSideOrOptions;
|
||||
}
|
||||
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
// todo: type in function validation, inverse side function validation
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
const metadata = new RelationMetadata(
|
||||
object.constructor, propertyName, RelationTypes.MANY_TO_ONE, typeFunction, inverseSide, true, options
|
||||
);
|
||||
defaultMetadataStorage.addRelationMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
export function ManyToMany<T>(isOwning: boolean, typeFunction: RelationTypeInFunction, options?: RelationOptions): Function;
|
||||
export function ManyToMany<T>(isOwning: boolean, typeFunction: RelationTypeInFunction, inverseSide?: PropertyTypeInFunction<T>, options?: RelationOptions): Function;
|
||||
export function ManyToMany<T>(isOwning: boolean,
|
||||
typeFunction: RelationTypeInFunction,
|
||||
inverseSideOrOptions: PropertyTypeInFunction<T>|RelationOptions,
|
||||
options?: RelationOptions): Function {
|
||||
let inverseSide: PropertyTypeInFunction<T>;
|
||||
if (typeof inverseSideOrOptions === "object") {
|
||||
options = <RelationOptions> inverseSideOrOptions;
|
||||
} else {
|
||||
inverseSide = <PropertyTypeInFunction<T>> inverseSideOrOptions;
|
||||
}
|
||||
|
||||
return function (object: Object, propertyName: string) {
|
||||
|
||||
// todo: type in function validation, inverse side function validation
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
const metadata = new RelationMetadata(
|
||||
object.constructor, propertyName, RelationTypes.MANY_TO_MANY, typeFunction, inverseSide, isOwning, options
|
||||
);
|
||||
defaultMetadataStorage.addRelationMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
23
src/decorator/Tables.ts
Normal file
23
src/decorator/Tables.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {defaultMetadataStorage} from "../metadata-builder/MetadataStorage";
|
||||
import {TableMetadata} from "../metadata-builder/metadata/TableMetadata";
|
||||
|
||||
/**
|
||||
* This decorator is used to mark classes that they gonna be Tables.
|
||||
*/
|
||||
export function Table(name: string) {
|
||||
return function (cls: Function) {
|
||||
const metadata = new TableMetadata(cls, name, false);
|
||||
defaultMetadataStorage.addTableMetadata(metadata);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Classes marked within this decorator can provide fields that can be used in a real tables
|
||||
* (they will be inherited).
|
||||
*/
|
||||
export function AbstractTable() {
|
||||
return function (cls: Function) {
|
||||
const metadata = new TableMetadata(cls, name, true);
|
||||
defaultMetadataStorage.addTableMetadata(metadata);
|
||||
};
|
||||
}
|
||||
45
src/driver/Driver.ts
Normal file
45
src/driver/Driver.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {ConnectionOptions} from "../connection/ConnectionOptions";
|
||||
import {SchemaBuilder} from "./schema-builder/SchemaBuilder";
|
||||
import {QueryBuilder} from "./query-builder/QueryBuilder";
|
||||
|
||||
/**
|
||||
* Driver communicates with specific database.
|
||||
*/
|
||||
export interface Driver {
|
||||
|
||||
/**
|
||||
* Access to the native implementation of the database.
|
||||
*/
|
||||
native: any;
|
||||
|
||||
/**
|
||||
* Gets database name to which this connection is made.
|
||||
*/
|
||||
db: string;
|
||||
|
||||
/**
|
||||
* Creates a query builder which can be used to build an sql queries.
|
||||
*/
|
||||
createQueryBuilder(): QueryBuilder;
|
||||
|
||||
/**
|
||||
* Creates a schema builder which can be used to build database/table schemas.
|
||||
*/
|
||||
createSchemaBuilder(): SchemaBuilder;
|
||||
|
||||
/**
|
||||
* Performs connection to the database based on given connection options.
|
||||
*/
|
||||
connect(options: ConnectionOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes connection with database.
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Executes a given SQL query.
|
||||
*/
|
||||
query<T>(query: string): Promise<T>;
|
||||
|
||||
}
|
||||
93
src/driver/MysqlDriver.ts
Normal file
93
src/driver/MysqlDriver.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {Driver} from "./Driver";
|
||||
import {ConnectionOptions} from "../connection/ConnectionOptions";
|
||||
import {SchemaBuilder} from "./schema-builder/SchemaBuilder";
|
||||
import {QueryBuilder} from "./query-builder/QueryBuilder";
|
||||
import {MysqlSchemaBuilder} from "./schema-builder/MysqlSchemaBuilder";
|
||||
|
||||
/**
|
||||
* This driver organizes work with mongodb database.
|
||||
*/
|
||||
export class MysqlDriver implements Driver {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private mysql: any;
|
||||
private connection: any;
|
||||
private connectionOptions: ConnectionOptions;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Getter Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get native(): any {
|
||||
return this.mysql;
|
||||
}
|
||||
|
||||
get db(): string {
|
||||
return this.connectionOptions.database;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(mysql: any) {
|
||||
this.mysql = mysql;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a query builder which can be used to build an sql queries.
|
||||
*/
|
||||
createQueryBuilder(): QueryBuilder {
|
||||
return new QueryBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a schema builder which can be used to build database/table schemas.
|
||||
*/
|
||||
createSchemaBuilder(): SchemaBuilder {
|
||||
return new MysqlSchemaBuilder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs connection to the database based on given connection options.
|
||||
*/
|
||||
connect(options: ConnectionOptions): Promise<void> {
|
||||
this.connectionOptions = options;
|
||||
this.connection = this.mysql.createConnection({
|
||||
host: options.host,
|
||||
user: options.username,
|
||||
password: options.password,
|
||||
database: options.database
|
||||
});
|
||||
return new Promise<void>((ok, fail) => this.connection.connect((err: any) => err ? fail(err) : ok()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes connection with database.
|
||||
*/
|
||||
disconnect(): Promise<void> {
|
||||
if (!this.connection)
|
||||
throw new Error("Connection is not established, cannot disconnect.");
|
||||
|
||||
return new Promise<void>((ok, fail) => {
|
||||
this.connection.end((err: any) => err ? fail(err) : ok());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a given SQL query.
|
||||
*/
|
||||
query<T>(query: string): Promise<T> {
|
||||
if (!this.connection) throw new Error("Connection is not established, cannot execute a query.");
|
||||
console.info("executing:", query);
|
||||
return new Promise<any>((ok, fail) => this.connection.query(query, (err: any, result: any) => err ? fail(err) : ok(result)));
|
||||
}
|
||||
|
||||
}
|
||||
287
src/driver/query-builder/QueryBuilder.ts
Normal file
287
src/driver/query-builder/QueryBuilder.ts
Normal file
@ -0,0 +1,287 @@
|
||||
/**
|
||||
* @author Umed Khudoiberdiev <info@zar.tj>
|
||||
*/
|
||||
export class QueryBuilder {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
getTableNameFromEntityCallback: (entity: Function) => string;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Pirvate properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private type: "select"|"update"|"delete";
|
||||
private selects: string[] = [];
|
||||
private froms: { entityOrTableName: Function|string, alias: string };
|
||||
private leftJoins: { join: string, alias: string, conditionType: string, condition: string }[] = [];
|
||||
private innerJoins: { join: string, alias: string, conditionType: string, condition: string }[] = [];
|
||||
private groupBys: string[] = [];
|
||||
private wheres: { type: "simple"|"and"|"or", condition: string }[] = [];
|
||||
private havings: { type: "simple"|"and"|"or", condition: string }[] = [];
|
||||
private orderBys: { sort: string, order: "ASC"|"DESC" }[] = [];
|
||||
private parameters: { [key: string]: string } = {};
|
||||
private limit: number;
|
||||
private offset: number;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
//delete(selection: string): this;
|
||||
//delete(selection: string[]): this;
|
||||
//delete(selection: string|string[]): this {
|
||||
delete(): this {
|
||||
this.type = "delete";
|
||||
// this.addSelection(selection);
|
||||
return this;
|
||||
}
|
||||
|
||||
// update(selection: string): this;
|
||||
// update(selection: string[]): this;
|
||||
// update(selection: string|string[]): this {
|
||||
update(): this {
|
||||
this.type = "update";
|
||||
// this.addSelection(selection);
|
||||
return this;
|
||||
}
|
||||
|
||||
select(selection: string): this;
|
||||
select(selection: string[]): this;
|
||||
select(selection: string|string[]): this {
|
||||
this.type = "select";
|
||||
this.addSelection(selection);
|
||||
return this;
|
||||
}
|
||||
|
||||
addSelect(selection: string): this;
|
||||
addSelect(selection: string[]): this;
|
||||
addSelect(selection: string|string[]): this {
|
||||
if (selection instanceof Array)
|
||||
this.selects = this.selects.concat(selection);
|
||||
else
|
||||
this.selects.push(selection);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
from(tableName: string, alias: string): this;
|
||||
from(entity: Function, alias: string): this;
|
||||
from(entityOrTableName: Function|string, alias: string): this {
|
||||
this.froms = { entityOrTableName: entityOrTableName, alias: alias };
|
||||
return this;
|
||||
}
|
||||
|
||||
innerJoin(join: string, alias: string, conditionType: string, condition: string): this {
|
||||
this.innerJoins.push({ join: join, alias: alias, conditionType: conditionType, condition: condition });
|
||||
return this;
|
||||
}
|
||||
|
||||
leftJoin(join: string, alias: string, conditionType: string, condition: string): this {
|
||||
this.leftJoins.push({ join: join, alias: alias, conditionType: conditionType, condition: condition });
|
||||
return this;
|
||||
}
|
||||
|
||||
where(where: string): this {
|
||||
this.wheres.push({ type: "simple", condition: where });
|
||||
return this;
|
||||
}
|
||||
|
||||
andWhere(where: string): this {
|
||||
this.wheres.push({ type: "and", condition: where });
|
||||
return this;
|
||||
}
|
||||
|
||||
orWhere(where: string): this {
|
||||
this.wheres.push({ type: "or", condition: where });
|
||||
return this;
|
||||
}
|
||||
|
||||
groupBy(groupBy: string): this {
|
||||
this.groupBys = [groupBy];
|
||||
return this;
|
||||
}
|
||||
|
||||
addGroupBy(groupBy: string): this {
|
||||
this.groupBys.push(groupBy);
|
||||
return this;
|
||||
}
|
||||
|
||||
having(having: string): this {
|
||||
this.havings.push({ type: "simple", condition: having });
|
||||
return this;
|
||||
}
|
||||
|
||||
andHaving(having: string): this {
|
||||
this.havings.push({ type: "and", condition: having });
|
||||
return this;
|
||||
}
|
||||
|
||||
orHaving(having: string): this {
|
||||
this.havings.push({ type: "or", condition: having });
|
||||
return this;
|
||||
}
|
||||
|
||||
orderBy(sort: string, order: "ASC"|"DESC" = "ASC"): this {
|
||||
this.orderBys = [{ sort: sort, order: order }];
|
||||
return this;
|
||||
}
|
||||
|
||||
addOrderBy(sort: string, order: "ASC"|"DESC" = "ASC"): this {
|
||||
this.orderBys.push({ sort: sort, order: order });
|
||||
return this;
|
||||
}
|
||||
|
||||
setLimit(limit: number): this {
|
||||
this.limit = limit;
|
||||
return this;
|
||||
}
|
||||
|
||||
setOffset(offset: number): this {
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
setParameter(key: string, value: string): this {
|
||||
this.parameters[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
setParameters(parameters: Object): this {
|
||||
Object.keys(parameters).forEach(key => this.parameters[key] = (<any> parameters)[key]);
|
||||
return this;
|
||||
}
|
||||
|
||||
getSql(): string {
|
||||
let sql = this.createSelectExpression();
|
||||
sql += this.createWhereExpression();
|
||||
sql += this.createLeftJoinExpression();
|
||||
sql += this.createInnerJoinExpression();
|
||||
sql += this.createGroupByExpression();
|
||||
sql += this.createHavingExpression();
|
||||
sql += this.createOrderByExpression();
|
||||
sql += this.createLimitExpression();
|
||||
sql += this.createOffsetExpression();
|
||||
sql = this.replaceParameters(sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Protected Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
protected addSelection(selection: string|string[]) {
|
||||
|
||||
if (selection instanceof Array)
|
||||
this.selects = selection;
|
||||
else
|
||||
this.selects = [selection];
|
||||
/*if (typeof selection === 'function') {
|
||||
this.selects = this.selects.concat(selection(this.generatePropertyValuesEntity()));
|
||||
|
||||
} else if (typeof selection === 'string') {
|
||||
this.selects.push(selection);
|
||||
|
||||
} else if (selection instanceof Array) {
|
||||
this.selects = this.selects.concat(selection);
|
||||
}*/
|
||||
}
|
||||
|
||||
protected getTableName() {
|
||||
if (this.froms.entityOrTableName instanceof Function) {
|
||||
return this.getTableNameFromEntityCallback(this.froms.entityOrTableName);
|
||||
} else {
|
||||
return <string> this.froms.entityOrTableName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected createSelectExpression() {
|
||||
// todo throw exception if selects or from missing
|
||||
const tableName = this.getTableName();
|
||||
switch (this.type) {
|
||||
case "select":
|
||||
return "SELECT " + this.selects.join(", ") + " FROM " + tableName + " " + this.froms.alias;// + " ";
|
||||
case "update":
|
||||
return "UPDATE " + tableName + " " + this.froms.alias;// + " ";
|
||||
case "delete":
|
||||
return "DELETE " + tableName + " " + this.froms.alias;// + " ";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected createWhereExpression() {
|
||||
if (!this.wheres || !this.wheres.length) return "";
|
||||
|
||||
return " WHERE " + this.wheres.map(where => {
|
||||
switch (where.type) {
|
||||
case "and":
|
||||
return "AND " + where.condition;
|
||||
case "or":
|
||||
return "OR " + where.condition;
|
||||
default:
|
||||
return where.condition;
|
||||
}
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
protected createInnerJoinExpression() {
|
||||
if (!this.innerJoins || !this.innerJoins.length) return "";
|
||||
|
||||
return this.innerJoins.map(join => {
|
||||
return " INNER JOIN " + join.join + " " + join.alias + " " + join.conditionType + " " + join.condition;
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
protected createLeftJoinExpression() {
|
||||
if (!this.leftJoins || !this.leftJoins.length) return "";
|
||||
|
||||
return this.leftJoins.map(join => {
|
||||
return " LEFT JOIN " + join.join + " " + join.alias + " " + join.conditionType + " " + join.condition;
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
protected createGroupByExpression() {
|
||||
if (!this.groupBys || !this.groupBys.length) return "";
|
||||
return " GROUP BY " + this.groupBys.join(", ");
|
||||
}
|
||||
|
||||
protected createHavingExpression() {
|
||||
if (!this.havings || !this.havings.length) return "";
|
||||
return " HAVING " + this.havings.map(having => {
|
||||
switch (having.type) {
|
||||
case "and":
|
||||
return " AND " + having.condition;
|
||||
case "or":
|
||||
return " OR " + having.condition;
|
||||
default:
|
||||
return " " + having.condition;
|
||||
}
|
||||
}).join(" ");
|
||||
}
|
||||
|
||||
protected createOrderByExpression() {
|
||||
if (!this.orderBys || !this.orderBys.length) return "";
|
||||
return " ORDER BY " + this.orderBys.map(order => order.sort + " " + order.order).join(", ");
|
||||
}
|
||||
|
||||
protected createLimitExpression() {
|
||||
if (!this.limit) return "";
|
||||
return " LIMIT " + this.limit;
|
||||
}
|
||||
|
||||
protected createOffsetExpression() {
|
||||
if (!this.offset) return "";
|
||||
return " OFFSET " + this.offset;
|
||||
}
|
||||
|
||||
protected replaceParameters(sql: string) {
|
||||
Object.keys(this.parameters).forEach(key => {
|
||||
sql = sql.replace(":" + key, '"' + this.parameters[key] + '"'); // .replace('"', '')
|
||||
});
|
||||
return sql;
|
||||
}
|
||||
|
||||
}
|
||||
169
src/driver/schema-builder/MysqlSchemaBuilder.ts
Normal file
169
src/driver/schema-builder/MysqlSchemaBuilder.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import {SchemaBuilder} from "./SchemaBuilder";
|
||||
import {MysqlDriver} from "../MysqlDriver";
|
||||
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
|
||||
import {ForeignKeyMetadata} from "../../metadata-builder/metadata/ForeignKeyMetadata";
|
||||
import {TableMetadata} from "../../metadata-builder/metadata/TableMetadata";
|
||||
|
||||
export class MysqlSchemaBuilder extends SchemaBuilder {
|
||||
|
||||
constructor(private driver: MysqlDriver) {
|
||||
super();
|
||||
}
|
||||
|
||||
getChangedColumns(tableName: string, columns: ColumnMetadata[]): Promise<{columnName: string, hasPrimaryKey: boolean}[]> {
|
||||
const sql = `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${this.driver.db}'`+
|
||||
` AND TABLE_NAME = '${tableName}'`;
|
||||
return this.query<any[]>(sql).then(results => {
|
||||
|
||||
return columns.filter(column => {
|
||||
const dbData = results.find(result => result.COLUMN_NAME === column.name);
|
||||
if (!dbData) return false;
|
||||
|
||||
const newType = this.normalizeType(column.type, column.length);
|
||||
const isNullable = column.isNullable === true ? "YES" : "NO";
|
||||
const hasDbColumnAutoIncrement = dbData.EXTRA.indexOf("auto_increment") !== -1;
|
||||
const hasDbColumnPrimaryIndex = dbData.COLUMN_KEY.indexOf("PRI") !== -1;
|
||||
return dbData.COLUMN_TYPE !== newType ||
|
||||
dbData.COLUMN_COMMENT !== column.comment ||
|
||||
dbData.IS_NULLABLE !== isNullable ||
|
||||
hasDbColumnAutoIncrement !== column.isAutoIncrement ||
|
||||
hasDbColumnPrimaryIndex !== column.isPrimary;
|
||||
}).map(column => {
|
||||
const dbData = results.find(result => result.COLUMN_NAME === column.name);
|
||||
const hasDbColumnPrimaryIndex = dbData.COLUMN_KEY.indexOf("PRI") !== -1;
|
||||
return { columnName: column.name, hasPrimaryKey: hasDbColumnPrimaryIndex };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
checkIfTableExist(tableName: string): Promise<boolean> {
|
||||
const sql = `SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '${this.driver.db}' AND TABLE_NAME = '${tableName}'`;
|
||||
return this.query<any[]>(sql).then(results => !!(results && results.length));
|
||||
}
|
||||
|
||||
addColumnQuery(tableName: string, column: ColumnMetadata): Promise<void> {
|
||||
const sql = `ALTER TABLE ${tableName} ADD ${this.buildCreateColumnSql(column, false)}`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
dropColumnQuery(tableName: string, columnName: string): Promise<void> {
|
||||
const sql = `ALTER TABLE ${tableName} DROP ${columnName}`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
addForeignKeyQuery(foreignKey: ForeignKeyMetadata): Promise<void> {
|
||||
const sql = `ALTER TABLE ${foreignKey.table.name} ADD CONSTRAINT \`${foreignKey.name}\` ` +
|
||||
`FOREIGN KEY (${foreignKey.columnNames.join(", ")}) ` +
|
||||
`REFERENCES ${foreignKey.referencedTable.name}(${foreignKey.referencedColumnNames.join(",")})`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
dropForeignKeyQuery(foreignKey: ForeignKeyMetadata): Promise<void>;
|
||||
dropForeignKeyQuery(tableName: string, foreignKeyName: string): Promise<void>;
|
||||
dropForeignKeyQuery(tableNameOrForeignKey: string|ForeignKeyMetadata, foreignKeyName?: string): Promise<void> {
|
||||
let tableName = <string> tableNameOrForeignKey;
|
||||
if (tableNameOrForeignKey instanceof ForeignKeyMetadata) {
|
||||
tableName = tableNameOrForeignKey.table.name;
|
||||
foreignKeyName = tableNameOrForeignKey.name;
|
||||
}
|
||||
|
||||
const sql = `ALTER TABLE ${tableName} DROP FOREIGN KEY \`${foreignKeyName}\``;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
getTableForeignQuery(table: TableMetadata): Promise<string[]> {
|
||||
const sql = `SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = "${this.driver.db}" `
|
||||
+ `AND TABLE_NAME = "${table.name}" AND REFERENCED_COLUMN_NAME IS NOT NULL`;
|
||||
return this.query<any[]>(sql).then(results => results.map(result => result.CONSTRAINT_NAME));
|
||||
}
|
||||
|
||||
getTableUniqueKeysQuery(tableName: string): Promise<string[]> {
|
||||
const sql = `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = "${this.driver.db}" ` +
|
||||
`AND TABLE_NAME = "${tableName}" AND CONSTRAINT_TYPE = 'UNIQUE'`;
|
||||
return this.query<any[]>(sql).then(results => results.map(result => result.CONSTRAINT_NAME));
|
||||
}
|
||||
|
||||
getPrimaryConstraintName(tableName: string): Promise<string> {
|
||||
const sql = `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = "${this.driver.db}"`
|
||||
+ ` AND TABLE_NAME = "${tableName}" AND CONSTRAINT_TYPE = 'PRIMARY KEY'`;
|
||||
return this.query<any[]>(sql).then(results => results && results.length ? results[0].CONSTRAINT_NAME : undefined);
|
||||
}
|
||||
|
||||
dropIndex(tableName: string, indexName: string): Promise<void> {
|
||||
const sql = `ALTER TABLE ${tableName} DROP INDEX \`${indexName}\``;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
addUniqueKey(tableName: string, columnName: string, keyName: string): Promise<void> {
|
||||
const sql = `ALTER TABLE ${tableName} ADD CONSTRAINT ${keyName} UNIQUE (${columnName})`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
getTableColumns(tableName: string): Promise<string[]> {
|
||||
const sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${this.driver.db}'`+
|
||||
` AND TABLE_NAME = '${tableName}'`;
|
||||
return this.query<any[]>(sql).then(results => results.map(result => result.COLUMN_NAME));
|
||||
}
|
||||
|
||||
changeColumnQuery(tableName: string, columnName: string, newColumn: ColumnMetadata, skipPrimary: boolean = false): Promise<void> {
|
||||
const sql = `ALTER TABLE ${tableName} CHANGE ${columnName} ${this.buildCreateColumnSql(newColumn, skipPrimary)}`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
createTableQuery(table: TableMetadata, columns: ColumnMetadata[]): Promise<void> {
|
||||
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, true)).join(", ");
|
||||
const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`;
|
||||
return this.query(sql).then(() => {});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private query<T>(sql: string) {
|
||||
return this.driver.query<T>(sql);
|
||||
}
|
||||
|
||||
private buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean) {
|
||||
var c = column.name + " " + this.normalizeType(column.type, column.length);
|
||||
if (column.isNullable !== true)
|
||||
c += " NOT NULL";
|
||||
if (column.isPrimary === true && !skipPrimary)
|
||||
c += " PRIMARY KEY";
|
||||
if (column.isAutoIncrement === true && !skipPrimary)
|
||||
c += " AUTO_INCREMENT";
|
||||
if (column.comment)
|
||||
c += " COMMENT '" + column.comment + "'";
|
||||
if (column.columnDefinition)
|
||||
c += " " + column.columnDefinition;
|
||||
return c;
|
||||
}
|
||||
|
||||
private normalizeType(type: any, length?: string) {
|
||||
|
||||
let realType: string;
|
||||
if (typeof type === "string") {
|
||||
realType = type.toLowerCase();
|
||||
|
||||
} else if (type.name && typeof type.name === "string") {
|
||||
realType = type.name.toLowerCase();
|
||||
}
|
||||
|
||||
switch (realType) {
|
||||
case "string":
|
||||
return "varchar(" + (length ? length : 255) + ")";
|
||||
case "text":
|
||||
return "text";
|
||||
case "number":
|
||||
return "double";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "integer":
|
||||
case "int":
|
||||
return "int(" + (length ? length : 11) + ")";
|
||||
}
|
||||
|
||||
throw new Error("Specified type (" + type + ") is not supported by current driver.");
|
||||
}
|
||||
|
||||
}
|
||||
23
src/driver/schema-builder/SchemaBuilder.ts
Normal file
23
src/driver/schema-builder/SchemaBuilder.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
|
||||
import {ForeignKeyMetadata} from "../../metadata-builder/metadata/ForeignKeyMetadata";
|
||||
import {TableMetadata} from "../../metadata-builder/metadata/TableMetadata";
|
||||
|
||||
export abstract class SchemaBuilder {
|
||||
|
||||
abstract getChangedColumns(tableName: string, columns: ColumnMetadata[]): Promise<{columnName: string, hasPrimaryKey: boolean}[]>;
|
||||
abstract checkIfTableExist(tableName: string): Promise<boolean>;
|
||||
abstract addColumnQuery(tableName: string, column: ColumnMetadata): Promise<void>;
|
||||
abstract dropColumnQuery(tableName: string, columnName: string): Promise<void>;
|
||||
abstract addForeignKeyQuery(foreignKey: ForeignKeyMetadata): Promise<void>;
|
||||
abstract dropForeignKeyQuery(foreignKey: ForeignKeyMetadata): Promise<void>;
|
||||
abstract dropForeignKeyQuery(tableName: string, foreignKeyName: string): Promise<void>;
|
||||
abstract getTableForeignQuery(table: TableMetadata): Promise<string[]>;
|
||||
abstract getTableUniqueKeysQuery(tableName: string): Promise<string[]>;
|
||||
abstract getPrimaryConstraintName(tableName: string): Promise<string>;
|
||||
abstract dropIndex(tableName: string, indexName: string): Promise<void>;
|
||||
abstract addUniqueKey(tableName: string, columnName: string, keyName: string): Promise<void>;
|
||||
abstract getTableColumns(tableName: string): Promise<string[]>;
|
||||
abstract changeColumnQuery(tableName: string, columnName: string, newColumn: ColumnMetadata, skipPrimary?: boolean): Promise<void>;
|
||||
abstract createTableQuery(table: TableMetadata, columns: ColumnMetadata[]): Promise<void>;
|
||||
|
||||
}
|
||||
183
src/metadata-builder/EntityMetadataBuilder.ts
Normal file
183
src/metadata-builder/EntityMetadataBuilder.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import {MetadataStorage} from "./MetadataStorage";
|
||||
import {PropertyMetadata} from "./metadata/PropertyMetadata";
|
||||
import {TableMetadata} from "./metadata/TableMetadata";
|
||||
import {EntityMetadata} from "./metadata/EntityMetadata";
|
||||
import {NamingStrategy} from "../naming-strategy/NamingStrategy";
|
||||
import {ColumnMetadata} from "./metadata/ColumnMetadata";
|
||||
import {ColumnOptions} from "./options/ColumnOptions";
|
||||
import {RelationTypes} from "./types/RelationTypes";
|
||||
import {ForeignKeyMetadata} from "./metadata/ForeignKeyMetadata";
|
||||
|
||||
/**
|
||||
* Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes.
|
||||
*/
|
||||
export class EntityMetadataBuilder {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(private metadataStorage: MetadataStorage,
|
||||
private namingStrategy: NamingStrategy) {
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Builds a complete metadata aggregations for the given entity classes.
|
||||
*/
|
||||
build(entityClasses: Function[]): EntityMetadata[] {
|
||||
|
||||
// filter the metadata only we need - those which are bind to the given table classes
|
||||
const tableMetadatas = this.metadataStorage.findTableMetadatasForClasses(entityClasses);
|
||||
const abstractTableMetadatas = this.metadataStorage.findAbstractTableMetadatasForClasses(entityClasses);
|
||||
const columnMetadatas = this.metadataStorage.findFieldMetadatasForClasses(entityClasses);
|
||||
const relationMetadatas = this.metadataStorage.findRelationMetadatasForClasses(entityClasses);
|
||||
const indexMetadatas = this.metadataStorage.findIndexMetadatasForClasses(entityClasses);
|
||||
const compoundIndexMetadatas = this.metadataStorage.findCompoundIndexMetadatasForClasses(entityClasses);
|
||||
|
||||
const entityMetadatas = tableMetadatas.map(tableMetadata => {
|
||||
|
||||
const constructorChecker = (opm: PropertyMetadata) => opm.target === tableMetadata.target;
|
||||
const constructorChecker2 = (opm: { target: Function }) => opm.target === tableMetadata.target;
|
||||
|
||||
let entityColumns = columnMetadatas.filter(constructorChecker);
|
||||
let entityRelations = relationMetadatas.filter(constructorChecker);
|
||||
let entityCompoundIndices = compoundIndexMetadatas.filter(constructorChecker2);
|
||||
let entityIndices = indexMetadatas.filter(constructorChecker);
|
||||
|
||||
// merge all columns in the abstract table extendings of this table
|
||||
abstractTableMetadatas.forEach(abstractMetadata => {
|
||||
if (!this.isTableMetadataExtendsAbstractMetadata(tableMetadata, abstractMetadata)) return;
|
||||
const constructorChecker = (opm: PropertyMetadata) => opm.target === abstractMetadata.target;
|
||||
const constructorChecker2 = (opm: { target: Function }) => opm.target === abstractMetadata.target;
|
||||
|
||||
const abstractColumns = columnMetadatas.filter(constructorChecker);
|
||||
const abstractRelations = entityRelations.filter(constructorChecker);
|
||||
const abstractCompoundIndices = entityCompoundIndices.filter(constructorChecker2);
|
||||
const abstractIndices = indexMetadatas.filter(constructorChecker);
|
||||
|
||||
const inheritedFields = this.filterObjectPropertyMetadatasIfNotExist(abstractColumns, entityColumns);
|
||||
const inheritedRelations = this.filterObjectPropertyMetadatasIfNotExist(abstractRelations, entityRelations);
|
||||
const inheritedIndices = this.filterObjectPropertyMetadatasIfNotExist(abstractIndices, entityIndices);
|
||||
|
||||
entityCompoundIndices = entityCompoundIndices.concat(abstractCompoundIndices);
|
||||
entityColumns = entityColumns.concat(inheritedFields);
|
||||
entityRelations = entityRelations.concat(inheritedRelations);
|
||||
entityIndices = entityIndices.concat(inheritedIndices);
|
||||
});
|
||||
|
||||
// generate columns for relations
|
||||
/* const relationColumns = entityRelations
|
||||
.filter(relation => relation.isOwning && (relation.relationType === RelationTypes.ONE_TO_ONE || relation.relationType ===RelationTypes.MANY_TO_ONE))
|
||||
.filter(relation => !entityColumns.find(column => column.name === relation.name))
|
||||
.map(relation => {
|
||||
const options: ColumnOptions = {
|
||||
type: "int", // todo: setup proper inverse side type later
|
||||
oldColumnName: relation.oldColumnName,
|
||||
isNullable: relation.isNullable
|
||||
};
|
||||
return new ColumnMetadata(tableMetadata.target, relation.name, false, false, false, options);
|
||||
});
|
||||
|
||||
const allColumns = entityColumns.concat(relationColumns);*/
|
||||
|
||||
const entityMetadata = new EntityMetadata(tableMetadata, entityColumns, entityRelations, entityIndices, entityCompoundIndices, []);
|
||||
|
||||
// set naming strategies
|
||||
tableMetadata.namingStrategy = this.namingStrategy;
|
||||
entityColumns.forEach(column => column.namingStrategy = this.namingStrategy);
|
||||
entityRelations.forEach(relation => relation.namingStrategy = this.namingStrategy);
|
||||
|
||||
// set properties map for relations
|
||||
entityRelations.forEach(relation => relation.ownerEntityPropertiesMap = entityMetadata.createPropertiesMap());
|
||||
|
||||
return entityMetadata;
|
||||
});
|
||||
|
||||
// generate columns and foreign keys for tables with relations
|
||||
entityMetadatas.forEach(metadata => {
|
||||
const foreignKeyRelations = metadata.ownerOneToOneRelations.concat(metadata.manyToOneRelations);
|
||||
foreignKeyRelations.map(relation => {
|
||||
const inverseSideMetadata = entityMetadatas.find(metadata => metadata.target === relation.type);
|
||||
|
||||
// find relational columns and if it does not exist - add it
|
||||
let relationalColumn = metadata.columns.find(column => column.name === relation.name);
|
||||
if (!relationalColumn) {
|
||||
const options: ColumnOptions = {
|
||||
type: inverseSideMetadata.primaryColumn.type,
|
||||
oldColumnName: relation.oldColumnName,
|
||||
isNullable: relation.isNullable
|
||||
};
|
||||
relationalColumn = new ColumnMetadata(metadata.target, relation.name, false, false, false, options);
|
||||
metadata.columns.push(relationalColumn);
|
||||
}
|
||||
|
||||
// create and add foreign key
|
||||
const foreignKey = new ForeignKeyMetadata(metadata.table, [relationalColumn], inverseSideMetadata.table, [inverseSideMetadata.primaryColumn]);
|
||||
metadata.foreignKeys.push(foreignKey);
|
||||
});
|
||||
});
|
||||
|
||||
// generate junction tables with its columns and foreign keys
|
||||
const junctionEntityMetadatas: EntityMetadata[] = [];
|
||||
entityMetadatas.forEach(metadata => {
|
||||
metadata.ownerManyToManyRelations.map(relation => {
|
||||
const inverseSideMetadata = entityMetadatas.find(metadata => metadata.target === relation.type);
|
||||
const tableName = metadata.table.name + "_" + relation.name + "_" +
|
||||
inverseSideMetadata.table.name + "_" + inverseSideMetadata.primaryColumn.name;
|
||||
|
||||
const tableMetadata = new TableMetadata(null, tableName, false);
|
||||
const column1options: ColumnOptions = {
|
||||
length: metadata.primaryColumn.length,
|
||||
type: metadata.primaryColumn.type,
|
||||
name: metadata.table.name + "_" + relation.name
|
||||
};
|
||||
const column2options: ColumnOptions = {
|
||||
length: inverseSideMetadata.primaryColumn.length,
|
||||
type: inverseSideMetadata.primaryColumn.type,
|
||||
name: inverseSideMetadata.table.name + "_" + inverseSideMetadata.primaryColumn.name
|
||||
};
|
||||
const columns = [
|
||||
new ColumnMetadata(null, null, false, false, false, column1options),
|
||||
new ColumnMetadata(null, null, false, false, false, column2options)
|
||||
];
|
||||
const foreignKeys = [
|
||||
new ForeignKeyMetadata(tableMetadata, [columns[0]], metadata.table, [metadata.primaryColumn]),
|
||||
new ForeignKeyMetadata(tableMetadata, [columns[1]], inverseSideMetadata.table, [inverseSideMetadata.primaryColumn]),
|
||||
];
|
||||
junctionEntityMetadatas.push(new EntityMetadata(tableMetadata, columns, [], [], [], foreignKeys));
|
||||
});
|
||||
});
|
||||
|
||||
const allEntityMetadatas = entityMetadatas.concat(junctionEntityMetadatas);
|
||||
|
||||
// set inverse side (related) entity metadatas for all relation metadatas
|
||||
allEntityMetadatas.forEach(entityMetadata => {
|
||||
entityMetadata.relations.forEach(relation => {
|
||||
relation.relatedEntityMetadata = allEntityMetadatas.find(m => m.target === relation.type);
|
||||
})
|
||||
});
|
||||
|
||||
return allEntityMetadatas;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private isTableMetadataExtendsAbstractMetadata(tableMetadata: TableMetadata, abstractMetadata: TableMetadata): boolean {
|
||||
return tableMetadata.target.prototype instanceof abstractMetadata.target;
|
||||
}
|
||||
|
||||
private filterObjectPropertyMetadatasIfNotExist<T extends PropertyMetadata>(newMetadatas: T[], existsMetadatas: T[]): T[] {
|
||||
return newMetadatas.filter(fieldFromMapped => {
|
||||
return existsMetadatas.reduce((found, fieldFromDocument) => {
|
||||
return fieldFromDocument.propertyName === fieldFromMapped.propertyName ? fieldFromDocument : found;
|
||||
}, null) === null;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
190
src/metadata-builder/MetadataStorage.ts
Normal file
190
src/metadata-builder/MetadataStorage.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import {TableMetadata} from "./metadata/TableMetadata";
|
||||
import {MetadataAlreadyExistsError} from "./error/MetadataAlreadyExistsError";
|
||||
import {MetadataWithSuchNameAlreadyExistsError} from "./error/MetadataWithSuchNameAlreadyExistsError";
|
||||
import {RelationMetadata} from "./metadata/RelationMetadata";
|
||||
import {IndexMetadata} from "./metadata/IndexMetadata";
|
||||
import {CompoundIndexMetadata} from "./metadata/CompoundIndexMetadata";
|
||||
import {ColumnMetadata} from "./metadata/ColumnMetadata";
|
||||
import {OrmEventSubscriberMetadata} from "./metadata/OrmEventSubscriberMetadata";
|
||||
|
||||
/**
|
||||
* Storage all metadatas of all available types: tables, fields, subscribers, relations, etc.
|
||||
* Each metadata represents some specifications of what it represents.
|
||||
*/
|
||||
export class MetadataStorage {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private _tableMetadatas: TableMetadata[] = [];
|
||||
private _ormEventSubscriberMetadatas: OrmEventSubscriberMetadata[] = [];
|
||||
private _fieldMetadatas: ColumnMetadata[] = [];
|
||||
private _indexMetadatas: IndexMetadata[] = [];
|
||||
private _compoundIndexMetadatas: CompoundIndexMetadata[] = [];
|
||||
private _relationMetadatas: RelationMetadata[] = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Getter Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get tableMetadatas(): TableMetadata[] {
|
||||
return this._tableMetadatas;
|
||||
}
|
||||
|
||||
get ormEventSubscriberMetadatas(): OrmEventSubscriberMetadata[] {
|
||||
return this._ormEventSubscriberMetadatas;
|
||||
}
|
||||
|
||||
get fieldMetadatas(): ColumnMetadata[] {
|
||||
return this._fieldMetadatas;
|
||||
}
|
||||
|
||||
get indexMetadatas(): IndexMetadata[] {
|
||||
return this._indexMetadatas;
|
||||
}
|
||||
|
||||
get compoundIndexMetadatas(): CompoundIndexMetadata[] {
|
||||
return this._compoundIndexMetadatas;
|
||||
}
|
||||
|
||||
get relationMetadatas(): RelationMetadata[] {
|
||||
return this._relationMetadatas;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Adder Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
addTableMetadata(metadata: TableMetadata) {
|
||||
if (this.hasTableMetadataWithObjectConstructor(metadata.target))
|
||||
throw new MetadataAlreadyExistsError("Table", metadata.target);
|
||||
|
||||
if (metadata.name && this.hasTableMetadataWithName(metadata.name))
|
||||
throw new MetadataWithSuchNameAlreadyExistsError("Table", metadata.name);
|
||||
|
||||
this.tableMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
addRelationMetadata(metadata: RelationMetadata) {
|
||||
if (this.hasRelationWithOneMetadataOnProperty(metadata.target, metadata.propertyName))
|
||||
throw new MetadataAlreadyExistsError("RelationMetadata", metadata.target, metadata.propertyName);
|
||||
|
||||
if (metadata.name && this.hasRelationWithOneMetadataWithName(metadata.target, metadata.name))
|
||||
throw new MetadataWithSuchNameAlreadyExistsError("RelationMetadata", metadata.name);
|
||||
|
||||
this.relationMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
addColumnMetadata(metadata: ColumnMetadata) {
|
||||
if (this.hasFieldMetadataOnProperty(metadata.target, metadata.propertyName))
|
||||
throw new MetadataAlreadyExistsError("Column", metadata.target);
|
||||
|
||||
if (metadata.name && this.hasFieldMetadataWithName(metadata.target, metadata.name))
|
||||
throw new MetadataWithSuchNameAlreadyExistsError("Column", metadata.name);
|
||||
|
||||
this.fieldMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
addOrmEventSubscriberMetadata(metadata: OrmEventSubscriberMetadata) {
|
||||
if (this.hasOrmEventSubscriberWithObjectConstructor(metadata.target))
|
||||
throw new MetadataAlreadyExistsError("OrmEventSubscriber", metadata.target);
|
||||
|
||||
this.ormEventSubscriberMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
addIndexMetadata(metadata: IndexMetadata) {
|
||||
if (this.hasFieldMetadataOnProperty(metadata.target, metadata.propertyName))
|
||||
throw new MetadataAlreadyExistsError("Index", metadata.target);
|
||||
|
||||
if (metadata.name && this.hasFieldMetadataWithName(metadata.target, metadata.name))
|
||||
throw new MetadataWithSuchNameAlreadyExistsError("Index", metadata.name);
|
||||
|
||||
this.indexMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
addCompoundIndexMetadata(metadata: CompoundIndexMetadata) {
|
||||
if (this.hasCompoundIndexMetadataWithObjectConstructor(metadata.target))
|
||||
throw new MetadataAlreadyExistsError("CompoundIndex", metadata.target);
|
||||
|
||||
this.compoundIndexMetadatas.push(metadata);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
findTableMetadatasForClasses(classes: Function[]): TableMetadata[] {
|
||||
return this.tableMetadatas.filter(metadata => classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
findCompoundIndexMetadatasForClasses(classes: Function[]): CompoundIndexMetadata[] {
|
||||
return this.compoundIndexMetadatas.filter(metadata => classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
findAbstractTableMetadatasForClasses(classes: Function[]): TableMetadata[] {
|
||||
return this.tableMetadatas.filter(metadata => metadata.isAbstract && classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
findIndexMetadatasForClasses(classes: Function[]): IndexMetadata[] {
|
||||
return this.indexMetadatas.filter(metadata => classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
findFieldMetadatasForClasses(classes: Function[]): ColumnMetadata[] {
|
||||
return this.fieldMetadatas.filter(metadata => classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
findRelationMetadatasForClasses(classes: Function[]): RelationMetadata[] {
|
||||
return this.relationMetadatas.filter(metadata => classes.indexOf(metadata.target) !== -1);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private hasTableMetadataWithObjectConstructor(constructor: Function): boolean {
|
||||
return this.tableMetadatas.reduce((found, metadata) => metadata.target === constructor ? metadata : found, null) !== null;
|
||||
}
|
||||
|
||||
private hasCompoundIndexMetadataWithObjectConstructor(constructor: Function): boolean {
|
||||
return this.compoundIndexMetadatas.reduce((found, metadata) => metadata.target === constructor ? metadata : found, null) !== null;
|
||||
}
|
||||
|
||||
private hasOrmEventSubscriberWithObjectConstructor(constructor: Function): boolean {
|
||||
return this.ormEventSubscriberMetadatas.reduce((found, metadata) => metadata.target === constructor ? metadata : found, null) !== null;
|
||||
}
|
||||
|
||||
private hasFieldMetadataOnProperty(constructor: Function, propertyName: string): boolean {
|
||||
return this.fieldMetadatas.reduce((found, metadata) => {
|
||||
return metadata.target === constructor && metadata.propertyName === propertyName ? metadata : found;
|
||||
}, null) !== null;
|
||||
}
|
||||
|
||||
private hasRelationWithOneMetadataOnProperty(constructor: Function, propertyName: string): boolean {
|
||||
return this.relationMetadatas.reduce((found, metadata) => {
|
||||
return metadata.target === constructor && metadata.propertyName === propertyName ? metadata : found;
|
||||
}, null) !== null;
|
||||
}
|
||||
|
||||
private hasTableMetadataWithName(name: string): boolean {
|
||||
return this.tableMetadatas.reduce((found, metadata) => metadata.name === name ? metadata : found, null) !== null;
|
||||
}
|
||||
|
||||
private hasFieldMetadataWithName(constructor: Function, name: string): boolean {
|
||||
return this.fieldMetadatas.reduce((found, metadata) => {
|
||||
return metadata.target === constructor && metadata.name === name ? metadata : found;
|
||||
}, null) !== null;
|
||||
}
|
||||
|
||||
private hasRelationWithOneMetadataWithName(constructor: Function, name: string): boolean {
|
||||
return this.relationMetadatas.reduce((found, metadata) => {
|
||||
return metadata.target === constructor && metadata.name === name ? metadata : found;
|
||||
}, null) !== null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default metadata storage used as singleton and can be used to storage all metadatas in the system.
|
||||
*/
|
||||
export let defaultMetadataStorage = new MetadataStorage();
|
||||
10
src/metadata-builder/error/MetadataAlreadyExistsError.ts
Normal file
10
src/metadata-builder/error/MetadataAlreadyExistsError.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class MetadataAlreadyExistsError extends Error {
|
||||
name = "MetadataAlreadyExistsError";
|
||||
|
||||
constructor(metadataType: string, constructor: Function, propertyName?: string) {
|
||||
super();
|
||||
this.message = metadataType + " metadata already exists for the class constructor " + JSON.stringify(constructor) +
|
||||
(propertyName ? " on property " + propertyName : "");
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
export class MetadataWithSuchNameAlreadyExistsError extends Error {
|
||||
name = "MetadataWithSuchNameAlreadyExistsError";
|
||||
|
||||
constructor(metadataType: string, name: string) {
|
||||
super();
|
||||
this.message = metadataType + " metadata with such name " + name + " already exists. " +
|
||||
"Do you apply something twice? Or maybe try to change a name?";
|
||||
}
|
||||
|
||||
}
|
||||
190
src/metadata-builder/metadata/ColumnMetadata.ts
Normal file
190
src/metadata-builder/metadata/ColumnMetadata.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import {PropertyMetadata} from "./PropertyMetadata";
|
||||
import {ColumnOptions} from "../options/ColumnOptions";
|
||||
import {NamingStrategy} from "../../naming-strategy/NamingStrategy";
|
||||
|
||||
/**
|
||||
* This metadata interface contains all information about some document's column.
|
||||
*/
|
||||
export class ColumnMetadata extends PropertyMetadata {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Public Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
namingStrategy: NamingStrategy;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Column name to be used in the database.
|
||||
*/
|
||||
private _name: string;
|
||||
|
||||
/**
|
||||
* The type of the column.
|
||||
*/
|
||||
private _type: string = "";
|
||||
|
||||
/**
|
||||
* Maximum length in the database.
|
||||
*/
|
||||
private _length: string = "";
|
||||
|
||||
/**
|
||||
* Indicates if this column is primary key.
|
||||
*/
|
||||
private _isPrimary: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates if this column is auto increment.
|
||||
*/
|
||||
private _isAutoIncrement: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates if value should be unqiue or not.
|
||||
*/
|
||||
private _isUnique: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates if can contain nulls or not.
|
||||
*/
|
||||
private _isNullable: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates if column will contain a created date or not.
|
||||
*/
|
||||
private _isCreateDate: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates if column will contain an updated date or not.
|
||||
*/
|
||||
private _isUpdateDate: boolean = false;
|
||||
|
||||
/**
|
||||
* Extra sql definition for the given column.
|
||||
*/
|
||||
private _columnDefinition: string = "";
|
||||
|
||||
/**
|
||||
* Column comment.
|
||||
*/
|
||||
private _comment: string = "";
|
||||
|
||||
/**
|
||||
* Old column name. Used to correctly alter tables when column name is changed.
|
||||
*/
|
||||
private _oldColumnName: string;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
constructor(target: Function,
|
||||
propertyName: string,
|
||||
isPrimaryKey: boolean,
|
||||
isCreateDate: boolean,
|
||||
isUpdateDate: boolean,
|
||||
options: ColumnOptions) {
|
||||
super(target, propertyName);
|
||||
|
||||
this._isPrimary = isPrimaryKey;
|
||||
this._isCreateDate = isCreateDate;
|
||||
this._isUpdateDate = isUpdateDate;
|
||||
this._name = options.name;
|
||||
this._type = this.convertType(options.type);
|
||||
|
||||
if (options.length)
|
||||
this._length = options.length;
|
||||
if (options.isAutoIncrement)
|
||||
this._isAutoIncrement = options.isAutoIncrement;
|
||||
if (options.isUnique)
|
||||
this._isUnique = options.isUnique;
|
||||
if (options.isNullable)
|
||||
this._isNullable = options.isNullable;
|
||||
if (options.columnDefinition)
|
||||
this._columnDefinition = options.columnDefinition;
|
||||
if (options.comment)
|
||||
this._comment = options.comment;
|
||||
if (options.oldColumnName)
|
||||
this._oldColumnName = options.oldColumnName;
|
||||
|
||||
if (!this._name)
|
||||
this._name = propertyName;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Accessors
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
get name(): string {
|
||||
return this.namingStrategy ? this.namingStrategy.columnName(this._name) : this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of the column.
|
||||
*/
|
||||
get type(): string {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
get length(): string {
|
||||
return this._length;
|
||||
}
|
||||
|
||||
get isPrimary(): boolean {
|
||||
return this._isPrimary;
|
||||
}
|
||||
|
||||
get isAutoIncrement(): boolean {
|
||||
return this._isAutoIncrement;
|
||||
}
|
||||
|
||||
get isUnique(): boolean {
|
||||
return this._isUnique;
|
||||
}
|
||||
|
||||
get isNullable(): boolean {
|
||||
return this._isNullable;
|
||||
}
|
||||
|
||||
get isCreateDate(): boolean {
|
||||
return this._isCreateDate;
|
||||
}
|
||||
|
||||
get isUpdateDate(): boolean {
|
||||
return this._isUpdateDate;
|
||||
}
|
||||
|
||||
get columnDefinition(): string {
|
||||
return this._columnDefinition;
|
||||
}
|
||||
|
||||
get comment(): string {
|
||||
return this._comment;
|
||||
}
|
||||
|
||||
get oldColumnName(): string {
|
||||
return this._oldColumnName;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private convertType(type: Function|string): string {
|
||||
// todo: throw exception if no type in type function
|
||||
if (type instanceof Function) {
|
||||
let typeName = (<any>type).name.toLowerCase();
|
||||
switch (typeName) {
|
||||
case "number":
|
||||
case "boolean":
|
||||
case "string":
|
||||
return typeName;
|
||||
}
|
||||
}
|
||||
return <string> type;
|
||||
}
|
||||
|
||||
}
|
||||
41
src/metadata-builder/metadata/CompoundIndexMetadata.ts
Normal file
41
src/metadata-builder/metadata/CompoundIndexMetadata.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This metadata interface contains all information about compound index on a document.
|
||||
*/
|
||||
export class CompoundIndexMetadata {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private _target: Function;
|
||||
|
||||
/**
|
||||
* Fields combination to be used as index.
|
||||
*/
|
||||
private _fields: string[];
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
constructor(target: Function, fields: string[]) {
|
||||
this._target = target;
|
||||
this._fields = fields;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Getters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The object class to which this metadata is attached.
|
||||
*/
|
||||
get target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
get fields() {
|
||||
return this._fields;
|
||||
}
|
||||
|
||||
}
|
||||
201
src/metadata-builder/metadata/EntityMetadata.ts
Normal file
201
src/metadata-builder/metadata/EntityMetadata.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import {TableMetadata} from "../metadata/TableMetadata";
|
||||
import {ColumnMetadata} from "../metadata/ColumnMetadata";
|
||||
import {RelationMetadata} from "../metadata/RelationMetadata";
|
||||
import {IndexMetadata} from "../metadata/IndexMetadata";
|
||||
import {CompoundIndexMetadata} from "../metadata/CompoundIndexMetadata";
|
||||
import {RelationTypes} from "../types/RelationTypes";
|
||||
import {ForeignKeyMetadata} from "./ForeignKeyMetadata";
|
||||
|
||||
/**
|
||||
* Contains all entity metadata.
|
||||
*/
|
||||
export class EntityMetadata {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private _table: TableMetadata;
|
||||
private _columns: ColumnMetadata[];
|
||||
private _relations: RelationMetadata[];
|
||||
private _indices: IndexMetadata[];
|
||||
private _compoundIndices: CompoundIndexMetadata[];
|
||||
private _foreignKeys: ForeignKeyMetadata[];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(table: TableMetadata,
|
||||
columns: ColumnMetadata[],
|
||||
relations: RelationMetadata[],
|
||||
indices: IndexMetadata[],
|
||||
compoundIndices: CompoundIndexMetadata[],
|
||||
foreignKeys: ForeignKeyMetadata[]) {
|
||||
this._table = table;
|
||||
this._columns = columns;
|
||||
this._relations = relations;
|
||||
this._indices = indices;
|
||||
this._compoundIndices = compoundIndices;
|
||||
this._foreignKeys = foreignKeys;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get target(): Function {
|
||||
return this._table.target;
|
||||
}
|
||||
|
||||
get table(): TableMetadata {
|
||||
return this._table;
|
||||
}
|
||||
|
||||
get columns(): ColumnMetadata[] {
|
||||
return this._columns;
|
||||
}
|
||||
|
||||
get relations(): RelationMetadata[] {
|
||||
return this._relations;
|
||||
}
|
||||
|
||||
get foreignKeys(): ForeignKeyMetadata[] {
|
||||
return this._foreignKeys;
|
||||
}
|
||||
|
||||
get oneToOneRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.ONE_TO_ONE);
|
||||
}
|
||||
|
||||
get ownerOneToOneRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.ONE_TO_ONE && relation.isOwning);
|
||||
}
|
||||
|
||||
get oneToManyRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.ONE_TO_MANY);
|
||||
}
|
||||
|
||||
get manyToOneRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.MANY_TO_ONE);
|
||||
}
|
||||
|
||||
get manyToManyRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.MANY_TO_MANY);
|
||||
}
|
||||
|
||||
get ownerManyToManyRelations(): RelationMetadata[] {
|
||||
return this._relations.filter(relation => relation.relationType === RelationTypes.MANY_TO_MANY && relation.isOwning);
|
||||
}
|
||||
|
||||
get indices(): IndexMetadata[] {
|
||||
return this._indices;
|
||||
}
|
||||
|
||||
get compoundIndices(): CompoundIndexMetadata[] {
|
||||
return this._compoundIndices;
|
||||
}
|
||||
|
||||
get primaryColumn(): ColumnMetadata {
|
||||
return this._columns.find(column => column.isPrimary);
|
||||
}
|
||||
|
||||
get createDateColumn(): ColumnMetadata {
|
||||
return this._columns.find(column => column.isCreateDate);
|
||||
}
|
||||
|
||||
get updateDateColumn(): ColumnMetadata {
|
||||
return this._columns.find(column => column.isUpdateDate);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new entity.
|
||||
*/
|
||||
create(): any {
|
||||
return new (<any> this.table.target)();
|
||||
}
|
||||
|
||||
createPropertiesMap(): any {
|
||||
const entity: any = {};
|
||||
this._columns.forEach(column => entity[column.name] = column.name);
|
||||
this._relations.forEach(relation => entity[relation.name] = relation.name);
|
||||
return entity;
|
||||
}
|
||||
|
||||
getEntityId(entity: any) {
|
||||
return entity[this.primaryColumn.propertyName];
|
||||
}
|
||||
|
||||
hasColumnWithPropertyName(propertyName: string): boolean {
|
||||
return !!this.findColumnWithPropertyName(propertyName);
|
||||
}
|
||||
|
||||
hasColumnWithDbName(name: string): boolean {
|
||||
return !!this.findColumnWithDbName(name);
|
||||
}
|
||||
|
||||
findColumnWithPropertyName(propertyName: string): ColumnMetadata {
|
||||
return this._columns.find(column => column.propertyName === propertyName);
|
||||
}
|
||||
|
||||
findColumnWithDbName(name: string): ColumnMetadata {
|
||||
return this._columns.find(column => column.name === name);
|
||||
}
|
||||
|
||||
hasRelationWithPropertyName(propertyName: string): boolean {
|
||||
return !!this.findRelationWithPropertyName(propertyName);
|
||||
}
|
||||
|
||||
hasRelationWithDbName(dbName: string): boolean {
|
||||
return !!this.findRelationWithDbName(dbName);
|
||||
}
|
||||
|
||||
findRelationWithPropertyName(propertyName: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.propertyName === propertyName);
|
||||
}
|
||||
|
||||
findRelationWithDbName(propertyName: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.name === propertyName);
|
||||
}
|
||||
|
||||
findRelationWithOneByPropertyName(propertyName: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.propertyName === propertyName && (relation.isOneToMany || relation.isOneToOne));
|
||||
}
|
||||
|
||||
findRelationWithOneByDbName(name: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.name === name && (relation.isOneToMany || relation.isOneToOne));
|
||||
}
|
||||
|
||||
findRelationWithManyByPropertyName(propertyName: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.propertyName === propertyName && (relation.isManyToOne || relation.isManyToMany));
|
||||
}
|
||||
|
||||
findRelationWithManyByDbName(name: string): RelationMetadata {
|
||||
return this._relations.find(relation => relation.name === name && (relation.isManyToOne || relation.isManyToMany));
|
||||
}
|
||||
|
||||
findRelationByPropertyName(name: string): RelationMetadata {
|
||||
return this.findRelationWithOneByPropertyName(name) || this.findRelationWithManyByPropertyName(name);
|
||||
}
|
||||
|
||||
hasRelationWithOneWithPropertyName(propertyName: string): boolean {
|
||||
return !!this.findRelationWithOneByPropertyName(propertyName);
|
||||
}
|
||||
|
||||
hasRelationWithManyWithPropertyName(propertyName: string): boolean {
|
||||
return !!this.findRelationWithManyByPropertyName(propertyName);
|
||||
}
|
||||
|
||||
hasRelationWithOneWithName(name: string): boolean {
|
||||
return !!this.findRelationWithOneByDbName(name);
|
||||
}
|
||||
|
||||
hasRelationWithManyWithName(name: string): boolean {
|
||||
return !!this.findRelationWithManyByDbName(name);
|
||||
}
|
||||
|
||||
}
|
||||
63
src/metadata-builder/metadata/ForeignKeyMetadata.ts
Normal file
63
src/metadata-builder/metadata/ForeignKeyMetadata.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {ColumnMetadata} from "./ColumnMetadata";
|
||||
import {TableMetadata} from "./TableMetadata";
|
||||
|
||||
/**
|
||||
* This metadata interface contains all information foreign keys.
|
||||
*/
|
||||
export class ForeignKeyMetadata {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private _table: TableMetadata;
|
||||
private _columns: ColumnMetadata[];
|
||||
private _referencedTable: TableMetadata;
|
||||
private _referencedColumns: ColumnMetadata[];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(table: TableMetadata, columns: ColumnMetadata[], referencedTable: TableMetadata, referencedColumns: ColumnMetadata[]) {
|
||||
this._table = table;
|
||||
this._columns = columns;
|
||||
this._referencedTable = referencedTable;
|
||||
this._referencedColumns = referencedColumns;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get table(): TableMetadata {
|
||||
return this._table;
|
||||
}
|
||||
|
||||
get columns(): ColumnMetadata[] {
|
||||
return this._columns;
|
||||
}
|
||||
|
||||
get referencedTable(): TableMetadata {
|
||||
return this._referencedTable;
|
||||
}
|
||||
|
||||
get referencedColumns(): ColumnMetadata[] {
|
||||
return this._referencedColumns;
|
||||
}
|
||||
|
||||
get columnNames(): string[] {
|
||||
return this.columns.map(column => column.name)
|
||||
}
|
||||
|
||||
get referencedColumnNames(): string[] {
|
||||
return this.referencedColumns.map(column => column.name)
|
||||
}
|
||||
|
||||
get name() {
|
||||
const key = `${this.table.name}_${this.columnNames.join("_")}` +
|
||||
`_${this.referencedTable.name}_${this.referencedColumnNames.join("_")}`;
|
||||
return "fk_" + require('sha1')(key);
|
||||
}
|
||||
|
||||
}
|
||||
13
src/metadata-builder/metadata/IndexMetadata.ts
Normal file
13
src/metadata-builder/metadata/IndexMetadata.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {PropertyMetadata} from "./PropertyMetadata";
|
||||
|
||||
/**
|
||||
* This metadata interface contains all information about some index on a field.
|
||||
*/
|
||||
export class IndexMetadata extends PropertyMetadata {
|
||||
|
||||
/**
|
||||
* The name of the index.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
}
|
||||
30
src/metadata-builder/metadata/OrmEventSubscriberMetadata.ts
Normal file
30
src/metadata-builder/metadata/OrmEventSubscriberMetadata.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
*/
|
||||
export class OrmEventSubscriberMetadata {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private _target: Function;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
constructor(target: Function) {
|
||||
this._target = target;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Getters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The object class to which this metadata is attached.
|
||||
*/
|
||||
get target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
}
|
||||
28
src/metadata-builder/metadata/PropertyMetadata.ts
Normal file
28
src/metadata-builder/metadata/PropertyMetadata.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* This represents metadata of some object's property.
|
||||
*/
|
||||
export abstract class PropertyMetadata {
|
||||
|
||||
private _target: Function;
|
||||
private _propertyName: string;
|
||||
|
||||
constructor(target: Function, propertyName: string) {
|
||||
this._target = target;
|
||||
this._propertyName = propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object class to which this metadata is attached.
|
||||
*/
|
||||
get target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the property of the object to which this metadata is attached.
|
||||
*/
|
||||
get propertyName() {
|
||||
return this._propertyName;
|
||||
}
|
||||
|
||||
}
|
||||
226
src/metadata-builder/metadata/RelationMetadata.ts
Normal file
226
src/metadata-builder/metadata/RelationMetadata.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import {PropertyMetadata} from "./PropertyMetadata";
|
||||
import {RelationTypes} from "../types/RelationTypes";
|
||||
import {RelationOptions} from "../options/RelationOptions";
|
||||
import {NamingStrategy} from "../../naming-strategy/NamingStrategy";
|
||||
import {TableMetadata} from "./TableMetadata";
|
||||
import {EntityMetadata} from "./EntityMetadata";
|
||||
|
||||
/**
|
||||
* Function that returns a type of the field. Returned value should be some class within which this relation is being created.
|
||||
*/
|
||||
export type RelationTypeInFunction = ((type?: any) => Function);
|
||||
|
||||
/**
|
||||
* Contains the name of the property of the object, or the function that returns this name.
|
||||
*/
|
||||
export type PropertyTypeInFunction<T> = string|((t: T) => string|any);
|
||||
|
||||
/**
|
||||
* This metadata interface contains all information about some document's relation.
|
||||
*/
|
||||
export class RelationMetadata extends PropertyMetadata {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Public Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
namingStrategy: NamingStrategy;
|
||||
ownerEntityPropertiesMap: Object;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Column name for this relation.
|
||||
*/
|
||||
private _name: string;
|
||||
|
||||
/**
|
||||
* Relation type.
|
||||
*/
|
||||
private _relationType: RelationTypes;
|
||||
|
||||
/**
|
||||
* The type of the field.
|
||||
*/
|
||||
private _type: RelationTypeInFunction;
|
||||
|
||||
/**
|
||||
* Inverse side of the relation.
|
||||
*/
|
||||
private _inverseSideProperty: PropertyTypeInFunction<any>;
|
||||
|
||||
/**
|
||||
* Indicates if this side is an owner of this relation.
|
||||
*/
|
||||
private _isOwning: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be inserted to the db.
|
||||
*/
|
||||
private _isCascadeInsert: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be updated in the db.
|
||||
*/
|
||||
private _isCascadeUpdate: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be remove from the db.
|
||||
*/
|
||||
private _isCascadeRemove: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object always will be left-joined when this object is being loaded.
|
||||
*/
|
||||
private _isAlwaysLeftJoin: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object always will be inner-joined when this object is being loaded.
|
||||
*/
|
||||
private _isAlwaysInnerJoin: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if relation column value can be nullable or not.
|
||||
*/
|
||||
private _isNullable: boolean = true;
|
||||
|
||||
/**
|
||||
* Old column name.
|
||||
*/
|
||||
private _oldColumnName: string;
|
||||
|
||||
/**
|
||||
* Related entity metadata.
|
||||
*/
|
||||
private _relatedEntityMetadata: EntityMetadata;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
constructor(target: Function,
|
||||
propertyName: string,
|
||||
relationType: RelationTypes,
|
||||
type: RelationTypeInFunction,
|
||||
inverseSideProperty: PropertyTypeInFunction<any>,
|
||||
isOwning: boolean,
|
||||
options: RelationOptions) {
|
||||
super(target, propertyName);
|
||||
this._relationType = relationType;
|
||||
this._type = type;
|
||||
this._isOwning = isOwning;
|
||||
this._inverseSideProperty = inverseSideProperty;
|
||||
|
||||
if (options.name)
|
||||
this._name = options.name;
|
||||
if (options.isAlwaysInnerJoin)
|
||||
this._isAlwaysInnerJoin = options.isAlwaysInnerJoin;
|
||||
if (options.isAlwaysLeftJoin)
|
||||
this._isAlwaysLeftJoin = options.isAlwaysLeftJoin;
|
||||
if (options.isCascadeInsert)
|
||||
this._isCascadeInsert = options.isCascadeInsert;
|
||||
if (options.isCascadeUpdate)
|
||||
this._isCascadeUpdate = options.isCascadeUpdate;
|
||||
if (options.isCascadeRemove)
|
||||
this._isCascadeRemove = options.isCascadeRemove;
|
||||
if (options.oldColumnName)
|
||||
this._oldColumnName = options.oldColumnName;
|
||||
if (options.isNullable)
|
||||
this._isNullable = options.isNullable;
|
||||
|
||||
if (!this._name)
|
||||
this._name = propertyName;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Accessors
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
get name(): string {
|
||||
return this.namingStrategy ? this.namingStrategy.relationName(this._name) : this._name;
|
||||
}
|
||||
|
||||
get relatedEntityMetadata(): EntityMetadata {
|
||||
return this._relatedEntityMetadata;
|
||||
}
|
||||
|
||||
set relatedEntityMetadata(metadata: EntityMetadata) {
|
||||
this._relatedEntityMetadata = metadata;
|
||||
}
|
||||
|
||||
get relationType(): RelationTypes {
|
||||
return this._relationType;
|
||||
}
|
||||
|
||||
get type(): Function {
|
||||
return this._type();
|
||||
}
|
||||
|
||||
get inverseSideProperty(): string {
|
||||
return this.computeInverseSide(this._inverseSideProperty);
|
||||
}
|
||||
|
||||
get isOwning(): boolean {
|
||||
return this._isOwning;
|
||||
}
|
||||
|
||||
get isCascadeInsert(): boolean {
|
||||
return this._isCascadeInsert;
|
||||
}
|
||||
|
||||
get isCascadeUpdate(): boolean {
|
||||
return this._isCascadeUpdate;
|
||||
}
|
||||
|
||||
get isCascadeRemove(): boolean {
|
||||
return this._isCascadeRemove;
|
||||
}
|
||||
|
||||
get isAlwaysLeftJoin(): boolean {
|
||||
return this._isAlwaysLeftJoin;
|
||||
}
|
||||
|
||||
get isAlwaysInnerJoin(): boolean {
|
||||
return this._isAlwaysInnerJoin;
|
||||
}
|
||||
|
||||
get isOneToOne(): boolean {
|
||||
return this.relationType === RelationTypes.ONE_TO_ONE;
|
||||
}
|
||||
|
||||
get isOneToMany(): boolean {
|
||||
return this.relationType === RelationTypes.ONE_TO_MANY;
|
||||
}
|
||||
|
||||
get isManyToOne(): boolean {
|
||||
return this.relationType === RelationTypes.MANY_TO_ONE;
|
||||
}
|
||||
|
||||
get isManyToMany(): boolean {
|
||||
return this.relationType === RelationTypes.MANY_TO_MANY;
|
||||
}
|
||||
|
||||
get isNullable(): boolean {
|
||||
return this._isNullable;
|
||||
}
|
||||
|
||||
get oldColumnName(): string {
|
||||
return this._oldColumnName;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private computeInverseSide(inverseSide: PropertyTypeInFunction<any>): string {
|
||||
if (typeof inverseSide === "function")
|
||||
return (<Function> inverseSide)(this.ownerEntityPropertiesMap);
|
||||
if (typeof inverseSide === "string")
|
||||
return <string> inverseSide;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
52
src/metadata-builder/metadata/TableMetadata.ts
Normal file
52
src/metadata-builder/metadata/TableMetadata.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import {NamingStrategy} from "../../naming-strategy/NamingStrategy";
|
||||
|
||||
/**
|
||||
* This metadata interface contains all information about some table.
|
||||
*/
|
||||
export class TableMetadata {
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Public Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
namingStrategy: NamingStrategy;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Private Properties
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private _target: Function;
|
||||
private _name: string;
|
||||
private _isAbstract: boolean;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
constructor(target: Function, name: string, isAbstract: boolean) {
|
||||
this._target = target;
|
||||
this._name = name;
|
||||
this._isAbstract = isAbstract;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Getters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Target entity of this table.
|
||||
* Target can be empty only for junction tables.
|
||||
*/
|
||||
get target() {
|
||||
return this._target;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.namingStrategy ? this.namingStrategy.tableName(this._name) : this._name;
|
||||
}
|
||||
|
||||
get isAbstract() {
|
||||
return this._isAbstract;
|
||||
}
|
||||
|
||||
}
|
||||
11
src/metadata-builder/options/ColumnOptions.ts
Normal file
11
src/metadata-builder/options/ColumnOptions.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface ColumnOptions {
|
||||
name?: string;
|
||||
type?: string;
|
||||
length?: string;
|
||||
isAutoIncrement?: boolean;
|
||||
isUnique?: boolean;
|
||||
isNullable?: boolean;
|
||||
columnDefinition?: string;
|
||||
comment?: string;
|
||||
oldColumnName?: string;
|
||||
}
|
||||
43
src/metadata-builder/options/RelationOptions.ts
Normal file
43
src/metadata-builder/options/RelationOptions.ts
Normal file
@ -0,0 +1,43 @@
|
||||
export interface RelationOptions {
|
||||
|
||||
/**
|
||||
* Field name to be used in the database.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be inserted to the db.
|
||||
*/
|
||||
isCascadeInsert?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be updated in the db.
|
||||
*/
|
||||
isCascadeUpdate?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object can be allowed to be remove from the db.
|
||||
*/
|
||||
isCascadeRemove?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object always will be left-joined when this object is being loaded.
|
||||
*/
|
||||
isAlwaysLeftJoin?: boolean;
|
||||
|
||||
/**
|
||||
* If set to true then it means that related object always will be inner-joined when this object is being loaded.
|
||||
*/
|
||||
isAlwaysInnerJoin?: boolean;
|
||||
|
||||
/**
|
||||
* Old column name. Used to make safe schema updates.
|
||||
*/
|
||||
oldColumnName?: string;
|
||||
|
||||
/**
|
||||
* Indicates if relation column value can be nullable or not.
|
||||
*/
|
||||
isNullable?: boolean;
|
||||
|
||||
}
|
||||
58
src/metadata-builder/types/ColumnTypes.ts
Normal file
58
src/metadata-builder/types/ColumnTypes.ts
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Lists all types that can be a table column.
|
||||
*/
|
||||
export class ColumnTypes {
|
||||
|
||||
static SMALLINT = "smallint";
|
||||
static INTEGER = "integer";
|
||||
static BIGINT = "bigint";
|
||||
static DECIMAL = "decimal";
|
||||
static FLOAT = "float";
|
||||
static STRING = "string";
|
||||
static TEXT = "text";
|
||||
static BINARY = "binary";
|
||||
static BLOB = "blob";
|
||||
static BOOLEAN = "boolean";
|
||||
static DATE = "date";
|
||||
static DATETIME = "datetime";
|
||||
static TIME = "time";
|
||||
static ARRAY = "array";
|
||||
static JSON = "json";
|
||||
|
||||
static isTypeSupported(type: string): boolean {
|
||||
switch (type) {
|
||||
case this.SMALLINT:
|
||||
case this.INTEGER:
|
||||
case this.BIGINT:
|
||||
case this.DECIMAL:
|
||||
case this.FLOAT:
|
||||
case this.STRING:
|
||||
case this.TEXT:
|
||||
case this.BINARY:
|
||||
case this.BLOB:
|
||||
case this.BOOLEAN:
|
||||
case this.DATE:
|
||||
case this.DATETIME:
|
||||
case this.TIME:
|
||||
case this.ARRAY:
|
||||
case this.JSON:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static validateTypeInFunction(typeFunction: () => Function): boolean {
|
||||
if (!typeFunction || typeof typeFunction !== "function")
|
||||
return false;
|
||||
|
||||
let type = typeFunction();
|
||||
if (!type)
|
||||
return false;
|
||||
|
||||
if (typeof type === "string" && !ColumnTypes.isTypeSupported(type))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
7
src/metadata-builder/types/RelationTypes.ts
Normal file
7
src/metadata-builder/types/RelationTypes.ts
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
export enum RelationTypes {
|
||||
ONE_TO_ONE = 1,
|
||||
ONE_TO_MANY = 2,
|
||||
MANY_TO_ONE = 3,
|
||||
MANY_TO_MANY = 4
|
||||
}
|
||||
20
src/naming-strategy/DefaultNamingStrategy.ts
Normal file
20
src/naming-strategy/DefaultNamingStrategy.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {NamingStrategy} from "./NamingStrategy";
|
||||
|
||||
/**
|
||||
* Naming strategy that is used by default.
|
||||
*/
|
||||
export class DefaultNamingStrategy implements NamingStrategy {
|
||||
|
||||
tableName(className: string): string {
|
||||
return className;
|
||||
}
|
||||
|
||||
columnName(propertyName: string): string {
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
relationName(propertyName: string): string {
|
||||
return propertyName;
|
||||
}
|
||||
|
||||
}
|
||||
22
src/naming-strategy/NamingStrategy.ts
Normal file
22
src/naming-strategy/NamingStrategy.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Naming strategy defines how auto-generated names for such things like table name, or table column gonna be
|
||||
* generated.
|
||||
*/
|
||||
export interface NamingStrategy {
|
||||
|
||||
/**
|
||||
* Gets the table name from the given class name.
|
||||
*/
|
||||
tableName(className: string): string;
|
||||
|
||||
/**
|
||||
* Gets the table's column name from the given property name.
|
||||
*/
|
||||
columnName(propertyName: string): string;
|
||||
|
||||
/**
|
||||
* Gets the table's relation name from the given property name.
|
||||
*/
|
||||
relationName(propertyName: string): string;
|
||||
|
||||
}
|
||||
283
src/repository/Repository.ts
Normal file
283
src/repository/Repository.ts
Normal file
@ -0,0 +1,283 @@
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
|
||||
import {OrmBroadcaster} from "../subscriber/OrmBroadcaster";
|
||||
import {QueryBuilder} from "../driver/query-builder/QueryBuilder";
|
||||
import {DynamicCascadeOptions} from "./cascade/CascadeOption";
|
||||
import {EntityCreator} from "./creator/EntityCreator";
|
||||
|
||||
/**
|
||||
* Repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
|
||||
*/
|
||||
export class Repository<Entity> {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private _connection: Connection;
|
||||
private _metadata: EntityMetadata;
|
||||
private broadcaster: OrmBroadcaster<Entity>;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection,
|
||||
metadata: EntityMetadata,
|
||||
broadcaster: OrmBroadcaster<Entity>) {
|
||||
this._connection = connection;
|
||||
this._metadata = metadata;
|
||||
this.broadcaster = broadcaster;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Getter Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get metadata(): EntityMetadata {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
get connection(): Connection {
|
||||
return this._connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new entity.
|
||||
*/
|
||||
create(): Entity {
|
||||
return <Entity> this.metadata.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if entity has an id.
|
||||
*/
|
||||
hasId(entity: Entity): boolean {
|
||||
return entity && this.metadata.primaryColumn && entity.hasOwnProperty(this.metadata.primaryColumn.propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates entity from the given json data. If fetchAllData param is specified then entity data will be
|
||||
* loaded from the database first, then filled with given json data.
|
||||
*/
|
||||
createFromJson(json: any, fetchProperty?: boolean): Promise<Entity>;
|
||||
createFromJson(json: any, fetchConditions?: Object): Promise<Entity>;
|
||||
createFromJson(json: any, fetchOption?: boolean|Object): Promise<Entity> {
|
||||
const creator = new EntityCreator(this.connection);
|
||||
return creator.createFromJson<Entity>(json, this.metadata, fetchOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a entities from the given array of plain javascript objects. If fetchAllData param is specified then
|
||||
* entities data will be loaded from the database first, then filled with given json data.
|
||||
*/
|
||||
createManyFromJson(objects: any[], fetchProperties?: boolean[]): Promise<Entity[]>;
|
||||
createManyFromJson(objects: any[], fetchConditions?: Object[]): Promise<Entity[]>;
|
||||
createManyFromJson(objects: any[], fetchOption?: boolean[]|Object[]): Promise<Entity[]> {
|
||||
return Promise.all(objects.map((object, key) => {
|
||||
const fetchConditions = (fetchOption && fetchOption[key]) ? fetchOption[key] : undefined;
|
||||
return this.createFromJson(object, fetchConditions);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new query builder that can be used to build an sql query.
|
||||
*/
|
||||
createQueryBuilder(alias?: string): QueryBuilder {
|
||||
const queryBuilder = this.connection.driver.createQueryBuilder();
|
||||
queryBuilder.getTableNameFromEntityCallback = entity => this.getTableNameFromEntityCallback(entity);
|
||||
if (alias)
|
||||
queryBuilder.select("*").from(this.metadata.target, alias);
|
||||
|
||||
return queryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes query. Expects query will return object in Entity format and creates Entity object from that result.
|
||||
*/
|
||||
queryOne(query: string): Promise<Entity> {
|
||||
return this.connection.driver
|
||||
.query<any[]>(query)
|
||||
.then(results => this.createFromJson(results[0]))
|
||||
.then(entity => {
|
||||
this.broadcaster.broadcastAfterLoaded(entity);
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes query. Expects query will return objects in Entity format and creates Entity objects from that result.
|
||||
*/
|
||||
queryMany(query: string): Promise<Entity[]> {
|
||||
return this.connection.driver
|
||||
.query<any[]>(query)
|
||||
.then(results => this.createManyFromJson(results))
|
||||
.then(entities => {
|
||||
this.broadcaster.broadcastAfterLoadedAll(entities);
|
||||
return entities;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes query and returns raw result.
|
||||
*/
|
||||
query(query: string): Promise<any> {
|
||||
return this.connection.driver.query(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives number of rows found by a given query.
|
||||
*/
|
||||
queryCount(query: any): Promise<number> {
|
||||
return this.connection.driver
|
||||
.query(query)
|
||||
.then(result => parseInt(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities that match given conditions.
|
||||
*/
|
||||
find(conditions?: Object): Promise<Entity[]> {
|
||||
const builder = this.createQueryBuilder("entity");
|
||||
Object.keys(conditions).forEach(key => {
|
||||
builder.where("entity." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
|
||||
});
|
||||
return this.queryMany(builder.getSql());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds one entity that matches given condition.
|
||||
*/
|
||||
findOne(conditions: Object): Promise<Entity> {
|
||||
const builder = this.createQueryBuilder("entity");
|
||||
Object.keys(conditions).forEach(key => {
|
||||
builder.where("entity." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
|
||||
});
|
||||
return this.queryOne(builder.getSql());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entity with given id.
|
||||
*/
|
||||
findById(id: any): Promise<Entity> {
|
||||
const builder = this.createQueryBuilder("entity")
|
||||
.where("entity." + this.metadata.primaryColumn.name + "=:id")
|
||||
.setParameter("id", id);
|
||||
|
||||
|
||||
return this.queryOne(builder.getSql());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Persist starts
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Saves a given entity. If entity is not inserted yet then it inserts a new entity.
|
||||
* If entity already inserted then performs its update.
|
||||
*/
|
||||
persist(entity: Entity, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>): Promise<Entity> {
|
||||
|
||||
|
||||
|
||||
// if (!this.schema.isEntityTypeCorrect(entity))
|
||||
// throw new BadEntityInstanceException(entity, this.schema.entityClass);
|
||||
|
||||
// const remover = new EntityRemover<Entity>(this.connection);
|
||||
// const persister = new EntityPersister<Entity>(this.connection);
|
||||
|
||||
return remover.computeRemovedRelations(this.metadata, entity, dynamicCascadeOptions)
|
||||
.then(result => persister.persist(this.metadata, entity, dynamicCascadeOptions))
|
||||
.then(result => remover.executeRemoveOperations())
|
||||
.then(result => remover.executeUpdateInverseSideRelationRemoveIds())
|
||||
.then(result => entity);
|
||||
}
|
||||
|
||||
computeChangeSet(entity: Entity) {
|
||||
// if there is no primary key - there is no way to determine if object needs to be updated or insert
|
||||
// since we cannot get the target object without identifier, that's why we always do insert for such objects
|
||||
if (!this.metadata.primaryColumn)
|
||||
return this.insert(entity);
|
||||
|
||||
// load entity from the db
|
||||
this.findById(this.metadata.getEntityId(entity)).then(dbEntity => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
insert(entity: Entity) {
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Persist ends
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Removes a given entity.
|
||||
*/
|
||||
remove(entity: Entity, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>): Promise<void> {
|
||||
// if (!this.schema.isEntityTypeCorrect(entity))
|
||||
// throw new BadEntityInstanceException(entity, this.schema.entityClass);
|
||||
|
||||
const remover = new EntityRemover<Entity>(this.connection);
|
||||
return remover.registerEntityRemoveOperation(this.metadata, this.metadata.getEntityId(entity), dynamicCascadeOptions)
|
||||
.then(results => remover.executeRemoveOperations())
|
||||
.then(results => remover.executeUpdateInverseSideRelationRemoveIds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes entity by a given id.
|
||||
*/
|
||||
removeById(id: string): Promise<void> {
|
||||
const builder = this.createQueryBuilder("entity")
|
||||
.delete()
|
||||
.where("entity." + this.metadata.primaryColumn.name + "=:id")
|
||||
.setParameter("id", id);
|
||||
|
||||
return this.query(builder.getSql());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes entities by a given simple conditions.
|
||||
*/
|
||||
removeByConditions(conditions: Object): Promise<any> {
|
||||
const builder = this.createQueryBuilder("entity").delete();
|
||||
Object.keys(conditions).forEach(key => {
|
||||
builder.where("entity." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
|
||||
});
|
||||
return this.query(builder.getSql());
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities by given criteria and returns them with the total number of
|
||||
*/
|
||||
queryManyAndCount(query: string, countQuery: string): Promise<{ entities: Entity[] }> {
|
||||
return Promise.all<any>([
|
||||
this.queryMany(query),
|
||||
this.queryCount(countQuery)
|
||||
]).then(([entities, count]) => {
|
||||
return { entities: <Entity[]> entities, count: <number> count };
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*private dbObjectToEntity(dbObject: any): Promise<Entity> {
|
||||
const hydrator = new EntityHydrator<Entity>(this.connection);
|
||||
return hydrator.hydrate(this.metadata, dbObject, joinFields);
|
||||
}*/
|
||||
|
||||
private getTableNameFromEntityCallback(entity: Function) {
|
||||
const metadata = this.connection.getMetadata(entity);
|
||||
// todo throw exception if metadata is missing
|
||||
return metadata.table.name;
|
||||
}
|
||||
|
||||
}
|
||||
67
src/repository/cascade/CascadeOption.ts
Normal file
67
src/repository/cascade/CascadeOption.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Cascade options used to set cascade operations over document relations.
|
||||
*/
|
||||
export interface CascadeOption {
|
||||
|
||||
insert?: boolean;
|
||||
update?: boolean;
|
||||
remove?: boolean;
|
||||
field: string|any;
|
||||
cascades?: CascadeOption[]|((object: any) => CascadeOption[]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cascade options can be set using multiple forms:
|
||||
*
|
||||
* Form1: nested fields
|
||||
*
|
||||
* let cascades = {
|
||||
* answers: {
|
||||
* insert: true,
|
||||
* update: true,
|
||||
* remove: true,
|
||||
* cascades: {
|
||||
* name: {
|
||||
* insert: true,
|
||||
* update: true,
|
||||
* remove: true
|
||||
* }
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* // ...
|
||||
* };
|
||||
*
|
||||
* Form2: flat fields
|
||||
*
|
||||
* let cascades = {
|
||||
* 'answers': { insert: true, update: true, remove: true },
|
||||
* 'answers.name': { insert: true, update: true, remove: true },
|
||||
* // ...
|
||||
* };
|
||||
*
|
||||
* Form3: field in CascadeOption object
|
||||
*
|
||||
* let cascades2 = {
|
||||
* 'answers': { insert: true, update: true, remove: true },
|
||||
* 'answers.name': { insert: true, update: true, remove: true },
|
||||
* // ...
|
||||
* };
|
||||
*
|
||||
* Form4: typesafe using typed objects in function arguments
|
||||
*
|
||||
* let cascades3 = (vote: Vote) => [{
|
||||
* field: vote.answers,
|
||||
* insert: true,
|
||||
* update: true,
|
||||
* remove: true,
|
||||
* cascades: (voteAnswer: VoteAnswer) => [{
|
||||
* field: voteAnswer.results,
|
||||
* insert: true,
|
||||
* update: true,
|
||||
* remove: true
|
||||
* }]
|
||||
* }];
|
||||
*
|
||||
*/
|
||||
export type DynamicCascadeOptions<Entity> = CascadeOption[]|((entity: Entity) => CascadeOption[])|Object;
|
||||
86
src/repository/cascade/CascadeOptionUtils.ts
Normal file
86
src/repository/cascade/CascadeOptionUtils.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import {CascadeOption, DynamicCascadeOptions} from "./CascadeOption";
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
import {RelationSchema} from "../../schema/RelationSchema";
|
||||
|
||||
export class CascadeOptionUtils {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Static Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
static prepareCascadeOptions(schema: DocumentSchema, cascadeOptions: DynamicCascadeOptions<any>): CascadeOption[] {
|
||||
if (cascadeOptions instanceof Function) {
|
||||
return (<((document: Document) => CascadeOption[])> cascadeOptions)(schema.createPropertiesMirror());
|
||||
|
||||
} else if (cascadeOptions instanceof Object && !(cascadeOptions instanceof Array)) {
|
||||
return CascadeOptionUtils.convertFromObjectMap(cascadeOptions);
|
||||
}
|
||||
|
||||
return <CascadeOption[]> cascadeOptions;
|
||||
}
|
||||
|
||||
static find(cascadeOptions: CascadeOption[], fieldName: string) {
|
||||
return cascadeOptions ? cascadeOptions.reduce((found, cascade) => cascade.field === fieldName ? cascade : found, null) : null;
|
||||
}
|
||||
|
||||
static isCascadeRemove(relation: RelationSchema, cascadeOption?: CascadeOption): boolean {
|
||||
if (relation.isCascadeRemove && !cascadeOption)
|
||||
return true;
|
||||
if (relation.isCascadeRemove && cascadeOption && cascadeOption.remove !== false)
|
||||
return true;
|
||||
if (cascadeOption && cascadeOption.remove)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static isCascadeInsert(relation: RelationSchema, cascadeOption?: CascadeOption): boolean {
|
||||
if (relation.isCascadeInsert && !cascadeOption)
|
||||
return true;
|
||||
if (relation.isCascadeInsert && cascadeOption && cascadeOption.insert !== false)
|
||||
return true;
|
||||
if (cascadeOption && cascadeOption.insert)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static isCascadeUpdate(relation: RelationSchema, cascadeOption?: CascadeOption): boolean {
|
||||
if (relation.isCascadeUpdate && !cascadeOption)
|
||||
return true;
|
||||
if (relation.isCascadeUpdate && cascadeOption && cascadeOption.update !== false)
|
||||
return true;
|
||||
if (cascadeOption && cascadeOption.update)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static isCascadePersist(relation: RelationSchema, cascadeOption?: CascadeOption): boolean {
|
||||
return this.isCascadeInsert(relation, cascadeOption) || this.isCascadeUpdate(relation, cascadeOption);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Static Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static convertFromObjectMap(object: any): CascadeOption[] {
|
||||
if (!object)
|
||||
return [];
|
||||
|
||||
return Object.keys(object).map(key => {
|
||||
let subCascadeKeys = Object.keys(object).filter(k => k.substr(0, key.length + 1) === key + ".");
|
||||
let subCascades = subCascadeKeys.reduce((v: any, k: string) => { v[k.substr(key.length + 1)] = object[k]; return v; }, {});
|
||||
if (key.indexOf(".") !== -1) return null;
|
||||
|
||||
return <CascadeOption> {
|
||||
field: key,
|
||||
insert: !!object[key].insert,
|
||||
update: !!object[key].update,
|
||||
remove: !!object[key].remove,
|
||||
cascades: this.convertFromObjectMap(object[key].cascades ? object[key].cascades : subCascades)
|
||||
};
|
||||
}).filter(option => option !== null);
|
||||
}
|
||||
|
||||
}
|
||||
136
src/repository/creator/EntityCreator.ts
Normal file
136
src/repository/creator/EntityCreator.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
|
||||
|
||||
export class EntityCreator {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
createFromJson<Entity>(object: any, metadata: EntityMetadata, fetchProperty?: boolean): Promise<Entity>;
|
||||
createFromJson<Entity>(object: any, metadata: EntityMetadata, fetchProperty?: Object): Promise<Entity>;
|
||||
createFromJson<Entity>(object: any, metadata: EntityMetadata, fetchOption?: boolean|Object): Promise<Entity> {
|
||||
return this.objectToEntity(object, metadata, fetchOption);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
getLoadMap(metadata: EntityMetadata) {
|
||||
// let tableUsageIndices = 0;
|
||||
const columns = metadata.columns.map(column => {
|
||||
return metadata.table.name + "." + column.name + " as " + metadata.table.name + "_" + column.name;
|
||||
});
|
||||
|
||||
const qb = this.connection.driver
|
||||
.createQueryBuilder()
|
||||
.select(columns)
|
||||
.from(metadata.table.name, metadata.table.name);
|
||||
|
||||
metadata.relations.filter(relation => relation.isAlwaysLeftJoin || relation.isAlwaysInnerJoin).map(relation => {
|
||||
const table = metadata.table.name;
|
||||
const column = metadata.primaryColumn.name;
|
||||
const relationTable = relation.relatedEntityMetadata.table.name;
|
||||
const relationColumn = relation.relatedEntityMetadata.primaryColumn.name;
|
||||
const condition = table + "." + column + "=" + relationTable + "." + relationColumn;
|
||||
const selectedColumns = relation.relatedEntityMetadata.columns.map(column => {
|
||||
return relationTable + "." + column.name + " as " + relationTable + "_" + column.name;
|
||||
});
|
||||
if (relation.isAlwaysLeftJoin) {
|
||||
return qb.addSelect(selectedColumns).leftJoin(relationTable, relationTable, "ON", condition);
|
||||
} else { // else can be only always inner join
|
||||
return qb.addSelect(selectedColumns).innerJoin(relationTable, relationTable, "ON", condition);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(qb.getSql());
|
||||
}
|
||||
|
||||
private objectToEntity(object: any, metadata: EntityMetadata, doFetchProperties?: boolean): Promise<any>;
|
||||
private objectToEntity(object: any, metadata: EntityMetadata, fetchConditions?: Object): Promise<any>;
|
||||
private objectToEntity(object: any, metadata: EntityMetadata, fetchOption?: boolean|Object): Promise<any> {
|
||||
if (!object)
|
||||
throw new Error("Given object is empty, cannot initialize empty object.");
|
||||
|
||||
this.getLoadMap(metadata);
|
||||
|
||||
const doFetch = !!fetchOption;
|
||||
const entityPromise = this.loadDependOnFetchOption(object, metadata, fetchOption);
|
||||
|
||||
// todo: this needs strong optimization. since repository.findById makes here multiple operations and each time loads lot of data by cascades
|
||||
|
||||
return entityPromise.then((entity: any) => {
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
if (!entity) entity = metadata.create();
|
||||
|
||||
// copy values from the object to the entity
|
||||
Object.keys(object)
|
||||
.filter(key => metadata.hasColumnWithDbName(key))
|
||||
.forEach(key => entity[key] = object[key]);
|
||||
|
||||
// second copy relations and pre-load them
|
||||
Object.keys(object)
|
||||
.filter(key => metadata.hasRelationWithPropertyName(key))
|
||||
.forEach(key => {
|
||||
const relation = metadata.findRelationWithPropertyName(key);
|
||||
const relationEntityMetadata = this.connection.getMetadata(relation.target);
|
||||
|
||||
if (object[key] instanceof Array) {
|
||||
const subPromises = object[key].map((i: any) => {
|
||||
return this.objectToEntity(i, relationEntityMetadata, doFetch);
|
||||
});
|
||||
promises.push(Promise.all(subPromises).then(subEntities => entity[key] = subEntities));
|
||||
|
||||
} else if (object[key] instanceof Object) {
|
||||
const subPromise = this.objectToEntity(object[key], relationEntityMetadata, doFetch);
|
||||
promises.push(subPromise.then(subEntity => entity[key] = subEntity));
|
||||
}
|
||||
});
|
||||
|
||||
// todo: here actually we need to find and save to entity object three things:
|
||||
// * related entity where id is stored in the current entity
|
||||
// * related entity where id is stored in related entity
|
||||
// * related entities from many-to-many table
|
||||
|
||||
// now find relations by entity ids stored in entities
|
||||
Object.keys(entity)
|
||||
.filter(key => metadata.hasRelationWithDbName(key))
|
||||
.forEach(key => {
|
||||
const relation = metadata.findRelationWithDbName(key);
|
||||
const relationEntityMetadata = this.connection.getMetadata(relation.target);
|
||||
// todo.
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(_ => entity);
|
||||
});
|
||||
}
|
||||
|
||||
private loadDependOnFetchOption(object: any, metadata: EntityMetadata, fetchOption?: boolean|Object): Promise<any> {
|
||||
const repository = this.connection.getRepository(metadata.target);
|
||||
|
||||
if (!!fetchOption && fetchOption instanceof Object)
|
||||
return repository.findOne(fetchOption);
|
||||
|
||||
if (!!fetchOption && repository.hasId(object))
|
||||
return repository.findById(object[metadata.primaryColumn.name]);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
11
src/repository/error/BadDocumentInstanceError.ts
Normal file
11
src/repository/error/BadDocumentInstanceError.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class BadDocumentInstanceError extends Error {
|
||||
name = "BadDocumentInstanceError";
|
||||
|
||||
constructor(document: any, expectedClass: Function) {
|
||||
super();
|
||||
document = typeof document === "object" ? JSON.stringify(document) : document;
|
||||
this.message = "Cannot persist document of this class because given document is not instance " +
|
||||
"of " + expectedClass + ", but given " + document;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/repository/error/FieldTypeNotSupportedError.ts
Normal file
9
src/repository/error/FieldTypeNotSupportedError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class FieldTypeNotSupportedError extends Error {
|
||||
name = "FieldTypeNotSupportedError";
|
||||
|
||||
constructor(fieldType: string|Function, field: string, document: any) {
|
||||
super();
|
||||
this.message = fieldType + " is not supported set on the field " + field + " on the document " + document;
|
||||
}
|
||||
|
||||
}
|
||||
9
src/repository/error/NoDocumentWithSuchIdError.ts
Normal file
9
src/repository/error/NoDocumentWithSuchIdError.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class NoDocumentWithSuchIdError extends Error {
|
||||
name = "NoDocumentWithSuchIdError";
|
||||
|
||||
constructor(id: any, collection: string) {
|
||||
super();
|
||||
this.message = "Cannot find a " + collection + " document with given document id (" + id + "), has it been already removed?";
|
||||
}
|
||||
|
||||
}
|
||||
10
src/repository/error/WrongFieldTypeInDocumentError.ts
Normal file
10
src/repository/error/WrongFieldTypeInDocumentError.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export class WrongFieldTypeInDocumentError extends Error {
|
||||
name = "WrongFieldTypeInDocumentError";
|
||||
|
||||
constructor(expectedType: string|Function, fieldName: string, document: any) {
|
||||
super();
|
||||
this.message = fieldName + " expected to be a type " + expectedType + ", but " + document[fieldName] +
|
||||
" value is given for document " + JSON.stringify(document);
|
||||
}
|
||||
|
||||
}
|
||||
178
src/repository/hydration/DocumentHydrator.ts
Normal file
178
src/repository/hydration/DocumentHydrator.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import {Driver} from "../../driver/Driver";
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {Repository} from "./../Repository";
|
||||
import {JoinFieldOption} from "./JoinFieldOption";
|
||||
import {RelationSchema} from "../../schema/RelationSchema";
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
|
||||
/**
|
||||
* Loads the document
|
||||
*/
|
||||
export class DocumentHydrator<Document> {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
hydrate(schema: DocumentSchema,
|
||||
dbObject: Object|any,
|
||||
joinFields?: JoinFieldOption[]|any[]): Promise<Document> {
|
||||
|
||||
let allPromises: Promise<any>[] = [];
|
||||
let document: Document|any = schema.create();
|
||||
let isDocumentSkipped = false;
|
||||
|
||||
if (schema.idField) // remember that id field cannot be in embed documents
|
||||
document[schema.idField.name] = schema.getIdValue(dbObject[this.connection.driver.getIdFieldName()]);
|
||||
schema.fields.filter(field => dbObject[field.name] !== undefined).forEach(field => {
|
||||
|
||||
if (dbObject[field.name] instanceof Array && field.isTypeDocument()) {
|
||||
let embedTypeSchema = this.connection.getSchema(<Function> field.type);
|
||||
let subCondition = this.getSubFieldCondition(joinFields, field.name);
|
||||
let promises = dbObject[field.name].map((i: any) => this.hydrate(embedTypeSchema, i, subCondition));
|
||||
allPromises.push(Promise.all(promises).then((subDocuments: any[]) => {
|
||||
document[field.propertyName] = subDocuments;
|
||||
}));
|
||||
} else if (dbObject[field.name] instanceof Object && field.isTypeDocument()) {
|
||||
let embedTypeSchema = this.connection.getSchema(<Function> field.type);
|
||||
let subCondition = this.getSubFieldCondition(joinFields, field.name);
|
||||
allPromises.push(this.hydrate(embedTypeSchema, dbObject[field.name], subCondition).then(subDocument => {
|
||||
document[field.propertyName] = subDocument;
|
||||
}));
|
||||
} else {
|
||||
document[field.propertyName] = dbObject[field.name];
|
||||
}
|
||||
});
|
||||
|
||||
schema.relationWithOnes.forEach(relation => {
|
||||
|
||||
let relationId = dbObject[relation.name];
|
||||
let canLoadRelation = this.canLoadRelation(relation, joinFields);
|
||||
let isLoadInnerTyped = this.isInnerJoin(joinFields, relation.name) || relation.isAlwaysInnerJoin;
|
||||
|
||||
if (!canLoadRelation)
|
||||
return;
|
||||
if (!relationId && isLoadInnerTyped) {
|
||||
isDocumentSkipped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let relatedRepo = this.connection.getRepository(<Function> relation.type);
|
||||
let subFields = this.getSubFields(joinFields, relation.name);
|
||||
let conditions: any = { [this.connection.driver.getIdFieldName()]: relationId };
|
||||
let subCondition = this.getSubFieldCondition(joinFields, relation.name);
|
||||
if (subCondition)
|
||||
Object.keys(subCondition).forEach(key => conditions[key] = subCondition[key]);
|
||||
|
||||
allPromises.push(relatedRepo.findOne(conditions, null, subFields).then(foundRelation => {
|
||||
if (!foundRelation && isLoadInnerTyped) {
|
||||
isDocumentSkipped = true;
|
||||
} else if (foundRelation) {
|
||||
document[relation.propertyName] = foundRelation;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
schema.relationWithManies.forEach(relation => {
|
||||
|
||||
let canLoadRelation = this.canLoadRelation(relation, joinFields);
|
||||
let isLoadInnerTyped = this.isInnerJoin(joinFields, relation.name) || relation.isAlwaysInnerJoin;
|
||||
|
||||
if (!canLoadRelation)
|
||||
return;
|
||||
if ((!dbObject[relation.name] || !dbObject[relation.name].length) && isLoadInnerTyped) {
|
||||
isDocumentSkipped = true;
|
||||
return;
|
||||
}
|
||||
if (!dbObject[relation.name] || !dbObject[relation.name].length)
|
||||
return;
|
||||
|
||||
let relatedRepo = this.connection.getRepository(<Function> relation.type);
|
||||
let subFields = this.getSubFields(joinFields, relation.name);
|
||||
let findPromises = dbObject[relation.name].map((i: any) => {
|
||||
let conditions: any = { [this.connection.driver.getIdFieldName()]: i };
|
||||
let subCondition = this.getSubFieldCondition(joinFields, relation.name);
|
||||
if (subCondition)
|
||||
Object.keys(subCondition).forEach(key => conditions[key] = subCondition[key]);
|
||||
return relatedRepo.findOne(conditions, null, subFields);
|
||||
});
|
||||
|
||||
allPromises.push(Promise.all(findPromises).then(foundRelations => {
|
||||
foundRelations = foundRelations.filter(relation => !!relation);
|
||||
if ((!foundRelations || !foundRelations.length) && isLoadInnerTyped) {
|
||||
isDocumentSkipped = true;
|
||||
} else if (foundRelations) {
|
||||
document[relation.propertyName] = foundRelations;
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(allPromises).then(results => !isDocumentSkipped ? document : null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private canLoadRelation(relation: RelationSchema, joinFields?: JoinFieldOption[]|any[]): boolean {
|
||||
return this.hasKey(joinFields, relation.propertyName)
|
||||
|| relation.isAlwaysLeftJoin
|
||||
|| relation.isAlwaysInnerJoin;
|
||||
}
|
||||
|
||||
private hasKey(joinFields: JoinFieldOption[]|any[], key: string) {
|
||||
return (
|
||||
(<string[]> joinFields).indexOf(key) !== -1 ||
|
||||
joinFields.reduce((found, field) => (field instanceof Array && field[0] === key) ? true : found, false) ||
|
||||
joinFields.reduce((found: any, field: JoinFieldOption) => (field && field.field && field.field === key) ? true : found, false)
|
||||
);
|
||||
}
|
||||
|
||||
private isInnerJoin(joinFields: JoinFieldOption[]|any[], key: string) {
|
||||
return joinFields.reduce((sub, field) => {
|
||||
if (field instanceof Array && field[0] === key)
|
||||
return field[1];
|
||||
if (field instanceof Object && field.field && field.field === key)
|
||||
return field.inner;
|
||||
|
||||
return sub;
|
||||
}, null);
|
||||
}
|
||||
|
||||
private getSubFields(joinFields: JoinFieldOption[]|any[], key: string) {
|
||||
return joinFields.reduce((sub, field) => {
|
||||
if (field instanceof Array && field[0] === key)
|
||||
return field[1];
|
||||
if (field instanceof Object && field.field && field.field === key)
|
||||
return field.joins;
|
||||
|
||||
return sub;
|
||||
}, null);
|
||||
}
|
||||
|
||||
private getSubFieldCondition(joinFields: JoinFieldOption[]|any[], key: string): any {
|
||||
return joinFields.reduce((sub, field) => {
|
||||
if (field instanceof Array && field[0] === key)
|
||||
return field[2];
|
||||
if (field instanceof Object && field.field && field.field === key)
|
||||
return field.condition;
|
||||
|
||||
return sub;
|
||||
}, null);
|
||||
}
|
||||
|
||||
}
|
||||
9
src/repository/hydration/JoinFieldOption.ts
Normal file
9
src/repository/hydration/JoinFieldOption.ts
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
export interface JoinFieldOption {
|
||||
|
||||
inner?: boolean;
|
||||
field: string|any;
|
||||
condition?: any;
|
||||
joins: JoinFieldOption|any[]; // todo: check its type - looks wrong
|
||||
|
||||
}
|
||||
3
src/repository/hydration/JoinFieldOptionUtils.ts
Normal file
3
src/repository/hydration/JoinFieldOptionUtils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export class JoinFieldOptionUtils {
|
||||
|
||||
}
|
||||
13
src/repository/operation/InverseSideUpdateOperation.ts
Normal file
13
src/repository/operation/InverseSideUpdateOperation.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
import {RelationSchema} from "../../schema/RelationSchema";
|
||||
|
||||
/**
|
||||
* Represents single inverse side update operation.
|
||||
*/
|
||||
export interface InverseSideUpdateOperation {
|
||||
documentSchema: DocumentSchema;
|
||||
getDocumentId: () => any;
|
||||
inverseSideDocumentId: any;
|
||||
inverseSideDocumentSchema: DocumentSchema;
|
||||
inverseSideDocumentRelation: RelationSchema;
|
||||
}
|
||||
14
src/repository/operation/PersistOperation.ts
Normal file
14
src/repository/operation/PersistOperation.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
import {InverseSideUpdateOperation} from "./InverseSideUpdateOperation";
|
||||
|
||||
/**
|
||||
* Persist operation.
|
||||
*/
|
||||
export interface PersistOperation {
|
||||
allowedPersist: boolean;
|
||||
deepness: number;
|
||||
document: any;
|
||||
dbObject: Object;
|
||||
schema: DocumentSchema;
|
||||
afterExecution?: ((document: any) => InverseSideUpdateOperation)[];
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
import {PersistOperation} from "./PersistOperation";
|
||||
|
||||
/**
|
||||
* Represents single remove operation. Remove operation is used to keep in memory what document with what id
|
||||
* should be removed in some future.
|
||||
*/
|
||||
export interface PersistOperationGrouppedByDeepness {
|
||||
deepness: number;
|
||||
operations: PersistOperation[];
|
||||
}
|
||||
10
src/repository/operation/RemoveOperation.ts
Normal file
10
src/repository/operation/RemoveOperation.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
|
||||
/**
|
||||
* Represents single remove operation. Remove operation is used to keep in memory what document with what id
|
||||
* should be removed in some future.
|
||||
*/
|
||||
export interface RemoveOperation {
|
||||
schema: DocumentSchema;
|
||||
id: string;
|
||||
}
|
||||
23
src/repository/persistence/DbObjectFieldValidator.ts
Normal file
23
src/repository/persistence/DbObjectFieldValidator.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Validates given type.
|
||||
*/
|
||||
export class DbObjectFieldValidator {
|
||||
private static supportedTypes = ["string", "number", "boolean", "date"];
|
||||
|
||||
static isTypeSupported(type: string) {
|
||||
return this.supportedTypes.indexOf(type) !== -1;
|
||||
}
|
||||
|
||||
static validateArray(array: any[], type: string): boolean {
|
||||
return array.filter(item => !this.validate(item, type)).length === 0;
|
||||
}
|
||||
|
||||
static validate(value: any, type: string): boolean {
|
||||
let foundTypeToCheckIndex = this.supportedTypes.indexOf(type);
|
||||
return typeof value === this.supportedTypes[foundTypeToCheckIndex] ||
|
||||
(type === "date" && (value instanceof Date || !isNaN(Date.parse(value))));
|
||||
}
|
||||
|
||||
// private static
|
||||
|
||||
}
|
||||
148
src/repository/persistence/DocumentPersister.ts
Normal file
148
src/repository/persistence/DocumentPersister.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
|
||||
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";
|
||||
import {DocumentToDbObjectTransformer} from "./DocumentToDbObjectTransformer";
|
||||
import {PersistOperation} from "./../operation/PersistOperation";
|
||||
import {InverseSideUpdateOperation} from "./../operation/InverseSideUpdateOperation";
|
||||
import {PersistOperationGrouppedByDeepness} from "../operation/PersistOperationGrouppedByDeepness";
|
||||
|
||||
export class EntityPersister {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
persist<Entity>(schema: DocumentSchema, document: Document, cascadeOptions?: DynamicCascadeOptions<Document>): Promise<Entity> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DocumentPersister<Document> {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
persist(schema: DocumentSchema, document: Document, cascadeOptions?: DynamicCascadeOptions<Document>): Promise<Document> {
|
||||
let transformer = new DocumentToDbObjectTransformer<Document>(this.connection);
|
||||
let dbObject = transformer.transform(schema, document, cascadeOptions);
|
||||
let groupedPersistOperations = this.groupPersistOperationsByDeepness(transformer.persistOperations);
|
||||
groupedPersistOperations = groupedPersistOperations.sort(groupedOperation => groupedOperation.deepness * -1);
|
||||
|
||||
let pendingPromise: Promise<any>;
|
||||
let relationWithOneDocumentIdsToBeUpdated: InverseSideUpdateOperation[] = [];
|
||||
groupedPersistOperations.map(groupedPersistOperation => {
|
||||
pendingPromise = Promise.all([pendingPromise]).then(() => {
|
||||
return Promise.all(groupedPersistOperation.operations.filter(persistOperation => !!persistOperation.allowedPersist).map((persistOperation: PersistOperation) =>
|
||||
this.save(persistOperation.schema, persistOperation.document, persistOperation.dbObject).then(document => {
|
||||
if (persistOperation.afterExecution) {
|
||||
persistOperation.afterExecution.forEach(afterExecution => {
|
||||
relationWithOneDocumentIdsToBeUpdated.push(afterExecution(document));
|
||||
});
|
||||
}
|
||||
return document;
|
||||
})
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
transformer.postPersistOperations.forEach(postPersistOperation => postPersistOperation());
|
||||
|
||||
return Promise.all([pendingPromise])
|
||||
.then(result => this.save(schema, document, dbObject))
|
||||
.then(result => this.updateRelationInverseSideIds(relationWithOneDocumentIdsToBeUpdated))
|
||||
.then(result => document);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private groupPersistOperationsByDeepness(persistOperations: PersistOperation[]): PersistOperationGrouppedByDeepness[] {
|
||||
let groupedOperations: PersistOperationGrouppedByDeepness[] = [];
|
||||
persistOperations.forEach(persistOperation => {
|
||||
let groupedOperation = groupedOperations.reduce((found, groupedOperation) => groupedOperation.deepness === persistOperation.deepness ? groupedOperation : found, null);
|
||||
if (!groupedOperation) {
|
||||
groupedOperation = { deepness: persistOperation.deepness, operations: [] };
|
||||
groupedOperations.push(groupedOperation);
|
||||
}
|
||||
|
||||
groupedOperation.operations.push(persistOperation);
|
||||
});
|
||||
return groupedOperations;
|
||||
}
|
||||
|
||||
private save(schema: DocumentSchema, document: Document|any, dbObject: Object): Promise<Document> {
|
||||
let documentId = schema.getDocumentId(document);
|
||||
let driver = this.connection.driver;
|
||||
let broadcaster = this.connection.getBroadcaster(schema.documentClass);
|
||||
|
||||
if (documentId) {
|
||||
// let conditions = driver.createIdCondition(schema.getIdValue(documentId)/*, schema.idField.isObjectId*/);
|
||||
let conditions = schema.createIdCondition(documentId);
|
||||
broadcaster.broadcastBeforeUpdate({ document: document, conditions: conditions });
|
||||
return driver.replaceOne(schema.name, conditions, dbObject, { upsert: true }).then(saved => {
|
||||
broadcaster.broadcastAfterUpdate({ document: document, conditions: conditions });
|
||||
return document;
|
||||
});
|
||||
} else {
|
||||
broadcaster.broadcastBeforeInsert({ document: document });
|
||||
return driver.insertOne(schema.name, dbObject).then(result => {
|
||||
if (result.insertedId)
|
||||
document[schema.idField.name] = schema.getIdValue(result.insertedId); // String(result.insertedId);
|
||||
broadcaster.broadcastAfterInsert({ document: document });
|
||||
return document;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateRelationInverseSideIds(relationOperations: InverseSideUpdateOperation[]): Promise<any> {
|
||||
let updateInverseSideWithIdPromises = relationOperations
|
||||
.filter(relationOperation => !!relationOperation.inverseSideDocumentRelation)
|
||||
.map(relationOperation => {
|
||||
|
||||
let inverseSideSchema = relationOperation.inverseSideDocumentSchema;
|
||||
let inverseSideProperty = relationOperation.inverseSideDocumentRelation.name;
|
||||
let id = relationOperation.getDocumentId(); // this.connection.driver.createObjectId(relationOperation.getDocumentId(), relationOperation.documentSchema.idField.isObjectId);
|
||||
// let findCondition = this.connection.driver.createIdCondition(inverseSideSchema.getIdValue(relationOperation.inverseSideDocumentId)/*, inverseSideSchema.idField.isObjectId*/);
|
||||
let findCondition = inverseSideSchema.createIdCondition(relationOperation.inverseSideDocumentId);
|
||||
|
||||
if (inverseSideSchema.hasRelationWithOneWithName(inverseSideProperty))
|
||||
return this.connection.driver.setOneRelation(inverseSideSchema.name, findCondition, inverseSideProperty, id);
|
||||
if (inverseSideSchema.hasRelationWithManyWithName(inverseSideProperty))
|
||||
return this.connection.driver.setManyRelation(inverseSideSchema.name, findCondition, inverseSideProperty, id);
|
||||
});
|
||||
|
||||
return Promise.all(updateInverseSideWithIdPromises);
|
||||
}
|
||||
|
||||
}
|
||||
194
src/repository/persistence/EntityToDbObjectTransformer.ts
Normal file
194
src/repository/persistence/EntityToDbObjectTransformer.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
|
||||
import {DbObjectColumnValidator} from "./DbObjectColumnValidator";
|
||||
import {ColumnTypeNotSupportedError} from "../error/ColumnTypeNotSupportedError";
|
||||
import {PersistOperation} from "./../operation/PersistOperation";
|
||||
import {CascadeOptionUtils} from "../cascade/CascadeOptionUtils";
|
||||
import {InverseSideUpdateOperation} from "../operation/InverseSideUpdateOperation";
|
||||
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
|
||||
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";
|
||||
|
||||
/**
|
||||
* Makes a transformation of a given entity to the entity that can be saved to the db.
|
||||
*/
|
||||
export class EntityToDbObjectTransformer {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
private _persistOperations: PersistOperation[];
|
||||
private _postPersistOperations: Function[];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get postPersistOperations() {
|
||||
return this._postPersistOperations;
|
||||
}
|
||||
|
||||
get persistOperations() {
|
||||
return this._persistOperations;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Transforms given entity into object that can be persisted into the db.
|
||||
*/
|
||||
transform(metadata: EntityMetadata,
|
||||
entity: any,
|
||||
cascadeOptionsInCallback?: DynamicCascadeOptions<any>): Object {
|
||||
|
||||
this._persistOperations = [];
|
||||
this._postPersistOperations = [];
|
||||
return this.entityToDbObject(0, metadata, entity, cascadeOptionsInCallback);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private entityToDbObject(deepness: number,
|
||||
metadata: EntityMetadata,
|
||||
entity: any,
|
||||
cascadeOptionsInCallback?: DynamicCascadeOptions<any>): Object {
|
||||
|
||||
const cascadeOptions = CascadeOptionUtils.prepareCascadeOptions(metadata, cascadeOptionsInCallback);
|
||||
const dbObject = {};
|
||||
|
||||
//
|
||||
if (metadata.createDateColumn && !metadata.getEntityId(entity))
|
||||
entity[metadata.createDateColumn.propertyName] = new Date();
|
||||
if (metadata.updateDateColumn)
|
||||
entity[metadata.updateDateColumn.propertyName] = new Date();
|
||||
|
||||
//
|
||||
Object.keys(entity).forEach(propertyName => {
|
||||
const cascadeOption = CascadeOptionUtils.find(cascadeOptions, propertyName);
|
||||
|
||||
if (metadata.hasColumnWithPropertyName(propertyName))
|
||||
this.parseColumn(metadata, dbObject, entity, propertyName);
|
||||
|
||||
if (metadata.hasRelationWithOneWithPropertyName(propertyName))
|
||||
this.parseRelationWithOne(deepness, metadata, dbObject, entity, propertyName, cascadeOption);
|
||||
|
||||
if (metadata.hasRelationWithManyWithPropertyName(propertyName))
|
||||
this.parseRelationWithMany(deepness, metadata, dbObject, entity, propertyName, cascadeOption);
|
||||
|
||||
});
|
||||
return dbObject;
|
||||
}
|
||||
|
||||
private parseColumn(metadata: EntityMetadata, dbObject: any, entity: any, propertyName: string) {
|
||||
const column = metadata.findColumnWithPropertyName(propertyName);
|
||||
dbObject[column.name] = entity[propertyName];
|
||||
}
|
||||
|
||||
private parseRelationWithOne(deepness: number,
|
||||
metadata: EntityMetadata,
|
||||
dbObject: any,
|
||||
entity: any,
|
||||
columnName: any,
|
||||
cascadeOption?: CascadeOption) {
|
||||
|
||||
const relation = metadata.findRelationWithOneByPropertyName(columnName);
|
||||
const addFunction = (id: any) => dbObject[relation.name] = id;
|
||||
this.parseRelation(deepness, metadata, entity, relation, entity[columnName], addFunction, cascadeOption);
|
||||
}
|
||||
|
||||
|
||||
private parseRelationWithMany( deepness: number,
|
||||
metadata: EntityMetadata,
|
||||
dbObject: any,
|
||||
entity: any,
|
||||
columnName: any,
|
||||
cascadeOption?: CascadeOption) {
|
||||
|
||||
const relation = metadata.findRelationWithManyByPropertyName(columnName);
|
||||
const addFunction = (id: any) => dbObject[relation.name].push(id);
|
||||
|
||||
dbObject[relation.name] = [];
|
||||
entity[columnName].forEach((columnItem: any) => {
|
||||
this.parseRelation(deepness, metadata, entity, relation, columnItem, addFunction, cascadeOption);
|
||||
});
|
||||
}
|
||||
|
||||
private parseRelation(deepness: number,
|
||||
metadata: EntityMetadata,
|
||||
entity: any,
|
||||
relation: RelationMetadata,
|
||||
value: any,
|
||||
addFunction: (objectId: any) => void,
|
||||
cascadeOption?: CascadeOption) {
|
||||
|
||||
const relationTypeMetadata = this.connection.getMetadata(relation.type);
|
||||
const relatedEntityId = value ? relationTypeMetadata.getEntityId(value) : null;
|
||||
|
||||
if (relatedEntityId && !CascadeOptionUtils.isCascadeUpdate(relation, cascadeOption)) {
|
||||
addFunction(this.createObjectId(relatedEntityId, relationTypeMetadata));
|
||||
|
||||
} else if (value) {
|
||||
// check if we already added this object for persist (can happen when object of the same instance is used in different places)
|
||||
let operationOnThisValue = this._persistOperations.reduce((found, operation) => operation.entity === value ? operation : found, null);
|
||||
let subCascades = cascadeOption ? cascadeOption.cascades : undefined;
|
||||
let relatedDbObject = this.entityToDbObject(deepness + 1, relationTypeMetadata, value, subCascades);
|
||||
let doPersist = CascadeOptionUtils.isCascadePersist(relation, cascadeOption);
|
||||
|
||||
let afterExecution = (insertedRelationEntity: any) => {
|
||||
let id = relationTypeMetadata.getEntityId(insertedRelationEntity);
|
||||
addFunction(this.createObjectId(id, relationTypeMetadata));
|
||||
const inverseSideRelationMetadata = relationTypeMetadata.findRelationByPropertyName(relation.inverseSideProperty);
|
||||
return <InverseSideUpdateOperation> {
|
||||
inverseSideEntityId: id,
|
||||
inverseSideEntityMetadata: relationTypeMetadata,
|
||||
inverseSideEntityRelation: inverseSideRelationMetadata,
|
||||
entityMetadata: metadata,
|
||||
getEntityId: () => metadata.getEntityId(entity)
|
||||
};
|
||||
};
|
||||
|
||||
if (!operationOnThisValue) {
|
||||
operationOnThisValue = <PersistOperation> {
|
||||
allowedPersist: false,
|
||||
deepness: deepness,
|
||||
entity: value,
|
||||
metadata: relationTypeMetadata,
|
||||
dbObject: relatedDbObject,
|
||||
afterExecution: []
|
||||
};
|
||||
this._persistOperations.push(operationOnThisValue);
|
||||
}
|
||||
|
||||
// this check is required because we check
|
||||
operationOnThisValue.afterExecution.push(afterExecution);
|
||||
operationOnThisValue.allowedPersist = operationOnThisValue.allowedPersist || doPersist;
|
||||
}
|
||||
}
|
||||
|
||||
private createObjectId(id: any, metadata: EntityMetadata): any {
|
||||
if (metadata.idColumn.isAutoGenerated && !id) {
|
||||
return this.connection.driver.generateId();
|
||||
} else if (metadata.idColumn.isAutoGenerated && id) {
|
||||
return id;
|
||||
} else if (metadata.idColumn.isObjectId) {
|
||||
return this.connection.driver.createObjectId(id);
|
||||
}
|
||||
|
||||
throw new Error("Cannot create object id");
|
||||
// return this.connection.driver.createObjectId(id, metadata.idColumn.isObjectId);
|
||||
}
|
||||
|
||||
}
|
||||
237
src/repository/removement/DocumentRemover.ts
Normal file
237
src/repository/removement/DocumentRemover.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import {DocumentSchema} from "../../schema/DocumentSchema";
|
||||
import {Connection} from "../../connection/Connection";
|
||||
import {RelationSchema} from "../../schema/RelationSchema";
|
||||
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
|
||||
import {RemoveOperation} from "./../operation/RemoveOperation";
|
||||
import {InverseSideUpdateOperation} from "./../operation/InverseSideUpdateOperation";
|
||||
import {CascadeOptionUtils} from "../cascade/CascadeOptionUtils";
|
||||
import {NoDocumentWithSuchIdError} from "../error/NoDocumentWithSuchIdError";
|
||||
import {ObjectID} from "mongodb";
|
||||
|
||||
/**
|
||||
* Helps to remove a document and all its relations by given cascade operations.
|
||||
*/
|
||||
export class DocumentRemover<Document> {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
private inverseSideUpdateOperations: InverseSideUpdateOperation[] = [];
|
||||
private removeOperations: RemoveOperation[] = [];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Computes and creates a list of all related documents that should exist in the given document, but are missing.
|
||||
* Those missing documents are treated as removed (even if they are not loaded at all, so be careful).
|
||||
*/
|
||||
computeRemovedRelations(schema: DocumentSchema,
|
||||
document: Document,
|
||||
dynamicCascadeOptions?: DynamicCascadeOptions<Document>): Promise<void> {
|
||||
|
||||
let documentId = schema.getDocumentId(document);
|
||||
let cascadeOptions = CascadeOptionUtils.prepareCascadeOptions(schema, dynamicCascadeOptions);
|
||||
if (!documentId)
|
||||
return Promise.resolve();
|
||||
|
||||
// load original document so we can compare and calculate changed set
|
||||
// const query = this.connection.driver.createIdCondition(schema.getIdValue(documentId));
|
||||
const query = schema.createIdCondition(documentId);
|
||||
return this.connection.driver.findOne(schema.name, query).then((dbObject: any) => {
|
||||
if (!dbObject)
|
||||
return Promise.resolve();
|
||||
// throw new NoDocumentWithSuchIdException(documentId, schema.name);
|
||||
|
||||
// iterate throw each key in the document and find relations to compute removals of
|
||||
let promises = Object.keys(dbObject).map(originalDocumentProperty => {
|
||||
|
||||
let relationWithOne = schema.findRelationWithOneByDbName(originalDocumentProperty);
|
||||
if (relationWithOne) {
|
||||
let id = schema.getIdValue(dbObject[originalDocumentProperty]);
|
||||
return this.computeRelationToBeRemoved(id, relationWithOne, document, cascadeOptions);
|
||||
}
|
||||
|
||||
let relationWithMany = schema.findRelationWithManyByDbName(originalDocumentProperty);
|
||||
if (relationWithMany) {
|
||||
return Promise.all(dbObject[originalDocumentProperty].map((id: any) => {
|
||||
return this.computeRelationToBeRemoved(schema.getIdValue(id), relationWithMany, document, cascadeOptions);
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
return Promise.all<any>(promises).then(function() {});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes all remove operations. This means all document that are saved to be removed gonna remove now.
|
||||
*/
|
||||
executeRemoveOperations(): Promise<void> {
|
||||
return Promise.all(this.removeOperations.map(operation => {
|
||||
let broadcaster = this.connection.getBroadcaster(operation.schema.documentClass);
|
||||
// const query = this.connection.driver.createIdCondition(operation.schema.getIdValue(operation.id));
|
||||
const query = operation.schema.createIdCondition(operation.id);
|
||||
broadcaster.broadcastBeforeRemove({ documentId: operation.id });
|
||||
return this.connection.driver.deleteOne(operation.schema.name, query).then(result => {
|
||||
broadcaster.broadcastAfterRemove({ documentId: operation.id });
|
||||
});
|
||||
})).then(function() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs all inverse side update operations. These operations mean when we remove some document which is used
|
||||
* in another document, we must go to that document and remove this usage from him. This is what this function does.
|
||||
*/
|
||||
executeUpdateInverseSideRelationRemoveIds(): Promise<void> {
|
||||
let inverseSideUpdates = this.excludeRemovedDocumentsFromInverseSideUpdateOperations();
|
||||
let updateInverseSideWithIdPromises = inverseSideUpdates.map(relationOperation => {
|
||||
|
||||
let inverseSideSchema = relationOperation.inverseSideDocumentSchema;
|
||||
let inverseSideProperty = relationOperation.inverseSideDocumentRelation.name;
|
||||
let id = relationOperation.getDocumentId(); // this.connection.driver.createObjectId(relationOperation.getDocumentId());
|
||||
// let findCondition = this.connection.driver.createIdCondition(inverseSideSchema.getIdValue(relationOperation.inverseSideDocumentId));
|
||||
const findCondition = inverseSideSchema.createIdCondition(relationOperation.inverseSideDocumentId);
|
||||
|
||||
if (inverseSideSchema.hasRelationWithOneWithName(inverseSideProperty))
|
||||
return this.connection.driver.unsetOneRelation(inverseSideSchema.name, findCondition, inverseSideProperty, id);
|
||||
if (inverseSideSchema.hasRelationWithManyWithPropertyName(inverseSideProperty))
|
||||
return this.connection.driver.unsetManyRelation(inverseSideSchema.name, findCondition, inverseSideProperty, id);
|
||||
});
|
||||
|
||||
return Promise.all(updateInverseSideWithIdPromises).then(function() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers given document id of the given schema for a removal operation.
|
||||
*/
|
||||
registerDocumentRemoveOperation(schema: DocumentSchema,
|
||||
documentId: string,
|
||||
dynamicCascadeOptions?: DynamicCascadeOptions<Document>): Promise<void> {
|
||||
|
||||
let cascadeOptions = CascadeOptionUtils.prepareCascadeOptions(schema, dynamicCascadeOptions);
|
||||
|
||||
// load original document so we can compare and calculate which of its relations to remove by cascades
|
||||
// const query = this.connection.driver.createIdCondition(schema.getIdValue(documentId));
|
||||
const query = schema.createIdCondition(documentId);
|
||||
return this.connection.driver.findOne(schema.name, query).then((dbObject: any) => {
|
||||
if (!dbObject)
|
||||
return Promise.resolve();
|
||||
// throw new NoDocumentWithSuchIdException(documentId, schema.name);
|
||||
|
||||
// iterate throw each key in the db document and find relations to compute removals of
|
||||
let promises = Object.keys(dbObject).map(originalDocumentProperty => {
|
||||
|
||||
let relationWithOneField = schema.findRelationWithOneByDbName(originalDocumentProperty);
|
||||
if (relationWithOneField) {
|
||||
let id = schema.getIdValue(dbObject[originalDocumentProperty]);
|
||||
return this.parseRelationForRemovalOperation(id, schema, documentId, relationWithOneField, cascadeOptions);
|
||||
}
|
||||
|
||||
let relationWithManyField = schema.findRelationWithManyByDbName(originalDocumentProperty);
|
||||
if (relationWithManyField)
|
||||
return Promise.all(dbObject[originalDocumentProperty].map((id: any) => {
|
||||
return this.parseRelationForRemovalOperation(schema.getIdValue(id), schema, documentId, relationWithManyField, cascadeOptions);
|
||||
}));
|
||||
});
|
||||
|
||||
// register a new remove operation
|
||||
this.removeOperations.push({ schema: schema, id: documentId });
|
||||
return Promise.all<any>(promises).then(function() {});
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Computes if item with given id in a given relation should be removed or not.
|
||||
*/
|
||||
private computeRelationToBeRemoved(id: string, relation: RelationSchema, document: Document|any, cascadeOptions?: CascadeOption[]): Promise<void> {
|
||||
let cascadeOption = CascadeOptionUtils.find(cascadeOptions, relation.propertyName);
|
||||
let relatedSchema = this.connection.getSchema(<Function> relation.type);
|
||||
let subCascades = cascadeOption ? cascadeOption.cascades : undefined;
|
||||
|
||||
// if cascades are not enabled for this relation then skip it
|
||||
if (!CascadeOptionUtils.isCascadeRemove(relation, cascadeOption)) return;
|
||||
|
||||
// if such document id already marked for remove operations then do nothing - no need to add it again
|
||||
if (this.isRemoveOperation(id, relatedSchema)) return;
|
||||
|
||||
// if related document with given id does exists in the document then it means that nothing is removed from document and we dont have to remove anything from the db
|
||||
let isThisIdInDocumentsRelation = !!document[relation.propertyName];
|
||||
if (document[relation.propertyName] instanceof Array)
|
||||
isThisIdInDocumentsRelation = document[relation.propertyName].filter((item: any) => {
|
||||
const idFieldValue = relatedSchema.getDocumentId(item);
|
||||
return idFieldValue instanceof ObjectID ? idFieldValue.equals(id) : idFieldValue === id;
|
||||
}).length > 0;
|
||||
|
||||
if (isThisIdInDocumentsRelation) return;
|
||||
|
||||
return this.registerDocumentRemoveOperation(relatedSchema, id, subCascades);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse given documents relation and registers relations's data remove operations.
|
||||
*/
|
||||
private parseRelationForRemovalOperation(id: string,
|
||||
schema: DocumentSchema,
|
||||
documentId: string,
|
||||
relation: RelationSchema,
|
||||
cascadeOptions?: CascadeOption[]): Promise<void> {
|
||||
|
||||
let cascadeOption = CascadeOptionUtils.find(cascadeOptions, relation.propertyName);
|
||||
let relatedSchema = this.connection.getSchema(<Function> relation.type);
|
||||
let subCascades = cascadeOption ? cascadeOption.cascades : undefined;
|
||||
|
||||
// if removal operation already registered then no need to register it again
|
||||
if (this.isRemoveOperation(id, relatedSchema)) return;
|
||||
|
||||
// add new inverse side update operation
|
||||
if (relation.inverseSideProperty) {
|
||||
const inverseSideRelationSchema = relatedSchema.findRelationByPropertyName(relation.inverseSideProperty);
|
||||
this.inverseSideUpdateOperations.push({
|
||||
inverseSideDocumentId: id,
|
||||
inverseSideDocumentSchema: relatedSchema,
|
||||
inverseSideDocumentRelation: inverseSideRelationSchema,
|
||||
documentSchema: schema,
|
||||
getDocumentId: () => documentId
|
||||
});
|
||||
}
|
||||
|
||||
// register document and its relations for removal if cascade operation is set
|
||||
if (CascadeOptionUtils.isCascadeRemove(relation, cascadeOption)) {
|
||||
return this.registerDocumentRemoveOperation(relatedSchema, id, subCascades);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if remove operation with given document id and schema is registered or not.
|
||||
*/
|
||||
private isRemoveOperation(id: string, schema: DocumentSchema): boolean {
|
||||
return this.removeOperations.filter(operation => operation.id === id && operation.schema === schema).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* From operations that are scheduled for update we remove all updates of documents that are scheduled for removal.
|
||||
* Since they are removed there is no since in updating them, e.g. they are removed and nothing to update.
|
||||
*/
|
||||
private excludeRemovedDocumentsFromInverseSideUpdateOperations(): InverseSideUpdateOperation[] {
|
||||
return this.inverseSideUpdateOperations.filter(updateOperation => {
|
||||
return this.removeOperations.filter(removeOperation => removeOperation.id === updateOperation.inverseSideDocumentId).length === 0;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
220
src/schema-creator/SchemaCreator.ts
Normal file
220
src/schema-creator/SchemaCreator.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import {Connection} from "../connection/Connection";
|
||||
import {TableMetadata} from "../metadata-builder/metadata/TableMetadata";
|
||||
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
|
||||
import {ForeignKeyMetadata} from "../metadata-builder/metadata/ForeignKeyMetadata";
|
||||
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
|
||||
import {SchemaBuilder} from "../driver/schema-builder/SchemaBuilder";
|
||||
|
||||
/**
|
||||
* Creates indexes based on the given metadata
|
||||
*/
|
||||
export class SchemaCreator {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private connection: Connection;
|
||||
private schemaBuilder: SchemaBuilder;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(connection: Connection) {
|
||||
this.connection = connection;
|
||||
this.schemaBuilder = connection.driver.createSchemaBuilder();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Creates complete schemas for the given entity metadatas.
|
||||
*/
|
||||
create(): Promise<void> {
|
||||
const metadatas = this.connection.metadatas;
|
||||
return Promise.resolve()
|
||||
.then(_ => this.dropForeignKeysForAll(metadatas))
|
||||
.then(_ => this.createTablesForAll(metadatas))
|
||||
.then(_ => this.updateOldColumnsForAll(metadatas))
|
||||
.then(_ => this.dropRemovedColumnsForAll(metadatas))
|
||||
.then(_ => this.addNewColumnsForAll(metadatas))
|
||||
.then(_ => this.updateExistColumnsForAll(metadatas))
|
||||
.then(_ => this.createForeignKeysForAll(metadatas))
|
||||
.then(_ => this.updateUniqueKeysForAll(metadatas))
|
||||
.then(_ => this.removePrimaryKeyForAll(metadatas))
|
||||
.then(_ => {})
|
||||
.catch(err => console.log(err));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private dropForeignKeysForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.dropForeignKeys(metadata.table, metadata.foreignKeys)));
|
||||
}
|
||||
|
||||
private createTablesForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.createNewTable(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private updateOldColumnsForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.updateOldColumns(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private dropRemovedColumnsForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.dropRemovedColumns(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private addNewColumnsForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.addNewColumns(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private updateExistColumnsForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.updateExistColumns(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private createForeignKeysForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.createForeignKeys(metadata.table, metadata.foreignKeys)));
|
||||
}
|
||||
|
||||
private updateUniqueKeysForAll(metadatas: EntityMetadata[]) {
|
||||
return Promise.all(metadatas.map(metadata => this.updateUniqueKeys(metadata.table, metadata.columns)));
|
||||
}
|
||||
|
||||
private removePrimaryKeyForAll(metadatas: EntityMetadata[]) {
|
||||
const queries = metadatas
|
||||
.filter(metadata => !metadata.primaryColumn)
|
||||
.map(metadata => this.removePrimaryKey(metadata.table));
|
||||
return Promise.all(queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all (old) foreign keys that exist in the table, but does not exist in the metadata.
|
||||
*/
|
||||
private dropForeignKeys(table: TableMetadata, foreignKeys: ForeignKeyMetadata[]) {
|
||||
return this.schemaBuilder.getTableForeignQuery(table).then(dbKeys => {
|
||||
const dropKeysQueries = dbKeys
|
||||
.filter(dbKey => !foreignKeys.find(foreignKey => foreignKey.name === dbKey))
|
||||
.map(dbKey => this.schemaBuilder.dropForeignKeyQuery(table.name, dbKey));
|
||||
|
||||
return Promise.all(dropKeysQueries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new table if it does not exist.
|
||||
*/
|
||||
private createNewTable(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.checkIfTableExist(table.name).then(exist => {
|
||||
if (!exist)
|
||||
return this.schemaBuilder.createTableQuery(table, columns);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames (and updates) all columns that has "oldColumnName".
|
||||
*/
|
||||
private updateOldColumns(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.getTableColumns(table.name).then(dbColumns => {
|
||||
const updates = columns
|
||||
.filter(column => !!column.oldColumnName && column.name !== column.oldColumnName)
|
||||
.filter(column => dbColumns.indexOf(column.oldColumnName) !== -1)
|
||||
.map(column => this.schemaBuilder.changeColumnQuery(table.name, column.oldColumnName, column));
|
||||
|
||||
return Promise.all(updates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all columns exist (left old) in the table, but does not exist in the metadata.
|
||||
*/
|
||||
private dropRemovedColumns(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.getTableColumns(table.name).then(dbColumns => {
|
||||
const dropColumnQueries = dbColumns
|
||||
.filter(dbColumn => !columns.find(column => column.name === dbColumn))
|
||||
.map(dbColumn => this.schemaBuilder.dropColumnQuery(table.name, dbColumn));
|
||||
|
||||
return Promise.all(dropColumnQueries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds columns from metadata which does not exist in the table.
|
||||
*/
|
||||
private addNewColumns(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.getTableColumns(table.name).then(dbColumns => {
|
||||
const newColumnQueries = columns
|
||||
.filter(column => dbColumns.indexOf(column.name) === -1)
|
||||
.map(column => this.schemaBuilder.addColumnQuery(table.name, column));
|
||||
|
||||
return Promise.all(newColumnQueries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all exist columns which metadata has changed.
|
||||
*/
|
||||
private updateExistColumns(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.getChangedColumns(table.name, columns).then(changedColumns => {
|
||||
const updateQueries = changedColumns.map(changedColumn => {
|
||||
const column = columns.find(column => column.name === changedColumn.columnName);
|
||||
return this.schemaBuilder.changeColumnQuery(table.name, column.name, column, changedColumn.hasPrimaryKey);
|
||||
});
|
||||
|
||||
return Promise.all(updateQueries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates foreign keys which does not exist in the table yet.
|
||||
*/
|
||||
private createForeignKeys(table: TableMetadata, foreignKeys: ForeignKeyMetadata[]) {
|
||||
return this.schemaBuilder.getTableForeignQuery(table).then(dbKeys => {
|
||||
const dropKeysQueries = foreignKeys
|
||||
.filter(foreignKey => dbKeys.indexOf(foreignKey.name) === -1)
|
||||
.map(foreignKey => this.schemaBuilder.addForeignKeyQuery(foreignKey));
|
||||
|
||||
return Promise.all(dropKeysQueries);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates unique keys which are missing in db yet, and drops unique keys which exist in the db,
|
||||
* but does not exist in the metadata anymore.
|
||||
*/
|
||||
private updateUniqueKeys(table: TableMetadata, columns: ColumnMetadata[]) {
|
||||
return this.schemaBuilder.getTableUniqueKeysQuery(table.name).then(dbKeys => {
|
||||
|
||||
// first find metadata columns that should be unique and update them if they are not unique in db
|
||||
const addQueries = columns
|
||||
.filter(column => column.isUnique)
|
||||
.filter(column => dbKeys.indexOf("uk_" + column.name) === -1)
|
||||
.map(column => this.schemaBuilder.addUniqueKey(table.name, column.name, "uk_" + column.name));
|
||||
|
||||
// second find columns in db that are unique, however in metadata columns they are not unique
|
||||
const dropQueries = columns
|
||||
.filter(column => !column.isUnique)
|
||||
.filter(column => dbKeys.indexOf("uk_" + column.name) !== -1)
|
||||
.map(column => this.schemaBuilder.dropIndex(table.name, "uk_" + column.name));
|
||||
|
||||
return Promise.all([addQueries, dropQueries]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes primary key from the table (if it was before and does not exist in the metadata anymore).
|
||||
*/
|
||||
private removePrimaryKey(table: TableMetadata): Promise<void> {
|
||||
|
||||
// find primary key name in db and remove it, because we don't have (anymore) primary key in the metadata
|
||||
return this.schemaBuilder.getPrimaryConstraintName(table.name).then(constraintName => {
|
||||
if (constraintName)
|
||||
return this.schemaBuilder.dropIndex(table.name, constraintName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
109
src/subscriber/OrmBroadcaster.ts
Normal file
109
src/subscriber/OrmBroadcaster.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import {UpdateEvent} from "./event/UpdateEvent";
|
||||
import {RemoveEvent} from "./event/RemoveEvent";
|
||||
import {InsertEvent} from "./event/InsertEvent";
|
||||
import {OrmSubscriber} from "./OrmSubscriber";
|
||||
|
||||
/**
|
||||
* Broadcaster provides a helper methods to broadcast events to the subscribers.
|
||||
*/
|
||||
export class OrmBroadcaster<Entity> {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private subscribers: OrmSubscriber<Entity|any>[];
|
||||
private _entityClass: Function;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(subscribers: OrmSubscriber<Entity|any>[], entityClass: Function) {
|
||||
this.subscribers = subscribers;
|
||||
this._entityClass = entityClass;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessors
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get entityClass(): Function {
|
||||
return this._entityClass;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
broadcastBeforeInsert(event: InsertEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.beforeInsert)
|
||||
.forEach(subscriber => subscriber.beforeInsert(event));
|
||||
}
|
||||
|
||||
broadcastAfterInsert(event: InsertEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.afterInsert)
|
||||
.forEach(subscriber => subscriber.afterInsert(event));
|
||||
}
|
||||
|
||||
broadcastBeforeUpdate(event: UpdateEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.beforeUpdate)
|
||||
.forEach(subscriber => subscriber.beforeUpdate(event));
|
||||
}
|
||||
|
||||
broadcastAfterUpdate(event: UpdateEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.afterUpdate)
|
||||
.forEach(subscriber => subscriber.afterUpdate(event));
|
||||
}
|
||||
|
||||
broadcastAfterRemove(event: RemoveEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.afterRemove)
|
||||
.forEach(subscriber => subscriber.afterRemove(event));
|
||||
}
|
||||
|
||||
broadcastBeforeRemove(event: RemoveEvent<Entity>) {
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.beforeRemove)
|
||||
.forEach(subscriber => subscriber.beforeRemove(event));
|
||||
}
|
||||
|
||||
broadcastAfterLoadedAll(entities: Entity[]) {
|
||||
if (!entities || entities.length) return;
|
||||
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.afterLoad)
|
||||
.forEach(subscriber => {
|
||||
entities.forEach(entity => subscriber.afterLoad(entity));
|
||||
});
|
||||
}
|
||||
|
||||
broadcastAfterLoaded(entity: Entity) {
|
||||
if (!entity) return;
|
||||
|
||||
this.subscribers
|
||||
.filter(subscriber => this.isAllowedSubscribers(subscriber))
|
||||
.filter(subscriber => !!subscriber.afterLoad)
|
||||
.forEach(subscriber => subscriber.afterLoad(entity));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private isAllowedSubscribers(subscriber: OrmSubscriber<Entity|any>) {
|
||||
return !subscriber.listenTo() || subscriber.listenTo() === Object || subscriber.listenTo() === this._entityClass;
|
||||
}
|
||||
|
||||
}
|
||||
50
src/subscriber/OrmSubscriber.ts
Normal file
50
src/subscriber/OrmSubscriber.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {UpdateEvent} from "./event/UpdateEvent";
|
||||
import {RemoveEvent} from "./event/RemoveEvent";
|
||||
import {InsertEvent} from "./event/InsertEvent";
|
||||
|
||||
/**
|
||||
* Classes that implement this interface are subscribers that subscribe for the specific events of the ODM.
|
||||
*/
|
||||
export interface OrmSubscriber<Entity> {
|
||||
|
||||
/**
|
||||
* Returns the class of the entity to which events will listen.
|
||||
*/
|
||||
listenTo(): Function;
|
||||
|
||||
/**
|
||||
* Called after entity is loaded.
|
||||
*/
|
||||
afterLoad?(entity: Entity): void;
|
||||
|
||||
/**
|
||||
* Called before entity is inserted.
|
||||
*/
|
||||
beforeInsert?(event: InsertEvent<Entity>): void;
|
||||
|
||||
/**
|
||||
* Called after entity is inserted.
|
||||
*/
|
||||
afterInsert?(event: InsertEvent<Entity>): void;
|
||||
|
||||
/**
|
||||
* Called before entity is updated.
|
||||
*/
|
||||
beforeUpdate?(event: UpdateEvent<Entity>): void;
|
||||
|
||||
/**
|
||||
* Called after entity is updated.
|
||||
*/
|
||||
afterUpdate?(event: UpdateEvent<Entity>): void;
|
||||
|
||||
/**
|
||||
* Called before entity is replaced.
|
||||
*/
|
||||
beforeRemove?(event: RemoveEvent<Entity>): void;
|
||||
|
||||
/**
|
||||
* Called after entity is removed.
|
||||
*/
|
||||
afterRemove?(event: RemoveEvent<Entity>): void;
|
||||
|
||||
}
|
||||
8
src/subscriber/event/InsertEvent.ts
Normal file
8
src/subscriber/event/InsertEvent.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This event is used on insert events.
|
||||
*/
|
||||
export interface InsertEvent<Entity> {
|
||||
|
||||
entity?: Entity;
|
||||
|
||||
}
|
||||
10
src/subscriber/event/RemoveEvent.ts
Normal file
10
src/subscriber/event/RemoveEvent.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This event is used on remove events.
|
||||
*/
|
||||
export interface RemoveEvent<Entity> {
|
||||
|
||||
entity?: Entity;
|
||||
conditions?: any;
|
||||
entityId?: string;
|
||||
|
||||
}
|
||||
10
src/subscriber/event/UpdateEvent.ts
Normal file
10
src/subscriber/event/UpdateEvent.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This event is used on update events.
|
||||
*/
|
||||
export interface UpdateEvent<Entity> {
|
||||
|
||||
entity?: Entity;
|
||||
options?: any;
|
||||
conditions?: any;
|
||||
|
||||
}
|
||||
30
src/util/OrmUtils.ts
Normal file
30
src/util/OrmUtils.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
/**
|
||||
* Common utility functions.
|
||||
*/
|
||||
export class OrmUtils {
|
||||
|
||||
/**
|
||||
* Makes "require()" all js files (or custom extension files) in the given directory.
|
||||
* @deprecated use npm module instead
|
||||
*/
|
||||
static requireAll(directories: string[], extension: string = ".js"): any[] {
|
||||
let files: any[] = [];
|
||||
directories.forEach((dir: string) => {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.readdirSync(dir).forEach((file: string) => {
|
||||
if (fs.statSync(dir + "/" + file).isDirectory()) {
|
||||
let requiredFiles = this.requireAll([dir + "/" + file], extension);
|
||||
requiredFiles.forEach((file: string) => files.push(file));
|
||||
} else if (path.extname(file) === extension) {
|
||||
files.push(require(dir + "/" + file));
|
||||
}
|
||||
});
|
||||
}
|
||||
}); // todo: implement recursion
|
||||
return files;
|
||||
}
|
||||
|
||||
}
|
||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "1.8.0",
|
||||
"compilerOptions": {
|
||||
"outDir": "build/es5",
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"declaration": true
|
||||
},
|
||||
"exclude": [
|
||||
"build",
|
||||
"node_modules",
|
||||
"typings/browser.d.ts",
|
||||
"typings/browser"
|
||||
]
|
||||
}
|
||||
54
tslint.json
Normal file
54
tslint.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"rules": {
|
||||
"class-name": true,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"indent": [
|
||||
true,
|
||||
"spaces"
|
||||
],
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": true,
|
||||
"no-internal-module": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-var-keyword": true,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"double"
|
||||
],
|
||||
"semicolon": true,
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace",
|
||||
"index-signature": "nospace",
|
||||
"parameter": "nospace",
|
||||
"property-declaration": "nospace",
|
||||
"variable-declaration": "nospace"
|
||||
}
|
||||
],
|
||||
"variable-name": [
|
||||
true,
|
||||
"ban-keywords"
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
||||
13
typings.json
Normal file
13
typings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"ambientDevDependencies": {
|
||||
"mocha": "github:DefinitelyTyped/DefinitelyTyped/mocha/mocha.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
|
||||
"assertion-error": "github:DefinitelyTyped/DefinitelyTyped/assertion-error/assertion-error.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
|
||||
"chai": "github:DefinitelyTyped/DefinitelyTyped/chai/chai.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
|
||||
"sinon": "github:DefinitelyTyped/DefinitelyTyped/sinon/sinon.d.ts#7a3ca1f0b8a0960af9fc1838f3234cc9d6ce0645",
|
||||
"mockery": "github:DefinitelyTyped/DefinitelyTyped/mockery/mockery.d.ts#6f6e5c7dd9effe21fee14eb65fe340ecbbc8580a"
|
||||
},
|
||||
"ambientDependencies": {
|
||||
"node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#cf7f980ba6cf09f75aaa9b5e53010db3c00b9aaf",
|
||||
"es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#6697d6f7dadbf5773cb40ecda35a76027e0783b2"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user