-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3e47627
Showing
47 changed files
with
15,478 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
23 changes: 23 additions & 0 deletions
23
apps/service-account/src/account/cqrs/command/handler/create-account.handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
apps/service-account/src/account/cqrs/command/impl/create-account.command.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
apps/service-account/src/account/cqrs/saga/create-user.saga.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }), | ||
), | ||
); | ||
}; | ||
} |
Oops, something went wrong.