Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
hardyscc committed Apr 5, 2020
0 parents commit 3e47627
Show file tree
Hide file tree
Showing 47 changed files with 15,478 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# compiled output
/dist
/node_modules

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# Tests
/coverage
/.nyc_output

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
178 changes: 178 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# NestJS CQRS Microservices Starter Project.

## Description

A starter project which featuring advanced microservice pattern with GraphQL, based on Domain-Driven Design (DDD) using the command query responsibility segregation (CQRS) design pattern.

## Technologies

- [GraphQL](https://graphql.org/)
- [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/)
- [TypeORM](https://typeorm.io/)
- [NestJS](https://docs.nestjs.com/)
- [NestJS GraphQL](https://docs.nestjs.com/graphql/quick-start)
- [NestJS Federation](https://docs.nestjs.com/graphql/federation)
- [NestJS TypeORM](https://docs.nestjs.com/techniques/database)
- [NestJS CQRS](https://docs.nestjs.com/recipes/cqrs)
- [NestJS Event Store](https://github.com/juicycleff/nestjs-event-store)

## Installation

```bash
git clone https://github.com/hardyscc/nestjs-cqrs-starter.git <Your_Project_Name>
cd <Your_Project_Name>

npm install
```

## Usage

### Start MySQL

Start MySQL docker instance.

```bash
docker run -d -e "MYSQL_ROOT_PASSWORD=Admin12345" -e "MYSQL_USER=usr" -e "MYSQL_PASSWORD=User12345" -e "MYSQL_DATABASE=development" -p 3306:3306 --name some-mysql bitnami/mysql:5.7.27
```

Connect using MySQL docker instance command line.

```bash
docker exec -it some-mysql mysql -uroot -p"Admin12345"
```

Create the Databases for testing

```sql
CREATE DATABASE service_user;
GRANT ALL PRIVILEGES ON service_user.* TO 'usr'@'%';

CREATE DATABASE service_account;
GRANT ALL PRIVILEGES ON service_account.* TO 'usr'@'%';
FLUSH PRIVILEGES;
```

Clean-up all data if need to re-testing again

```sql
DELETE FROM service_account.ACCOUNT;
DELETE FROM service_user.USER;
```

### Start EventStore

```bash
docker run --name some-eventstore -d -p 2113:2113 -p 1113:1113 eventstore/eventstore
```

Create the Persistent Subscriptions

```bash
curl -L -X PUT "http://localhost:2113/subscriptions/%24svc-user/account" \
-H "Content-Type: application/json" \
-H "Authorization: Basic YWRtaW46Y2hhbmdlaXQ=" \
-d "{}"

curl -L -X PUT "http://localhost:2113/subscriptions/%24svc-account/user" \
-H "Content-Type: application/json" \
-H "Authorization: Basic YWRtaW46Y2hhbmdlaXQ=" \
-d "{}"
```

### Start the microservices

```bash
# Start the user service
nest start service-user

# Start the account service
nest start service-account

# start the gateway
nest start gateway
```

## Testing

Goto GraphQL Playground - http://localhost:3000/graphql

### Create user with a default saving account

```graphql
mutation {
createUser(input: { name: "John" }) {
id
name
}
}
```

OR

```bash
curl -H 'Content-Type: application/json' \
-d '{"query": "mutation { createUser(input: { name: \"John\" }) { id name } }"}' \
http://localhost:3000/graphql
```

You should see something like this

1. Under `service-user` console

```sql
Async CreateUserHandler... CreateUserCommand
query: START TRANSACTION
query: INSERT INTO `USER`(`id`, `name`, `nickName`, `status`) VALUES (?, ?, DEFAULT, DEFAULT) -- PARAMETERS: ["4d04689b-ef40-4a08-8a27-6fa420790ddb","John"]
query: SELECT `User`.`id` AS `User_id`, `User`.`status` AS `User_status` FROM `USER` `User` WHERE `User`.`id` = ? -- PARAMETERS: ["4d04689b-ef40-4a08-8a27-6fa420790ddb"]
query: COMMIT
Async ActivateUserHandler... ActivateUserCommand
query: UPDATE `USER` SET `status` = ? WHERE `id` IN (?) -- PARAMETERS: ["A","4d04689b-ef40-4a08-8a27-6fa420790ddb"]
```

1. under `service-account` console

```sql
Async CreateAccountHandler... CreateAccountCommand
query: START TRANSACTION
query: INSERT INTO `ACCOUNT`(`id`, `name`, `balance`, `userId`) VALUES (?, ?, DEFAULT, ?) -- PARAMETERS: ["57c3cc9e-4aa9-4ea8-8c7f-5d4653ee709f","Saving","4d04689b-ef40-4a08-8a27-6fa420790ddb"]
query: SELECT `Account`.`id` AS `Account_id`, `Account`.`balance` AS `Account_balance` FROM `ACCOUNT` `Account` WHERE `Account`.`id` = ? -- PARAMETERS: ["57c3cc9e-4aa9-4ea8-8c7f-5d4653ee709f"]
query: COMMIT
```

### Query the users

```graphql
query {
users {
id
name
accounts {
id
name
balance
}
}
}
```

Output :

```json
{
"data": {
"users": [
{
"id": "4d04689b-ef40-4a08-8a27-6fa420790ddb",
"name": "John",
"accounts": [
{
"id": "57c3cc9e-4aa9-4ea8-8c7f-5d4653ee709f",
"name": "Saving",
"balance": 0
}
]
}
]
}
}
```
23 changes: 23 additions & 0 deletions apps/gateway/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { GraphQLGatewayModule } from '@nestjs/graphql';

@Module({
imports: [
GraphQLGatewayModule.forRoot({
gateway: {
serviceList: [
{
name: 'user',
url: process.env.USER_ENDPOINT || 'http://localhost:4001/graphql',
},
{
name: 'account',
url:
process.env.ACCOUNT_ENDPOINT || 'http://localhost:4002/graphql',
},
],
},
}),
],
})
export class AppModule {}
8 changes: 8 additions & 0 deletions apps/gateway/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
9 changes: 9 additions & 0 deletions apps/gateway/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
9 changes: 9 additions & 0 deletions apps/gateway/tsconfig.app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false,
"outDir": "../../dist/apps/gateway"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
48 changes: 48 additions & 0 deletions apps/service-account/src/account/account.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { UserActivatedEvent } from '@hardyscc/common/event/user-activated.event';
import { UserCreatedEvent } from '@hardyscc/common/event/user-created.event';
import {
EventStoreSubscriptionType,
NestjsEventStoreModule,
} from '@hardyscc/nestjs-event-store';
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CreateAccountHandler } from './cqrs/command/handler/create-account.handler';
import { CreateUserSaga } from './cqrs/saga/create-user.saga';
import { Account } from './model/account.model';
import { AccountResolver } from './resolver/account.resolver';
import { UserResolver } from './resolver/user.resolver';
import { AccountService } from './service/account.service';

const CommandHandlers = [CreateAccountHandler];
const Sagas = [CreateUserSaga];

@Module({
imports: [
CqrsModule,
NestjsEventStoreModule.forFeature({
featureStreamName: '$svc-account',
subscriptionsDelay: 2000,
subscriptions: [
{
type: EventStoreSubscriptionType.Persistent,
stream: '$svc-user',
persistentSubscriptionName: 'account',
},
],
eventHandlers: {
UserCreatedEvent: (data) => new UserCreatedEvent(data),
UserActivatedEvent: (data) => new UserActivatedEvent(data),
},
}),
TypeOrmModule.forFeature([Account]),
],
providers: [
AccountService,
AccountResolver,
UserResolver,
...CommandHandlers,
...Sagas,
],
})
export class AccountModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs';
import { AccountService } from '../../../service/account.service';
import { CreateAccountCommand } from '../impl/create-account.command';

@CommandHandler(CreateAccountCommand)
export class CreateAccountHandler
implements ICommandHandler<CreateAccountCommand> {
constructor(
private readonly accountService: AccountService,
private readonly publisher: EventPublisher,
) {}

async execute(command: CreateAccountCommand) {
console.log(`Async ${this.constructor.name}...`, command.constructor.name);
const { input } = command;
const account = this.publisher.mergeObjectContext(
await this.accountService.create(input),
);
account.createAccount();
account.commit();
return account;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ICommand } from '@nestjs/cqrs';
import { CreateAccountInput } from '../../../input/create-account.input';

export class CreateAccountCommand implements ICommand {
constructor(public readonly input: CreateAccountInput) {}
}
20 changes: 20 additions & 0 deletions apps/service-account/src/account/cqrs/saga/create-user.saga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UserCreatedEvent } from '@hardyscc/common/event/user-created.event';
import { Injectable } from '@nestjs/common';
import { ICommand, ofType, Saga } from '@nestjs/cqrs';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { CreateAccountCommand } from '../command/impl/create-account.command';

@Injectable()
export class CreateUserSaga {
@Saga()
userCreated = (events$: Observable<any>): Observable<ICommand> => {
return events$.pipe(
ofType(UserCreatedEvent),
map(
(event) =>
new CreateAccountCommand({ name: 'Saving', userId: event.user.id }),
),
);
};
}
Loading

0 comments on commit 3e47627

Please sign in to comment.