├── .gitignore ├── README.md ├── lesson1-functional └── index.ts ├── lesson2-oop └── index.ts ├── lesson3-projections-per-stream └── index.ts ├── lesson4-testing └── index.ts ├── lesson5-nestjs-mvc ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── main.ts │ └── purchase │ │ ├── projections │ │ ├── all.ts │ │ └── avg-cost-report.ts │ │ ├── purchase.controller.spec.ts │ │ ├── purchase.controller.ts │ │ └── types.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json ├── lesson6-nestjs-advanced ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── app.module.ts │ ├── app.service.ts │ ├── common │ │ ├── basic-types.ts │ │ ├── domain-event.ts │ │ └── event-store │ │ │ ├── event-store.service.spec.ts │ │ │ ├── event-store.service.ts │ │ │ └── meta.interface.ts │ ├── main.ts │ └── purchase │ │ ├── commands │ │ ├── make-purchase │ │ │ ├── make-purchase.command.ts │ │ │ └── make-purchase.handler.ts │ │ └── refund │ │ │ ├── refund-purchase-handler.ts │ │ │ └── refund-purchase.command.ts │ │ ├── domain-events.ts │ │ ├── projections │ │ ├── all.ts │ │ └── avg-cost-report.ts │ │ └── purchase.controller.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock ├── main.ts ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea 4 | *.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDD & Event Sourcing Basics In TypeScript 2 | 3 | To run the lessons first open the `main.ts` file for instructions on selecting a specific lesson to "run". 4 | 5 | To run the code in your terminal once a lesson(s) is selected: 6 | 7 | 1. `npm install` 8 | 2. `npm run run` 9 | 10 | ## About Lesson 5+ 11 | 12 | Lesson 5 is a stand-alone nestjs project. Take a look at the `./lesson5-nestjs-mvc/src/purchase` directory for the goodies. 13 | 14 | To run the sample web app, navigate to `./lesson5-nestjs-mvc` and run `npm install && npm run start`. 15 | 16 | There are two pages that use event sourcing: 17 | 18 | 1. `localhost:3000/purchase`: Shows how to use event sourcing to create a new purchase, display a list of existing purchases and refund specific purchases. 19 | 2. `localhost:3000/purchase/report`: Demonstrates how to use projections to generate reports based off of events in the store. 20 | 21 | ## TODOs 22 | 23 | - Projections using DI 24 | - Projections for single purchases cached vs. replayed when requested 25 | - Add event store persistance (to a file I suppose) + persist projections too 26 | - Add ability for application to replay projections on startup 27 | - Add support for optimistic concurrency? 28 | - Example using EventStoreDB? 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lesson1-functional/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Imagine we were building some logic for an POS system payment device. 3 | * We can use TypeScript's type aliasing to make many domain specific types explicit. 4 | * Note, for example, how some of the Domain Events contain the same data (or none at all other than ids), 5 | * yet the named type itself represents some domain concept that is not the same as the others that have the same "shape". 6 | * 7 | * This highlights how domain events are themselves not just data, but represent actual real-world "things". 8 | * By using a robust type system, they can be made explicit. 9 | * 10 | * Implications of this include code that reads much clearer, focusing on domain semantics vs. technical jargon. 11 | * 12 | * Note: Any code comments beginning with "#### Action ####" means it's code that you can uncomment to interact with 😊. 13 | */ 14 | 15 | // Utils 16 | import { v4 as uuid } from 'uuid' 17 | function now(): TimeStamp { 18 | return new Date().getTime(); 19 | } 20 | 21 | // Base types 22 | type UUID = string; 23 | type TimeStamp = number; 24 | type JSONString = string; 25 | type DomainEvent = { eventId: UUID, at: TimeStamp }; 26 | 27 | // Domain Events 28 | type PurchaseRequested = DomainEvent & { purchaseId: UUID, amount: number } 29 | type PurchaseSuccessful = DomainEvent & { purchaseId: UUID }; 30 | type PurchaseRefunded = DomainEvent & { purchaseId: UUID } 31 | 32 | // Primitive event store: By storing each event as a serialized JSON string we will 33 | // simulate having to serialize our events and send it to an event store over the network. 34 | let eventStore: JSONString[] = []; 35 | 36 | // ************* 37 | // Let's begin!! 38 | // ************* 39 | 40 | // Let's try to "make a purchase" and put it into our event store. 41 | const purchaseId = uuid(); 42 | const requested: PurchaseRequested = { eventId: uuid(), at: now(), purchaseId, amount: 100.00 }; 43 | const success: PurchaseSuccessful = { eventId: uuid(), at: now(), purchaseId }; 44 | 45 | eventStore.push(JSON.stringify(requested), JSON.stringify(success)); 46 | 47 | // But, there's a problem... 48 | // No information about the _type_ being serialized is stored! 49 | // How do we know what kind of event it is when deserializing? 50 | // 51 | // We'll need to store the type along with the event itself... let's try again! 52 | // npm package `ts-nameof` gives us a way to "get" the type (this is one of a few ways). 53 | // (It enables a new global function "nameof()") 54 | 55 | // Let's define a "Meta" type that will store both the event AND the type 😅 56 | type Meta = { event: DomainEvent, type: string } 57 | eventStore = []; 58 | 59 | // Some helpers! 60 | function append(event: DomainEvent, type: string) { 61 | eventStore.push(JSON.stringify({ event, type })); 62 | } 63 | function getAllEvents() { 64 | return eventStore 65 | .map(e => JSON.parse(e) as Meta); 66 | } 67 | function getEvents(type: string) { 68 | getAllEvents() 69 | .filter(e => e.type == type); 70 | } 71 | 72 | // Let's append the original 2 events that we were working with: 73 | append(requested, nameof()); 74 | append(success, nameof()); 75 | 76 | // #### Action #### 77 | // Take a look at the serialized results 78 | // ################ 79 | // console.log(eventStore); 80 | 81 | 82 | // Now, let's build a basic projection to show us the counts of certain events. 83 | class PurchaseCountsProjection { 84 | Requests: number = 0; 85 | Purchases: number = 0; 86 | 87 | // We'll pass in the `Meta` type so we can test against 88 | // the type in order to decide what effect that event 89 | // has against our projection. 90 | apply(events: Meta[]): void { 91 | for (const event of events) { 92 | switch (event.type) { 93 | case nameof(): 94 | this.Requests++; 95 | break; 96 | case nameof(): 97 | this.Purchases++; 98 | break; 99 | } 100 | } 101 | } 102 | } 103 | 104 | // Let's try it by making 2 more purchase requests: 105 | const requested2: PurchaseRequested = { eventId: uuid(), at: now(), purchaseId: uuid(), amount: 200.00 }; 106 | const requested3: PurchaseRequested = { eventId: uuid(), at: now(), purchaseId: uuid(), amount: 199.00 }; 107 | append(requested2, nameof()); 108 | append(requested3, nameof()); 109 | 110 | const projection = new PurchaseCountsProjection(); 111 | projection.apply(getAllEvents()); 112 | 113 | // #### Action #### 114 | // We should now have 3 requested purchases and 1 successful! Cool stuff! 115 | // 👇 116 | // ################ 117 | 118 | //console.log(projection); -------------------------------------------------------------------------------- /lesson2-oop/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Let's try to build something like what we had for the "functional.ts" examples, 3 | * but now using an OOP approach. 4 | * 5 | * We won't cover concepts introduced in the "functional.ts" examples, but will build upon them. 6 | */ 7 | 8 | // Utils 9 | import {v4 as uuid} from 'uuid' 10 | 11 | function now(): TimeStamp { 12 | return new Date().getTime(); 13 | } 14 | 15 | // Some extra types for better readability 16 | export type TimeStamp = number; 17 | export type UUID = string; 18 | 19 | // Some base classes, types 20 | export abstract class DomainEvent { 21 | EventId: UUID; 22 | At: TimeStamp; 23 | 24 | constructor() { 25 | this.EventId = uuid(); 26 | this.At = new Date().getTime(); 27 | } 28 | } 29 | 30 | export interface Meta { 31 | Event: DomainEvent; 32 | Type: string; 33 | } 34 | 35 | export class EventStore { 36 | private _log: string[]; 37 | 38 | constructor() { 39 | this._log = []; 40 | } 41 | 42 | append(event: DomainEvent, type: string) { 43 | this._log.push(JSON.stringify({Event: event, Type: type})); 44 | } 45 | 46 | getAll() { 47 | return this._log 48 | .map(e => JSON.parse(e) as Meta); 49 | } 50 | } 51 | 52 | // Let's build some domain events! 53 | export class PurchaseRequested extends DomainEvent { 54 | PurchaseId: UUID; 55 | Amount: number; 56 | 57 | constructor(id: UUID, amount: number) { 58 | super(); 59 | this.PurchaseId = id; 60 | this.Amount = amount; 61 | } 62 | } 63 | 64 | export class PurchaseSuccessful extends DomainEvent { 65 | PurchaseId: UUID; 66 | 67 | constructor(id: UUID) { 68 | super(); 69 | this.PurchaseId = id; 70 | } 71 | } 72 | 73 | export class PurchaseRefunded extends DomainEvent { 74 | PurchaseId: UUID; 75 | 76 | constructor(id: UUID) { 77 | super(); 78 | this.PurchaseId = id; 79 | } 80 | } 81 | 82 | // ************* 83 | // Let's try use-case to demonstrate how this works. 84 | // ************* 85 | 86 | let store = new EventStore(); 87 | 88 | // Let's try to make a purchase that's successful. 89 | const id = uuid(); 90 | 91 | store.append(new PurchaseRequested(id, 199.00), nameof()); 92 | store.append(new PurchaseSuccessful(id), nameof()); 93 | 94 | 95 | // #### Action #### 96 | // Uncomment to see results 👇 97 | // ################ 98 | 99 | //console.log(store); 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /lesson3-projections-per-stream/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In event sourcing, an event store (the log) isn't just one massive log (logically). 3 | * Often, we want to group certain sets of events together. 4 | * 5 | * For example, all events associated with the same purchase (PurchaseRequested, PurchaseSuccessful, etc.) 6 | * are all "about" the same thing -> the same purchase. 7 | * 8 | * We would store all events that are "about" the same domain entity or process _a stream_. 9 | * In DDD terms, an aggregate is associated to one stream. 10 | * The stream is identified by some unique Id that is common across all events for that stream. 11 | * Ex. a purchase Id. 12 | * 13 | * Let's look at processing some events and creating a projection per stream! 14 | */ 15 | 16 | import { v4 as uuid } from 'uuid'; 17 | import {DomainEvent, Meta, PurchaseRefunded, PurchaseRequested, PurchaseSuccessful, UUID} from "../lesson2-oop"; 18 | 19 | // Event store with stream support 20 | export interface MetaWithStream extends Meta { 21 | StreamId: UUID; 22 | } 23 | 24 | export class EventStoreWithStreams { 25 | private _log: string[]; 26 | 27 | constructor() { 28 | this._log = []; 29 | } 30 | 31 | append(streamId: UUID, event: DomainEvent, type: string): void { 32 | const meta: MetaWithStream = {StreamId: streamId, Event: event, Type: type} 33 | this._log.push(JSON.stringify(meta)); 34 | } 35 | 36 | getAll(): MetaWithStream[] { 37 | return this._log 38 | .map(e => JSON.parse(e) as MetaWithStream); 39 | } 40 | 41 | getForStream(streamId: UUID): MetaWithStream[] { 42 | return this.getAll() 43 | .filter(meta => meta.StreamId === streamId); 44 | } 45 | } 46 | 47 | // Let's begin!! 48 | const store = new EventStoreWithStreams(); 49 | 50 | // 1. Successful purchase 51 | const purchaseId = uuid(); 52 | store.append(purchaseId, new PurchaseRequested(purchaseId, 199.00), nameof()); 53 | store.append(purchaseId, new PurchaseSuccessful(purchaseId), nameof()); 54 | 55 | // 2. Refunded purchase 56 | const purchaseId2 = uuid(); 57 | store.append(purchaseId2, new PurchaseRequested(purchaseId2, 50.00), nameof()); 58 | store.append(purchaseId2, new PurchaseSuccessful(purchaseId2), nameof()); 59 | store.append(purchaseId2, new PurchaseRefunded(purchaseId2), nameof()); 60 | 61 | // 3. Purchase currently "in-flight" (being requested) 62 | const purchaseId3 = uuid(); 63 | store.append(purchaseId3, new PurchaseRequested(purchaseId3, 500.99), nameof()); 64 | 65 | 66 | // Here's a projection to show us a general view of a purchase. 67 | export class PurchaseOverviewProjection { 68 | Id: UUID = ""; 69 | PurchaseAmount: number = 0; 70 | PurchasedAt: Date = new Date(); 71 | WasRefunded: boolean = false; 72 | IsRequestInFlight: boolean = false; 73 | 74 | apply(events: Meta[]){ 75 | for(const meta of events) { 76 | switch(meta.Type) { 77 | case nameof(): 78 | this.applyPurchaseRequested(meta.Event as PurchaseRequested); 79 | break; 80 | case nameof(): 81 | this.applyPurchaseSuccessful(meta.Event as PurchaseSuccessful); 82 | break; 83 | case nameof(): 84 | this.applyPurchaseRefunded(meta.Event as PurchaseRefunded); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | private applyPurchaseRequested(event: PurchaseRequested) { 91 | this.Id = event.PurchaseId; 92 | this.PurchaseAmount = event.Amount; 93 | this.PurchasedAt = new Date(event.At); 94 | this.IsRequestInFlight = true; 95 | } 96 | 97 | private applyPurchaseSuccessful(event: PurchaseSuccessful) { 98 | this.IsRequestInFlight = false; 99 | } 100 | 101 | private applyPurchaseRefunded(event: PurchaseRefunded) { 102 | this.WasRefunded = true; 103 | } 104 | } 105 | 106 | // Let's run the projection for each of our purchase "streams" 107 | const projection1 = new PurchaseOverviewProjection(); 108 | const projection2 = new PurchaseOverviewProjection(); 109 | const projection3 = new PurchaseOverviewProjection(); 110 | 111 | projection1.apply(store.getForStream(purchaseId)); 112 | projection2.apply(store.getForStream(purchaseId2)); 113 | projection3.apply(store.getForStream(purchaseId3)); 114 | 115 | // #### Action #### 116 | // See the results here 117 | // ################ 118 | 119 | // console.log(`Purchase 1:`) 120 | // console.log(projection1) 121 | // console.log("------------------------") 122 | // console.log(`Purchase 2:`) 123 | // console.log(projection2) 124 | // console.log("------------------------") 125 | // console.log(`Purchase 3:`) 126 | // console.log(projection3) 127 | // console.log("------------------------") 128 | -------------------------------------------------------------------------------- /lesson4-testing/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * How do we test event sourced systems? 3 | * 4 | * It's actually somewhat straightforward - and this technique exposes how focusing 5 | * on the domain events rather than state (as our industry is so often doing) 6 | * makes our software easier to read, work with from a domain semantics POV, to test, etc. 7 | * 8 | * We'll use the projection we created in the previous lesson for testing ;) 9 | * 10 | */ 11 | 12 | import {v4 as uuid} from "uuid"; 13 | import {EventStoreWithStreams, PurchaseOverviewProjection} from "../lesson3-projections-per-stream"; 14 | import {PurchaseRefunded, PurchaseRequested, PurchaseSuccessful, UUID} from "../lesson2-oop"; 15 | 16 | // Helpers 17 | const fillProjection = (streamId: UUID, store: EventStoreWithStreams) => { 18 | const projection = new PurchaseOverviewProjection(); 19 | projection.apply(store.getForStream(streamId)); 20 | return projection; 21 | } 22 | 23 | // Tests 24 | 25 | const Successful_purchase_should_project_correct_purchase_amount = () => { 26 | let store = new EventStoreWithStreams(); 27 | const purchaseId = uuid(); 28 | store.append(purchaseId, new PurchaseRequested(purchaseId, 199.00), nameof()); 29 | store.append(purchaseId, new PurchaseSuccessful(purchaseId), nameof()); 30 | 31 | const projection = fillProjection(purchaseId, store); 32 | console.log(`Successful purchase should project correct purchase amount: ${projection.PurchaseAmount === 199.00}`); 33 | } 34 | 35 | const Refunded_purchase_should_project_not_in_flight = () => { 36 | let store = new EventStoreWithStreams(); 37 | const purchaseId = uuid(); 38 | store.append(purchaseId, new PurchaseRequested(purchaseId, 199.00), nameof()); 39 | store.append(purchaseId, new PurchaseSuccessful(purchaseId), nameof()); 40 | store.append(purchaseId, new PurchaseRefunded(purchaseId), nameof()); 41 | 42 | const projection = fillProjection(purchaseId, store); 43 | console.log(`Refunded purchase should project not in-flight: ${(!projection.IsRequestInFlight)}`); 44 | } 45 | 46 | // #### Action #### 47 | // Uncomment to run and see the test results 48 | // ################ 49 | // Successful_purchase_should_project_correct_purchase_amount(); 50 | // Refunded_purchase_should_project_not_in_flight(); 51 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # production mode 47 | $ npm run start:prod 48 | ``` 49 | 50 | ## Test 51 | 52 | ```bash 53 | # unit tests 54 | $ npm run test 55 | 56 | # e2e tests 57 | $ npm run test:e2e 58 | 59 | # test coverage 60 | $ npm run test:cov 61 | ``` 62 | 63 | ## Support 64 | 65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 66 | 67 | ## Stay in touch 68 | 69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 70 | - Website - [https://nestjs.com](https://nestjs.com/) 71 | - Twitter - [@nestframework](https://twitter.com/nestframework) 72 | 73 | ## License 74 | 75 | Nest is [MIT licensed](LICENSE). 76 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-sourcing", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.0.0", 25 | "@nestjs/core": "^7.0.0", 26 | "@nestjs/platform-express": "^7.0.0", 27 | "reflect-metadata": "^0.1.13", 28 | "rimraf": "^3.0.2", 29 | "rxjs": "^6.5.4", 30 | "uuid": "^8.3.1" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/cli": "^7.0.0", 34 | "@nestjs/schematics": "^7.0.0", 35 | "@nestjs/testing": "^7.0.0", 36 | "@types/express": "^4.17.3", 37 | "@types/jest": "26.0.10", 38 | "@types/node": "^13.9.1", 39 | "@types/supertest": "^2.0.8", 40 | "@typescript-eslint/eslint-plugin": "3.9.1", 41 | "@typescript-eslint/parser": "3.9.1", 42 | "eslint": "7.7.0", 43 | "eslint-config-prettier": "^6.10.0", 44 | "eslint-plugin-import": "^2.20.1", 45 | "jest": "26.4.2", 46 | "prettier": "^1.19.1", 47 | "supertest": "^4.0.2", 48 | "ts-jest": "26.2.0", 49 | "ts-loader": "^6.2.1", 50 | "ts-node": "9.0.0", 51 | "tsconfig-paths": "^3.9.0", 52 | "typescript": "^3.7.4" 53 | }, 54 | "jest": { 55 | "moduleFileExtensions": [ 56 | "js", 57 | "json", 58 | "ts" 59 | ], 60 | "rootDir": "src", 61 | "testRegex": ".spec.ts$", 62 | "transform": { 63 | "^.+\\.(t|j)s$": "ts-jest" 64 | }, 65 | "coverageDirectory": "../coverage", 66 | "testEnvironment": "node" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { PurchaseController } from './purchase/purchase.controller'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [AppController, PurchaseController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/purchase/projections/all.ts: -------------------------------------------------------------------------------- 1 | import { PurchaseRefunded } from './../types'; 2 | import { Meta, PurchaseMade, UUID } from '../types' 3 | 4 | export interface Purchase { 5 | Id: UUID; 6 | Amount: number; 7 | At: Date; 8 | WasRefunded: boolean; 9 | } 10 | 11 | export class AllPurchasesProjection { 12 | private _purchases: Purchase[] = []; 13 | 14 | public get Purchases() { 15 | return this._purchases; 16 | } 17 | 18 | apply(events: Meta[]){ 19 | for(const meta of events) { 20 | switch(meta.Type) { 21 | case PurchaseMade.TypeName: 22 | this.applyPurchaseMade(meta.Event as PurchaseMade); 23 | break; 24 | case PurchaseRefunded.TypeName: 25 | this.applyPurchaseRefunded(meta.Event as PurchaseRefunded); 26 | break; 27 | } 28 | } 29 | } 30 | applyPurchaseRefunded(event: PurchaseRefunded) { 31 | this._purchases.find(p => p.Id === event.PurchaseId).WasRefunded = true; 32 | } 33 | 34 | private applyPurchaseMade(event: PurchaseMade) { 35 | this._purchases.push({ Id: event.PurchaseId, Amount: event.Amount, At: new Date(event.At), WasRefunded: false }); 36 | } 37 | } -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/purchase/projections/avg-cost-report.ts: -------------------------------------------------------------------------------- 1 | import { Meta, PurchaseMade } from '../types' 2 | 3 | export class AvgCostProjection { 4 | private _prices: Array = []; 5 | 6 | public get Avg() : number { 7 | const sum = this._prices.reduce( 8 | (a:number, b:number) => a + b, 0 9 | ); 10 | 11 | if(sum > 0) { 12 | return sum / this._prices.length; 13 | } else { 14 | return 0; 15 | } 16 | } 17 | 18 | apply(events: Meta[]){ 19 | for(const meta of events) { 20 | switch(meta.Type) { 21 | case PurchaseMade.TypeName: 22 | const e = meta.Event as PurchaseMade; 23 | this._prices.push(e.Amount); 24 | break; 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/purchase/purchase.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PurchaseController } from './purchase.controller'; 3 | 4 | describe('PurchaseController', () => { 5 | let controller: PurchaseController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [PurchaseController], 10 | }).compile(); 11 | 12 | controller = module.get(PurchaseController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/purchase/purchase.controller.ts: -------------------------------------------------------------------------------- 1 | import { AvgCostProjection } from './projections/avg-cost-report'; 2 | import { Body, Controller, Delete, Get, Post, Redirect, Req } from '@nestjs/common'; 3 | import { AllPurchasesProjection, Purchase } from './projections/all'; 4 | import { EventStore, PurchaseMade, PurchaseRefunded } from './types' 5 | import { v4 as createUuid } from 'uuid'; 6 | 7 | // For sample purposes, a static event store. 8 | const store = new EventStore(); 9 | 10 | class CreatePurchaseRequest { 11 | public amount: string; 12 | } 13 | 14 | class RefundPurchaseRequest { 15 | public id: string; 16 | } 17 | 18 | @Controller('purchase') 19 | export class PurchaseController { 20 | @Get() 21 | all() { 22 | const projection = new AllPurchasesProjection(); 23 | projection.apply(store.getAll()); 24 | const purchases = projection.Purchases; 25 | 26 | let html = ` 27 |

