mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
implemented @transaction decorators
This commit is contained in:
parent
1f7065fa67
commit
6d81649a27
@ -4,18 +4,33 @@ import {Post} from "./entity/Post";
|
||||
|
||||
const options: ConnectionOptions = {
|
||||
driver: {
|
||||
type: "sqlite",
|
||||
storage: "temp/sqlitedb.db"
|
||||
// type: "postgres",
|
||||
// host: "localhost",
|
||||
// port: 5432,
|
||||
// username: "root",
|
||||
// password: "admin",
|
||||
// database: "test"
|
||||
type: "oracle",
|
||||
host: "localhost",
|
||||
username: "system",
|
||||
password: "oracle",
|
||||
port: 1521,
|
||||
sid: "xe.oracle.docker",
|
||||
// type: "mssql",
|
||||
// host: "192.168.1.10",
|
||||
// username: "sa",
|
||||
// password: "admin12345",
|
||||
// database: "test",
|
||||
// port: 1521
|
||||
// type: "sqlite",
|
||||
// storage: "temp/sqlitedb.db"
|
||||
},
|
||||
logging: {
|
||||
logQueries: true,
|
||||
logSchemaCreation: true
|
||||
},
|
||||
autoSchemaSync: true,
|
||||
dropSchemaOnConnection: true,
|
||||
entities: [
|
||||
Post
|
||||
]
|
||||
entities: [Post]
|
||||
};
|
||||
|
||||
createConnection(options).then(connection => {
|
||||
|
||||
@ -9,7 +9,6 @@ export class Post {
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
@Index({ unique: true })
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
|
||||
44
src/decorator/transaction/Transaction.ts
Normal file
44
src/decorator/transaction/Transaction.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {getMetadataArgsStorage, getConnection} from "../../index";
|
||||
|
||||
/**
|
||||
* Wraps some method into the transaction.
|
||||
* Note, method result will return a promise if this decorator applied.
|
||||
* Note, all database operations in the wrapped method should be executed using entity managed passed as a first parameter
|
||||
* into the wrapped method.
|
||||
* If you want to control at what position in your method parameters entity manager should be injected,
|
||||
* then use @TransactionEntityManager() decorator.
|
||||
*/
|
||||
export function Transaction(connectionName: string = "default"): Function {
|
||||
return function (target: Object, methodName: string, descriptor: PropertyDescriptor) {
|
||||
|
||||
// save original method - we gonna need it
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
// override method descriptor with proxy method
|
||||
descriptor.value = function(...args: any[]) {
|
||||
return getConnection(connectionName)
|
||||
.entityManager
|
||||
.transaction(entityManager => {
|
||||
|
||||
// gets all @TransactionEntityManager() decorator usages for this method
|
||||
const indices = getMetadataArgsStorage()
|
||||
.transactionEntityManagers
|
||||
.filterByTarget(target.constructor)
|
||||
.toArray()
|
||||
.filter(transactionEntityManager => transactionEntityManager.methodName === methodName)
|
||||
.map(transactionEntityManager => transactionEntityManager.index);
|
||||
|
||||
let argsWithInjectedEntityManager: any[];
|
||||
if (indices.length) { // if there are @TransactionEntityManager() decorator usages the inject them
|
||||
argsWithInjectedEntityManager = [...args];
|
||||
indices.forEach(index => argsWithInjectedEntityManager.splice(index, 0, entityManager));
|
||||
|
||||
} else { // otherwise inject it as a first parameter
|
||||
argsWithInjectedEntityManager = [entityManager, ...args];
|
||||
}
|
||||
|
||||
return originalMethod.apply(this, argsWithInjectedEntityManager);
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
16
src/decorator/transaction/TransactionEntityManager.ts
Normal file
16
src/decorator/transaction/TransactionEntityManager.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {getMetadataArgsStorage} from "../../index";
|
||||
import {TransactionEntityMetadataArgs} from "../../metadata-args/TransactionEntityMetadataArgs";
|
||||
|
||||
/**
|
||||
* Injects transaction's entity manager into the method wrapped with @Transaction decorator.
|
||||
*/
|
||||
export function TransactionEntityManager(): Function {
|
||||
return function (object: Object, methodName: string, index: number) {
|
||||
const args: TransactionEntityMetadataArgs = {
|
||||
target: object.constructor,
|
||||
methodName: methodName,
|
||||
index: index,
|
||||
};
|
||||
getMetadataArgsStorage().transactionEntityManagers.add(args);
|
||||
};
|
||||
}
|
||||
@ -5,7 +5,7 @@ import {ConnectionManager} from "./connection/ConnectionManager";
|
||||
import {Connection} from "./connection/Connection";
|
||||
import {MetadataArgsStorage} from "./metadata-args/MetadataArgsStorage";
|
||||
import {ConnectionOptions} from "./connection/ConnectionOptions";
|
||||
import {getFromContainer, defaultContainer} from "./container";
|
||||
import {getFromContainer} from "./container";
|
||||
import {ObjectType} from "./common/ObjectType";
|
||||
import {Repository} from "./repository/Repository";
|
||||
import {EntityManager} from "./entity-manager/EntityManager";
|
||||
@ -54,6 +54,8 @@ export * from "./decorator/entity/EmbeddableEntity";
|
||||
export * from "./decorator/entity/SingleEntityChild";
|
||||
export * from "./decorator/entity/Entity";
|
||||
export * from "./decorator/entity/TableInheritance";
|
||||
export * from "./decorator/transaction/Transaction";
|
||||
export * from "./decorator/transaction/TransactionEntityManager";
|
||||
export * from "./decorator/tree/TreeLevelColumn";
|
||||
export * from "./decorator/tree/TreeParent";
|
||||
export * from "./decorator/tree/TreeChildren";
|
||||
|
||||
@ -15,6 +15,7 @@ import {RelationIdMetadataArgs} from "./RelationIdMetadataArgs";
|
||||
import {InheritanceMetadataArgs} from "./InheritanceMetadataArgs";
|
||||
import {DiscriminatorValueMetadataArgs} from "./DiscriminatorValueMetadataArgs";
|
||||
import {EntityRepositoryMetadataArgs} from "./EntityRepositoryMetadataArgs";
|
||||
import {TransactionEntityMetadataArgs} from "./TransactionEntityMetadataArgs";
|
||||
|
||||
/**
|
||||
* Storage all metadatas of all available types: tables, fields, subscribers, relations, etc.
|
||||
@ -33,6 +34,7 @@ export class MetadataArgsStorage {
|
||||
|
||||
readonly tables = new TargetMetadataArgsCollection<TableMetadataArgs>();
|
||||
readonly entityRepositories = new TargetMetadataArgsCollection<EntityRepositoryMetadataArgs>();
|
||||
readonly transactionEntityManagers = new TargetMetadataArgsCollection<TransactionEntityMetadataArgs>();
|
||||
readonly namingStrategies = new TargetMetadataArgsCollection<NamingStrategyMetadataArgs>();
|
||||
readonly entitySubscribers = new TargetMetadataArgsCollection<EntitySubscriberMetadataArgs>();
|
||||
readonly indices = new TargetMetadataArgsCollection<IndexMetadataArgs>();
|
||||
|
||||
21
src/metadata-args/TransactionEntityMetadataArgs.ts
Normal file
21
src/metadata-args/TransactionEntityMetadataArgs.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Used to inject transaction's entity managed into the method wrapped with @Transaction decorator.
|
||||
*/
|
||||
export interface TransactionEntityMetadataArgs {
|
||||
|
||||
/**
|
||||
* Target class on which decorator is used.
|
||||
*/
|
||||
readonly target: Function;
|
||||
|
||||
/**
|
||||
* Method on which decorator is used.
|
||||
*/
|
||||
readonly methodName: string;
|
||||
|
||||
/**
|
||||
* Index of the parameter on which decorator is used.
|
||||
*/
|
||||
readonly index: number;
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import {Transaction} from "../../../../../src/decorator/transaction/Transaction";
|
||||
import {Post} from "../entity/Post";
|
||||
import {EntityManager} from "../../../../../src/entity-manager/EntityManager";
|
||||
import {TransactionEntityManager} from "../../../../../src/decorator/transaction/TransactionEntityManager";
|
||||
import {Category} from "../entity/Category";
|
||||
|
||||
export class PostController {
|
||||
|
||||
@Transaction("mysql") // "mysql" is a connection name. you can not pass it if you are using default connection.
|
||||
async save(post: Post, category: Category, @TransactionEntityManager() entityManager: EntityManager) {
|
||||
await entityManager.persist(post);
|
||||
await entityManager.persist(category);
|
||||
}
|
||||
|
||||
// this save is not wrapped into the transaction
|
||||
async nonSafeSave(entityManager: EntityManager, post: Post, category: Category) {
|
||||
await entityManager.persist(post);
|
||||
await entityManager.persist(category);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
|
||||
@Entity()
|
||||
export class Category {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import {Entity} from "../../../../../src/decorator/entity/Entity";
|
||||
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
|
||||
import {Column} from "../../../../../src/decorator/columns/Column";
|
||||
|
||||
@Entity()
|
||||
export class Post {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
title: string;
|
||||
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
import "reflect-metadata";
|
||||
import {createTestingConnections, closeTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
|
||||
import {Connection} from "../../../../src/connection/Connection";
|
||||
import {Post} from "./entity/Post";
|
||||
import {expect} from "chai";
|
||||
import {PostController} from "./controller/PostController";
|
||||
import {Category} from "./entity/Category";
|
||||
|
||||
describe("transaction > method wrapped into transaction decorator", () => {
|
||||
|
||||
let connections: Connection[];
|
||||
before(async () => connections = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
schemaCreate: true,
|
||||
dropSchemaOnConnection: true,
|
||||
enabledDrivers: ["mysql"] // since @Transaction accepts a specific connection name we can use only one connection and its name
|
||||
}));
|
||||
beforeEach(() => reloadTestingDatabases(connections));
|
||||
after(() => closeTestingConnections(connections));
|
||||
|
||||
// create a fake controller
|
||||
const controller = new PostController();
|
||||
|
||||
it("should execute all operations in the method in a transaction", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new Post();
|
||||
post.title = "successfully saved post";
|
||||
|
||||
const category = new Category();
|
||||
category.name = "successfully saved category";
|
||||
|
||||
// call controller method
|
||||
await controller.save.apply(controller, [post, category]);
|
||||
|
||||
// controller should have saved both post and category successfully
|
||||
const loadedPost = await connection.entityManager.findOne(Post, { title: "successfully saved post" });
|
||||
expect(loadedPost).not.to.be.empty;
|
||||
loadedPost!.should.be.eql(post);
|
||||
|
||||
const loadedCategory = await connection.entityManager.findOne(Category, { name: "successfully saved category" });
|
||||
expect(loadedCategory).not.to.be.empty;
|
||||
loadedCategory!.should.be.eql(category);
|
||||
|
||||
})));
|
||||
|
||||
it("should rollback transaction if any operation in the method failed", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new Post();
|
||||
post.title = "successfully saved post";
|
||||
|
||||
const category = new Category(); // this will fail because no name set
|
||||
|
||||
// call controller method and make its rejected since controller action should fail
|
||||
let throwError: any;
|
||||
try {
|
||||
await controller.save.apply(controller, [post, category]);
|
||||
} catch (err) {
|
||||
throwError = err;
|
||||
}
|
||||
expect(throwError).not.to.be.empty;
|
||||
|
||||
const loadedPost = await connection.entityManager.findOne(Post, { title: "successfully saved post" });
|
||||
expect(loadedPost).to.be.empty;
|
||||
|
||||
const loadedCategory = await connection.entityManager.findOne(Category, { name: "successfully saved category" });
|
||||
expect(loadedCategory).to.be.empty;
|
||||
|
||||
})));
|
||||
|
||||
it("should rollback transaction if any operation in the method failed", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new Post(); // this will fail because no title set
|
||||
|
||||
const category = new Category();
|
||||
category.name = "successfully saved category";
|
||||
|
||||
// call controller method and make its rejected since controller action should fail
|
||||
let throwError: any;
|
||||
try {
|
||||
await controller.save.apply(controller, [post, category]);
|
||||
} catch (err) {
|
||||
throwError = err;
|
||||
}
|
||||
expect(throwError).not.to.be.empty;
|
||||
|
||||
const loadedPost = await connection.entityManager.findOne(Post, { title: "successfully saved post" });
|
||||
expect(loadedPost).to.be.empty;
|
||||
|
||||
const loadedCategory = await connection.entityManager.findOne(Category, { name: "successfully saved category" });
|
||||
expect(loadedCategory).to.be.empty;
|
||||
|
||||
})));
|
||||
|
||||
it("should save even if second operation failed in method not wrapped into @Transaction decorator", () => Promise.all(connections.map(async connection => {
|
||||
|
||||
const post = new Post(); // this will be saved in any cases because its valid
|
||||
post.title = "successfully saved post";
|
||||
|
||||
const category = new Category(); // this will fail because no name set
|
||||
|
||||
// call controller method and make its rejected since controller action should fail
|
||||
let throwError: any;
|
||||
try {
|
||||
await controller.nonSafeSave.apply(controller, [connection.entityManager, post, category]);
|
||||
|
||||
} catch (err) {
|
||||
throwError = err;
|
||||
}
|
||||
expect(throwError).not.to.be.empty;
|
||||
|
||||
// controller should have saved both post and category successfully
|
||||
const loadedPost = await connection.entityManager.findOne(Post, { title: "successfully saved post" });
|
||||
expect(loadedPost).not.to.be.empty;
|
||||
loadedPost!.should.be.eql(post);
|
||||
|
||||
const loadedCategory = await connection.entityManager.findOne(Category, { name: "successfully saved category" });
|
||||
expect(loadedCategory).to.be.empty;
|
||||
|
||||
})));
|
||||
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user