Skip to content

Commit

Permalink
feat(transactions): add transaction support to BaseClass
Browse files Browse the repository at this point in the history
  • Loading branch information
goldcaddy77 committed Apr 5, 2020
1 parent a354587 commit c91ec14
Show file tree
Hide file tree
Showing 31 changed files with 5,481 additions and 41 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.js
Expand Up @@ -37,6 +37,9 @@ module.exports = {
// Turn this off for now, but fix later
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/ban-ts-ignore': 'off'
'@typescript-eslint/ban-ts-ignore': 'off',

// Conflicts with Warthog's fieldName_operator query syntax
'@typescript-eslint/camelcase': 'off'
}
};
106 changes: 106 additions & 0 deletions README.md
Expand Up @@ -286,6 +286,112 @@ Almost all config in Warthog is driven by environment variables. The following i

All of the auto-generation magic comes from the decorators added to the attributes on your models. Warthog decorators are convenient wrappers around TypeORM decorators (to create DB schema) and TypeGraphQL (to create GraphQL schema). You can find a list of decorators available in the [src/decorators](./src/decorators) folder. Most of these are also used in the [examples](./examples) folder in this project.

## Transactions

There are a few ways to handle transactions in the framework, depending if you want to use `BaseService` or use your repositories directly.

### Using BaseService

To wrap BaseService operations in a transaction, you do 3 things:

1. Create a function decorated with the `@Transaction` method decorator
2. Inject `@TransactionManager` as a function parameter
3. Pass the `@TransactionManager` into calls to `BaseService`

#### @Transaction method decorator

The `@Transaction` decorator opens up a new transaction that is then available via the `@TransactionManager`. It will automatically close the transaction when the function returns, so it is important to `await` your service calls and not return a promise in this function.

```typescript
@Transaction()
async createTwoItems() {
// ...
}
```

#### @TransactionManager decorator

The `@TransactionManager` is essentially the same as a TypeORM EntityManger, except it wraps everything inside of it's transaction.

```typescript
@Transaction()
async createTwoItems(
@TransactionManager() manager?: EntityManager
) {
// ...
}
```

#### Pass manager to BaseService

You can pass the entity manager into any of the `BaseService` methods to ensure they're part of the transaction.

```typescript
@Transaction()
async createTwoItems(
@TransactionManager() manager?: EntityManager
) {
this.create(data, userId, { manager })
}
```

#### Example

```typescript
@Service('UserService')
export class UserService extends BaseService<User> {
constructor(@InjectRepository(User) protected readonly repository: Repository<User>) {
super(User, repository);
}

// GOOD: successful transaction
@Transaction()
async successfulTransaction(
data: DeepPartial<User>,
userId: string,
@TransactionManager() manager?: EntityManager
): Promise<User[]> {
return Promise.all([
this.create(data, userId, { manager }),
this.create(data, userId, { manager })
]);
}

// GOOD: successful rollback when something errors
@Transaction()
async failedTransaction(
data: DeepPartial<User>,
userId: string,
@TransactionManager() manager?: EntityManager
): Promise<User[]> {
const invalidUserData = {};

const users = await Promise.all([
this.create(data, userId, { manager }),
this.create(invalidUserData, userId, { manager }) // This one fails
]);

return users;
}

// BAD: you can't return a promise here. The function will return and the first
// user will be saved even though the 2nd one fails
@Transaction()
async failedTransaction(
data: DeepPartial<User>,
userId: string,
@TransactionManager() manager?: EntityManager
): Promise<User[]> {
return await Promise.all([
this.create(data, userId, { manager }),
this.create(invalidUserData, userId, { manager })
]);
}
}
```

