├── .eslintignore ├── .prettierignore ├── .gitignore ├── packages ├── storage-eventstore │ ├── ormconfig.json │ ├── package.json │ ├── entity │ │ ├── stream.entity.ts │ │ └── event.entity.ts │ ├── eventstore.module.ts │ └── eventstore.service.ts ├── microservice-auth │ ├── index.ts │ ├── domain │ │ ├── model │ │ │ ├── index.ts │ │ │ └── account.model.ts │ │ ├── dto │ │ │ ├── index.ts │ │ │ └── authCredentials.dto.ts │ │ └── event │ │ │ ├── index.ts │ │ │ ├── handler │ │ │ ├── index.ts │ │ │ ├── loggedIn.handler.ts │ │ │ └── accountRegistred.handler.ts │ │ │ └── interface │ │ │ ├── loggedIn.event.ts │ │ │ └── accountRegistred.event.ts │ ├── infrastructure │ │ ├── index.ts │ │ ├── api │ │ │ ├── auth.client.ts │ │ │ ├── auth.proto │ │ │ └── auth.controller.ts │ │ └── persistance │ │ │ └── entity │ │ │ ├── refreshToken.entity.ts │ │ │ └── account.entity.ts │ ├── business │ │ ├── command │ │ │ ├── index.ts │ │ │ ├── interface │ │ │ │ ├── login.command.ts │ │ │ │ └── registerAccount.command.ts │ │ │ └── handler │ │ │ │ ├── index.ts │ │ │ │ ├── registerAccount.handler.ts │ │ │ │ └── login.handler.ts │ │ ├── query │ │ │ ├── index.ts │ │ │ ├── interface │ │ │ │ ├── doesEmailAlreadyExist.query.ts │ │ │ │ └── getUserIdAfterValidation.query.ts │ │ │ └── handler │ │ │ │ ├── index.ts │ │ │ │ ├── doesEmailAlreadyExist.handler.ts │ │ │ │ └── getUserIdAfterValidation.handler.ts │ │ ├── sagas │ │ │ └── updateQuery.saga.ts │ │ └── auth.service.ts │ ├── package.json │ └── auth.module.ts ├── microservice-profile │ ├── index.ts │ ├── domain │ │ ├── dto │ │ │ ├── index.ts │ │ │ └── profile.dto.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ └── profile.model.ts │ │ └── event │ │ │ ├── index.ts │ │ │ ├── handler │ │ │ ├── index.ts │ │ │ └── profileCreated.handler.ts │ │ │ └── interface │ │ │ └── profileCreated.event.ts │ ├── infrastructure │ │ ├── index.ts │ │ └── api │ │ │ ├── profile.proto │ │ │ ├── profile.client.ts │ │ │ └── profile.controller.ts │ ├── business │ │ ├── command │ │ │ ├── index.ts │ │ │ ├── handler │ │ │ │ ├── index.ts │ │ │ │ └── createProfile.handler.ts │ │ │ └── interface │ │ │ │ └── createProfile.command.ts │ │ └── profile.service.ts │ ├── package.json │ └── profile.module.ts ├── api-gateway │ ├── types.ts │ ├── middlewares │ │ └── reqId.middleware.ts │ ├── package.json │ ├── module.ts │ ├── index.ts │ └── controllers │ │ ├── profile.controller.ts │ │ └── auth.controller.ts ├── utils-grpc │ ├── package.json │ ├── types.ts │ ├── formatHttpResponse.ts │ ├── formatGrpcResponse.ts │ ├── mapping.ts │ └── exception.ts ├── utils-logger │ ├── package.json │ ├── nest.logger.ts │ └── typeorm.logger.ts └── config │ ├── config.module.ts │ ├── package.json │ └── config.service.ts ├── index.ts ├── .prettierrc.js ├── .editorconfig ├── .vscode └── settings.json ├── .eslintrc.js ├── backpack.config.js ├── LICENCE ├── package.json ├── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | build -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build -------------------------------------------------------------------------------- /packages/storage-eventstore/ormconfig.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/microservice-auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.module'; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile.module'; 2 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '@shitake/api-gateway'; 2 | 3 | bootstrap(); 4 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.model'; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile.dto'; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile.model'; 2 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authCredentials.dto'; 2 | -------------------------------------------------------------------------------- /packages/api-gateway/types.ts: -------------------------------------------------------------------------------- 1 | export type ControllerResponse = Promise<{ data: unknown }>; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/infrastructure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api/profile.client'; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface/profileCreated.event'; 2 | -------------------------------------------------------------------------------- /packages/microservice-profile/business/command/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface/createProfile.command'; 2 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface/accountRegistred.event'; 2 | export * from './interface/loggedIn.event'; 3 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api/auth.client'; 2 | 3 | export * from './persistance/entity/account.entity'; 4 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface/registerAccount.command'; 2 | export * from './interface/login.command'; 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/interface/login.command.ts: -------------------------------------------------------------------------------- 1 | export class LoginCommand { 2 | public constructor(public readonly accountId: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface/doesEmailAlreadyExist.query'; 2 | export * from './interface/getUserIdAfterValidation.query'; 3 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/interface/doesEmailAlreadyExist.query.ts: -------------------------------------------------------------------------------- 1 | export class DoesEmailExistQuery { 2 | public constructor(public readonly email: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /packages/microservice-profile/business/command/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateProfileHandler } from './createProfile.handler'; 2 | 3 | export const ProfileCommandHandlers = [CreateProfileHandler]; 4 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/event/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { ProfileCreatedHandler } from './profileCreated.handler'; 2 | 3 | export const ProfileEventHandlers = [ProfileCreatedHandler]; 4 | -------------------------------------------------------------------------------- /packages/microservice-profile/business/command/interface/createProfile.command.ts: -------------------------------------------------------------------------------- 1 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 2 | 3 | export class CreateProfileCommand { 4 | public constructor(public readonly userDto: ProfileDto) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { AccountRegistredHandler } from './accountRegistred.handler'; 2 | import { LoggedInHandler } from './loggedIn.handler'; 3 | 4 | export const AuthEventHandlers = [AccountRegistredHandler, LoggedInHandler]; 5 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { RegisterAccountHandler } from './registerAccount.handler'; 2 | import { LoginCommandHandler } from './login.handler'; 3 | 4 | export const AuthCommandHandlers = [RegisterAccountHandler, LoginCommandHandler]; 5 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/interface/loggedIn.event.ts: -------------------------------------------------------------------------------- 1 | import { AuthHashedCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 2 | 3 | export class LoggedInEvent { 4 | public constructor(public readonly uuid: string, public readonly refreshToken: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/event/interface/profileCreated.event.ts: -------------------------------------------------------------------------------- 1 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 2 | 3 | export class ProfileCreatedEvent { 4 | public constructor(public readonly uuid: string, public readonly data: ProfileDto) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-eventstore/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/storage-eventstore", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils-grpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/utils-grpc", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": {}, 10 | "devDependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils-grpc/types.ts: -------------------------------------------------------------------------------- 1 | import grpc from 'grpc'; 2 | 3 | export interface GrpcStatus { 4 | code: grpc.status; 5 | message?: string; 6 | details?: unknown[]; 7 | } 8 | 9 | export interface GrpcAnswer { 10 | status: GrpcStatus; 11 | data: T; 12 | } 13 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/interface/registerAccount.command.ts: -------------------------------------------------------------------------------- 1 | import { AuthClearTextCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 2 | 3 | export class RegisterAccountCommand { 4 | public constructor(public readonly authCredentialsDto: AuthClearTextCredentialsDto) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/interface/accountRegistred.event.ts: -------------------------------------------------------------------------------- 1 | import { AuthHashedCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 2 | 3 | export class AccountRegistredEvent { 4 | public constructor(public readonly uuid: string, public readonly data: AuthHashedCredentialsDto) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/handler/index.ts: -------------------------------------------------------------------------------- 1 | import { DoesEmailExistHandler } from './doesEmailAlreadyExist.handler'; 2 | import { GetUserIdAfterValidationHandler } from './getUserIdAfterValidation.handler'; 3 | 4 | export const AuthQueryHandlers = [DoesEmailExistHandler, GetUserIdAfterValidationHandler]; 5 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/interface/getUserIdAfterValidation.query.ts: -------------------------------------------------------------------------------- 1 | import { AuthClearTextCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 2 | 3 | export class GetUserIdAfterValidationQuery { 4 | public constructor(public readonly authCredentialsDto: AuthClearTextCredentialsDto) {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/microservice-profile/infrastructure/api/profile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package profile; 4 | 5 | // service Query {} 6 | 7 | service Command { 8 | rpc CreateProfile(UserDto) returns (Empty) {} 9 | } 10 | 11 | message UserDto { 12 | string firstName = 1; 13 | string lastName = 2; 14 | string email = 3; 15 | } 16 | 17 | message Empty {} -------------------------------------------------------------------------------- /packages/utils-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/utils-logger", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "pino": "^5.12.2" 11 | }, 12 | "devDependencies": { 13 | "@types/pino": "^5.8.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService(`${process.env.NODE_ENV}.env`), 9 | }, 10 | ], 11 | exports: [ConfigService], 12 | }) 13 | export class ConfigModule {} 14 | -------------------------------------------------------------------------------- /packages/microservice-profile/infrastructure/api/profile.client.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { ClientOptions, Transport } from '@nestjs/microservices'; 4 | 5 | export const profileClientOptions: ClientOptions = { 6 | transport: Transport.GRPC, 7 | options: { 8 | package: 'profile', 9 | protoPath: path.join(__dirname, './profile.proto'), 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/microservice-profile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/microservice-profile", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "uuid": "^3.3.2" 11 | }, 12 | "devDependencies": { 13 | "@types/uuid": "^3.4.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/storage-eventstore/entity/stream.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Unique, OneToMany, ManyToOne } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Stream { 5 | @PrimaryGeneratedColumn('uuid') 6 | public streamId!: string; 7 | 8 | @Column('int') 9 | public version!: number; 10 | 11 | @Column('text') 12 | public type!: string; 13 | } 14 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/api/auth.client.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { ClientOptions, Transport } from '@nestjs/microservices'; 4 | 5 | export const authClientOptions: ClientOptions = { 6 | transport: Transport.GRPC, 7 | options: { 8 | url: 'localhost:50501', 9 | package: 'auth', 10 | protoPath: path.join(__dirname, './auth.proto'), 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/config", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "dotenv": "^7.0.0", 11 | "joi": "^14.3.1" 12 | }, 13 | "devDependencies": { 14 | "@types/dotenv": "^6.1.1", 15 | "@types/joi": "^14.3.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/dto/profile.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsString, Length } from 'class-validator'; 2 | import { ApiModelProperty } from '@nestjs/swagger'; 3 | 4 | export class ProfileDto { 5 | @ApiModelProperty() 6 | @IsDefined() 7 | @IsString() 8 | @Length(1, 50) 9 | public readonly firstName!: string; 10 | 11 | @ApiModelProperty() 12 | @IsDefined() 13 | @IsString() 14 | @Length(1, 50) 15 | public readonly lastName!: string; 16 | } 17 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/model/profile.model.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@nestjs/cqrs'; 2 | 3 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 4 | import { ProfileCreatedEvent } from '@shitake/microservice-profile/domain/event/'; 5 | 6 | export class Profile extends AggregateRoot { 7 | public constructor(private readonly uuid: string) { 8 | super(); 9 | } 10 | 11 | public create(createProfileDto: ProfileDto) { 12 | this.apply(new ProfileCreatedEvent(this.uuid, createProfileDto)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils-logger/nest.logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import pino from 'pino'; 3 | 4 | const log = pino(); 5 | 6 | export class NestLogger implements LoggerService { 7 | public log(message: any, context?: string | undefined) { 8 | log.info({ context }, message); 9 | } 10 | public error(message: any, trace?: string | undefined, context?: string | undefined) { 11 | log.error({ trace, context }, message); 12 | } 13 | public warn(message: any, context?: string | undefined) { 14 | log.error({ context }, message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/utils-grpc/formatHttpResponse.ts: -------------------------------------------------------------------------------- 1 | import { GrpcAnswer } from './types'; 2 | import { GrpcToHttpExceptionMapping } from './mapping'; 3 | import { InternalServerErrorException } from '@nestjs/common'; 4 | 5 | export const formatHttpResponse = async

(grpcAnswer: GrpcAnswer

): Promise<{ data: P }> => { 6 | if (grpcAnswer.status.code !== 0) { 7 | const exception = GrpcToHttpExceptionMapping[grpcAnswer.status.code] || InternalServerErrorException; 8 | throw new exception(grpcAnswer.status.message || 'Unknown'); 9 | } 10 | return { data: grpcAnswer.data }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/microservice-profile/business/profile.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommandBus } from '@nestjs/cqrs'; 3 | 4 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 5 | import { CreateProfileCommand } from '@shitake/microservice-profile/business/command'; 6 | 7 | @Injectable() 8 | export class ProfileService { 9 | public constructor(private readonly commandBus: CommandBus) {} 10 | 11 | public async createProfile(userDto: ProfileDto) { 12 | return this.commandBus.execute(new CreateProfileCommand(userDto)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/api-gateway/middlewares/reqId.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware } from '@nestjs/common'; 2 | import uuid from 'uuid'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | @Injectable() 6 | export class RequestIdMiddleware implements NestMiddleware { 7 | public use(req: Request & { id: string }, res: Response, next: NextFunction): void { 8 | const reqId = req.headers['X-Request-Id'] && req.headers['X-Request-Id']; 9 | req.id = (reqId && reqId.toString()) || uuid.v4(); 10 | res.setHeader('X-Request-Id', req.id); 11 | next(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/microservice-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/microservice-auth", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "argon2": "^0.21.0", 11 | "class-transformer": "^0.2.0", 12 | "class-validator": "^0.9.1", 13 | "jsonwebtoken": "^8.5.1", 14 | "uuid": "^3.3.2" 15 | }, 16 | "devDependencies": { 17 | "@types/jsonwebtoken": "^8.3.2", 18 | "@types/protobufjs": "^6.0.0", 19 | "@types/uuid": "^3.4.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/persistance/entity/refreshToken.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Unique, Column, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'refreshToken' }) 4 | export class RefreshTokenEntity { 5 | @PrimaryColumn('uuid', { nullable: false }) 6 | public id!: string; 7 | 8 | @Index({ unique: true }) 9 | @Column('text', { unique: true, nullable: false }) 10 | public refreshToken!: string; 11 | 12 | @CreateDateColumn() 13 | public createdAt!: Date; 14 | 15 | @UpdateDateColumn() 16 | public updatedAt!: Date; 17 | } 18 | -------------------------------------------------------------------------------- /packages/microservice-profile/infrastructure/api/profile.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { GrpcMethod } from '@nestjs/microservices'; 3 | 4 | import { ProfileService } from '@shitake/microservice-profile/business/profile.service'; 5 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 6 | 7 | @Controller() 8 | export class ProfileController { 9 | public constructor(private readonly profileService: ProfileService) {} 10 | 11 | @GrpcMethod('Command') 12 | public async createProfile(profileDto: ProfileDto) { 13 | await this.profileService.createProfile(profileDto); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shitake/api-gateway", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "devDependencies": { 10 | "@types/express-rate-limit": "^3.3.0", 11 | "@types/helmet": "^0.0.43", 12 | "@types/pino-http": "^4.0.2", 13 | "pino-http": "^4.1.0", 14 | "pino-pretty": "^2.6.0" 15 | }, 16 | "dependencies": { 17 | "@types/express": "^4.16.1", 18 | "express-rate-limit": "^3.4.0", 19 | "grpc": "^1.19.0", 20 | "helmet": "^3.16.0", 21 | "swagger-ui-express": "^4.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/dto/authCredentials.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsDefined, IsEmail } from 'class-validator'; 2 | import { ApiModelProperty } from '@nestjs/swagger'; 3 | 4 | export class AuthClearTextCredentialsDto { 5 | @ApiModelProperty() 6 | @IsDefined() 7 | @IsEmail() 8 | public readonly email!: string; 9 | 10 | @ApiModelProperty() 11 | @IsDefined() 12 | @IsString() 13 | public readonly password!: string; 14 | } 15 | 16 | export class AuthHashedCredentialsDto { 17 | @IsDefined() 18 | @IsEmail() 19 | public readonly email!: string; 20 | 21 | @IsDefined() 22 | @IsString() 23 | public readonly hashedPassword!: string; 24 | } 25 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/persistance/entity/account.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryColumn, Unique, Column, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | @Entity({ name: 'account' }) 4 | export class AccountEntity { 5 | @PrimaryColumn('uuid', { nullable: false }) 6 | public id!: string; 7 | 8 | @Index({ unique: true }) 9 | @Column('text', { unique: true, nullable: false }) 10 | public email!: string; 11 | 12 | @Column('text', { nullable: false }) 13 | public hashedPassword!: string; 14 | 15 | @CreateDateColumn() 16 | public createdAt!: Date; 17 | 18 | @UpdateDateColumn() 19 | public updatedAt!: Date; 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "flowide.enabled": false, 3 | "eslint.autoFixOnSave": true, 4 | "eslint.validate": [ 5 | "javascript", 6 | "javascriptreact", 7 | { "language": "typescript", "autoFix": true }, 8 | { "language": "typescriptreact", "autoFix": true } 9 | ], 10 | "editor.formatOnSave": true, 11 | "[javascript]": { 12 | "editor.formatOnSave": false 13 | }, 14 | "[javascriptreact]": { 15 | "editor.formatOnSave": false 16 | }, 17 | "[typescript]": { 18 | "editor.formatOnSave": false 19 | }, 20 | "[typescriptreact]": { 21 | "editor.formatOnSave": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils-grpc/formatGrpcResponse.ts: -------------------------------------------------------------------------------- 1 | import grpc from 'grpc'; 2 | 3 | import { GrpcException } from './exception'; 4 | import { GrpcStatus, GrpcAnswer } from './types'; 5 | 6 | export const formatGrpcResponse = async

( 7 | service: (...args: P) => Promise, 8 | args: P, 9 | ): Promise> => { 10 | let data: R = ({} as unknown) as R; 11 | let status: GrpcStatus; 12 | try { 13 | data = await service(...args); 14 | status = { code: grpc.status.OK }; 15 | } catch (e) { 16 | if (e instanceof GrpcException) { 17 | status = { code: e.code, message: e.message }; 18 | } else { 19 | throw e; 20 | } 21 | } 22 | return { data, status }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/handler/loggedIn.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | 3 | import { LoggedInEvent } from '@shitake/microservice-auth/domain/event'; 4 | 5 | import { EventstoreService } from '@shitake/storage-eventstore/eventstore.service'; 6 | 7 | @EventsHandler(LoggedInEvent) 8 | export class LoggedInHandler implements IEventHandler { 9 | public constructor(private readonly eventstoreService: EventstoreService) {} 10 | 11 | public handle(event: LoggedInEvent) { 12 | this.eventstoreService.createEvent(event.uuid, 'User', { refreshToken: event.refreshToken }, 'loggedIn', {}); 13 | // TODO dispatch event to external workers 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/model/account.model.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@nestjs/cqrs'; 2 | 3 | import { AuthHashedCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 4 | import { AccountRegistredEvent, LoggedInEvent } from '@shitake/microservice-auth/domain/event'; 5 | 6 | export class Account extends AggregateRoot { 7 | public constructor(private readonly uuid: string) { 8 | super(); 9 | } 10 | 11 | public register(authHashedCredentialDto: AuthHashedCredentialsDto) { 12 | this.apply(new AccountRegistredEvent(this.uuid, authHashedCredentialDto)); 13 | } 14 | 15 | public login(refreshToken: string) { 16 | this.apply(new LoggedInEvent(this.uuid, refreshToken)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/microservice-profile/domain/event/handler/profileCreated.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | 3 | import { ProfileCreatedEvent } from '@shitake/microservice-profile/domain/event'; 4 | 5 | import { EventstoreService } from '@shitake/storage-eventstore/eventstore.service'; 6 | 7 | @EventsHandler(ProfileCreatedEvent) 8 | export class ProfileCreatedHandler implements IEventHandler { 9 | public constructor(private readonly eventstoreService: EventstoreService) {} 10 | 11 | public handle(event: ProfileCreatedEvent) { 12 | this.eventstoreService.createEvent(event.uuid, 'User', event.data, 'profileCreated', {}); 13 | // TODO dispatch event to external workers 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/microservice-profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | 4 | import { ProfileController } from './infrastructure/api/profile.controller'; 5 | import { ProfileService } from './business/profile.service'; 6 | import { ProfileCommandHandlers } from './business/command/handler'; 7 | import { ProfileEventHandlers } from './domain/event/handler'; 8 | 9 | import { EventstoreService } from '@shitake/storage-eventstore/eventstore.service'; 10 | 11 | @Module({ 12 | imports: [CqrsModule], 13 | controllers: [ProfileController], 14 | providers: [ProfileService, ...ProfileCommandHandlers, ...ProfileEventHandlers, EventstoreService], 15 | }) 16 | export class ProfileModule {} 17 | -------------------------------------------------------------------------------- /packages/microservice-auth/domain/event/handler/accountRegistred.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | 3 | import { AccountRegistredEvent } from '@shitake/microservice-auth/domain/event'; 4 | 5 | import { EventstoreService } from '@shitake/storage-eventstore/eventstore.service'; 6 | 7 | @EventsHandler(AccountRegistredEvent) 8 | export class AccountRegistredHandler implements IEventHandler { 9 | public constructor(private readonly eventstoreService: EventstoreService) {} 10 | 11 | public handle(event: AccountRegistredEvent) { 12 | this.eventstoreService.createEvent(event.uuid, 'User', event.data, 'accountRegistred', {}); 13 | // TODO dispatch event to external workers 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/storage-eventstore/entity/event.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, Unique, ManyToOne, JoinColumn } from 'typeorm'; 2 | import { Stream } from './stream.entity'; 3 | 4 | @Entity() 5 | @Unique(['streamId', 'version']) 6 | export class Event { 7 | @PrimaryGeneratedColumn('increment', { type: 'bigint' }) 8 | public sequenceNum!: number; 9 | 10 | @ManyToOne(type => Stream) 11 | @JoinColumn({ name: 'streamId' }) 12 | public streamId!: string; 13 | 14 | @Column('int') 15 | public version!: number; 16 | 17 | @Column('jsonb') 18 | public data!: unknown; 19 | 20 | @Column('text') 21 | public type!: string; 22 | 23 | @Column('jsonb') 24 | public meta!: unknown; 25 | 26 | @CreateDateColumn() 27 | public logDate!: number; 28 | } 29 | -------------------------------------------------------------------------------- /packages/microservice-profile/business/command/handler/createProfile.handler.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 3 | 4 | import { CreateProfileCommand } from '@shitake/microservice-profile/business/command'; 5 | import { Profile } from '@shitake/microservice-profile/domain/model'; 6 | 7 | @CommandHandler(CreateProfileCommand) 8 | export class CreateProfileHandler implements ICommandHandler { 9 | public constructor(private readonly publisher: EventPublisher) {} 10 | 11 | public async execute(command: CreateProfileCommand) { 12 | const { userDto } = command; 13 | 14 | const user = this.publisher.mergeObjectContext(new Profile(uuid())); 15 | user.create(userDto); 16 | user.commit(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/storage-eventstore/eventstore.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | 4 | import { TypeormLogger } from '@shitake/utils-logger/typeorm.logger'; 5 | 6 | import { Event } from './entity/event.entity'; 7 | import { Stream } from './entity/stream.entity'; 8 | import { EventstoreService } from './eventstore.service'; 9 | 10 | @Global() 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forRoot({ 14 | name: 'eventConnection', 15 | type: 'postgres', 16 | url: 'postgres://shitake:password@localhost/events', 17 | entities: [Event, Stream], 18 | synchronize: true, 19 | logging: true, 20 | logger: new TypeormLogger(), 21 | }), 22 | ], 23 | providers: [EventstoreService], 24 | exports: [EventstoreService], 25 | }) 26 | export class EventstoreModule {} 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | "@typescript-eslint/no-parameter-properties": "off", 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/handler/doesEmailAlreadyExist.handler.ts: -------------------------------------------------------------------------------- 1 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 2 | import { InjectEntityManager } from '@nestjs/typeorm'; 3 | import { EntityManager } from 'typeorm'; 4 | 5 | import { AccountEntity } from '@shitake/microservice-auth/infrastructure'; 6 | import { DoesEmailExistQuery } from '@shitake/microservice-auth/business/query'; 7 | 8 | @QueryHandler(DoesEmailExistQuery) 9 | export class DoesEmailExistHandler implements IQueryHandler { 10 | public constructor( 11 | @InjectEntityManager('accountConnection') 12 | private readonly entityManager: EntityManager, 13 | ) {} 14 | 15 | public async execute(query: DoesEmailExistQuery) { 16 | const accountRepo = this.entityManager.getRepository(AccountEntity); 17 | return (await accountRepo.count({ email: query.email })) > 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-gateway/module.ts: -------------------------------------------------------------------------------- 1 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; 2 | 3 | import { EventstoreModule } from '@shitake/storage-eventstore/eventstore.module'; 4 | import { AuthModule } from '@shitake/microservice-auth'; 5 | import { ProfileModule } from '@shitake/microservice-profile'; 6 | 7 | import { AuthGatewayController } from './controllers/auth.controller'; 8 | import { ProfileGatewayController } from './controllers/profile.controller'; 9 | 10 | import { RequestIdMiddleware } from './middlewares/reqId.middleware'; 11 | 12 | @Module({ 13 | imports: [AuthModule, EventstoreModule, ProfileModule], 14 | controllers: [AuthGatewayController, ProfileGatewayController], 15 | }) 16 | export class ApplicationModule implements NestModule { 17 | public configure(consumer: MiddlewareConsumer): void { 18 | consumer.apply(RequestIdMiddleware).forRoutes('*'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | webpack: (config, options, webpack) => { 6 | config.entry.main = ['./index.ts']; 7 | 8 | config.resolve = { 9 | extensions: ['.ts', '.js', '.json', '.proto'], 10 | }; 11 | 12 | config.externals = [ 13 | nodeExternals({ 14 | whitelist: ['webpack/hot/poll?100', /@shitake/], 15 | }), 16 | ]; 17 | 18 | config.module.rules.push({ 19 | test: /\.ts$/, 20 | loader: 'awesome-typescript-loader', 21 | }); 22 | config.module.rules.push({ test: /.proto$/, use: 'cop' }); 23 | 24 | config.plugins.push(new CopyPlugin([{ from: 'packages/**/*.proto', to: '[name].proto' }])); 25 | 26 | return config; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/api/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package auth; 4 | 5 | import "google/protobuf/any.proto"; 6 | 7 | // service Query {} 8 | 9 | service Command { 10 | rpc Register(AuthCredentialsDto) returns (AccountRef) {} 11 | rpc Login(AuthCredentialsDto) returns (TokensRef) {} 12 | } 13 | 14 | message AuthCredentialsDto { 15 | string email = 1; 16 | string password = 2; 17 | } 18 | 19 | message AccountRef { 20 | message Status { 21 | int32 code = 1; 22 | string message = 2; 23 | repeated google.protobuf.Any details =3; 24 | } 25 | message Data { 26 | string id = 1; 27 | } 28 | Status status = 1; 29 | Data data = 2; 30 | } 31 | 32 | message TokensRef { 33 | message Status { 34 | int32 code = 1; 35 | string message = 2; 36 | repeated google.protobuf.Any details =3; 37 | } 38 | message Data { 39 | string id = 1; 40 | string authToken = 2; 41 | string refreshToken = 3; 42 | } 43 | Status status = 1; 44 | Data data = 2; 45 | } 46 | 47 | message Empty {} -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/handler/registerAccount.handler.ts: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | import argon2 from 'argon2'; 3 | 4 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 5 | 6 | import { RegisterAccountCommand } from '@shitake/microservice-auth/business/command'; 7 | import { Account } from '@shitake/microservice-auth/domain/model'; 8 | 9 | @CommandHandler(RegisterAccountCommand) 10 | export class RegisterAccountHandler implements ICommandHandler { 11 | public constructor(private readonly publisher: EventPublisher) {} 12 | 13 | public async execute(command: RegisterAccountCommand) { 14 | const { 15 | authCredentialsDto: { email, password }, 16 | } = command; 17 | 18 | const hashedPassword = await argon2.hash(password); 19 | const accountId = uuid.v4(); 20 | 21 | const user = this.publisher.mergeObjectContext(new Account(accountId)); 22 | user.register({ email, hashedPassword }); 23 | user.commit(); 24 | 25 | return accountId; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2019-current Tycho Tatitscheff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/microservice-auth/infrastructure/api/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, UsePipes, ValidationPipe, UseFilters } from '@nestjs/common'; 2 | import { GrpcMethod } from '@nestjs/microservices'; 3 | 4 | import { AuthService } from '@shitake/microservice-auth/business/auth.service'; 5 | import { AuthClearTextCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 6 | 7 | import { formatGrpcResponse } from '@shitake/utils-grpc/formatGrpcResponse'; 8 | 9 | @Controller() 10 | export class AuthController { 11 | public constructor(private readonly authService: AuthService) {} 12 | 13 | @GrpcMethod('Command') 14 | public async register(authCredentialsDto: AuthClearTextCredentialsDto) { 15 | const serviceFn = this.authService.register.bind(this.authService); 16 | return formatGrpcResponse(serviceFn, [authCredentialsDto]); 17 | } 18 | 19 | @GrpcMethod('Command') 20 | public async login(authCredentialsDto: AuthClearTextCredentialsDto) { 21 | const serviceFn = this.authService.login.bind(this.authService); 22 | return formatGrpcResponse(serviceFn, [authCredentialsDto]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shitake", 3 | "version": "1.0.0", 4 | "description": "cqrs test", 5 | "main": "index.ts", 6 | "author": "TychoTa", 7 | "license": "MIT", 8 | "private": true, 9 | "dependencies": { 10 | "@grpc/proto-loader": "^0.5.0", 11 | "@nestjs/common": "^6.0.5", 12 | "@nestjs/core": "^6.0.5", 13 | "@nestjs/cqrs": "^6.0.0", 14 | "@nestjs/microservices": "^6.0.5", 15 | "@nestjs/platform-express": "^6.1.0", 16 | "@nestjs/swagger": "^3.0.2", 17 | "@nestjs/typeorm": "^6.0.0", 18 | "grpc": "^1.19.0", 19 | "reflect-metadata": "0.1.12", 20 | "rxjs": "^6.4.0", 21 | "pg": "^7.9.0", 22 | "typeorm": "^0.2.16" 23 | }, 24 | "devDependencies": { 25 | "@typescript-eslint/eslint-plugin": "^1.6.0", 26 | "@typescript-eslint/parser": "^1.6.0", 27 | "awesome-typescript-loader": "^5.2.1", 28 | "backpack-core": "^0.8.3", 29 | "copy-webpack-plugin": "^5.0.2", 30 | "eslint": "^5.16.0", 31 | "eslint-config-prettier": "^4.1.0", 32 | "eslint-plugin-prettier": "^3.0.1", 33 | "prettier": "^1.16.4", 34 | "typescript": "^3.4.1", 35 | "webpack": "^4.29.6" 36 | }, 37 | "workspaces": [ 38 | "packages/**" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as Joi from 'joi'; 3 | import * as fs from 'fs'; 4 | 5 | export interface EnvConfig { 6 | [key: string]: string; 7 | } 8 | 9 | export class ConfigService { 10 | private readonly envConfig: EnvConfig; 11 | 12 | public constructor(filePath: string) { 13 | const config = dotenv.parse(fs.readFileSync(filePath)); 14 | this.envConfig = this.validateInput(config); 15 | } 16 | 17 | /** 18 | * Ensures all needed variables are set, and returns the validated JavaScript object 19 | * including the applied default values. 20 | */ 21 | private validateInput(envConfig: EnvConfig): EnvConfig { 22 | const envVarsSchema: Joi.ObjectSchema = Joi.object({ 23 | NODE_ENV: Joi.string() 24 | .valid(['development', 'production', 'test', 'provision']) 25 | .default('development'), 26 | PORT: Joi.number().default(3000), 27 | API_AUTH_ENABLED: Joi.boolean().required(), 28 | }); 29 | 30 | const { error, value: validatedEnvConfig } = Joi.validate(envConfig, envVarsSchema); 31 | if (error) { 32 | throw new Error(`Config validation error: ${error.message}`); 33 | } 34 | return validatedEnvConfig; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/storage-eventstore/eventstore.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectConnection, InjectEntityManager } from '@nestjs/typeorm'; 3 | import { Connection, EntityManager } from 'typeorm'; 4 | 5 | import { Event } from './entity/event.entity'; 6 | import { Stream } from './entity/stream.entity'; 7 | 8 | @Injectable() 9 | export class EventstoreService { 10 | public constructor( 11 | @InjectEntityManager('eventConnection') 12 | private readonly entityManager: EntityManager, 13 | ) {} 14 | 15 | public async createEvent( 16 | streamId: string, 17 | streamType: string, 18 | eventData: D, 19 | eventType: string, 20 | eventMetadata: M, 21 | ) { 22 | const streamRepo = this.entityManager.getRepository('stream'); 23 | let stream; 24 | try { 25 | stream = await streamRepo.findOneOrFail(streamId); 26 | } catch (e) { 27 | stream = await streamRepo.save({ streamId, version: 0, type: streamType }); 28 | } 29 | 30 | const eventRepo = this.entityManager.getRepository('event'); 31 | await eventRepo.save({ 32 | data: eventData, 33 | type: eventType, 34 | meta: eventMetadata, 35 | version: stream.version + 1, 36 | streamId: stream.streamId, 37 | }); 38 | 39 | await streamRepo.update({ streamId: stream.streamId }, { ...stream, version: stream.version + 1 }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/api-gateway/index.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 3 | import helmet from 'helmet'; 4 | import rateLimit from 'express-rate-limit'; 5 | 6 | import { NestLogger } from '@shitake/utils-logger/nest.logger'; 7 | 8 | import { ApplicationModule } from './module'; 9 | 10 | declare const module: unknown; 11 | 12 | export async function bootstrap(): Promise { 13 | const app = await NestFactory.create(ApplicationModule, { logger: new NestLogger() }); 14 | 15 | const { profileClientOptions } = await import('@shitake/microservice-profile/infrastructure'); 16 | const { authClientOptions } = await import('@shitake/microservice-auth/infrastructure'); 17 | 18 | app.connectMicroservice(profileClientOptions); 19 | app.connectMicroservice(authClientOptions); 20 | 21 | app.startAllMicroservices(); 22 | 23 | app.use(helmet()); 24 | app.use( 25 | new rateLimit({ 26 | windowMs: 0.5 * 60 * 1000, // 30sec 27 | max: 100, // limit each IP to 300 requests per windowMs 28 | }), 29 | ); 30 | 31 | const options = new DocumentBuilder() 32 | .setTitle('Shitake') 33 | .setDescription('A CRQS Test') 34 | .setVersion('1.0') 35 | .addBearerAuth() 36 | .build(); 37 | 38 | const document = SwaggerModule.createDocument(app, options); 39 | SwaggerModule.setup('api', app, document); 40 | 41 | await app.listen(3001); 42 | } 43 | -------------------------------------------------------------------------------- /packages/microservice-auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | 5 | import { AuthController } from './infrastructure/api/auth.controller'; 6 | import { AuthService } from './business/auth.service'; 7 | import { AuthCommandHandlers } from './business/command/handler'; 8 | import { AuthEventHandlers } from './domain/event/handler'; 9 | import { AuthSagas } from './business/sagas/updateQuery.saga'; 10 | import { AuthQueryHandlers } from './business/query/handler'; 11 | import { AccountEntity } from './infrastructure/persistance/entity/account.entity'; 12 | import { RefreshTokenEntity } from './infrastructure/persistance/entity/refreshToken.entity'; 13 | 14 | import { EventstoreService } from '@shitake/storage-eventstore/eventstore.service'; 15 | import { TypeormLogger } from '@shitake/utils-logger/typeorm.logger'; 16 | 17 | @Module({ 18 | imports: [ 19 | CqrsModule, 20 | TypeOrmModule.forRoot({ 21 | name: 'accountConnection', 22 | type: 'postgres', 23 | url: 'postgres://shitake:password@localhost/accounts', 24 | entities: [AccountEntity, RefreshTokenEntity], 25 | synchronize: true, 26 | logging: true, 27 | logger: new TypeormLogger(), 28 | }), 29 | ], 30 | controllers: [AuthController], 31 | providers: [ 32 | AuthService, 33 | ...AuthCommandHandlers, 34 | ...AuthEventHandlers, 35 | ...AuthQueryHandlers, 36 | AuthSagas, 37 | EventstoreService, 38 | ], 39 | }) 40 | export class AuthModule {} 41 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/query/handler/getUserIdAfterValidation.handler.ts: -------------------------------------------------------------------------------- 1 | import argon2 from 'argon2'; 2 | 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 4 | import { InjectEntityManager } from '@nestjs/typeorm'; 5 | import { EntityManager } from 'typeorm'; 6 | 7 | import { AccountEntity } from '@shitake/microservice-auth/infrastructure'; 8 | import { GetUserIdAfterValidationQuery } from '@shitake/microservice-auth/business/query'; 9 | 10 | import { GrpcPermissionDeniedException } from '@shitake//utils-grpc/exception'; 11 | 12 | @QueryHandler(GetUserIdAfterValidationQuery) 13 | export class GetUserIdAfterValidationHandler implements IQueryHandler { 14 | public constructor( 15 | @InjectEntityManager('accountConnection') 16 | private readonly entityManager: EntityManager, 17 | ) {} 18 | 19 | public async execute(query: GetUserIdAfterValidationQuery) { 20 | const accountRepo = this.entityManager.getRepository(AccountEntity); 21 | 22 | const accountThatMatchedEmail = await accountRepo.findOne({ email: query.authCredentialsDto.email }); 23 | if (!accountThatMatchedEmail) { 24 | throw new GrpcPermissionDeniedException('Email or password is incorrect.'); 25 | } 26 | 27 | const hashedPassword = accountThatMatchedEmail.hashedPassword; 28 | const passwordValid = await argon2.verify(hashedPassword, query.authCredentialsDto.password); 29 | if (!passwordValid) { 30 | throw new GrpcPermissionDeniedException('Email or password is incorrect.'); 31 | } 32 | 33 | return accountThatMatchedEmail.id; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/utils-logger/typeorm.logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, QueryRunner } from 'typeorm'; 2 | import pino from 'pino'; 3 | 4 | const log = pino().child({ context: 'TypeOrm' }); 5 | 6 | export class TypeormLogger implements Logger { 7 | public logQuery(query: string, parameters?: any[] | undefined, queryRunner?: QueryRunner | undefined) { 8 | log.debug({ parameters }, query); 9 | } 10 | public logQueryError( 11 | error: string, 12 | query: string, 13 | parameters?: any[] | undefined, 14 | queryRunner?: QueryRunner | undefined, 15 | ) { 16 | log.error({ query, parameters }, error); 17 | } 18 | public logQuerySlow( 19 | time: number, 20 | query: string, 21 | parameters?: any[] | undefined, 22 | queryRunner?: QueryRunner | undefined, 23 | ) { 24 | log.warn({ query, parameters, time }, time.toString()); 25 | } 26 | public logSchemaBuild(message: string, queryRunner?: QueryRunner | undefined) { 27 | log.error({ message }, message); 28 | } 29 | public logMigration(message: string, queryRunner?: QueryRunner | undefined) { 30 | log.error({ message }, message); 31 | } 32 | public log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner | undefined) { 33 | let logFn; 34 | switch (level) { 35 | case 'log': 36 | logFn = log.debug; 37 | break; 38 | case 'info': 39 | logFn = log.info; 40 | break; 41 | case 'warn': 42 | logFn = log.warn; 43 | break; 44 | } 45 | log.error({ message }, message); 46 | } 47 | 48 | // implement all methods from logger class 49 | } 50 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/sagas/updateQuery.saga.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Saga, ofType } from '@nestjs/cqrs'; 3 | import { InjectEntityManager } from '@nestjs/typeorm'; 4 | import { EntityManager } from 'typeorm'; 5 | 6 | import { Observable } from 'rxjs'; 7 | import { map } from 'rxjs/operators'; 8 | 9 | import { AccountRegistredEvent, LoggedInEvent } from '@shitake/microservice-auth/domain/event'; 10 | import { AccountEntity } from '@shitake/microservice-auth/infrastructure'; 11 | import { RefreshTokenEntity } from '../../infrastructure/persistance/entity/refreshToken.entity'; 12 | 13 | @Injectable() 14 | export class AuthSagas { 15 | public constructor( 16 | @InjectEntityManager('accountConnection') 17 | private readonly entityManager: EntityManager, 18 | ) {} 19 | 20 | @Saga() 21 | public accountRegistred = (events$: Observable): Observable => { 22 | return events$.pipe( 23 | ofType(AccountRegistredEvent), 24 | map(event => { 25 | const accountRepo = this.entityManager.getRepository(AccountEntity); 26 | accountRepo.save({ id: event.uuid, email: event.data.email, hashedPassword: event.data.hashedPassword }); 27 | }), 28 | ); 29 | }; 30 | 31 | @Saga() 32 | public loggedIn = (events$: Observable): Observable => { 33 | return events$.pipe( 34 | ofType(LoggedInEvent), 35 | map(event => { 36 | const refreshTokenRepo = this.entityManager.getRepository(RefreshTokenEntity); 37 | refreshTokenRepo.save({ id: event.uuid, refreshToken: event.refreshToken }); 38 | }), 39 | ); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CommandBus, QueryBus } from '@nestjs/cqrs'; 3 | 4 | import { AuthClearTextCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 5 | import { RegisterAccountCommand, LoginCommand } from '@shitake/microservice-auth/business/command'; 6 | import { DoesEmailExistQuery, GetUserIdAfterValidationQuery } from '@shitake/microservice-auth/business/query/'; 7 | 8 | import { GrpcAlreadyExistException } from '@shitake/utils-grpc/exception'; 9 | 10 | @Injectable() 11 | export class AuthService { 12 | public constructor(private readonly commandBus: CommandBus, private readonly queryBus: QueryBus) {} 13 | 14 | public async register(authCredentialsDto: AuthClearTextCredentialsDto) { 15 | await this.assertEmailDoesNotExist(authCredentialsDto.email); 16 | const id = await this.commandBus.execute(new RegisterAccountCommand(authCredentialsDto)); 17 | return { id }; 18 | } 19 | 20 | public async login(authCredentialsDto: AuthClearTextCredentialsDto) { 21 | const id = await this.queryBus.execute( 22 | new GetUserIdAfterValidationQuery(authCredentialsDto), 23 | ); 24 | const { authToken, refreshToken } = await this.commandBus.execute(new LoginCommand(id)); 25 | return { authToken, refreshToken, id }; 26 | } 27 | 28 | public async assertEmailDoesNotExist(email: string) { 29 | const emailAlreadyExist = await this.queryBus.execute(new DoesEmailExistQuery(email)); 30 | if (emailAlreadyExist) { 31 | throw new GrpcAlreadyExistException('Email already exist'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/microservice-auth/business/command/handler/login.handler.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import jsonwebtoken from 'jsonwebtoken'; 3 | 4 | import { InjectEntityManager } from '@nestjs/typeorm'; 5 | import { EntityManager } from 'typeorm'; 6 | 7 | import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; 8 | 9 | import { LoginCommand } from '@shitake/microservice-auth/business/command'; 10 | import { Account } from '@shitake/microservice-auth/domain/model'; 11 | import { AccountEntity } from '@shitake/microservice-auth/infrastructure'; 12 | 13 | @CommandHandler(LoginCommand) 14 | export class LoginCommandHandler implements ICommandHandler { 15 | public constructor( 16 | private readonly publisher: EventPublisher, 17 | @InjectEntityManager('accountConnection') 18 | private readonly entityManager: EntityManager, 19 | ) {} 20 | 21 | public async execute(command: LoginCommand) { 22 | const { accountId } = command; 23 | 24 | const accountRepo = this.entityManager.getRepository(AccountEntity); 25 | const account = await accountRepo.findOneOrFail(accountId); 26 | 27 | // https://security.stackexchange.com/questions/41743/how-many-bytes-should-an-authorization-token-have 28 | const refreshToken = crypto.randomBytes(25).toString('base64'); 29 | 30 | const user = this.publisher.mergeObjectContext(new Account(account.id)); 31 | user.login(refreshToken); 32 | user.commit(); 33 | 34 | const result = { 35 | refreshToken, 36 | // Todo change the secret 37 | authToken: jsonwebtoken.sign({ id: accountId }, 'SECRET', { notBefore: '-10s', expiresIn: '10min' }), 38 | id: accountId, 39 | }; 40 | 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/api-gateway/controllers/profile.controller.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | import { Controller, OnModuleInit, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common'; 4 | import { ClientGrpc, Client } from '@nestjs/microservices'; 5 | 6 | import { profileClientOptions } from '@shitake/microservice-profile/infrastructure/'; 7 | import { ProfileDto } from '@shitake/microservice-profile/domain/dto'; 8 | import { 9 | ApiUseTags, 10 | ApiBearerAuth, 11 | ApiCreatedResponse, 12 | ApiBadRequestResponse, 13 | ApiUnauthorizedResponse, 14 | ApiForbiddenResponse, 15 | } from '@nestjs/swagger'; 16 | import { formatHttpResponse } from '@shitake/utils-grpc/formatHttpResponse'; 17 | import { GrpcAnswer } from '@shitake/utils-grpc/types'; 18 | 19 | import { ControllerResponse } from '../types'; 20 | 21 | interface ProfileCommand { 22 | createProfile(dto: ProfileDto): Observable; 23 | } 24 | 25 | @ApiUseTags('profile') 26 | @ApiBearerAuth() 27 | @Controller('profile') 28 | export class ProfileGatewayController implements OnModuleInit { 29 | @Client(profileClientOptions) 30 | private readonly profileClient!: ClientGrpc; 31 | 32 | private profileCommand!: ProfileCommand; 33 | 34 | public onModuleInit(): void { 35 | this.profileCommand = this.profileClient.getService('Command'); 36 | } 37 | 38 | @ApiCreatedResponse({ description: 'The account has been successfully created.' }) 39 | @ApiBadRequestResponse({ description: 'The format of the body is incorrect. We need an email and a password.' }) 40 | @ApiUnauthorizedResponse({ description: 'You are not logged.' }) 41 | @ApiForbiddenResponse({ description: 'You are not authorized to create a profile.' }) 42 | @ApiBearerAuth() 43 | @Post() 44 | @UsePipes(new ValidationPipe({ transform: true })) 45 | public async createProfile(@Body() dto: ProfileDto): ControllerResponse { 46 | const grpcAnswer = await this.profileCommand.createProfile(dto).toPromise(); 47 | return formatHttpResponse(grpcAnswer); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/utils-grpc/mapping.ts: -------------------------------------------------------------------------------- 1 | import grpc from 'grpc'; 2 | import { 3 | HttpException, 4 | InternalServerErrorException, 5 | HttpStatus, 6 | BadRequestException, 7 | GatewayTimeoutException, 8 | NotFoundException, 9 | ConflictException, 10 | UnauthorizedException, 11 | ForbiddenException, 12 | ServiceUnavailableException, 13 | } from '@nestjs/common'; 14 | import { isObject } from 'util'; 15 | 16 | const createHttpExceptionBody = (message: object | string, error?: string, statusCode?: number) => { 17 | if (!message) { 18 | return { statusCode, error }; 19 | } 20 | return isObject(message) && !Array.isArray(message) ? message : { statusCode, error, message }; 21 | }; 22 | 23 | const createHttpException = (status: number, defaultError: string = '') => { 24 | class CustomHttpException extends HttpException { 25 | public constructor(message?: string | object | any, error = defaultError) { 26 | super(createHttpExceptionBody(message, error, status), status); 27 | } 28 | } 29 | return CustomHttpException; 30 | }; 31 | 32 | export const GrpcToHttpExceptionMapping = { 33 | [grpc.status.OK]: null, 34 | [grpc.status.CANCELLED]: createHttpException(499, 'Client Closed Request'), 35 | [grpc.status.UNKNOWN]: InternalServerErrorException, 36 | [grpc.status.INVALID_ARGUMENT]: BadRequestException, 37 | [grpc.status.DEADLINE_EXCEEDED]: GatewayTimeoutException, 38 | [grpc.status.NOT_FOUND]: NotFoundException, 39 | [grpc.status.ALREADY_EXISTS]: ConflictException, 40 | [grpc.status.PERMISSION_DENIED]: ForbiddenException, 41 | [grpc.status.UNAUTHENTICATED]: UnauthorizedException, 42 | [grpc.status.RESOURCE_EXHAUSTED]: createHttpException(HttpStatus.TOO_MANY_REQUESTS, 'Too Many Request'), 43 | [grpc.status.FAILED_PRECONDITION]: BadRequestException, 44 | [grpc.status.DEADLINE_EXCEEDED]: GatewayTimeoutException, 45 | [grpc.status.ABORTED]: ConflictException, 46 | [grpc.status.OUT_OF_RANGE]: BadRequestException, 47 | [grpc.status.UNIMPLEMENTED]: createHttpException(501, 'Not Implemented'), 48 | [grpc.status.INTERNAL]: InternalServerErrorException, 49 | [grpc.status.UNAVAILABLE]: ServiceUnavailableException, 50 | [grpc.status.DATA_LOSS]: InternalServerErrorException, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/api-gateway/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | import { Controller, OnModuleInit, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common'; 4 | import { 5 | ApiUseTags, 6 | ApiCreatedResponse, 7 | ApiBadRequestResponse, 8 | ApiConflictResponse, 9 | ApiUnauthorizedResponse, 10 | } from '@nestjs/swagger'; 11 | import { ClientGrpc, Client } from '@nestjs/microservices'; 12 | 13 | import { authClientOptions } from '@shitake/microservice-auth/infrastructure/'; 14 | import { AuthClearTextCredentialsDto } from '@shitake/microservice-auth/domain/dto'; 15 | import { GrpcAnswer } from '@shitake/utils-grpc/types'; 16 | import { formatHttpResponse } from '@shitake/utils-grpc/formatHttpResponse'; 17 | 18 | import { ControllerResponse } from '../types'; 19 | 20 | interface AuthCommand { 21 | register(dto: AuthClearTextCredentialsDto): Observable>; 22 | login(dto: AuthClearTextCredentialsDto): Observable; 23 | } 24 | 25 | @ApiUseTags('auth') 26 | @Controller('auth') 27 | export class AuthGatewayController implements OnModuleInit { 28 | @Client(authClientOptions) 29 | private readonly authClient!: ClientGrpc; 30 | 31 | private authCommand!: AuthCommand; 32 | 33 | public onModuleInit(): void { 34 | this.authCommand = this.authClient.getService('Command'); 35 | } 36 | 37 | @ApiCreatedResponse({ description: 'The account has been successfully created.' }) 38 | @ApiBadRequestResponse({ description: 'The format of the body is incorrect. We need a valid email and a password.' }) 39 | @ApiConflictResponse({ description: 'The email already exist. Try to login instead.' }) 40 | @Post('/register') 41 | @UsePipes(new ValidationPipe({ transform: true })) 42 | public async register(@Body() dto: AuthClearTextCredentialsDto): ControllerResponse { 43 | const grpcAnswer = await this.authCommand.register(dto).toPromise(); 44 | return formatHttpResponse(grpcAnswer); 45 | } 46 | 47 | @ApiCreatedResponse({ description: 'The account has been successfully created.' }) 48 | @ApiBadRequestResponse({ description: 'The format of the body is incorrect. We need a valid email and a password.' }) 49 | @ApiUnauthorizedResponse({ description: 'The email or the password is incorrect' }) 50 | @Post('/login') 51 | @UsePipes(new ValidationPipe({ transform: true })) 52 | public async login(@Body() dto: AuthClearTextCredentialsDto): ControllerResponse { 53 | const grpcAnswer = await this.authCommand.login(dto).toPromise(); 54 | return formatHttpResponse(grpcAnswer); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shitkake 2 | 3 | ## 🎉Context 4 | 5 | I work as an architect at [BAM](https://www.bam.tech/). 6 | 7 | I'm currently staffed as the architect for the frontend part on a dating app that use a lot of CQRS / hexagonal architecture for the backend. 8 | 9 | Last week, I had to estimate and sale a project that would need such an architecture. 10 | 11 | I realised that while I would do personally it either akka stream or elixir with commanded, no one in my company would be able to work on it (we are specialists of JS, and we do a bit of php/python). 12 | 13 | Thus I create this test project to battle test [Nest](https://docs.nestjs.com/) and standardise some good practices. 14 | 15 | **This repo is a demo.** 16 | It is **not used in production**, it will be updated a bit, documented a bit but won't be use in reality. 17 | 18 | **What will be used will be closed source** but may serve to update this repo. 19 | 20 | ## 🏆Goals 21 | 22 | - ✅testability 23 | - 🔒security 24 | - 📖readability 25 | - 🖼separation of concern 26 | - 🏆standardisation 27 | - 📈performance / scalability 28 | 29 | ## ⚠️When not use this 30 | 31 | Everytime, except if: 32 | 33 | - you need CQRS-ES 34 | - you need microservice 35 | - you need to do it in node 36 | - you love typescript 37 | - you are ok to read a repo with little to no doc (so far) 38 | 39 | ## 🛣Roadmap / starmap 40 | 41 | ### Planned for really near future 42 | 43 | - [ ] 12 factor config 44 | - [ ] AuthGuard with JWT 45 | - [ ] Unit Test 46 | 47 | ### Probably just after 48 | 49 | - [ ] E2E Test 50 | - [ ] Better linter 51 | - [ ] Task inter microservice: so far with Celery (python) and RabbitMQ 52 | - [ ] Document: 53 | - Why CQRS? Short term Pro? Short term Cons? Maintainability ? Links to learn more. 54 | - Why Onion architecture? Short term Pro? Short term Cons? Maintainability ? Links to learn more. 55 | - Why GRQC? Short term Pro? Short term Cons? Maintainability ? Links to learn more. 56 | - Why Celery/RabbitMQ? Short term Pro? Short term Cons? Maintainability ? Links to learn more. 57 | - How to create new µService/Query/Command ? Step by step. 58 | 59 | ### Possibly in the future 60 | 61 | - [ ] Deploying on kubernetes with helm 62 | - [ ] CI with docker 63 | - [ ] CD (green/blue deployment, [Terminus](https://docs.nestjs.com/recipes/terminus)) 64 | - [ ] Improve dev server to auto restart 65 | - [ ] Invest [citus](https://www.citusdata.com/) for eventStore scalling and/or snapshot from https://dev.to/kspeakman/event-storage-in-postgres-4dk2 66 | - [ ] analytics with InfluxDB/Graphana 67 | 68 | ### Unlikely but maybe 69 | 70 | - [ ] tracing 71 | - [ ] service mesh 72 | 73 | ### I would dream of it but lets be realistic 74 | 75 | - [ ] a celery like framework base on RabbitMQ 76 | 77 | ## 📜 Licence 78 | 79 | MIT 80 | 81 | If you like it, let's have a beer when you are in Paris. 82 | -------------------------------------------------------------------------------- /packages/utils-grpc/exception.ts: -------------------------------------------------------------------------------- 1 | import grpc from 'grpc'; 2 | 3 | export class GrpcException extends Error { 4 | public readonly message: string; 5 | public constructor(public readonly code: number, private readonly error: string | object) { 6 | super(); 7 | this.message = error.toString(); 8 | } 9 | public getError() { 10 | return this.error; 11 | } 12 | } 13 | 14 | export class GrpcCanceledException extends GrpcException { 15 | public constructor(error: string | object) { 16 | super(grpc.status.CANCELLED, error); 17 | } 18 | } 19 | 20 | export class GrpcUnkownException extends GrpcException { 21 | public constructor(error: string | object) { 22 | super(grpc.status.UNKNOWN, error); 23 | } 24 | } 25 | 26 | export class GrpcInvalidArgumentException extends GrpcException { 27 | public constructor(error: string | object) { 28 | super(grpc.status.INVALID_ARGUMENT, error); 29 | } 30 | } 31 | 32 | export class GrpcDeadlineExceededException extends GrpcException { 33 | public constructor(error: string | object) { 34 | super(grpc.status.DEADLINE_EXCEEDED, error); 35 | } 36 | } 37 | 38 | export class GrpcNotFoundException extends GrpcException { 39 | public constructor(error: string | object) { 40 | super(grpc.status.NOT_FOUND, error); 41 | } 42 | } 43 | 44 | export class GrpcAlreadyExistException extends GrpcException { 45 | public constructor(error: string | object) { 46 | super(grpc.status.ALREADY_EXISTS, error); 47 | } 48 | } 49 | 50 | export class GrpcPermissionDeniedException extends GrpcException { 51 | public constructor(error: string | object) { 52 | super(grpc.status.PERMISSION_DENIED, error); 53 | } 54 | } 55 | 56 | export class GrpcUnauthenticatedException extends GrpcException { 57 | public constructor(error: string | object) { 58 | super(grpc.status.UNAUTHENTICATED, error); 59 | } 60 | } 61 | 62 | export class GrpcRessourceExhaustedException extends GrpcException { 63 | public constructor(error: string | object) { 64 | super(grpc.status.RESOURCE_EXHAUSTED, error); 65 | } 66 | } 67 | 68 | export class GrpcFailedPreconditionException extends GrpcException { 69 | public constructor(error: string | object) { 70 | super(grpc.status.FAILED_PRECONDITION, error); 71 | } 72 | } 73 | 74 | export class GrpcAbortedException extends GrpcException { 75 | public constructor(error: string | object) { 76 | super(grpc.status.ABORTED, error); 77 | } 78 | } 79 | 80 | export class GrpcOutOfRangeException extends GrpcException { 81 | public constructor(error: string | object) { 82 | super(grpc.status.OUT_OF_RANGE, error); 83 | } 84 | } 85 | 86 | export class GrpcUnimplementedException extends GrpcException { 87 | public constructor(error: string | object) { 88 | super(grpc.status.UNIMPLEMENTED, error); 89 | } 90 | } 91 | 92 | export class GrpcInternalException extends GrpcException { 93 | public constructor(error: string | object) { 94 | super(grpc.status.CANCELLED, error); 95 | } 96 | } 97 | 98 | export class GrpcUnavailableException extends GrpcException { 99 | public constructor(error: string | object) { 100 | super(grpc.status.UNAVAILABLE, error); 101 | } 102 | } 103 | 104 | export class GrpcDataLossException extends GrpcException { 105 | public constructor(error: string | object) { 106 | super(grpc.status.DATA_LOSS, error); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Tycho Tatitscheff. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Introduction 4 | 5 | First off, thank you for considering contributing to Shitake. It's a small, new project but still you contribute to build something awesome. 6 | 7 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 8 | 9 | Shitake is an open source project and we love to receive contributions from our community — you! There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into Elasticsearch itself. 10 | 11 | ## Ground Rules 12 | 13 | Responsibilities 14 | 15 | - Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. 16 | - Keep feature versions as small as possible, preferably one new feature per version. 17 | - Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See the [Code of Conduct](./CODE_OF_CONDUCT.md). 18 | 19 | Unsure where to begin contributing to Atom? You can start by looking through these beginner and help-wanted issues: 20 | 21 | - Beginner issues - issues which should only require a few lines of code, and a test or two. 22 | - Help wanted issues - issues which should be a bit more involved than beginner issues. 23 | 24 | Working on your first Pull Request? You can learn how from this _free_ series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 25 | 26 | At this point, you're ready to make your changes! Feel free to ask for help; everyone is a beginner at first :smile_cat: 27 | 28 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your branch so it's easier to merge. 29 | 30 | ## Getting started 31 | 32 | For something that is bigger than a one or two line fix: 33 | 34 | 1. Create your own fork of the code 35 | 2. Do the changes in your fork 36 | 3. If you like the change and think the project could use it: 37 | - Be sure you have followed the code style for the project. 38 | - Note the Shitake Code of Conduct. 39 | 40 | ## How to report a bug 41 | 42 | If you find a security vulnerability, do NOT open an issue. Email tychot@bam.tech instead. 43 | 44 | In order to determine whether you are dealing with a security issue, ask yourself these two questions: 45 | 46 | - Can I access something that's not mine, or something I shouldn't have access to? 47 | - Can I disable something for other people? 48 | 49 | If the answer to either of those two questions are "yes", then you're probably dealing with a security issue. Note that even if you answer "no" to both questions, you may still be dealing with a security issue, so if you're unsure, just email us at tychot@bam.tech 50 | 51 | --- 52 | 53 | When filing an issue, make sure to answer these five questions: 54 | 55 | 1. What version of Go are you using (go version)? 56 | 2. What operating system and processor architecture are you using? 57 | 3. What did you do? 58 | 4. What did you expect to see? 59 | 5. What did you see instead? 60 | General questions should go to the golang-nuts mailing list instead of the issue tracker. The gophers there will answer or ask you to file an issue if you've tripped over a bug. 61 | 62 | ## How to suggest a feature or enhancement 63 | 64 | If you find yourself wishing for a feature that doesn't exist in Shitake, you are probably not alone. There are bound to be others out there with similar needs. Many of the features that Shitake has today have been added because our users saw the need. Open an issue on our issues list on GitHub which describes the feature you would like to see, why you need it, and how it should work. 65 | 66 | ## Code review process 67 | 68 | I will look at Pull Requests on a regular basis. 69 | 70 | After feedback has been given we expect responses within two weeks. After two weeks we may close the pull request if it isn't showing any activity. 71 | 72 | ## Code, commit message and labeling conventions 73 | 74 | ### Explain if you use any commit message conventions 75 | 76 | TODO 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": ["es6"] /* Specify library files to be included in the compilation. */, 7 | "allowJs": false /* Allow javascript files to be compiled. */, 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true /* Enable all strict type-checking options. */, 25 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | 49 | /* Source Map Options */ 50 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | 55 | /* Experimental Options */ 56 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 57 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 58 | } 59 | } 60 | --------------------------------------------------------------------------------