Existing Purchases:

28 |
    `; 29 | for (const p of purchases) { 30 | html += ` 31 |
  • 32 |
    Purchase Id: ${p.Id}
    33 |
    Amount: ${p.Amount}
    34 |
    At: ${p.At}
    35 | ${p.WasRefunded ? '
    Was refunded...
    ' : ''} 36 | ${renderRefundButton(p)} 37 |
  • `; 38 | }; 39 | html += `
`; 40 | 41 | html+= renderCreatePurchaseForm(); 42 | 43 | return html; 44 | } 45 | 46 | @Get('/report') 47 | avgPrice() { 48 | const projection = new AvgCostProjection(); 49 | projection.apply(store.getAll()); 50 | const avg = projection.Avg; 51 | 52 | return ` 53 |

Avg Purchase Price

54 |
$${avg}
55 | `; 56 | } 57 | 58 | @Post() 59 | @Redirect() 60 | create(@Body() req: CreatePurchaseRequest) { 61 | const evt: PurchaseMade = new PurchaseMade(createUuid(), parseInt(req.amount)); 62 | store.append(evt.PurchaseId, evt, PurchaseMade.TypeName); 63 | return { 64 | url: '/purchase' 65 | }; 66 | } 67 | 68 | @Post('/refund') 69 | @Redirect() 70 | refund(@Body() req: RefundPurchaseRequest) { 71 | // Notice, that instead of "UPDATE" 'ing a table row we 72 | // append a new event to the ledger! 73 | 74 | const evt: PurchaseRefunded = new PurchaseRefunded(req.id); 75 | store.append(evt.PurchaseId, evt, PurchaseRefunded.TypeName); 76 | 77 | return { 78 | url: '/purchase' 79 | }; 80 | } 81 | 82 | } 83 | 84 | function renderRefundButton(p: Purchase) : string { 85 | if(p.WasRefunded) 86 | return ''; 87 | 88 | return ` 89 |
90 | 91 | 92 |
93 | `; 94 | } 95 | 96 | function renderCreatePurchaseForm() { 97 | return ` 98 |

Make A Purchase:

99 |
100 | 101 | 102 | 103 |
104 | `; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/src/purchase/types.ts: -------------------------------------------------------------------------------- 1 | import { v4 as createUuid } from 'uuid'; 2 | 3 | /** 4 | * Some Base Types 5 | */ 6 | 7 | export type UUID = string; 8 | export type TimeStamp = number; 9 | 10 | 11 | /** 12 | * Domain Events 13 | */ 14 | 15 | export abstract class DomainEvent { 16 | static TypeName: string = 'DomainEvent'; 17 | EventId: UUID; 18 | At: TimeStamp; 19 | 20 | constructor() { 21 | this.EventId = createUuid(); 22 | this.At = new Date().getTime(); 23 | } 24 | } 25 | 26 | export class PurchaseMade extends DomainEvent { 27 | static TypeName: string = 'PurchaseMade'; 28 | PurchaseId: UUID; 29 | Amount: number; 30 | 31 | constructor(id: UUID, amount: number) { 32 | super(); 33 | this.PurchaseId = id; 34 | this.Amount = amount; 35 | } 36 | } 37 | 38 | export class PurchaseRefunded extends DomainEvent { 39 | static TypeName: string = 'PurchaseRefunded'; 40 | PurchaseId: UUID; 41 | 42 | constructor(id: UUID) { 43 | super(); 44 | this.PurchaseId = id; 45 | } 46 | } 47 | 48 | /** 49 | * Event Store 50 | */ 51 | 52 | export interface Meta { 53 | Event: DomainEvent; 54 | Type: string; 55 | StreamId: UUID 56 | } 57 | 58 | export class EventStore { 59 | private _log: Meta[]; 60 | 61 | constructor() { 62 | this._log = []; 63 | } 64 | 65 | append(streamId: UUID, event: DomainEvent, type: string): void { 66 | const meta: Meta = {StreamId: streamId, Event: event, Type: type} 67 | this._log.push(meta); 68 | } 69 | 70 | getAll(): Meta[] { 71 | return this._log; 72 | } 73 | 74 | getForStream(streamId: UUID): Meta[] { 75 | return this.getAll() 76 | .filter(meta => meta.StreamId === streamId); 77 | } 78 | } -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /lesson5-nestjs-mvc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # production mode 47 | $ npm run start:prod 48 | ``` 49 | 50 | ## Test 51 | 52 | ```bash 53 | # unit tests 54 | $ npm run test 55 | 56 | # e2e tests 57 | $ npm run test:e2e 58 | 59 | # test coverage 60 | $ npm run test:cov 61 | ``` 62 | 63 | ## Support 64 | 65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 66 | 67 | ## Stay in touch 68 | 69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 70 | - Website - [https://nestjs.com](https://nestjs.com/) 71 | - Twitter - [@nestframework](https://twitter.com/nestframework) 72 | 73 | ## License 74 | 75 | Nest is [MIT licensed](LICENSE). 76 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-sourcing", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.0.0", 25 | "@nestjs/core": "^7.0.0", 26 | "@nestjs/cqrs": "^7.0.1", 27 | "@nestjs/platform-express": "^7.0.0", 28 | "class-transformer": "^0.3.1", 29 | "class-validator": "^0.12.2", 30 | "reflect-metadata": "^0.1.13", 31 | "rimraf": "^3.0.2", 32 | "rxjs": "^6.5.4", 33 | "uuid": "^8.3.1" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "^7.0.0", 37 | "@nestjs/schematics": "^7.0.0", 38 | "@nestjs/testing": "^7.0.0", 39 | "@types/express": "^4.17.3", 40 | "@types/jest": "26.0.10", 41 | "@types/node": "^13.9.1", 42 | "@types/supertest": "^2.0.8", 43 | "@typescript-eslint/eslint-plugin": "3.9.1", 44 | "@typescript-eslint/parser": "3.9.1", 45 | "eslint": "7.7.0", 46 | "eslint-config-prettier": "^6.10.0", 47 | "eslint-plugin-import": "^2.20.1", 48 | "jest": "26.4.2", 49 | "prettier": "^1.19.1", 50 | "supertest": "^4.0.2", 51 | "ts-jest": "26.2.0", 52 | "ts-loader": "^6.2.1", 53 | "ts-node": "9.0.0", 54 | "tsconfig-paths": "^3.9.0", 55 | "typescript": "^3.7.4" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "js", 60 | "json", 61 | "ts" 62 | ], 63 | "rootDir": "src", 64 | "testRegex": ".spec.ts$", 65 | "transform": { 66 | "^.+\\.(t|j)s$": "ts-jest" 67 | }, 68 | "coverageDirectory": "../coverage", 69 | "testEnvironment": "node" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { AppService } from './app.service'; 2 | import { PurchaseController } from './purchase/purchase.controller'; 3 | import { EventStoreService } from './common/event-store/event-store.service'; 4 | import { Module } from '@nestjs/common'; 5 | import { CqrsModule } from '@nestjs/cqrs'; 6 | import { AllProjection_HandlePurchaseRefunded, AllProjection_HandlePurchaseMade } from './purchase/projections/all'; 7 | import { AvgCostProjection_HandlePurchaseMade } from './purchase/projections/avg-cost-report'; 8 | import { MakePurchaseHandler } from './purchase/commands/make-purchase/make-purchase.handler'; 9 | import { RefundPurchaseHandler } from './purchase/commands/refund/refund-purchase-handler'; 10 | 11 | @Module({ 12 | imports: [CqrsModule], 13 | controllers: [PurchaseController], 14 | providers: [AppService, EventStoreService, MakePurchaseHandler, RefundPurchaseHandler, 15 | AllProjection_HandlePurchaseRefunded, AllProjection_HandlePurchaseMade, 16 | AvgCostProjection_HandlePurchaseMade], 17 | }) 18 | export class AppModule {} 19 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/common/basic-types.ts: -------------------------------------------------------------------------------- 1 | export type UUID = string; 2 | export type TimeStamp = number; -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/common/domain-event.ts: -------------------------------------------------------------------------------- 1 | import { v4 as createUuid } from 'uuid'; 2 | import { TimeStamp, UUID } from './basic-types'; 3 | 4 | export class DomainEvent { 5 | static TypeName: string = 'DomainEvent'; 6 | EventId: UUID; 7 | At: TimeStamp; 8 | 9 | constructor() { 10 | this.EventId = createUuid(); 11 | this.At = new Date().getTime(); 12 | } 13 | } -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/common/event-store/event-store.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EventStoreService } from './event-store.service'; 3 | 4 | describe('EventStoreService', () => { 5 | let service: EventStoreService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [EventStoreService], 10 | }).compile(); 11 | 12 | service = module.get(EventStoreService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/common/event-store/event-store.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { UUID } from '../basic-types'; 3 | import { DomainEvent } from '../domain-event'; 4 | import { Meta } from './meta.interface'; 5 | 6 | @Injectable() 7 | export class EventStoreService { 8 | private _log: Meta[]; 9 | 10 | constructor() { 11 | this._log = []; 12 | } 13 | 14 | append(streamId: UUID, event: DomainEvent, type: string): void { 15 | const meta: Meta = {StreamId: streamId, Event: event, Type: type} 16 | this._log.push(meta); 17 | } 18 | 19 | getAll(): Meta[] { 20 | return this._log; 21 | } 22 | 23 | getForStream(streamId: UUID): Meta[] { 24 | return this.getAll() 25 | .filter(meta => meta.StreamId === streamId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/common/event-store/meta.interface.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from "../basic-types"; 2 | import { DomainEvent } from "../domain-event"; 3 | 4 | export interface Meta { 5 | Event: DomainEvent; 6 | Type: string; 7 | StreamId: UUID 8 | } -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.useGlobalPipes(new ValidationPipe()); 8 | await app.listen(3000); 9 | } 10 | bootstrap(); 11 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/commands/make-purchase/make-purchase.command.ts: -------------------------------------------------------------------------------- 1 | export class MakePurchaseCommand { 2 | constructor(public readonly amount: number) { } 3 | } -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/commands/make-purchase/make-purchase.handler.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreService } from './../../../common/event-store/event-store.service'; 2 | import { CommandHandler, EventBus } from "@nestjs/cqrs"; 3 | import { ICommandHandler } from "@nestjs/cqrs/dist/interfaces/commands/command-handler.interface"; 4 | import { MakePurchaseCommand } from "./make-purchase.command"; 5 | import { PurchaseMade } from 'src/purchase/domain-events'; 6 | import { v4 as createUuid } from 'uuid'; 7 | 8 | 9 | @CommandHandler(MakePurchaseCommand) 10 | export class MakePurchaseHandler implements ICommandHandler { 11 | 12 | constructor( 13 | private readonly _store: EventStoreService, 14 | private readonly _bus: EventBus 15 | ) { } 16 | 17 | async execute(cmd: MakePurchaseCommand) { 18 | const evt: PurchaseMade = new PurchaseMade(createUuid(), cmd.amount); 19 | this._store.append(evt.PurchaseId, evt, PurchaseMade.TypeName); 20 | this._bus.publish(evt); 21 | 22 | console.log(`Purchase ${evt.PurchaseId} was made.`) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/commands/refund/refund-purchase-handler.ts: -------------------------------------------------------------------------------- 1 | import { RefundPurchaseCommand } from './refund-purchase.command'; 2 | import { EventStoreService } from '../../../common/event-store/event-store.service'; 3 | import { CommandHandler, EventBus } from "@nestjs/cqrs"; 4 | import { ICommandHandler } from "@nestjs/cqrs/dist/interfaces/commands/command-handler.interface"; 5 | import { PurchaseRefunded } from 'src/purchase/domain-events'; 6 | 7 | 8 | @CommandHandler(RefundPurchaseCommand) 9 | export class RefundPurchaseHandler implements ICommandHandler { 10 | 11 | constructor( 12 | private readonly _store: EventStoreService, 13 | private readonly _bus: EventBus 14 | ) { } 15 | 16 | async execute(cmd: RefundPurchaseCommand) { 17 | // Notice, that instead of "UPDATE" 'ing a table row we 18 | // append a new event to the ledger! 19 | const evt: PurchaseRefunded = new PurchaseRefunded(cmd.id); 20 | this._store.append(evt.PurchaseId, evt, PurchaseRefunded.TypeName); 21 | this._bus.publish(evt); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/commands/refund/refund-purchase.command.ts: -------------------------------------------------------------------------------- 1 | export class RefundPurchaseCommand { 2 | constructor(public id: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/domain-events.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from 'src/common/domain-event'; 2 | import { UUID } from "src/common/basic-types"; 3 | 4 | export class PurchaseMade extends DomainEvent { 5 | static TypeName: string = 'PurchaseMade'; 6 | PurchaseId: UUID; 7 | Amount: number; 8 | 9 | constructor(id: UUID, amount: number) { 10 | super(); 11 | this.PurchaseId = id; 12 | this.Amount = amount; 13 | } 14 | } 15 | 16 | export class PurchaseRefunded extends DomainEvent { 17 | static TypeName: string = 'PurchaseRefunded'; 18 | PurchaseId: UUID; 19 | 20 | constructor(id: UUID) { 21 | super(); 22 | this.PurchaseId = id; 23 | } 24 | } -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/projections/all.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from "@nestjs/cqrs"; 2 | import { UUID } from "src/common/basic-types"; 3 | import { PurchaseMade, PurchaseRefunded } from "../domain-events"; 4 | 5 | export interface Purchase { 6 | Id: UUID; 7 | Amount: number; 8 | At: Date; 9 | WasRefunded: boolean; 10 | } 11 | 12 | class AllPurchasesProjection { 13 | private _purchases: Purchase[] = []; 14 | 15 | public get Purchases() { 16 | return this._purchases; 17 | } 18 | 19 | applyPurchaseRefunded(event: PurchaseRefunded) { 20 | this._purchases.find(p => p.Id === event.PurchaseId).WasRefunded = true; 21 | } 22 | 23 | applyPurchaseMade(event: PurchaseMade) { 24 | this._purchases.push({ Id: event.PurchaseId, Amount: event.Amount, At: new Date(event.At), WasRefunded: false }); 25 | } 26 | } 27 | 28 | // Singleton for the purpose of our sample application. 29 | // In a prod setting you would want to store/cache this in a cache, DB, etc. 30 | // Also, you might want to consider using DI to inject the projection vs. an in-memory singleton. 31 | export const allPurchasesProjection = new AllPurchasesProjection(); 32 | 33 | @EventsHandler(PurchaseRefunded) 34 | export class AllProjection_HandlePurchaseRefunded implements IEventHandler { 35 | handle(event: PurchaseRefunded) { 36 | allPurchasesProjection.applyPurchaseRefunded(event); 37 | } 38 | } 39 | 40 | @EventsHandler(PurchaseMade) 41 | export class AllProjection_HandlePurchaseMade implements IEventHandler { 42 | handle(event: PurchaseMade) { 43 | allPurchasesProjection.applyPurchaseMade(event); 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/projections/avg-cost-report.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from "@nestjs/cqrs"; 2 | import { PurchaseMade } from "../domain-events"; 3 | 4 | class AvgCostProjection { 5 | private _sum: number = 0; 6 | private _numOfPurchases: number = 0; 7 | private _avg: number = 0; 8 | 9 | public get Avg() { 10 | return this._avg; 11 | } 12 | 13 | applyPurchaseMade(event: PurchaseMade) { 14 | this._sum += event.Amount; 15 | this._numOfPurchases++; 16 | 17 | // Compared to the simpler nestjs sample lesson, we compute the results immediately 18 | // when a new event is applied. "Fetching" the projection will not perform any 19 | // computation. 20 | this._avg = this._sum / this._numOfPurchases; 21 | console.log(`sum: ${this._sum} | num: ${this._numOfPurchases} | avg: ${this._avg}`) 22 | } 23 | } 24 | 25 | // As sample app, we create a singleton instance of the projection for the entire app to use. 26 | // In a prod setting this would be stored to a database/store. So instead 27 | // of modifying in-memory objects, we'd be issuing SQL statements (etc.) to update the projection. 28 | export const avgCostProjection: AvgCostProjection = new AvgCostProjection(); 29 | 30 | @EventsHandler(PurchaseMade) 31 | export class AvgCostProjection_HandlePurchaseMade implements IEventHandler { 32 | handle(event: PurchaseMade) { 33 | avgCostProjection.applyPurchaseMade(event); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/src/purchase/purchase.controller.ts: -------------------------------------------------------------------------------- 1 | import { EventStoreService } from './../common/event-store/event-store.service'; 2 | import { allPurchasesProjection, Purchase } from './projections/all'; 3 | import { CommandBus } from '@nestjs/cqrs'; 4 | import { avgCostProjection } from './projections/avg-cost-report'; 5 | import { Body, Controller, Get, Post, Redirect } from '@nestjs/common'; 6 | import { IsNumberString } from "class-validator"; 7 | import { MakePurchaseCommand } from './commands/make-purchase/make-purchase.command'; 8 | import { RefundPurchaseCommand } from './commands/refund/refund-purchase.command'; 9 | 10 | class MakePurchaseRequest { 11 | @IsNumberString() 12 | public amount: string; 13 | } 14 | 15 | class RefundPurchaseRequest { 16 | public id: string; 17 | } 18 | 19 | @Controller('purchase') 20 | export class PurchaseController { 21 | 22 | constructor( 23 | private _commandBus: CommandBus, 24 | private _store: EventStoreService 25 | ) { } 26 | 27 | @Get() 28 | all() { 29 | const purchases = allPurchasesProjection.Purchases; 30 | 31 | let html = ` 32 |

Existing Purchases:

33 |
    `; 34 | for (const p of purchases) { 35 | html += ` 36 |
  • 37 |
    Purchase Id: ${p.Id}
    38 |
    Amount: ${p.Amount}
    39 |
    At: ${p.At}
    40 | ${p.WasRefunded ? '
    Was refunded...
    ' : ''} 41 | ${renderRefundButton(p)} 42 |
  • `; 43 | }; 44 | html += `
`; 45 | 46 | html += renderCreatePurchaseForm(); 47 | 48 | return html; 49 | } 50 | 51 | @Get('/raw') 52 | raw() { 53 | return this._store.getAll(); 54 | } 55 | 56 | @Get('/report') 57 | avgPrice() { 58 | return ` 59 |

Avg Purchase Price

60 |
$${avgCostProjection.Avg}
61 | `; 62 | } 63 | 64 | @Post() 65 | @Redirect() 66 | async create(@Body() req: MakePurchaseRequest) { 67 | await this._commandBus.execute(new MakePurchaseCommand(parseFloat(req.amount))); 68 | return { 69 | url: '/purchase' 70 | }; 71 | } 72 | 73 | @Post('/refund') 74 | @Redirect() 75 | async refund(@Body() req: RefundPurchaseRequest) { 76 | await this._commandBus.execute(new RefundPurchaseCommand(req.id)); 77 | return { 78 | url: '/purchase' 79 | }; 80 | } 81 | 82 | } 83 | 84 | function renderRefundButton(p: Purchase) : string { 85 | if(p.WasRefunded) 86 | return ''; 87 | 88 | return ` 89 |
90 | 91 | 92 |
93 | `; 94 | } 95 | 96 | function renderCreatePurchaseForm() { 97 | return ` 98 |

Make A Purchase:

99 |
100 | 101 | 102 | 103 |
104 | `; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/purchase (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/purchase') 21 | .expect(200); 22 | }); 23 | 24 | it('/purchase (POST)', () => { 25 | return request(app.getHttpServer()) 26 | .post('/purchase') 27 | .send({ amount: '500' }) 28 | .expect(302); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "roots": [ 5 | "../" 6 | ], 7 | "modulePaths": [ 8 | "../" 9 | ], 10 | "moduleDirectories": [ 11 | "node_modules", 12 | "../" 13 | ], 14 | "testEnvironment": "node", 15 | "testRegex": ".e2e-spec.ts$", 16 | "transform": { 17 | "^.+\\.(t|j)s$": "ts-jest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /lesson6-nestjs-advanced/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Uncomment the section you want to 'run'. 3 | * Inside each of those files you'll find more code that you can 4 | * interact with and uncomment to view results, etc. 5 | */ 6 | 7 | import './lesson1-functional/index' 8 | // import './lesson2-oop/index' 9 | //import './lesson3-projections-per-stream/index' 10 | //import './lesson4-testing/index' 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-test", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ts-nameof/common": { 8 | "version": "4.2.1", 9 | "resolved": "https://registry.npmjs.org/@ts-nameof/common/-/common-4.2.1.tgz", 10 | "integrity": "sha512-bWwYOFTTd6Nq7RUqJalIii1su3OyR+N1W0smp5PzHGUVycHuJ7MRiyCQrWgMt1lbbp3GY6cvCttm7Zo5E7A1xQ==", 11 | "dev": true 12 | }, 13 | "@ts-nameof/transforms-common": { 14 | "version": "4.2.1", 15 | "resolved": "https://registry.npmjs.org/@ts-nameof/transforms-common/-/transforms-common-4.2.1.tgz", 16 | "integrity": "sha512-mg6nkkHqNlKU1B15pEtiikSKadJSOxFFWpi7tEVB7Yhjx0PhoTcSe7oxK0fXHZGeoTGF2SMZjmCsGnbtfcgk6w==", 17 | "dev": true, 18 | "requires": { 19 | "@ts-nameof/common": "^4.2.0" 20 | } 21 | }, 22 | "@ts-nameof/transforms-ts": { 23 | "version": "4.2.1", 24 | "resolved": "https://registry.npmjs.org/@ts-nameof/transforms-ts/-/transforms-ts-4.2.1.tgz", 25 | "integrity": "sha512-/zI2+DEQ1wvM0UQY5znG4efGbNSyBp+h4pzQZssGcst1748Np92RCWUnGfyV1qDlpIVma4S8Rx5HgkoDLPfMMg==", 26 | "dev": true, 27 | "requires": { 28 | "@ts-nameof/common": "^4.2.0", 29 | "@ts-nameof/transforms-common": "^4.2.1" 30 | } 31 | }, 32 | "@types/ts-nameof": { 33 | "version": "4.2.1", 34 | "resolved": "https://registry.npmjs.org/@types/ts-nameof/-/ts-nameof-4.2.1.tgz", 35 | "integrity": "sha512-NgMmD70b8NwX6BYhKh70xWkquAow6iq3kRAPp4sMAk4Mre9PrqZgvpq5+JbDLWIJPc4Q9ctkwSQjVAOlfuvlyQ==", 36 | "dev": true 37 | }, 38 | "@types/uuid": { 39 | "version": "8.3.0", 40 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", 41 | "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==" 42 | }, 43 | "balanced-match": { 44 | "version": "1.0.0", 45 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 46 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 47 | "dev": true 48 | }, 49 | "brace-expansion": { 50 | "version": "1.1.11", 51 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 52 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 53 | "dev": true, 54 | "requires": { 55 | "balanced-match": "^1.0.0", 56 | "concat-map": "0.0.1" 57 | } 58 | }, 59 | "concat-map": { 60 | "version": "0.0.1", 61 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 62 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 63 | "dev": true 64 | }, 65 | "fs.realpath": { 66 | "version": "1.0.0", 67 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 68 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 69 | "dev": true 70 | }, 71 | "glob": { 72 | "version": "7.1.6", 73 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 74 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 75 | "dev": true, 76 | "requires": { 77 | "fs.realpath": "^1.0.0", 78 | "inflight": "^1.0.4", 79 | "inherits": "2", 80 | "minimatch": "^3.0.4", 81 | "once": "^1.3.0", 82 | "path-is-absolute": "^1.0.0" 83 | } 84 | }, 85 | "inflight": { 86 | "version": "1.0.6", 87 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 88 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 89 | "dev": true, 90 | "requires": { 91 | "once": "^1.3.0", 92 | "wrappy": "1" 93 | } 94 | }, 95 | "inherits": { 96 | "version": "2.0.4", 97 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 98 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 99 | "dev": true 100 | }, 101 | "minimatch": { 102 | "version": "3.0.4", 103 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 104 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 105 | "dev": true, 106 | "requires": { 107 | "brace-expansion": "^1.1.7" 108 | } 109 | }, 110 | "once": { 111 | "version": "1.4.0", 112 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 113 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 114 | "dev": true, 115 | "requires": { 116 | "wrappy": "1" 117 | } 118 | }, 119 | "path-is-absolute": { 120 | "version": "1.0.1", 121 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 122 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 123 | "dev": true 124 | }, 125 | "path-parse": { 126 | "version": "1.0.6", 127 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 128 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 129 | "dev": true 130 | }, 131 | "resolve": { 132 | "version": "1.17.0", 133 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 134 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 135 | "dev": true, 136 | "requires": { 137 | "path-parse": "^1.0.6" 138 | } 139 | }, 140 | "ts-nameof": { 141 | "version": "5.0.0", 142 | "resolved": "https://registry.npmjs.org/ts-nameof/-/ts-nameof-5.0.0.tgz", 143 | "integrity": "sha512-KKebM+HvZdtiFLVUtkvTmlJNzmMPMMwzw7yCcTCA5XRmwDupX/JuPEfnXYpu/Bfb8d+voF1ih2fdizqgKSIO+g==", 144 | "dev": true, 145 | "requires": { 146 | "@ts-nameof/common": "^4.2.0", 147 | "@ts-nameof/transforms-ts": "^4.2.1", 148 | "glob": "^7.1.6" 149 | } 150 | }, 151 | "ttypescript": { 152 | "version": "1.5.12", 153 | "resolved": "https://registry.npmjs.org/ttypescript/-/ttypescript-1.5.12.tgz", 154 | "integrity": "sha512-1ojRyJvpnmgN9kIHmUnQPlEV1gq+VVsxVYjk/NfvMlHSmYxjK5hEvOOU2MQASrbekTUiUM7pR/nXeCc8bzvMOQ==", 155 | "dev": true, 156 | "requires": { 157 | "resolve": ">=1.9.0" 158 | } 159 | }, 160 | "typescript": { 161 | "version": "4.0.3", 162 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", 163 | "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==" 164 | }, 165 | "uuid": { 166 | "version": "8.3.1", 167 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", 168 | "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" 169 | }, 170 | "wrappy": { 171 | "version": "1.0.2", 172 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 173 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 174 | "dev": true 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-sourcing-typescript", 3 | "version": "1.0.0", 4 | "description": "Exploring the basics of event sourcing in TypeScript", 5 | "main": "main.js", 6 | "dependencies": { 7 | "@types/uuid": "^8.3.0", 8 | "typescript": "^4.0.3", 9 | "uuid": "^8.3.1" 10 | }, 11 | "devDependencies": { 12 | "@types/ts-nameof": "^4.2.1", 13 | "ts-nameof": "^5.0.0", 14 | "ttypescript": "^1.5.12" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "build": "ttsc main.ts", 19 | "run": "ttsc main.ts && node main.js" 20 | }, 21 | "author": "", 22 | "license": "ISC" 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | "plugins": [{ "transform": "ts-nameof", "type": "raw" }], 6 | 7 | /* Basic Options */ 8 | // "incremental": true, /* Enable incremental compilation */ 9 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 10 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 11 | // "lib": [], /* Specify library files to be included in the compilation. */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 16 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 17 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 18 | // "outFile": "./", /* Concatenate and emit output to single file. */ 19 | // "outDir": "./dist", /* Redirect output structure to the directory. */ 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, /* Enable all strict type-checking options. */ 31 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 32 | // "strictNullChecks": true, /* Enable strict null checks. */ 33 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 35 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 36 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 37 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 38 | 39 | /* Additional Checks */ 40 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 41 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 42 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 43 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | --------------------------------------------------------------------------------