See the [TypeORM Transaction Docs](https://github.com/typeorm/typeorm/blob/master/docs/transactions.md#transaction-decorators) for more info.

## Complex use cases/ejecting

Warthog makes building simple CRUD endpoints incredibly easy. However, since it is built on top of TypeORM and TypeGraphQL it is flexible enough to handle complex use cases as well.
Expand Down
9 changes: 9 additions & 0 deletions examples/11-transactions/.env
@@ -0,0 +1,9 @@
NODE_ENV=development
PGUSER=postgres
WARTHOG_APP_HOST=localhost
WARTHOG_APP_PORT=4100
WARTHOG_DB_DATABASE=warthog-11-transactions
WARTHOG_DB_USERNAME=postgres
WARTHOG_DB_PASSWORD=
WARTHOG_DB_SYNCHRONIZE=true
WARTHOG_SUBSCRIPTIONS=true
20 changes: 20 additions & 0 deletions examples/11-transactions/README.md
@@ -0,0 +1,20 @@
## Example 11

## Setup

Run `yarn bootstrap && yarn start`

## Bootstrapping the App

Running `DEBUG=* yarn bootstrap` will do the following:

- Install packages
- Create the example DB
- Seed the database with test data

## Running the App

To run the project, run `yarn start`. This will:

- Run the API server
- Open GraphQL Playground
207 changes: 207 additions & 0 deletions examples/11-transactions/generated/binding.ts
@@ -0,0 +1,207 @@
import 'graphql-import-node'; // Needed so you can import *.graphql files

import { makeBindingClass, Options } from 'graphql-binding'
import { GraphQLResolveInfo, GraphQLSchema } from 'graphql'
import { IResolvers } from 'graphql-tools/dist/Interfaces'
import * as schema from './schema.graphql'

export interface Query {
users: <T = Array<User>>(args: { offset?: Int | null, limit?: Int | null, where?: UserWhereInput | null, orderBy?: UserOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T>
}

export interface Mutation {
successfulTransaction: <T = Array<User>>(args: { data: UserCreateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
failedTransaction: <T = Array<User>>(args: { data: UserCreateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T>
}

export interface Subscription {}

export interface Binding {
query: Query
mutation: Mutation
subscription: Subscription
request: <T = any>(query: string, variables?: {[key: string]: any}) => Promise<T>
delegate(operation: 'query' | 'mutation', fieldName: string, args: {
[key: string]: any;
}, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise<any>;
delegateSubscription(fieldName: string, args?: {
[key: string]: any;
}, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise<AsyncIterator<any>>;
getAbstractResolvers(filterSchema?: GraphQLSchema | string): IResolvers;
}

export interface BindingConstructor<T> {
new(...args: any[]): T
}

export const Binding = makeBindingClass<BindingConstructor<Binding>>({ schema: schema as any })

/**
* Types
*/

export type UserOrderByInput = 'createdAt_ASC' |
'createdAt_DESC' |
'updatedAt_ASC' |
'updatedAt_DESC' |
'deletedAt_ASC' |
'deletedAt_DESC' |
'firstName_ASC' |
'firstName_DESC' |
'lastName_ASC' |
'lastName_DESC'

export interface BaseWhereInput {
id_eq?: String | null
id_in?: String[] | String | null
createdAt_eq?: String | null
createdAt_lt?: String | null
createdAt_lte?: String | null
createdAt_gt?: String | null
createdAt_gte?: String | null
createdById_eq?: String | null
updatedAt_eq?: String | null
updatedAt_lt?: String | null
updatedAt_lte?: String | null
updatedAt_gt?: String | null
updatedAt_gte?: String | null
updatedById_eq?: String | null
deletedAt_all?: Boolean | null
deletedAt_eq?: String | null
deletedAt_lt?: String | null
deletedAt_lte?: String | null
deletedAt_gt?: String | null
deletedAt_gte?: String | null
deletedById_eq?: String | null
}

export interface UserCreateInput {
firstName: String
lastName: String
}

export interface UserUpdateInput {
firstName?: String | null
lastName?: String | null
}

export interface UserWhereInput {
id_eq?: ID_Input | null
id_in?: ID_Output[] | ID_Output | null
createdAt_eq?: DateTime | null
createdAt_lt?: DateTime | null
createdAt_lte?: DateTime | null
createdAt_gt?: DateTime | null
createdAt_gte?: DateTime | null
createdById_eq?: ID_Input | null
createdById_in?: ID_Output[] | ID_Output | null
updatedAt_eq?: DateTime | null
updatedAt_lt?: DateTime | null
updatedAt_lte?: DateTime | null
updatedAt_gt?: DateTime | null
updatedAt_gte?: DateTime | null
updatedById_eq?: ID_Input | null
updatedById_in?: ID_Output[] | ID_Output | null
deletedAt_all?: Boolean | null
deletedAt_eq?: DateTime | null
deletedAt_lt?: DateTime | null
deletedAt_lte?: DateTime | null
deletedAt_gt?: DateTime | null
deletedAt_gte?: DateTime | null
deletedById_eq?: ID_Input | null
deletedById_in?: ID_Output[] | ID_Output | null
firstName_eq?: String | null
firstName_contains?: String | null
firstName_startsWith?: String | null
firstName_endsWith?: String | null
firstName_in?: String[] | String | null
lastName_eq?: String | null
lastName_contains?: String | null
lastName_startsWith?: String | null
lastName_endsWith?: String | null
lastName_in?: String[] | String | null
}

export interface UserWhereUniqueInput {
id: ID_Output
}

export interface BaseGraphQLObject {
id: ID_Output
createdAt: DateTime
createdById: String
updatedAt?: DateTime | null
updatedById?: String | null
deletedAt?: DateTime | null
deletedById?: String | null
version: Int
}

export interface DeleteResponse {
id: ID_Output
}

export interface BaseModel extends BaseGraphQLObject {
id: ID_Output
createdAt: DateTime
createdById: String
updatedAt?: DateTime | null
updatedById?: String | null
deletedAt?: DateTime | null
deletedById?: String | null
version: Int
}

export interface BaseModelUUID extends BaseGraphQLObject {
id: ID_Output
createdAt: DateTime
createdById: String
updatedAt?: DateTime | null
updatedById?: String | null
deletedAt?: DateTime | null
deletedById?: String | null
version: Int
}

export interface StandardDeleteResponse {
id: ID_Output
}

export interface User extends BaseGraphQLObject {
id: ID_Output
createdAt: DateTime
createdById: String
updatedAt?: DateTime | null
updatedById?: String | null
deletedAt?: DateTime | null
deletedById?: String | null
version: Int
firstName: String
lastName: String
}

/*
The `Boolean` scalar type represents `true` or `false`.
*/
export type Boolean = boolean

/*
The javascript `Date` as string. Type represents date and time as the ISO Date string.
*/
export type DateTime = Date | string

/*
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
*/
export type ID_Input = string | number
export type ID_Output = string

/*
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
*/
export type Int = number

/*
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
*/
export type String = string

0 comments on commit c91ec14

Please sign in to comment.