first commit

This commit is contained in:
Umed Khudoiberdiev 2016-02-21 11:38:43 +05:00
commit 4309b8d810
83 changed files with 5195 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/
node_modules/
typings/
npm-debug.log

14
README.md Normal file
View 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
View File

@ -0,0 +1 @@
eval(require("typescript").transpile(require("fs").readFileSync("./gulpfile.ts").toString()));

158
gulpfile.ts Normal file
View 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
View 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"
}
}

View 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));

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

View 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));

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

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

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

View 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));

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

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

View 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));

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

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

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

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

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

View 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!`;
}
}

View File

@ -0,0 +1,9 @@
export class ConnectionNotFoundError extends Error {
name = "ConnectionNotFoundError";
constructor(name: string) {
super();
this.message = `No connection ${name} found.`;
}
}

View 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!`;
}
}

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,7 @@
export enum RelationTypes {
ONE_TO_ONE = 1,
ONE_TO_MANY = 2,
MANY_TO_ONE = 3,
MANY_TO_MANY = 4
}

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

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

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

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

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

View 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();
}
}

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

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

View 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?";
}
}

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

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

View File

@ -0,0 +1,9 @@
export interface JoinFieldOption {
inner?: boolean;
field: string|any;
condition?: any;
joins: JoinFieldOption|any[]; // todo: check its type - looks wrong
}

View File

@ -0,0 +1,3 @@
export class JoinFieldOptionUtils {
}

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

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

View File

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

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

View 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
}

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

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

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

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

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

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

View File

@ -0,0 +1,8 @@
/**
* This event is used on insert events.
*/
export interface InsertEvent<Entity> {
entity?: Entity;
}

View File

@ -0,0 +1,10 @@
/**
* This event is used on remove events.
*/
export interface RemoveEvent<Entity> {
entity?: Entity;
conditions?: any;
entityId?: string;
}

View 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
View 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
View 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
View 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
View 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"
}
}