From 44f25c85a99cb388f78a27a291bf55887e494c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sat, 18 Nov 2017 11:02:27 +0100 Subject: [PATCH] docs(@nestjs) initial commit --- .htaccess | 15 + package.json | 2 +- src/app/app-routing.module.ts | 59 ++++ src/app/app.component.spec.ts | 32 -- src/app/app.component.ts | 27 +- src/app/app.module.ts | 16 +- src/app/constants.ts | 2 + src/app/core/config/config.service.ts | 6 - src/app/core/config/dev-config.service.ts | 5 - src/app/core/config/prod-config.service.ts | 5 - src/app/core/core.module.ts | 21 -- src/app/homepage/footer/footer.component.scss | 4 +- src/app/homepage/header/header.component.html | 2 +- src/app/homepage/homepage.component.html | 56 +++- src/app/homepage/homepage.component.scss | 84 ++++- src/app/homepage/homepage.component.ts | 32 +- .../menu/menu-item/menu-item.component.html | 1 + .../menu/menu-item/menu-item.component.scss | 7 +- src/app/homepage/menu/menu.component.html | 2 +- src/app/homepage/menu/menu.component.ts | 19 +- .../circular-dependency.component.html | 57 +++- .../circular-dependency.component.ts | 67 ++++ .../dependency-injection.component.html | 12 +- .../dependency-injection.component.ts | 66 ++-- .../e2e-testing/e2e-testing.component.html | 8 +- .../e2e-testing/e2e-testing.component.ts | 1 + .../hierarchical-injector.component.html | 26 +- .../hierarchical-injector.component.ts | 22 ++ .../mixin-components.component.html | 1 + .../unit-testing/unit-testing.component.html | 16 +- .../unit-testing/unit-testing.component.ts | 56 ++++ .../components/components.component.html | 21 +- .../pages/components/components.component.ts | 46 +++ .../controllers/controllers.component.html | 63 +++- .../controllers/controllers.component.ts | 60 ++++ .../exception-filters.component.html | 47 ++- .../exception-filters.component.ts | 49 +++ .../express-instance.component.html | 2 +- .../lifecycle-events.component.html | 6 +- .../lifecycle-events.component.ts | 29 +- .../multiple-servers.component.html | 2 +- .../first-steps/first-steps.component.html | 24 +- .../pages/guards/guards.component.html | 66 +++- .../homepage/pages/guards/guards.component.ts | 76 +++++ .../interceptors/interceptors.component.html | 46 ++- .../interceptors/interceptors.component.ts | 73 ++++ .../introduction/introduction.component.html | 15 +- .../basics/basics.component.html | 29 +- .../microservices/basics/basics.component.ts | 34 +- .../custom-transport.component.html | 21 +- .../custom-transport.component.ts | 99 +++++- .../exception-filters.component.html | 8 +- .../exception-filters.component.ts | 15 + .../microservices/redis/redis.component.html | 5 +- .../middlewares/middlewares.component.html | 61 +++- .../middlewares/middlewares.component.ts | 130 ++++++- .../pages/modules/modules.component.html | 23 +- .../pages/modules/modules.component.ts | 18 + .../homepage/pages/pipes/pipes.component.html | 33 +- .../homepage/pages/pipes/pipes.component.ts | 68 ++++ .../pages/recipes/cqrs/cqrs.component.html | 150 +++++++++ .../pages/recipes/cqrs/cqrs.component.scss | 0 .../pages/recipes/cqrs/cqrs.component.spec.ts | 25 ++ .../pages/recipes/cqrs/cqrs.component.ts | 317 ++++++++++++++++++ .../recipes/mongodb/mongodb.component.html | 94 ++++++ .../recipes/mongodb/mongodb.component.scss | 0 .../recipes/mongodb/mongodb.component.spec.ts | 25 ++ .../recipes/mongodb/mongodb.component.ts | 163 +++++++++ .../recipes/passport/passport.component.html | 55 +++ .../recipes/passport/passport.component.scss | 0 .../passport/passport.component.spec.ts | 25 ++ .../recipes/passport/passport.component.ts | 177 ++++++++++ .../sql-sequelize.component.html | 71 ++++ .../sql-sequelize.component.scss | 0 .../sql-sequelize.component.spec.ts | 25 ++ .../sql-sequelize/sql-sequelize.component.ts | 119 +++++++ .../sql-typeorm/sql-typeorm.component.html | 16 +- .../sql-typeorm/sql-typeorm.component.ts | 13 +- .../recipes/swagger/swagger.component.html | 3 + .../recipes/swagger/swagger.component.scss | 0 .../recipes/swagger/swagger.component.spec.ts | 25 ++ .../recipes/swagger/swagger.component.ts | 15 + .../websockets/adapter/adapter.component.html | 13 +- .../websockets/adapter/adapter.component.ts | 46 ++- .../exception-filters.component.html | 8 +- .../exception-filters.component.ts | 16 + .../gateways/gateways.component.html | 16 +- .../websockets/gateways/gateways.component.ts | 21 ++ .../components/tabs/tabs.component.html | 8 + .../components/tabs/tabs.component.scss | 26 ++ .../components/tabs/tabs.component.spec.ts | 25 ++ .../shared/components/tabs/tabs.component.ts | 11 + src/app/shared/pipes/extension.pipe.spec.ts | 8 + src/app/shared/pipes/extension.pipe.ts | 10 + src/app/store/app-store.module.ts | 27 -- src/app/store/common/index.ts | 1 - .../common/interfaces/action.interface.ts | 4 - .../common/interfaces/app-state.interface.ts | 8 - src/app/store/common/interfaces/index.ts | 2 - src/app/store/initial-state.ts | 7 - src/app/store/root-effects.ts | 3 - src/app/store/root-reducers.ts | 8 - .../user/interfaces/user-state.interface.ts | 3 - src/app/store/user/reducer.ts | 7 - src/app/store/user/selectors.ts | 3 - src/favicon.ico | Bin 1150 -> 1150 bytes src/favicon.png | Bin 0 -> 3379 bytes src/index.html | 17 +- src/scss/hljs.scss | 4 +- src/scss/variables.scss | 4 +- src/styles.scss | 9 +- 111 files changed, 3154 insertions(+), 377 deletions(-) create mode 100644 .htaccess delete mode 100644 src/app/app.component.spec.ts delete mode 100644 src/app/core/config/config.service.ts delete mode 100644 src/app/core/config/dev-config.service.ts delete mode 100644 src/app/core/config/prod-config.service.ts delete mode 100644 src/app/core/core.module.ts create mode 100644 src/app/homepage/pages/recipes/cqrs/cqrs.component.html create mode 100644 src/app/homepage/pages/recipes/cqrs/cqrs.component.scss create mode 100644 src/app/homepage/pages/recipes/cqrs/cqrs.component.spec.ts create mode 100644 src/app/homepage/pages/recipes/cqrs/cqrs.component.ts create mode 100644 src/app/homepage/pages/recipes/mongodb/mongodb.component.html create mode 100644 src/app/homepage/pages/recipes/mongodb/mongodb.component.scss create mode 100644 src/app/homepage/pages/recipes/mongodb/mongodb.component.spec.ts create mode 100644 src/app/homepage/pages/recipes/mongodb/mongodb.component.ts create mode 100644 src/app/homepage/pages/recipes/passport/passport.component.html create mode 100644 src/app/homepage/pages/recipes/passport/passport.component.scss create mode 100644 src/app/homepage/pages/recipes/passport/passport.component.spec.ts create mode 100644 src/app/homepage/pages/recipes/passport/passport.component.ts create mode 100644 src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.html create mode 100644 src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.scss create mode 100644 src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.spec.ts create mode 100644 src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts create mode 100644 src/app/homepage/pages/recipes/swagger/swagger.component.html create mode 100644 src/app/homepage/pages/recipes/swagger/swagger.component.scss create mode 100644 src/app/homepage/pages/recipes/swagger/swagger.component.spec.ts create mode 100644 src/app/homepage/pages/recipes/swagger/swagger.component.ts create mode 100644 src/app/shared/components/tabs/tabs.component.html create mode 100644 src/app/shared/components/tabs/tabs.component.scss create mode 100644 src/app/shared/components/tabs/tabs.component.spec.ts create mode 100644 src/app/shared/components/tabs/tabs.component.ts create mode 100644 src/app/shared/pipes/extension.pipe.spec.ts create mode 100644 src/app/shared/pipes/extension.pipe.ts delete mode 100644 src/app/store/app-store.module.ts delete mode 100644 src/app/store/common/index.ts delete mode 100644 src/app/store/common/interfaces/action.interface.ts delete mode 100644 src/app/store/common/interfaces/app-state.interface.ts delete mode 100644 src/app/store/common/interfaces/index.ts delete mode 100644 src/app/store/initial-state.ts delete mode 100644 src/app/store/root-effects.ts delete mode 100644 src/app/store/root-reducers.ts delete mode 100644 src/app/store/user/interfaces/user-state.interface.ts delete mode 100644 src/app/store/user/reducer.ts delete mode 100644 src/app/store/user/selectors.ts create mode 100644 src/favicon.png diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000000..1c56776897 --- /dev/null +++ b/.htaccess @@ -0,0 +1,15 @@ +RewriteEngine On +RewriteBase / + +#if the request is not secure +RewriteCond %{HTTPS} off +#redirect to the secure version +RewriteRule (.*) https://%{HTTP_HOST}/$1 [R=301,L] + +#These are your existing rules +RewriteCond %{REQUEST_FILENAME} -s [OR] +RewriteCond %{REQUEST_FILENAME} -l [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^.*$ - [NC,L] + +RewriteRule ^(.*) /index.html [NC,L] \ No newline at end of file diff --git a/package.json b/package.json index 9a324d1412..f4a359dc74 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "zone.js": "^0.8.14" }, "devDependencies": { - "@angular/cli": "1.2.3", + "@angular/cli": "^1.5.0", "@angular/compiler-cli": "^4.0.0", "@angular/language-service": "^4.0.0", "@types/jasmine": "~2.5.53", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ab03dfc59a..1003fd59c1 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -39,6 +39,10 @@ import { GlobalPrefixComponent } from './homepage/pages/faq/global-prefix/global import { LifecycleEventsComponent } from './homepage/pages/faq/lifecycle-events/lifecycle-events.component'; import { HybridApplicationComponent } from './homepage/pages/faq/hybrid-application/hybrid-application.component'; import { MultipleServersComponent } from './homepage/pages/faq/multiple-servers/multiple-servers.component'; +import { MongodbComponent } from './homepage/pages/recipes/mongodb/mongodb.component'; +import { SqlSequelizeComponent } from './homepage/pages/recipes/sql-sequelize/sql-sequelize.component'; +import { PassportComponent } from './homepage/pages/recipes/passport/passport.component'; +import { CqrsComponent } from './homepage/pages/recipes/cqrs/cqrs.component'; const routes: Routes = [ { @@ -52,142 +56,197 @@ const routes: Routes = [ { path: 'first-steps', component: FirstStepsComponent, + data: { title: 'First Steps' }, }, { path: 'controllers', component: ControllersComponent, + data: { title: 'Controllers' }, }, { path: 'components', component: ComponentsComponent, + data: { title: 'Components' }, }, { path: 'modules', component: ModulesComponent, + data: { title: 'Modules' }, }, { path: 'middlewares', component: MiddlewaresComponent, + data: { title: 'Middlewares' }, }, { path: 'pipes', component: PipesComponent, + data: { title: 'Pipes' }, }, { path: 'guards', component: GuardsComponent, + data: { title: 'Guards' }, }, { path: 'exception-filters', component: ExceptionFiltersComponent, + data: { title: 'Exception Filters' }, }, { path: 'interceptors', component: InterceptorsComponent, + data: { title: 'Interceptors' }, }, { path: 'advanced/dependency-injection', component: DependencyInjectionComponent, + data: { title: 'Dependency Injection' }, }, { path: 'advanced/async-components', component: AsyncComponentsComponent, + data: { title: 'Async Components' }, }, { path: 'advanced/mixins', component: MixinComponentsComponent, + data: { title: 'Mixin Class' }, }, { path: 'advanced/hierarchical-injector', component: HierarchicalInjectorComponent, + data: { title: 'Hierarchical Injector' }, }, { path: 'advanced/circular-dependency', component: CircularDependencyComponent, + data: { title: 'Circular Dependency' }, }, { path: 'advanced/unit-testing', component: UnitTestingComponent, + data: { title: 'Unit Testing' }, }, { path: 'advanced/e2e-testing', component: E2eTestingComponent, + data: { title: 'E2E Testing' }, }, { path: 'websockets/gateways', component: GatewaysComponent, + data: { title: 'Gateways' }, }, { path: 'websockets/pipes', component: WsPipesComponent, + data: { title: 'Pipes - Gateways' }, }, { path: 'websockets/exception-filters', component: WsExceptionFiltersComponent, + data: { title: 'Exception Filters - Gateways' }, }, { path: 'websockets/guards', component: WsGuardsComponent, + data: { title: 'Guards - Gateways' }, }, { path: 'websockets/interceptors', component: WsInterceptorsComponent, + data: { title: 'Interceptors - Gateways' }, }, { path: 'websockets/adapter', component: AdapterComponent, + data: { title: 'Adapter - Gateways' }, }, { path: 'microservices/basics', component: BasicsComponent, + data: { title: 'Microservices' }, }, { path: 'microservices/redis', component: RedisComponent, + data: { title: 'Redis - Microservices' }, }, { path: 'microservices/pipes', component: MicroservicesPipesComponent, + data: { title: 'Pipes - Microservices' }, }, { path: 'microservices/exception-filters', component: MicroservicesExceptionFiltersComponent, + data: { title: 'Exception Filters - Microservices' }, }, { path: 'microservices/guards', component: MicroservicesGuardsComponent, + data: { title: 'Guards - Microservices' }, }, { path: 'microservices/interceptors', component: MicroservicesInterceptorsComponent, + data: { title: 'Interceptors - Microservices' }, }, { path: 'microservices/custom-transport', component: CustomTransportComponent, + data: { title: 'Custom Transport - Microservices' }, }, { path: 'recipes/sql-typeorm', component: SqlTypeormComponent, + data: { title: 'SQL (TypeORM)' }, + }, + { + path: 'recipes/mongodb', + component: MongodbComponent, + data: { title: 'MongoDB (Mongoose)' }, + }, + { + path: 'recipes/passport', + component: PassportComponent, + data: { title: 'Passport integration' }, + }, + { + path: 'recipes/sql-sequelize', + component: SqlSequelizeComponent, + data: { title: 'SQL (Sequelize)' }, + }, + { + path: 'recipes/cqrs', + component: CqrsComponent, + data: { title: 'CQRS' }, }, { path: 'faq/express-instance', component: ExpressInstanceComponent, + data: { title: 'Express Instance - FAQ' }, }, { path: 'faq/global-prefix', component: GlobalPrefixComponent, + data: { title: 'Global Prefix - FAQ' }, }, { path: 'faq/lifecycle-events', component: LifecycleEventsComponent, + data: { title: 'Lifecycle Events - FAQ' }, }, { path: 'faq/hybrid-application', component: HybridApplicationComponent, + data: { title: 'Hybrid Application - FAQ' }, }, { path: 'faq/multiple-servers', component: MultipleServersComponent, + data: { title: 'HTTPS & Multiple Servers - FAQ' }, }, ] }, diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts deleted file mode 100644 index 9510495a2d..0000000000 --- a/src/app/app.component.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TestBed, async } from '@angular/core/testing'; - -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent - ], - }).compileComponents(); - })); - - it('should create the app', async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); - - it(`should have as title 'app'`, async(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app.title).toEqual('app'); - })); - - it('should render title in a h1 tag', async(() => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.debugElement.nativeElement; - expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); - })); -}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8b57a8f2a5..41e331e79e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,8 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { Router, NavigationEnd } from '@angular/router'; +import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; +import { Title } from '@angular/platform-browser'; + +import { TITLE_SUFFIX, HOMEPAGE_TITLE } from './constants'; @Component({ selector: 'app-root', @@ -7,11 +10,29 @@ import { Router, NavigationEnd } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush }) export class AppComponent implements OnInit { - constructor(private readonly router: Router) { } + constructor( + private readonly titleService: Title, + private readonly router: Router, + private readonly activatedRoute: ActivatedRoute) {} ngOnInit() { this.router.events .filter((ev) => ev instanceof NavigationEnd) - .subscribe(() => window.scroll(0, 0)); + .subscribe((ev) => { + window.scroll(0, 0); + this.updateTitle(); + }); + } + + updateTitle() { + const route = this.activatedRoute.snapshot.firstChild; + if (!route) { + return undefined; + } + const childRoute = route.firstChild; + const { data: { title } } = childRoute; + const pageTitle = title ? title : HOMEPAGE_TITLE; + + this.titleService.setTitle(pageTitle + TITLE_SUFFIX); } } \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d18f671b01..18012aa0a8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -4,7 +4,6 @@ import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; -import { CoreModule } from './core/core.module'; import { HomepageComponent } from './homepage/homepage.component'; import { HeaderComponent } from './homepage/header/header.component'; import { FooterComponent } from './homepage/footer/footer.component'; @@ -48,12 +47,18 @@ import { MultipleServersComponent } from './homepage/pages/faq/multiple-servers/ import { HierarchicalInjectorComponent } from './homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component'; import { SqlTypeormComponent } from './homepage/pages/recipes/sql-typeorm/sql-typeorm.component'; import { MixinComponentsComponent } from './homepage/pages/advanced/mixin-components/mixin-components.component'; +import { SqlSequelizeComponent } from './homepage/pages/recipes/sql-sequelize/sql-sequelize.component'; +import { MongodbComponent } from './homepage/pages/recipes/mongodb/mongodb.component'; +import { PassportComponent } from './homepage/pages/recipes/passport/passport.component'; +import { SwaggerComponent } from './homepage/pages/recipes/swagger/swagger.component'; +import { CqrsComponent } from './homepage/pages/recipes/cqrs/cqrs.component'; +import { TabsComponent } from './shared/components/tabs/tabs.component'; +import { ExtensionPipe } from './shared/pipes/extension.pipe'; @NgModule({ imports: [ BrowserModule, AppRoutingModule, - CoreModule, PerfectScrollbarModule.forRoot({ suppressScrollX: true, }), @@ -103,6 +108,13 @@ import { MixinComponentsComponent } from './homepage/pages/advanced/mixin-compon HierarchicalInjectorComponent, SqlTypeormComponent, MixinComponentsComponent, + SqlSequelizeComponent, + MongodbComponent, + PassportComponent, + SwaggerComponent, + CqrsComponent, + TabsComponent, + ExtensionPipe, ], bootstrap: [AppComponent] }) diff --git a/src/app/constants.ts b/src/app/constants.ts index e69de29bb2..eb6b554491 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -0,0 +1,2 @@ +export const HOMEPAGE_TITLE = 'Documentation'; +export const TITLE_SUFFIX = ' | Nest - A progressive Node.js web framework'; \ No newline at end of file diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts deleted file mode 100644 index d576b9dc4b..0000000000 --- a/src/app/core/config/config.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export abstract class ConfigService { - abstract readonly API_URL: string; -} \ No newline at end of file diff --git a/src/app/core/config/dev-config.service.ts b/src/app/core/config/dev-config.service.ts deleted file mode 100644 index 65b79e6515..0000000000 --- a/src/app/core/config/dev-config.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ConfigService } from './config.service'; - -export class DevelopmentConfigService extends ConfigService { - readonly API_URL = 'http://localhost:3001'; -} \ No newline at end of file diff --git a/src/app/core/config/prod-config.service.ts b/src/app/core/config/prod-config.service.ts deleted file mode 100644 index c624a37d28..0000000000 --- a/src/app/core/config/prod-config.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ConfigService } from './config.service'; - -export class ProductionConfigService extends ConfigService { - readonly API_URL = 'http://prod-api.scali.io'; -} \ No newline at end of file diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts deleted file mode 100644 index 8b53653898..0000000000 --- a/src/app/core/core.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { ConfigService } from './config/config.service'; -import { environment } from '../../environments/environment'; -import { DevelopmentConfigService } from './config/dev-config.service'; -import { ProductionConfigService } from './config/prod-config.service'; - -@NgModule({ - imports: [], - providers: [ - { - provide: ConfigService, - useFactory() { - return !environment.production ? - new DevelopmentConfigService() : new ProductionConfigService(); - }, - } - ], - exports: [], -}) -export class CoreModule { } diff --git a/src/app/homepage/footer/footer.component.scss b/src/app/homepage/footer/footer.component.scss index 49a71cbbef..489cd812ce 100644 --- a/src/app/homepage/footer/footer.component.scss +++ b/src/app/homepage/footer/footer.component.scss @@ -17,7 +17,7 @@ @include media(medium) { padding: 15px; - margin: 30px -30px 0; + margin: 0 -30px; width: calc(100% + 60px); position: static; height: auto; @@ -38,7 +38,7 @@ footer { } p { - line-height: 20px; + line-height: 24px; } span { font-size: 13px; diff --git a/src/app/homepage/header/header.component.html b/src/app/homepage/header/header.component.html index 92c57977e9..4f61ef7ef8 100644 --- a/src/app/homepage/header/header.component.html +++ b/src/app/homepage/header/header.component.html @@ -8,6 +8,6 @@
- +
\ No newline at end of file diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index c31fb4a63b..2ff5f3b7f2 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -4,16 +4,68 @@ >
- Nest - A node.js framework built on top of TypeScript + + + + + + + + +
-
+
+
+
+

Sponsors

+ + + +

Nest is an MIT-licensed open source project. It can grow thanks to the support by these awesome people. If you'd like to join them, please read more here. Thanks!

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Become a sponsor + +
+

Made by diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss index b23a6a43c8..9b67c9b7f1 100644 --- a/src/app/homepage/homepage.component.scss +++ b/src/app/homepage/homepage.component.scss @@ -9,12 +9,18 @@ min-height: 100vh; } +.logo-wrapper { + svg { + width: 132px; + height: 61px; + } +} .container { @extend .transition; @extend .box-sizing; @include transform(translateX(345px)); width: calc(100% - 345px); - padding: 45px 50px 100px; + padding: 45px 50px 90px; display: inline-block; vertical-align: top; position: relative; @@ -42,7 +48,7 @@ line-height: 26px; a { - font-weight: 500; + font-weight: 600; color: $red-color; &:hover { color: #0894e2; @@ -53,10 +59,82 @@ color: $black-color; } h3 { font-size: 24px; } - + h5 { + color: $grey-color; + margin-top: -20px; + font-weight: 500; + font-size: 15px; + } @include media(normal) { p { text-align: justify; } } +} + +.sponsors-wrapper { + padding: 20px 40px 40px; + margin: 40px -50px 0; + background: #f3f3f3; + position: relative; + + h4, img { + display: inline-block; + vertical-align: middle; + } + + a { + color: $red-color; + font-weight: 600; + &:hover { + color: #0894e2; + } + } + + h4 { + color: #c2c2c2; + font-weight: 600; + margin: 0; + font-size: 16px; + } + + p { + color: #c2c2c2; + margin-top: 35px; + line-height: 26px; + } + + .logo-sponsor { + width: 150px; + margin-left: 25px; + } + + .btn-primary { + border: 2px solid $red-color; + color: $red-color; + padding: 12px 20px; + display: inline-block; + position: absolute; + right: 40px; + top: 50px; + + &:hover { + color: #0894e2; + border-color: #0894e2; + } + + @include media(medium) { + position: static; + margin-top: 30px; + } + } +} + +.backers-wrapper { + margin-top: 20px; + img { + width: 30px; + border-radius: 50%; + -webkit-border-radius: 50%; + } } \ No newline at end of file diff --git a/src/app/homepage/homepage.component.ts b/src/app/homepage/homepage.component.ts index a5586a16b8..344c83d3ae 100644 --- a/src/app/homepage/homepage.component.ts +++ b/src/app/homepage/homepage.component.ts @@ -1,22 +1,43 @@ -import { Component, ViewEncapsulation, HostListener, AfterViewInit, ChangeDetectionStrategy } from '@angular/core'; +import { + Component, + ViewEncapsulation, + HostListener, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + OnInit, +} from '@angular/core'; +import { Router, NavigationEnd } from '@angular/router'; @Component({ selector: 'app-homepage', templateUrl: './homepage.component.html', styleUrls: ['./homepage.component.scss'], encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class HomepageComponent implements AfterViewInit { +export class HomepageComponent implements OnInit, AfterViewInit { isSidebarOpened = true; - toggleSidebar() { - this.isSidebarOpened = !this.isSidebarOpened; + + constructor( + private readonly cd: ChangeDetectorRef, + private readonly router: Router, + ) {} + + ngOnInit(): void { + this.router.events.filter(e => e instanceof NavigationEnd).subscribe(() => { + this.checkWindowWidth(window.innerWidth); + }); } ngAfterViewInit() { this.checkWindowWidth(window.innerWidth); } + toggleSidebar() { + this.isSidebarOpened = !this.isSidebarOpened; + } + @HostListener('window:resize', ['$event']) onResize(event) { this.checkWindowWidth(event.target.innerWidth); @@ -26,6 +47,7 @@ export class HomepageComponent implements AfterViewInit { innerWidth = innerWidth ? innerWidth : window.innerWidth; if (innerWidth <= 768) { this.isSidebarOpened = false; + this.cd.detectChanges(); } } } diff --git a/src/app/homepage/menu/menu-item/menu-item.component.html b/src/app/homepage/menu/menu-item/menu-item.component.html index 5b74041ee7..bac5549f90 100644 --- a/src/app/homepage/menu/menu-item/menu-item.component.html +++ b/src/app/homepage/menu/menu-item/menu-item.component.html @@ -7,6 +7,7 @@

{{ title }}

  • {{ item.title }} diff --git a/src/app/homepage/menu/menu-item/menu-item.component.scss b/src/app/homepage/menu/menu-item/menu-item.component.scss index fca3d7aac2..9571a6ee8c 100644 --- a/src/app/homepage/menu/menu-item/menu-item.component.scss +++ b/src/app/homepage/menu/menu-item/menu-item.component.scss @@ -43,7 +43,12 @@ h3 { &:hover { color: $red-color; } - &.active { font-weight: 500; } + &.active { font-weight: 600; } + } + + .pending { + color: #afafaf !important; + pointer-events: none; } } diff --git a/src/app/homepage/menu/menu.component.html b/src/app/homepage/menu/menu.component.html index eba1cca5fd..ec9c388b19 100644 --- a/src/app/homepage/menu/menu.component.html +++ b/src/app/homepage/menu/menu.component.html @@ -1,4 +1,4 @@ -
  • \ No newline at end of file diff --git a/src/app/homepage/pages/advanced/circular-dependency/circular-dependency.component.ts b/src/app/homepage/pages/advanced/circular-dependency/circular-dependency.component.ts index beaa8cdb56..2a1091cd39 100644 --- a/src/app/homepage/pages/advanced/circular-dependency/circular-dependency.component.ts +++ b/src/app/homepage/pages/advanced/circular-dependency/circular-dependency.component.ts @@ -21,6 +21,21 @@ export class CatsService implements OnModuleInit { }`; } + get moduleRefJs() { + return ` +@Component() +@Dependencies(ModuleRef) +export class CatsService { + constructor(moduleRef) { + this.moduleRef = moduleRef; + } + + onModuleInit() { + this.service = this.moduleRef.get(Service); + } +}`; + } + get proxy() { return ` @Component() @@ -36,5 +51,57 @@ export class Proxy { } }`; } + + get forwardRef() { + return ` +@Component() +export class CatsService { + constructor( + @Inject(forwardRef(() => CommonService)) + private readonly commonService: CommonService, + ) {} +}`; + } + + get forwardRefJs() { + return ` +@Component() +@Dependencies(forwardRef(() => CommonService)) +export class CatsService { + constructor(commonService) { + this.commonService = commonService; + } +}`; + } + + get forwardRefCommon() { + return ` +@Component() +export class CommonService { + constructor( + @Inject(forwardRef(() => CatsService)) + private readonly catsService: CatsService, + ) {} +}`; + } + + get forwardRefCommonJs() { + return ` +@Component() +@Dependencies(forwardRef(() => CatsService)) +export class CommonService { + constructor(catsService) { + this.catsService = catsService; + } +}`; + } + + get forwardRefModule() { + return ` +@Module({ + modules: [forwardRef(() => CatsModule)], +}) +export class CommonModule {}`; + } } diff --git a/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.html b/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.html index 120216a728..00fc055483 100644 --- a/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.html +++ b/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.html @@ -19,7 +19,11 @@

    Use Value

  • bind specific value to the container, for example 3rd party library
  • Use Factory

    -
    {{ useFactory }}
    + + + +
    {{ useFactory }}
    +
    {{ useFactoryJs }}
    Notice If you wanna use components from module, you have to pass them inside inject array. Nest will pass instances as an arguments of the function in the same order.
    @@ -42,7 +46,11 @@

    Injection

    To inject custom component through constructor, we're using the @Inject() decorator. This decorator takes 1 argument - the token.

    -
    {{ inject }}
    + + + +
    {{ inject }}
    +
    {{ injectJs }}
    Notice The @Inject() decorator is imported from @nestjs/common package.
    diff --git a/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.ts b/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.ts index 5665dab418..e0e0672bf0 100644 --- a/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.ts +++ b/src/app/homepage/pages/advanced/dependency-injection/dependency-injection.component.ts @@ -10,41 +10,54 @@ import { BasePageComponent } from '../../page/page.component'; export class DependencyInjectionComponent extends BasePageComponent { get useValue() { return ` +const connectionProvider = { provide: 'ConnectionToken', useValue: null }; + @Module({ - components: [ - { - provide: 'ConnectionToken', - useValue: null, - }, - ], + components: [connectionProvider], })`; } get useFactory() { return ` +const connectionFactory = { + provide: 'ConnectionToken', + useFactory: (optionsProvider: OptionsProvider) => { + const options = optionsProvider.get(); + return new DatabaseConnection(options); + }, + inject: [OptionsProvider], +}; + +@Module({ + components: [connectionFactory], +})`; + } + + get useFactoryJs() { + return ` +const connectionFactory = { + provide: 'ConnectionToken', + useFactory: (optionsProvider) => { + const options = optionsProvider.get(); + return new DatabaseConnection(options); + }, + inject: [OptionsProvider], +}; + @Module({ - components: [ - { - provide: 'ConnectionToken', - useFactory: (optionsProvider: OptionsProvider) => { - const options = optionsProvider.get(); - return new DatabaseConnection(options); - }, - inject: [OptionsProvider], - }, - ], + components: [connectionFactory], })`; } get useClass() { return ` +const configServiceProvider = { + provide: ConfigService, + useClass: DevelopmentConfigService, +}; + @Module({ - components: [ - { - provide: ConfigService, - useClass: DevelopmentConfigService, - }, - ], + components: [configServiceProvider], }) `; } @@ -54,6 +67,15 @@ export class DependencyInjectionComponent extends BasePageComponent { @Component() class CatsRepository { constructor(@Inject('ConnectionToken') connection: Connection) {} +}`; + } + + get injectJs() { + return ` +@Component() +@Dependencies('ConnectionToken') +class CatsRepository { + constructor(connection) {} }`; } } \ No newline at end of file diff --git a/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.html b/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.html index a6e2f89bb6..e64237862d 100644 --- a/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.html +++ b/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.html @@ -12,7 +12,10 @@

    E2E Testing

    Let's create an e2e directory and test the CatsModule.

    - cats.e2e-spec.ts + + {{ 'cats.e2e-spec' | extension: e2eTestsT.isJsActive }} + +
    {{ e2eTests }}
    Hint Keep your e2e test files inside the e2e directory. The testing files should have a .e2e-spec or .e2e-test suffix. @@ -50,8 +53,7 @@

    E2E Testing

    get() - Makes possible to retrieve the instance of the component or controller available inside the processed module - (including imported ones). + Makes possible to retrieve the instance of the component or controller available inside the processed module. diff --git a/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.ts b/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.ts index 1e3ea882f9..08972bf4b2 100644 --- a/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.ts +++ b/src/app/homepage/pages/advanced/e2e-testing/e2e-testing.component.ts @@ -44,4 +44,5 @@ describe('Cats', () => { }); });`; } + } diff --git a/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.html b/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.html index 8ea4adc7e7..382efb6e71 100644 --- a/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.html +++ b/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.html @@ -9,25 +9,39 @@

    Hierarchical Injector

    Let's consider an example. We've a CoreModule:

    - core.module.ts + + {{ 'core.module' | extension: coreModuleT.isJsActive }} + +
    {{ coreModule }}

    This module imports the CommonModule and contains 2 components, successively CoreService and ContextService. The CommonModule contains single component, the CommonService. This component is exported, so it's available within CoreModule as well.

    - common.module.ts + + {{ 'common.module' | extension: commonModuleT.isJsActive }} + +
    {{ commonModule }}

    Let's take a look at ContextService now:

    - core/context.service.ts -
    {{ contextService }}
    + + {{ 'core/context.service' | extension: contextServiceT.isJsActive }} + + +
    {{ contextService }}
    +
    {{ contextServiceJs }}

    This class depends on the CommonService. This service's imported from the child module (CommonModule), so it's a typical situation. Now something that may turn out to be surprising, the CommonService:

    - common/common.service.ts -
    {{ commonService }}
    + + {{ 'common/common.service' | extension: commonServiceT.isJsActive }} + + +
    {{ commonService }}
    +
    {{ commonServiceJs }}

    The CommonService depends on the CoreService. It's possible, because the CommonService instance was resolved within the CoreModule context (was injected by the component, which belongs to the parent module).

    diff --git a/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.ts b/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.ts index a5ce701ef1..0ae0ec69d8 100644 --- a/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.ts +++ b/src/app/homepage/pages/advanced/hierarchical-injector/hierarchical-injector.component.ts @@ -34,11 +34,33 @@ export class ContextService { }`; } + get contextServiceJs() { + return ` +@Component() +@Dependencies(CommonService) +export class ContextService { + constructor(commonService) { + this.commonService = commonService; + } +}`; + } + get commonService() { return ` @Component() export class CommonService { constructor(private readonly coreService: CoreService) {} +}`; + } + + get commonServiceJs() { + return ` +@Component() +@Dependencies(CoreService) +export class CommonService { + constructor(coreService) { + this.coreService = coreService; + } }`; } } \ No newline at end of file diff --git a/src/app/homepage/pages/advanced/mixin-components/mixin-components.component.html b/src/app/homepage/pages/advanced/mixin-components/mixin-components.component.html index 54741dbaa1..a2c20fb5af 100644 --- a/src/app/homepage/pages/advanced/mixin-components/mixin-components.component.html +++ b/src/app/homepage/pages/advanced/mixin-components/mixin-components.component.html @@ -1,5 +1,6 @@

    Mixin Class

    +
    This chapter applies only to TypeScript

    TypeScript 2.2 adds support for the ECMAScript 2015 mixin class pattern. This pattern's quite useful since it's not easy to pass custom arguments to some Nest application building blocks, such as interceptors or guards. diff --git a/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.html b/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.html index 549b12f4a0..1d9c93c8f1 100644 --- a/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.html +++ b/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.html @@ -9,8 +9,12 @@

    Isolated tests

    Components, controllers, interceptors.. every Nest application building block is in fact just a simple class. Since each dependency is injected through constructor, you can easily mock them using abilities of the popular libraries, for example Jasmine or Jest.

    - cats.controller.spec.ts -
    {{ isolatedTests }}
    + + {{ 'cats.controller.spec' | extension: isolatedTestsT.isJsActive }} + + +
    {{ isolatedTests }}
    +
    {{ isolatedTestsJs }}
    Hint Keep your test files nearby tested classes. The testing files should have a .spec or .test suffix.
    @@ -24,8 +28,12 @@

    Testing utilities

    Nest has a special package @nestjs/testing, which gives a set of utilities to boost the testing process. Let's rewrite the example to make use of Nest Test static class.

    - cats.controller.spec.ts -
    {{ utils }}
    + + {{ 'cats.controller.spec' | extension: utilsT.isJsActive }} + + +
    {{ utils }}
    +
    {{ utilsJs }}

    The Test class has a single method - createTestingModule() which takes as an argument the module metadata (same object as the @Module() decorator). This method creates a TestingModule instance, which in turn provides a few methods, but only one of them is useful within the unit testing - the compile(). diff --git a/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.ts b/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.ts index cc56b59cb1..d1f241bc03 100644 --- a/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.ts +++ b/src/app/homepage/pages/advanced/unit-testing/unit-testing.component.ts @@ -33,6 +33,31 @@ describe('CatsController', () => { });`; } + get isolatedTestsJs() { + return ` +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; + +describe('CatsController', () => { + let catsController; + let catsService; + + beforeEach(() => { + catsService = new CatsService(); + catsController = new CatsController(catsService); + }); + + describe('findAll', () => { + it('should return an array of cats', async () => { + const result = ['test']; + jest.spyOn(catsService, 'findAll').mockImplementation(() => result); + + expect(await catsController.findAll()).toBe(result); + }); + }); +});`; + } + get utils() { return ` import { Test } from '@nestjs/testing'; @@ -63,4 +88,35 @@ describe('CatsController', () => { }); });` } + + get utilsJs() { + return ` +import { Test } from '@nestjs/testing'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; + +describe('CatsController', () => { + let catsController; + let catsService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + controllers: [CatsController], + components: [CatsService], + }).compile(); + + catsService = module.get(CatsService); + catsController = module.get(CatsController); + }); + + describe('findAll', () => { + it('should return an array of cats', async () => { + const result = ['test']; + jest.spyOn(catsService, 'findAll').mockImplementation(() => result); + + expect(await catsController.findAll()).toBe(result); + }); + }); +});` + } } \ No newline at end of file diff --git a/src/app/homepage/pages/components/components.component.html b/src/app/homepage/pages/components/components.component.html index d2d2c1d94d..41a8b9e19f 100644 --- a/src/app/homepage/pages/components/components.component.html +++ b/src/app/homepage/pages/components/components.component.html @@ -18,8 +18,12 @@

    Components

    Let's create a CatsService component:

    - cats.service.ts -
    {{ catsService }}
    + + {{ 'cats.service' | extension: catsServiceT.isJsActive }} + + +
    {{ catsService }}
    +
    {{ catsServiceJs }}

    There's nothing specifically about components. Here's a CatsService, basic class with one property and two methods. The only difference is that it has the @Component() decorator. The @Component() attaches the metadata, so Nest knows that this class is a Nest component. @@ -30,8 +34,12 @@

    Components

    Since we've the service class already done, let's use it inside the CatsController:

    - cats.controller.ts -
    {{ catsController }}
    + + {{ 'cats.controller' | extension: catsServiceT.isJsActive }} + + +
    {{ catsController }}
    +
    {{ catsControllerJs }}

    The CatsService is injected through the class constructor. Don't be afraid about the private readonly shortened syntax. @@ -59,7 +67,10 @@

    Last step

    The last thing is to tell the module that something called CatsService truly exists. The only way to do it is to open the app.module.ts file, and add the service into the components array of the @Module() decorator.

    - app.module.ts + + {{ 'app.module' | extension: appModuleT.isJsActive }} + +
    {{ appModule }}

    Now Nest will smoothly resolve the dependencies of the CatsController class. diff --git a/src/app/homepage/pages/components/components.component.ts b/src/app/homepage/pages/components/components.component.ts index 4fbf27cb83..1c7af33105 100644 --- a/src/app/homepage/pages/components/components.component.ts +++ b/src/app/homepage/pages/components/components.component.ts @@ -29,6 +29,27 @@ export class CatsService { `; } + get catsServiceJs() { + return ` +import { Component } from '@nestjs/common'; + +@Component() +export class CatsService { + constructor() { + this.cats = []; + } + + create(cat) { + this.cats.push(cat); + } + + findAll() { + return this.cats; + } +} +`; + } + get catsController() { return ` import { Controller, Get, Post, Body } from '@nestjs/common'; @@ -52,6 +73,31 @@ export class CatsController { }`; } + get catsControllerJs() { + return ` +import { Controller, Get, Post, Body, Bind, Dependencies } from '@nestjs/common'; +import { CatsService } from './cats.service'; + +@Controller('cats') +@Dependencies(CatsService) +export class CatsController { + constructor(catsService) { + this.catsService = catsService; + } + + @Post() + @Bind(Body()) + async create(createCatDto) { + this.catsService.create(createCatDto); + } + + @Get() + async findAll() { + return this.catsService.findAll(); + } +}`; + } + get constructorSyntax() { return ` constructor(private readonly catsService: CatsService) {}`; diff --git a/src/app/homepage/pages/controllers/controllers.component.html b/src/app/homepage/pages/controllers/controllers.component.html index 2b65b7bc55..8ea43a3c36 100644 --- a/src/app/homepage/pages/controllers/controllers.component.html +++ b/src/app/homepage/pages/controllers/controllers.component.html @@ -6,7 +6,10 @@

    Controllers

    To tell Nest that CatsController is a controller, you have to attach metadata to the class. Metadata can be attached using decorators.

    - cats.controller.ts + + {{ 'cats.controller' | extension: catsControllerT.isJsActive }} + +
    {{ catsController }}

    Metadata

    @@ -59,8 +62,12 @@

    Request object

    Hint There's a @types/express package and we strongly recommend to use it.
    - cats.controller.ts -
    {{ requestObject }}
    + + {{ 'cats.controller' | extension: requestObjectT.isJsActive }} + + +
    {{ requestObject }}
    +
    {{ requestObjectJs }}

    The request object contains headers, params, and e.g. body of the request, but in most cases, it's not necessary to grab them manually. We can use dedicated decorators instead, such as @Body() or @Query(), which are available out of the box. @@ -107,7 +114,10 @@

    More endpoints

    We've already created an endpoint to fetch the data. It'd be great to provide a way of creating the new records too. So.. What we're waiting for?! Let's create the POST handler:

    - cats.controller.ts + + {{ 'cats.controller' | extension: postEndpointT.isJsActive }} + +
    {{ postEndpoint }}

    It's really easy. Nest provides the rest of those endpoints decorators in the same fashion - @@ -125,8 +135,12 @@

    Async / await

    Every async function has to return the Promise. It means that you can return deffered value and Nest will resolve it by itself. Let's have a look on the below example:

    - cats.controller.ts -
    {{ asyncExample }}
    + + {{ 'cats.controller' | extension: asyncExampleT.isJsActive }} + + +
    {{ asyncExample }}
    +
    {{ asyncExampleJs }}

    It's fully valid.

    @@ -135,8 +149,12 @@

    Async / await

    observable streams. It makes the migration between simple web application and the Nest microservice much easier.

    - cats.controller.ts -
    {{ observableExample }}
    + + {{ 'cats.controller' | extension: observableExampleT.isJsActive }} + + +
    {{ observableExample }}
    +
    {{ observableExampleJs }}

    POST handler

    That's weird that this POST route handler doesn't accept any client params. We should definitely @@ -152,7 +170,10 @@

    POST handler

    Let's create the CreateCatDto:

    - dto/create-cat.dto.ts + + {{ 'dto/create-cat.dto' | extension: createCatSchemaT.isJsActive }} + +
    {{ createCatSchema }}

    It has only three basic properties. All of them are marked as a readonly, because we should @@ -161,14 +182,21 @@

    POST handler

    Now we can use the schema inside the CatsController:

    - cats.controller.ts -
    {{ exampleWithBody }}
    + + {{ 'cats.controller' | extension: exampleWithBodyT.isJsActive }} + + +
    {{ exampleWithBody }}
    +
    {{ exampleWithBodyJs }}

    Expressjs doesn't parse the body by default. We need the middleware, which name is body-parser. The usage is really simple, because Nest instance provides the use() method. It's a wrapper to the native express use() function:

    - server.ts + + {{ 'server' | extension: bodyParserT.isJsActive }} + +
    {{ bodyParser }}

    Last step

    @@ -179,7 +207,10 @@

    Last step

    The controller always belongs to the module, which mentioned about its in controllers array within @Module() decorator. Since we don't have any other modules except the root AppModule, let's use it for now:

    - app.module.ts + + {{ 'app.module' | extension: appModuleT.isJsActive }} + +
    {{ appModule }}

    Tada! We attached the metadata to the module class, so now Nest can easily reflect which controllers have to be initialized. @@ -190,7 +221,11 @@

    Express approach

    It was the only available option until Nest 4. To inject the response object, we need to use @Res() decorator. To show the differences, i'm going to rewrite the CatsController:

    -
    {{ expressWay }}
    + + + +
    {{ expressWay }}
    +
    {{ expressWayJs }}

    This manner is much less clear from my point of view. I definitely prefer the first approach, but to make the Nest backward compatible with the previous versions, this method is still available. Also, the response object gives more flexibility - you've full control of the response. diff --git a/src/app/homepage/pages/controllers/controllers.component.ts b/src/app/homepage/pages/controllers/controllers.component.ts index 05ebcc4ef2..bbede5e95f 100644 --- a/src/app/homepage/pages/controllers/controllers.component.ts +++ b/src/app/homepage/pages/controllers/controllers.component.ts @@ -34,6 +34,20 @@ export class CatsController { }`; } + get requestObjectJs(): string { + return ` +import { Controller, Bind, Get, Req } from '@nestjs/common'; + +@Controller('cats') +export class CatsController { + @Get() + @Bind(Req()) + findAll(request) { + return []; + } +}`; + } + get postEndpoint() { return ` import { Controller, Get, Post } from '@nestjs/common'; @@ -60,6 +74,14 @@ async findAll(): Promise { }`; } + get asyncExampleJs() { + return ` +@Get() +async findAll() { + return []; +}`; + } + get observableExample() { return ` @Get() @@ -68,6 +90,14 @@ findAll(): Observable { }`; } + get observableExampleJs() { + return ` +@Get() +findAll() { + return Observable.of([]); +}`; + } + get createCatSchema() { return ` export class CreateCatDto { @@ -85,6 +115,15 @@ async create(@Body() createCatDto: CreateCatDto) { }`; } + get exampleWithBodyJs() { + return ` +@Post() +@Bind(Body()) +async create(createCatDto) { +// TODO: Add some logic here +}`; + } + get appModule() { return ` import { Module } from '@nestjs/common'; @@ -127,6 +166,27 @@ export class CatsController { findAll(@Res() res) { res.status(HttpStatus.OK).json([]); } +}`; + } + + get expressWayJs() { + return ` +import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common'; + +@Controller('cats') +export class CatsController { + @Post() + @Bind(Res(), Body()) + create(res, createCatDto) { + // TODO: Add some logic here + res.status(HttpStatus.CREATED).send(); + } + + @Get() + @Bind(Res()) + findAll(res) { + res.status(HttpStatus.OK).json([]); + } }`; } } diff --git a/src/app/homepage/pages/exception-filters/exception-filters.component.html b/src/app/homepage/pages/exception-filters/exception-filters.component.html index 8be1fb143c..e8580ef7d7 100644 --- a/src/app/homepage/pages/exception-filters/exception-filters.component.html +++ b/src/app/homepage/pages/exception-filters/exception-filters.component.html @@ -21,8 +21,12 @@

    HttpException

    In the CatsController, we have a create() method (POST route). Let's assume that this route handler would throw an exception for some reason. We're gonna hardcode it:

    - cats.controller.ts -
    {{ createMethod }}
    + + {{ 'cats.controller' | extension: createMethodT.isJsActive }} + + +
    {{ createMethod }}
    +
    {{ createMethodJs }}
    Notice I've used the HttpStatus here. It's just a helper enum imported from the @nestjs/common package.
    @@ -38,14 +42,21 @@

    Exceptions Hierarchy

    The good practice is to create your own exceptions hierarchy. It means that every HTTP exception should inherit from the base HttpException class. As a result Nest will recognize every of your exception, and will fully take care about the error responses.

    - forbidden.exception.ts + + {{ 'forbidden.exception' | extension: forbiddenExceptionT.isJsActive }} + +
    {{ forbiddenException }}

    Since ForbiddenException extends the base HttpException, it will work well with the core exceptions handler, so now we can use this class inside the create() method.

    - cats.controller.ts -
    {{ forbiddenCreateMethod }}
    + + {{ 'cats.controller' | extension: forbiddenCreateMethodT.isJsActive }} + + +
    {{ forbiddenCreateMethod }}
    +
    {{ forbiddenCreateMethodJs }}

    Exception Filters

    The base exceptions handler is fine, but sometimes you may want to have a full control over the exceptions layer, for example add some logging or use the different JSON schema. @@ -54,8 +65,12 @@

    Exception Filters

    We're gonna create the filter, which responsibility is to catch the HttpException and override the message property.

    - http-exception.filter.ts -
    {{ httpExceptionFilter }}
    + + {{ 'http-exception.filter' | extension: httpExceptionFilterT.isJsActive }} + + +
    {{ httpExceptionFilter }}
    +
    {{ httpExceptionFilterJs }}

    The response is a native express response object. The exception is a currently processed exception.

    @@ -69,8 +84,12 @@

    Exception Filters

    The last step is to inform Nest that the HttpExceptionFilter should be used within the create() method.

    - cats.controller.ts -
    {{ forbiddenCreateMethodWithFilter }}
    + + {{ 'cats.controller' | extension: forbiddenCreateMethodWithFilterT.isJsActive }} + + +
    {{ forbiddenCreateMethodWithFilter }}
    +
    {{ forbiddenCreateMethodWithFilterJs }}
    Hint The @UseFilters() decorator is imported from the @nestjs/common package.
    @@ -81,13 +100,19 @@

    Exception Filters

    In the above example the HttpExceptionFilter is setuped only for the single create() route handler, but it's not the only way. In fact the exception filters can be method-scoped, controller-scoped and also global-scoped.

    - cats.controller.ts + + {{ 'cats.controller' | extension: controllerScopedFilterT.isJsActive }} + +
    {{ controllerScopedFilter }}

    This construction setups the HttpExceptionFilter for every route handler inside the CatsController. It's the example of the controller-scoped exception filter. The last available scope is the global-scoped exception filter.

    - server.ts + + {{ 'server' | extension: globalScopedFilterT.isJsActive }} + +
    {{ globalScopedFilter }}

    The global filters are used across entire application, for every controller, every route handler. diff --git a/src/app/homepage/pages/exception-filters/exception-filters.component.ts b/src/app/homepage/pages/exception-filters/exception-filters.component.ts index 5d7145661e..c6ccd1ba58 100644 --- a/src/app/homepage/pages/exception-filters/exception-filters.component.ts +++ b/src/app/homepage/pages/exception-filters/exception-filters.component.ts @@ -25,6 +25,16 @@ async create(@Body() createCatDto: CreateCatDto) { `; } + get createMethodJs() { + return ` +@Post() +@Bind(Body()) +async create(createCatDto) { + throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); +} +`; + } + get forbiddenResponse() { return ` { @@ -50,6 +60,15 @@ async create(@Body() createCatDto: CreateCatDto) { }`; } + get forbiddenCreateMethodJs() { + return ` +@Post() +@Bind(Body()) +async create(createCatDto) { + throw new ForbiddenException(); +}`; +} + get httpExceptionFilter() { return ` import { ExceptionFilter, Catch } from '@nestjs/common'; @@ -68,6 +87,24 @@ export class HttpExceptionFilter implements ExceptionFilter { }`; } + get httpExceptionFilterJs() { + return ` +import { Catch } from '@nestjs/common'; +import { HttpException } from '@nestjs/core'; + +@Catch(HttpException) +export class HttpExceptionFilter { + catch(exception, response) { + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + message: \`It's a message from the exception filter\`, + }); + } +}`; + } + get forbiddenCreateMethodWithFilter() { return ` @Post() @@ -78,6 +115,18 @@ async create(@Body() createCatDto: CreateCatDto) { `; } + get forbiddenCreateMethodWithFilterJs() { + return ` +@Post() +@UseFilters(new HttpExceptionFilter()) +@Bind(Body()) +async create(createCatDto) { + throw new ForbiddenException(); +} +`; + } + + get controllerScopedFilter() { return ` @UseFilters(new HttpExceptionFilter()) diff --git a/src/app/homepage/pages/faq/express-instance/express-instance.component.html b/src/app/homepage/pages/faq/express-instance/express-instance.component.html index ee6d7e2a58..b616275413 100644 --- a/src/app/homepage/pages/faq/express-instance/express-instance.component.html +++ b/src/app/homepage/pages/faq/express-instance/express-instance.component.html @@ -1,7 +1,7 @@

    Express Instance

    - Something you might wanna have a full control of the express instance lifecycle. + Sometimes you might wanna have a full control of the express instance lifecycle. It's pretty easy since NestFactory.create() method takes the express instance as a second argument.

    {{ expressInstance }}
    diff --git a/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.html b/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.html index 0a82880ff8..ad1b7f704d 100644 --- a/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.html +++ b/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.html @@ -5,5 +5,9 @@

    Lifecycle Events

    It's a good practice to use them for all the initialization stuff and avoid to put anything directly in the constructor. The constructor should be used only to initialize the class members and inject the required dependencies.

    -
    {{ lifecycleEvents }}
    + + + +
    {{ lifecycleEvents }}
    +
    {{ lifecycleEventsJs }}
    \ No newline at end of file diff --git a/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.ts b/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.ts index 2f00f12cf3..2bb7381f0c 100644 --- a/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.ts +++ b/src/app/homepage/pages/faq/lifecycle-events/lifecycle-events.component.ts @@ -10,16 +10,31 @@ import { BasePageComponent } from '../../page/page.component'; export class LifecycleEventsComponent extends BasePageComponent { get lifecycleEvents() { return ` -import { OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { Component, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; @Component() export class UsersService implements OnModuleInit, OnModuleDestroy { - onModuleInit() { - console.log('Module's initialized...'); - } - onModuleDestroy() { - console.log('Module's destroyed...'); - } + onModuleInit() { + console.log(\`Module's initialized...\`); + } + onModuleDestroy() { + console.log(\`Module's destroyed...\`); + } +}`; + } + + get lifecycleEventsJs() { + return ` +import { Component } from '@nestjs/common'; + +@Component() +export class UsersService { + onModuleInit() { + console.log(\`Module's initialized...\`); + } + onModuleDestroy() { + console.log(\`Module's destroyed...\`); + } }`; } } diff --git a/src/app/homepage/pages/faq/multiple-servers/multiple-servers.component.html b/src/app/homepage/pages/faq/multiple-servers/multiple-servers.component.html index 618f560cc4..9091846459 100644 --- a/src/app/homepage/pages/faq/multiple-servers/multiple-servers.component.html +++ b/src/app/homepage/pages/faq/multiple-servers/multiple-servers.component.html @@ -1,5 +1,5 @@
    -

    Multiple Simultaneous Servers

    +

    HTTPS & Multiple Simultaneous Servers

    Since you can have a full control of the express instance lifecycle, it's not a big deal to create a few multiple simultaneous servers (e.g. both HTTP & HTTPS).

    diff --git a/src/app/homepage/pages/first-steps/first-steps.component.html b/src/app/homepage/pages/first-steps/first-steps.component.html index ce1b497659..63fea6e35b 100644 --- a/src/app/homepage/pages/first-steps/first-steps.component.html +++ b/src/app/homepage/pages/first-steps/first-steps.component.html @@ -5,13 +5,28 @@

    First Steps

    The main idea is to get familiar with essential Nest application building blocks. You'll build a basic CRUD application which features covers a lot of ground at an introductory level.

    +

    Language

    +

    + We're in love with TypeScript, but above all - we love Node.js. + That's why Nest is compatible with both TypeScript and pure JavaScript. + Nest's is taking advantage of latest language features, so to use a framework with simple JavaScript we need a Babel compiler. +

    +

    + In the articles, we're mostly using TypeScript, but you can always switch the code snippets to the JavaScript version when it contains some TypeScript-specific expressions. +

    Setup

    Setting up a new project is quite simple with Starter repository. Just make sure that you have npm installed then use following commands in your OS terminal:

    -
    
    -$ git clone https://github.com/kamilmysliwiec/nest-typescript-starter.git project
    +
    +
    
    +$ git clone https://github.com/nestjs/typescript-starter.git project
    +$ cd project
    +$ npm install
    +$ npm run start
    +
    
    +$ git clone https://github.com/nestjs/javascript-starter.git project
     $ cd project
     $ npm install
     $ npm run start
    @@ -30,7 +45,10 @@

    Setup

    The server.ts contains single async function, which responsibility is to bootstrap our application:

    - server.ts + + {{ 'server' | extension: bootstrapSwitch.isJsActive }} + +
    {{ bootstrap }}

    We should always create the Nest application instance using the NestFactory. diff --git a/src/app/homepage/pages/guards/guards.component.html b/src/app/homepage/pages/guards/guards.component.html index fc0358410d..8e9ed9f640 100644 --- a/src/app/homepage/pages/guards/guards.component.html +++ b/src/app/homepage/pages/guards/guards.component.html @@ -23,8 +23,12 @@

    RolesGuard

    That's why we're gonna create RolesGuard, the guard which permit access only to users with a specific role.

    - roles.guard.ts -
    {{ rolesGuard }}
    + + {{ 'roles.guard' | extension: rolesGuardT.isJsActive }} + + +
    {{ rolesGuard }}
    +
    {{ rolesGuardJs }}

    Every guard provides canActivate() function. The guard might return its boolean answer synchronously or asynchronously (Promise or Observable). This return value controls the Nest behavior: @@ -39,18 +43,26 @@

    RolesGuard

    The second argument is a context. This object fulfills ExecutionContext interface and contains 2 members - parent and handler. The parent holds the type of the Controller class, which the handler belongs to. The handler is a reference to the route handler function.

    +
    + Hint Since canActivate() method can return Promise, it can be marked as async. +

    Usage

    - The guards can be controller-scoped and method-scoped. To setup the guard, we're using @UseGuards() decorator. This decorator takes infinite count of arguments. + The guards can be controller-scoped, method-scoped and global-scoped. To setup the guard, we're using @UseGuards() decorator. This decorator takes infinite count of arguments.

    - cats.controller.ts + + {{ 'cats.controller' | extension: useGuardsT.isJsActive }} + +
    {{ useGuards }}
    Notice The @UseGuards() decorator is imported from the @nestjs/common package.

    - Above construction attaches the guard to the every handler declared by this controller. If we'd decide to restrict only one of them, we just need to setup guard at method level. -

    + Above construction attaches the guard to the every handler declared by this controller. If we'd decide to restrict only one of them, we just need to setup the guard at method level. + To bind the global guard, we're using the useGlobalGuards() method of the Nest application instance: +

    +
    {{ globalGuard }}

    Reflector

    The guard is working now, but we're still not taking advantage of the most important guards feature, so the execution context. @@ -62,8 +74,12 @@

    Reflector

    That's why along with the guards, Nest provides the ability to attach custom metadata through @ReflectMetadata() decorator.

    - cats.controller.ts -
    {{ reflectMetadata }}
    + + {{ 'cats.controller' | extension: reflectMetadataT.isJsActive }} + + +
    {{ reflectMetadata }}
    +
    {{ reflectMetadataJs }}
    Notice The @ReflectMetadata() decorator is imported from the @nestjs/common package.
    @@ -71,19 +87,31 @@

    Reflector

    With above construction, we attached the roles metadata to the create() method. It's not a good practice to use @ReflectMetadata() directly. Instead, you should always create your own decorators.

    - roles.decorator.ts -
    {{ rolesDecorator }}
    + + {{ 'roles.decorator' | extension: rolesDecoratorT.isJsActive }} + + +
    {{ rolesDecorator }}
    +
    {{ rolesDecoratorJs }}

    This way is much cleaner. Since we've @Roles() decorator now, we can use it with create() method.

    - cats.controller.ts -
    {{ catsRolesDecorator }}
    + + {{ 'cats.controller' | extension: catsRolesDecoratorT.isJsActive }} + + +
    {{ catsRolesDecorator }}
    +
    {{ catsRolesDecoratorJs }}

    That's it. Let's focus on the RolesGuard again. Now, it simply returns true immediately, allowing request to proceed. To reflect the metadata, we'll use the Reflector helper class, which is provided out of the box within @nestjs/core.

    - roles.guard.ts -
    {{ rolesGuardExt }}
    + + {{ 'roles.guard' | extension: rolesGuardExtT.isJsActive }} + + +
    {{ rolesGuardExt }}
    +
    {{ rolesGuardExtJs }}
    Notice Guards same as controllers, components, interceptors and middlewares can inject dependencies through constructor.
    @@ -96,7 +124,11 @@

    Reflector

    We could make this guard even more generic if we'd add also the controller reflection part. To extract controller metadata, we're just using parent instead of handler function.

    -
    {{ controllerMetadata }}
    + + + +
    {{ controllerMetadata }}
    +
    {{ controllerMetadataJs }}

    Now, when user would try to call the /cats POST endpoint without enough privileges, Nest will automatically return below response: @@ -106,4 +138,8 @@

    Reflector

    In fact, the guard which returns false forces Nest to throw HttpException. This exception can be catched by the exception filter.

    +

    Custom error responses

    +

    + To change the default access refusal response, just throw a HttpException instead of returning false. +

    diff --git a/src/app/homepage/pages/guards/guards.component.ts b/src/app/homepage/pages/guards/guards.component.ts index e3d2f9115c..4771ab8d14 100644 --- a/src/app/homepage/pages/guards/guards.component.ts +++ b/src/app/homepage/pages/guards/guards.component.ts @@ -21,6 +21,18 @@ export class RolesGuard implements CanActivate { }`; } + get rolesGuardJs() { + return ` +import { Guard } from '@nestjs/common'; + +@Guard() +export class RolesGuard { + canActivate(dataOrRequest, context) { + return true; + } +}`; + } + get useGuards() { return ` @Controller('cats') @@ -38,6 +50,16 @@ async create(@Body() createCatDto: CreateCatDto) { }`; } + get reflectMetadataJs() { + return ` +@Post() +@ReflectMetadata('roles', ['admin']) +@Bind(Body()) +async create(createCatDto) { + this.catsService.create(createCatDto); +}`; + } + get rolesDecorator() { return ` import { ReflectMetadata } from '@nestjs/common'; @@ -45,6 +67,13 @@ import { ReflectMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);`; } + get rolesDecoratorJs() { + return ` +import { ReflectMetadata } from '@nestjs/common'; + +export const Roles = (...roles) => ReflectMetadata('roles', roles);`; + } + get catsRolesDecorator() { return ` @Post() @@ -54,6 +83,16 @@ async create(@Body() createCatDto: CreateCatDto) { }`; } + get catsRolesDecoratorJs() { + return ` +@Post() +@Roles('admin') +@Bind(Body()) +async create(createCatDto) { + this.catsService.create(createCatDto); +}`; + } + get rolesGuardExt() { return ` import { Guard, CanActivate, ExecutionContext } from '@nestjs/common'; @@ -78,11 +117,42 @@ export class RolesGuard implements CanActivate { }`; } + get rolesGuardExtJs() { + return ` +import { Guard, Dependencies } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; + +@Guard() +@Dependencies(Reflector) +export class RolesGuard { + constructor(reflector) { + this.reflector = reflector; + } + + canActivate(req, context) { + const { parent, handler } = context; + const roles = this.reflector.get('roles', handler); + if (!roles) { + return true; + } + + const user = req.user; + const hasRole = () => !!user.roles.find((role) => !!roles.find((item) => item === role)); + return user && user.roles && hasRole(); + } +}`; + } + get controllerMetadata() { return ` const roles = this.reflector.get('roles', parent);`; } + get controllerMetadataJs() { + return ` +const roles = this.reflector.get('roles', parent);`; + } + get forbidden() { return ` { @@ -90,4 +160,10 @@ const roles = this.reflector.get('roles', parent);`; "message": "Forbidden resource" }`; } + + get globalGuard() { + return ` +const app = await NestFactory.create(ApplicationModule); +app.useGlobalGuards(new RolesGuard());`; + } } \ No newline at end of file diff --git a/src/app/homepage/pages/interceptors/interceptors.component.html b/src/app/homepage/pages/interceptors/interceptors.component.html index b2ba537616..6aa665d956 100644 --- a/src/app/homepage/pages/interceptors/interceptors.component.html +++ b/src/app/homepage/pages/interceptors/interceptors.component.html @@ -31,8 +31,12 @@

    Before / After

    Let's create a simple LoggingInterceptor.

    - logging.interceptor.ts -
    {{ loggingInterceptor }}
    + + {{ 'logging.interceptor' | extension: loggingInterceptorT.isJsActive }} + + +
    {{ loggingInterceptor }}
    +
    {{ loggingInterceptorJs }}
    Notice Interceptors same as controllers, components, guards and middlewares can inject dependencies through constructor.
    @@ -41,25 +45,39 @@

    Before / After

    To setup the interceptor, we're using @UseInterceptors() decorator imported from the @nestjs/common package. - Same as guards, interceptors can be controller-scoped and method-scoped. + Same as guards, interceptors can be controller-scoped, method-scoped and global-scoped.

    - cats.controller.ts + + {{ 'cats.controller' | extension: useLoggingInterceptorT.isJsActive }} + +
    {{ useLoggingInterceptor }}

    Now every route handler within CatsController is using LoggingInterceptor. When someone would call GET /cats endpoint, you'll see similar output in your console window:

    {{ consoleOutput }}
    +

    + To bind the global interceptor, we're using the useGlobalInterceptors() method of the Nest application instance: +

    +
    {{ globalInterceptors }}

    Response mapping

    The stream$ is an Observable. This object contains the value returned from the route handler, so we can easily mutate it using map() operator.

    +
    + Notice The response mapping doesn't work with express response strategy (using @Res() object directly is not possible). +

    Let's create the TransformInterceptor which will pack the response and assign it to the data property.

    - transform.interceptor.ts -
    {{ transformInterceptor }}
    + + {{ 'transform.interceptor' | extension: transformInterceptorT.isJsActive }} + + +
    {{ transformInterceptor }}
    +
    {{ transformInterceptorJs }}
    Hint The intercept() method can be async.
    @@ -71,15 +89,23 @@

    Exception mapping

    Since stream$ is an Observable, we can use catch() operator to override throwed exception with a new stream:

    - exception.interceptor.ts -
    {{ exceptionMapping }}
    + + {{ 'exception.interceptor' | extension: exceptionMappingT.isJsActive }} + + +
    {{ exceptionMapping }}
    +
    {{ exceptionMappingJs }}

    Stream overriding

    Sometimes we might want to completely prevent calling the handler and return different value instead. The excellent example is a cache interceptor, which would store the cached responses with some ttl.

    - cache.interceptor.ts -
    {{ cacheInterceptor }}
    + + {{ 'cache.interceptor' | extension: cacheInterceptorT.isJsActive }} + + +
    {{ cacheInterceptor }}
    +
    {{ cacheInterceptorJs }}

    Here's a CacheInterceptor with hardcoded isCached variable and the cached response []. Since we're returning new stream created through of operator, the route handler won't be called. diff --git a/src/app/homepage/pages/interceptors/interceptors.component.ts b/src/app/homepage/pages/interceptors/interceptors.component.ts index f3b1f0e4d5..346fa0ac98 100644 --- a/src/app/homepage/pages/interceptors/interceptors.component.ts +++ b/src/app/homepage/pages/interceptors/interceptors.component.ts @@ -27,6 +27,24 @@ export class LoggingInterceptor implements NestInterceptor { }`; } + get loggingInterceptorJs() { + return ` +import { Interceptor } from '@nestjs/common'; +import 'rxjs/add/operator/do'; + +@Interceptor() +export class LoggingInterceptor { + intercept(dataOrRequest, context, stream$) { + console.log('Before...'); + const now = Date.now(); + + return stream$.do( + () => console.log(\`After... \${Date.now() - now}ms\`), + ); + } +}`; + } + get useLoggingInterceptor() { return ` @UseInterceptors(LoggingInterceptor) @@ -53,6 +71,19 @@ export class TransformInterceptor implements NestInterceptor { }`; } + get transformInterceptorJs() { + return ` +import { Interceptor } from '@nestjs/common'; +import 'rxjs/add/operator/map'; + +@Interceptor() +export class TransformInterceptor { + intercept(dataOrRequest, context, stream$) { + return stream$.map((data) => ({ data })); + } +}`; + } + get dataResponse() { return ` { @@ -78,6 +109,24 @@ export class CacheInterceptor implements NestInterceptor { }`; } + get cacheInterceptorJs() { + return ` +import { Interceptor } from '@nestjs/common'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; + +@Interceptor() +export class CacheInterceptor { + intercept(dataOrRequest, context, stream$) { + const isCached = true; + if (isCached) { + return Observable.of([]); + } + return stream$; + } +}`; + } + get exceptionMapping() { return ` import { Interceptor, NestInterceptor, ExecutionContext, HttpStatus } from '@nestjs/common'; @@ -95,4 +144,28 @@ export class ExceptionInterceptor implements NestInterceptor { } }`; } + + get exceptionMappingJs() { + return ` +import { Interceptor, HttpStatus } from '@nestjs/common'; +import { HttpException } from '@nestjs/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/observable/throw'; + +@Interceptor() +export class ExceptionInterceptor { + intercept(dataOrRequest, context, stream$) { + return stream$.catch((err) => Observable.throw( + new HttpException('Exception interceptor message', HttpStatus.BAD_GATEWAY), + )); + } +}`; + } + + get globalInterceptors() { + return ` +const app = await NestFactory.create(ApplicationModule); +app.useGlobalInterceptors(new LoggingInterceptor());`; + } } \ No newline at end of file diff --git a/src/app/homepage/pages/introduction/introduction.component.html b/src/app/homepage/pages/introduction/introduction.component.html index 16409bece2..4896f1de6d 100644 --- a/src/app/homepage/pages/introduction/introduction.component.html +++ b/src/app/homepage/pages/introduction/introduction.component.html @@ -1,6 +1,6 @@

    Introduction

    -

    Nest is a framework for building efficient, scalable Node.js web applications. It uses modern JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reactive Programming).

    +

    Nest is a framework for building efficient, scalable Node.js web applications. It uses modern JavaScript, is built with TypeScript (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

    Under the hood, Nest makes use of Express, allowing for easy use of the myriad third-party plugins which are available.

    Philosophy

    @@ -9,7 +9,7 @@

    Philosophy

    Features

      -
    • Built with TypeScript
    • +
    • Built with TypeScript (compatible with pure JavaScript + Babel)
    • Easy to learn - syntax similar to Angular
    • Familiar - based on well-known libraries (Express / socket.io)
    • Dependency Injection - built-in asynchronous IoC container with a hierarchical injector
    • @@ -26,7 +26,14 @@

      Features

      Installation

      Install the TypeScript starter project with Git:

      
      -$ git clone https://github.com/kamilmysliwiec/nest-typescript-starter.git project
      +$ git clone https://github.com/nestjs/nest-typescript-starter.git project
      +$ cd project
      +$ npm install
      +$ npm run start
      + +

      Install the JavaScript starter project with Git:

      +
      
      +$ git clone https://github.com/nestjs/javascript-starter.git project
       $ cd project
       $ npm install
       $ npm run start
      @@ -37,6 +44,6 @@

      Installation

      People

    diff --git a/src/app/homepage/pages/microservices/basics/basics.component.html b/src/app/homepage/pages/microservices/basics/basics.component.html index 4ff6610659..fa6467bdae 100644 --- a/src/app/homepage/pages/microservices/basics/basics.component.html +++ b/src/app/homepage/pages/microservices/basics/basics.component.html @@ -12,10 +12,13 @@

    Basics

    Let's create a simple microservice which will be listening to messages via TCP protocol. We're gonna start from the bootstrap() function.

    - server.ts + + {{ 'server' | extension: bootstrapT.isJsActive }} + +
    {{ bootstrap }}
    - Notice The Transport is a helper enum. + Notice Transport is a helper enum.

    The second argument of the createMicroservice() method is an options object. This object may have 3 members: @@ -43,8 +46,12 @@

    Patterns

    The Nest microservice recognizes the messages via patterns. The pattern is a plain JavaScript value - object, string, or even number. To create a pattern handler, we're using the @MessagePattern() decorator imported from the @nestjs/microservices package.

    - math.controller.ts -
    {{ mathController }}
    + + {{ 'math.controller' | extension: mathControllerT.isJsActive }} + + +
    {{ mathController }}
    +
    {{ mathControllerJs }}
    Notice We can register the pattern handlers only inside the @Controller() class.
    @@ -58,8 +65,12 @@

    Asynchronous responses

    Each pattern handler can be async, so you're able to return the Promise. Moreover, you can return the RxJS Observable, so the values would be emitted until the stream is completed.

    - math.controller.ts -
    {{ streaming }}
    + + {{ 'math.controller' | extension: streamingT.isJsActive }} + + +
    {{ streaming }}
    +
    {{ streamingJs }}

    Above message handler will respond 3 times (with each item from the array).

    @@ -75,7 +86,11 @@

    Client

    The ClientProxy has a send() method. This method is intended to call the microservice and returns the Observable with its response.

    -
    {{ sendMethod }}
    + + + +
    {{ sendMethod }}
    +
    {{ sendMethodJs }}

    It takes 2 arguments, the pattern and data. The pattern has to be same as this declared in the @MessagePattern() decorator. That's all. diff --git a/src/app/homepage/pages/microservices/basics/basics.component.ts b/src/app/homepage/pages/microservices/basics/basics.component.ts index 39effc92a5..fb50be1dcc 100644 --- a/src/app/homepage/pages/microservices/basics/basics.component.ts +++ b/src/app/homepage/pages/microservices/basics/basics.component.ts @@ -28,7 +28,6 @@ bootstrap(); return ` import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; -import { Observable } from 'rxjs/Observable'; @Controller() export class MathController { @@ -39,6 +38,20 @@ export class MathController { }`; } + get mathControllerJs() { + return ` +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; + +@Controller() +export class MathController { + @MessagePattern({ cmd: 'sum' }) + sum(data) { + return (data || []).reduce((a, b) => a + b); + } +}`; + } + get streaming() { return ` @MessagePattern({ cmd: 'sum' }) @@ -47,6 +60,14 @@ sum(data: number[]): Observable { }`; } + get streamingJs() { + return ` +@MessagePattern({ cmd: 'sum' }) +sum(data) { + return Observable.from([1, 2, 3]); +}`; + } + get clientDecorator() { return ` @Client({ transport: Transport.TCP, port: 5667 }) @@ -61,6 +82,17 @@ call(): Observable { const data = [1, 2, 3, 4, 5]; return this.client.send(pattern, data); +}`; + } + + get sendMethodJs() { + return ` +@Get() +call() { + const pattern = { cmd: 'sum' }; + const data = [1, 2, 3, 4, 5]; + + return this.client.send(pattern, data); }`; } } \ No newline at end of file diff --git a/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.html b/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.html index 5f8a046438..9d03f6be39 100644 --- a/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.html +++ b/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.html @@ -8,8 +8,12 @@

    Server

    Let's start from the RabbitMQServer which will match incoming messages to the right message handlers.

    - rabbitmq-server.ts -
    {{ rabbitMqServer }}
    + + {{ 'rabbitmq-server' | extension: rabbitMqServerT.isJsActive }} + + +
    {{ rabbitMqServer }}
    +
    {{ rabbitMqServerJs }}

    The CustomTransportStrategy forces to implement two fundamental methods - listen() and close(). Moreover, the RabbitMQServer shall extends the abstract Server class. This class supplies the core getHandlers() and send() methods, and helper transformToObservable() method. @@ -17,7 +21,10 @@

    Server

    The last step is to setup the RabbitMQServer:

    - server.ts + + {{ 'server' | extension: setupServerT.isJsActive }} + +
    {{ setupServer }}

    Client

    @@ -25,8 +32,12 @@

    Client

    Now it's time to create a client class, which shall extends the abstract ClientProxy class. To make it work, we only have to override sendSingleMessage() method.

    - rabbitmq-client.ts -
    {{ rabbitMqClient }}
    + + {{ 'rabbitmq-client' | extension: rabbitMqClientT.isJsActive }} + + +
    {{ rabbitMqClient }}
    +
    {{ rabbitMqClientJs }}

    Earlier, the Nest was responsible for creating the instance of the client class. We've been using the @Client() decorator. Now, when we've created our own solution, we can just create the RabbitMQClient instance directly, using new operator. diff --git a/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.ts b/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.ts index bd68610e99..7f5d5523f3 100644 --- a/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.ts +++ b/src/app/homepage/pages/microservices/custom-transport/custom-transport.component.ts @@ -65,6 +65,63 @@ export class RabbitMQServer extends Server implements CustomTransportStrategy { }`; } + get rabbitMqServerJs() { + return ` +import * as amqp from 'amqplib'; +import { Server } from '@nestjs/microservices'; +import { Observable } from 'rxjs/Observable'; + +export class RabbitMQServer extends Server { + constructor(host, queue) { + super(); + + this.host = host; + this.queue = queue; + this.server = null; + this.channel = null; + } + + async listen(callback) { + await this.init(); + this.channel.consume(\`\${this.queue}_sub\`, this.handleMessage.bind(this), { + noAck: true, + }); + } + + close() { + this.channel && this.channel.close(); + this.server && this.server.close(); + } + + async handleMessage(message) { + const { content } = message; + const messageObj = JSON.parse(content.toString()); + + const handlers = this.getHandlers(); + const pattern = JSON.stringify(messageObj.pattern); + if (!this.messageHandlers[pattern]) { + return; + } + + const handler = this.messageHandlers[pattern]; + const response$ = this.transformToObservable(await handler(messageObj.data)); + response$ && this.send(response$, (data) => this.sendMessage(data)); + } + + sendMessage(message) { + const buffer = Buffer.from(JSON.stringify(message)); + this.channel.sendToQueue(\`\${this.queue}_pub\`, buffer); + } + + async init() { + this.server = await amqp.connect(this.host); + this.channel = await this.server.createChannel(); + this.channel.assertQueue(\`\${this.queue}_sub\`, { durable: false }); + this.channel.assertQueue(\`\${this.queue}_pub\`, { durable: false }); + } +}`; + } + get setupServer() { return ` const app = await NestFactory.createMicroservice(ApplicationModule, { @@ -111,8 +168,48 @@ export class RabbitMQClient extends ClientProxy { }`; } + get rabbitMqClientJs() { + return ` +import * as amqp from 'amqplib'; +import { ClientProxy } from '@nestjs/microservices'; + +export class RabbitMQClient extends ClientProxy { + constructor(host, queue) { + super(); + + this.host = host; + this.queue = queue; + } + + async sendSingleMessage(messageObj, callback) { + const server = await amqp.connect(this.host); + const channel = await server.createChannel(); + + const { sub, pub } = this.getQueues(); + channel.assertQueue(sub, { durable: false }); + channel.assertQueue(pub, { durable: false }); + + channel.consume(pub, (message) => this.handleMessage(message, server, callback), { noAck: true }); + channel.sendToQueue(sub, Buffer.from(JSON.stringify(messageObj))); + } + + handleMessage(message, server, callback) { + const { content } = message; + const { err, response, disposed } = JSON.parse(content.toString()); + if (disposed) { + server.close(); + } + callback(err, response, disposed); + } + + getQueues() { + return { pub: \`\${this.queue}_pub\`, sub: \`\${this.queue}_sub\` }; + } +}`; + } + get rabbitMqClientNew() { return ` -private readonly client = new RabbitMQClient('amqp://localhost', 'example');`; +this.client = new RabbitMQClient('amqp://localhost', 'example');`; } } diff --git a/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.html b/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.html index a8b7b2d238..e7e473742d 100644 --- a/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.html +++ b/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.html @@ -17,8 +17,12 @@

    Exception Filters

    The Exception Filters are also really similar, and works exactly in the same way as the primary ones. The only difference is that catch() method should returns an Observable.

    - rpc-exception.filter.ts -
    {{ rpcExceptionFilter }}
    + + {{ 'rpc-exception.filter' | extension: rpcExceptionFilterT.isJsActive }} + + +
    {{ rpcExceptionFilter }}
    +
    {{ rpcExceptionFilterJs }}
    Notice It's impossible to setup the microservices exception filters globally.
    diff --git a/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.ts b/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.ts index 11f9e5ee41..d0f5e6b57b 100644 --- a/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.ts +++ b/src/app/homepage/pages/microservices/exception-filters/exception-filters.component.ts @@ -33,6 +33,21 @@ export class ExceptionFilter implements RpcExceptionFilter { catch(exception: RpcException): Observable { return Observable.throw(exception.getError()); } +}`; + } + + get rpcExceptionFilterJs() { + return ` +import { Catch } from '@nestjs/common'; +import { Observable } from 'rxjs/Observable'; +import { RpcException } from '@nestjs/microservices'; +import 'rxjs/add/observable/throw'; + +@Catch(RpcException) +export class ExceptionFilter { + catch(exception) { + return Observable.throw(exception.getError()); + } }`; } } \ No newline at end of file diff --git a/src/app/homepage/pages/microservices/redis/redis.component.html b/src/app/homepage/pages/microservices/redis/redis.component.html index 0b23a73015..e6da60eff1 100644 --- a/src/app/homepage/pages/microservices/redis/redis.component.html +++ b/src/app/homepage/pages/microservices/redis/redis.component.html @@ -7,7 +7,10 @@

    Redis

    To switch from the TCP transport strategy to Redis pub/sub, we only need to change the options object passed to the createMicroservice() method.

    - server.ts + + {{ 'server' | extension: optionsT.isJsActive }} + +
    {{ options }}
    Hint Don't forget to change the Transport.REDIS in the @Client() decorator too. diff --git a/src/app/homepage/pages/middlewares/middlewares.component.html b/src/app/homepage/pages/middlewares/middlewares.component.html index b7831accdd..941d6cd10d 100644 --- a/src/app/homepage/pages/middlewares/middlewares.component.html +++ b/src/app/homepage/pages/middlewares/middlewares.component.html @@ -11,8 +11,12 @@

    Middlewares

    Middleware is a class with @Middleware() decorator. This class should implement the NestMiddleware interface. Let's create an example, the LoggerMiddleware class.

    - logger.middleware.ts -
    {{ loggerMiddleware }}
    + + {{ 'logger.middleware' | extension: loggerMiddlewareT.isJsActive }} + + +
    {{ loggerMiddleware }}
    +
    {{ loggerMiddlewareJs }}

    The resolve() method has to return the regular expressjs middleware (req, res, next) => void.

    @@ -27,8 +31,12 @@

    Where to put the middlewares?

    Modules which include middlewares have to implement the NestModule interface. Let's setup the LoggerMiddleware at the ApplicationModule level.

    - app.module.ts -
    {{ applicationModule }}
    + + {{ 'app.module' | extension: applicationModuleT.isJsActive }} + + +
    {{ applicationModule }}
    +
    {{ applicationModuleJs }}
    Hint We could pass here (inside forRoutes()) the single object and just use RequestMethod.ALL.
    @@ -41,8 +49,12 @@

    Where to put the middlewares?

    The forRoutes() can take single object, multiple objects, controller class and even multiple controller classes. In most cases you'll probably just pass the controllers and separate them by comma. Below is an example with the single controller:

    - app.module.ts -
    {{ applicationModuleByControllers }}
    + + {{ 'app.module' | extension: applicationModuleByControllersT.isJsActive }} + + +
    {{ applicationModuleByControllers }}
    +
    {{ applicationModuleByControllersJs }}
    Hint The apply() method can take single middleware or an array of middlewares.
    @@ -51,14 +63,22 @@

    Pass arguments to the middleware

    Sometimes the behaviour of the middleware depends on the custom values e.g. array of user roles, options object etc. We can pass additional arguments to the resolve() using with() method. See below example:

    - app.module.ts -
    {{ applicationModuleWithMethod }}
    + + {{ 'app.module' | extension: applicationModuleWithMethodT.isJsActive }} + + +
    {{ applicationModuleWithMethod }}
    +
    {{ applicationModuleWithMethodJs }}

    We passed custom string - ApplicationModule to the with() method. Now we've to adjust the resolve() method of the LoggerMiddleware.

    - logger.middleware.ts -
    {{ loggerMiddlewareWithArgs }}
    + + {{ 'logger.middleware' | extension: loggerMiddlewareWithArgsT.isJsActive }} + + +
    {{ loggerMiddlewareWithArgs }}
    +
    {{ loggerMiddlewareWithArgsJs }}

    The value of the name property will be ApplicationModule.

    @@ -67,8 +87,12 @@

    Async Middlewares

    There's no contraindications to return the async function from the resolve() method. Also, it's possible to make the resolve() method async too. This pattern is called deffered middleware.

    - logger.middleware.ts -
    {{ defferedMiddleware }}
    + + {{ 'logger.middleware' | extension: defferedMiddlewareT.isJsActive }} + + +
    {{ defferedMiddleware }}
    +
    {{ defferedMiddlewareJs }}

    Functional Middlewares

    The LoggerMiddleware is quite short. It has no members, no additional methods, no dependencies. @@ -76,13 +100,20 @@

    Functional Middlewares

    This type of the middleware is called functional middleware. Let's transform the logger into the function.

    - logger.middleware.ts + + {{ 'logger.middleware' | extension: functionalMiddlewareT.isJsActive }} + +
    {{ functionalMiddleware }}

    Now use it within the ApplicationModule.

    - app.module.ts -
    {{ applyFunctionalMiddleware }}
    + + {{ 'app.module' | extension: applyFunctionalMiddlewareT.isJsActive }} + + +
    {{ applyFunctionalMiddleware }}
    +
    {{ applyFunctionalMiddlewareJs }}
    Hint Let's consider to use functional middlewares everytime when your middleware doesn't have any dependencies.
    diff --git a/src/app/homepage/pages/middlewares/middlewares.component.ts b/src/app/homepage/pages/middlewares/middlewares.component.ts index 0605275fc8..5ff053f540 100644 --- a/src/app/homepage/pages/middlewares/middlewares.component.ts +++ b/src/app/homepage/pages/middlewares/middlewares.component.ts @@ -18,7 +18,22 @@ export class LoggerMiddleware implements NestMiddleware { console.log('Request...'); next(); }; - } + } +}`; + } + + get loggerMiddlewareJs() { + return ` +import { Middleware } from '@nestjs/common'; + +@Middleware() +export class LoggerMiddleware { + resolve(...args) { + return (req, res, next) => { + console.log('Request...'); + next(); + }; + } }`; } @@ -41,6 +56,25 @@ export class ApplicationModule implements NestModule { }`; } + get applicationModuleJs() { + return ` +import { Module, RequestMethod } from '@nestjs/common'; +import { LoggerMiddleware } from './common/middlewares/logger.middleware'; +import { CatsModule } from './cats/cats.module'; + +@Module({ + modules: [CatsModule], +}) +export class ApplicationModule { + configure(consumer) { + consumer.apply(LoggerMiddleware).forRoutes( + { path: '/cats', method: RequestMethod.GET }, + { path: '/cats', method: RequestMethod.POST }, + ); + } +}`; + } + get applicationModuleByControllers() { return ` import { Module, NestModule, MiddlewaresConsumer, RequestMethod } from '@nestjs/common'; @@ -57,6 +91,22 @@ export class ApplicationModule implements NestModule { }`; } + get applicationModuleByControllersJs() { + return ` +import { Module, RequestMethod } from '@nestjs/common'; +import { LoggerMiddleware } from './common/middlewares/logger.middleware'; +import { CatsModule } from './cats/cats.module'; + +@Module({ + modules: [CatsModule], +}) +export class ApplicationModule { + configure(consumer) { + consumer.apply(LoggerMiddleware).forRoutes(CatsController); + } +}`; + } + get applicationModuleWithMethod() { return ` import { Module, NestModule, MiddlewaresConsumer } from '@nestjs/common'; @@ -76,6 +126,25 @@ export class ApplicationModule implements NestModule { }`; } + get applicationModuleWithMethodJs() { + return ` +import { Module } from '@nestjs/common'; +import { LoggerMiddleware } from './common/middlewares/logger.middleware'; +import { CatsModule } from './cats/cats.module'; +import { CatsController } from './cats/cats.controller'; + +@Module({ + modules: [CatsModule], +}) +export class ApplicationModule { + configure(consumer) { + consumer.apply(LoggerMiddleware) + .with('ApplicationModule') + .forRoutes(CatsController); + } +}`; + } + get loggerMiddlewareWithArgs() { return ` import { Middleware, NestMiddleware, ExpressMiddleware } from '@nestjs/common'; @@ -91,6 +160,21 @@ export class LoggerMiddleware implements NestMiddleware { }`; } + get loggerMiddlewareWithArgsJs() { + return ` +import { Middleware } from '@nestjs/common'; + +@Middleware() +export class LoggerMiddleware { + resolve(name) { + return (req, res, next) => { + console.log(\`[\${name}\] Request...\`); // [ApplicationModule] Request... + next(); + }; + } +}`; + } + get defferedMiddleware() { return ` import { Middleware, NestMiddleware, ExpressMiddleware } from '@nestjs/common'; @@ -108,6 +192,25 @@ export class LoggerMiddleware implements NestMiddleware { } }`; } + + get defferedMiddlewareJs() { + return ` +import { Middleware } from '@nestjs/common'; + +@Middleware() +export class LoggerMiddleware { + async resolve(name) { + await someAsyncFn(); + + return async (req, res, next) => { + await someAsyncFn(); + console.log(\`[\${name}\] Request...\`); // [ApplicationModule] Request... + next(); + }; + } +}`; + } + get functionalMiddleware() { return ` export const loggerMiddleware = (req, res, next) => { @@ -124,12 +227,29 @@ import { CatsModule } from './cats/cats.module'; import { CatsController } from './cats/cats.controller'; @Module({ - modules: [CatsModule], + modules: [CatsModule], }) export class ApplicationModule implements NestModule { - configure(consumer: MiddlewaresConsumer): void { - consumer.apply(loggerMiddleware).forRoutes(CatsController); - } + configure(consumer: MiddlewaresConsumer): void { + consumer.apply(loggerMiddleware).forRoutes(CatsController); + } }`; } + + get applyFunctionalMiddlewareJs() { + return ` +import { Module } from '@nestjs/common'; +import { loggerMiddleware } from './common/middlewares/logger.middleware'; +import { CatsModule } from './cats/cats.module'; +import { CatsController } from './cats/cats.controller'; + +@Module({ + modules: [CatsModule], +}) +export class ApplicationModule { + configure(consumer) { + consumer.apply(loggerMiddleware).forRoutes(CatsController); + } +}`; + } } \ No newline at end of file diff --git a/src/app/homepage/pages/modules/modules.component.html b/src/app/homepage/pages/modules/modules.component.html index 51ef337742..c9993ab929 100644 --- a/src/app/homepage/pages/modules/modules.component.html +++ b/src/app/homepage/pages/modules/modules.component.html @@ -40,13 +40,19 @@

    CatsModule

    The CatsController and CatsService belong to the same application domain. They should be moved to the feature module, the CatsModule.

    - cats/cats.module.ts + + {{ 'cats/cats.module' | extension: catsModuleT.isJsActive }} + +
    {{ catsModule }}

    I've created the cats.module.ts file and moved everything related to this module into the cats directory. The last thing we need to do is to import this module into the root module which name is ApplicationModule.

    - app.module.ts + + {{ 'app.module' | extension: appModuleT.isJsActive }} + +
    {{ appModule }}

    It's everything. Now Nest knows that besides ApplicationModule, it's essential to register the CatsModule too. @@ -63,7 +69,10 @@

    Shared Module

    Every module is a Shared Module in fact. Once created is reused by the each module. Let's imagine that we're gonna share the CatsService instance between few modules.

    - cats.module.ts + + {{ 'cats.module' | extension: catsModuleSharedT.isJsActive }} + +
    {{ catsModuleShared }}

    Now each module which would import the CatsModule has an access to the CatsService, and will share the same instance with all of the modules which are importing this module too. @@ -86,8 +95,12 @@

    Dependency Injection

    It's natural that module can inject components, which belongs to it (e.g. for the configuration purposes):

    - cats.module.ts -
    {{ catsModuleDi }}
    + + {{ 'cats.module' | extension: catsModuleDiT.isJsActive }} + + +
    {{ catsModuleDi }}
    +
    {{ catsModuleDiJs }}

    However, modules can't be injected by the components, because it creates a circular dependency.

    diff --git a/src/app/homepage/pages/modules/modules.component.ts b/src/app/homepage/pages/modules/modules.component.ts index 0c1cc3f2b4..4d4b1672b8 100644 --- a/src/app/homepage/pages/modules/modules.component.ts +++ b/src/app/homepage/pages/modules/modules.component.ts @@ -79,6 +79,24 @@ export class CatsModule { }`; } + get catsModuleDiJs() { + return ` +import { Module, Dependencies } from '@nestjs/common'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; + +@Module({ + controllers: [CatsController], + components: [CatsService], +}) +@Dependencies(CatsService) +export class CatsModule { + constructor(catsService) { + this.catsService = catsService; + } +}`; + } + get reExportExamle() { return ` @Module({ diff --git a/src/app/homepage/pages/pipes/pipes.component.html b/src/app/homepage/pages/pipes/pipes.component.html index ba578d1ce4..91d64db5f2 100644 --- a/src/app/homepage/pages/pipes/pipes.component.html +++ b/src/app/homepage/pages/pipes/pipes.component.html @@ -18,6 +18,9 @@

    What does it look like?

    validation.pipe.ts
    {{ validationPipe }}
    +
    + Notice The ValidationPipe works only with TypeScript. +

    Every pipe has to provide the transform() method. This method takes two arguments:

    @@ -50,7 +53,7 @@

    What does it look like?

    - Notice TypeScript interfaces are disappearing during the transpilation, so if you'd use interface instead of class, the metatype value will be Object. + Notice TypeScript interfaces dissapear during the transpilation, so if you'd use interface instead of class, the metatype value will be Object.

    How does it work?

    @@ -64,7 +67,7 @@

    How does it work?

    create-cat.dto.ts
    {{ createCatDto }}

    - This object always has to be correct, so we have to validate those three members. + This object always has to be correct, so we have to validate these three members. We could do this inside the route handler method, but we'd break the single responsibility rule (SRP). The second idea is to create validator class and delegate the task there, but we'll have to use this validator every time at the beginning of the method. So what about the validation middleware? It's a good idea, but it's not possible to create a generic middleware, which could be used across entire application. @@ -73,6 +76,7 @@

    How does it work?

    That's the first use-case, when you should consider to use a Pipe.

    Class validator

    +
    This section applies only to TypeScript

    Nest works well with the class-validator, the amazing library which allows to use decorator-based validation. Decorator based validation is really powerful with the pipe abilities, since we've an access to the metatype of the processed property. @@ -111,7 +115,7 @@

    Class validator

    Notice The @UsePipes() decorator is imported from the @nestjs/common package.

    - Since the ValidationPipe was created to be as generic as possible, we're gonna setup this as a global-scoped pipe, for every route handler across entire application. + Since the ValidationPipe was created to be as generic as possible, we're gonna setup it as a global-scoped pipe, for every route handler across entire application.

    server.ts
    {{ globalPipe }}
    @@ -125,4 +129,27 @@

    Transformer Pipe

    Sometimes the data passed from the client needs to undergo some changes. Also, some parts could be missed, so we must apply the default values. The transformer pipes fill the gap between the client request and the request handler.

    + + {{ 'parse-int.pipe' | extension: parseIntPipeT.isJsActive }} + + +
    {{ parseIntPipe }}
    +
    {{ parseIntPipeJs }}
    +

    + Here's a ParseIntPipe which resposibility is to parse string into integer value. + Now let's bind the pipe to the selected param: +

    + + + +
    {{ bindParam }}
    +
    {{ bindParamJs }}
    +

    + Also it's possible to omit the first argument: +

    + + + +
    {{ bindBodyParam }}
    +
    {{ bindBodyParamJs }}
    diff --git a/src/app/homepage/pages/pipes/pipes.component.ts b/src/app/homepage/pages/pipes/pipes.component.ts index b1c7590ab8..f351895d5a 100644 --- a/src/app/homepage/pages/pipes/pipes.component.ts +++ b/src/app/homepage/pages/pipes/pipes.component.ts @@ -121,4 +121,72 @@ async function bootstrap() { } bootstrap();`; } + + get parseIntPipe() { + return ` +import { HttpException } from '@nestjs/core'; +import { PipeTransform, Pipe, ArgumentMetadata, HttpStatus } from '@nestjs/common'; + +@Pipe() +export class ParseIntPipe implements PipeTransform { + async transform(value: string, metadata: ArgumentMetadata) { + const val = parseInt(value, 10); + if (isNaN(val)) { + throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST); + } + return val; + } +}`; + } + + get parseIntPipeJs() { + return ` +import { HttpException } from '@nestjs/core'; +import { Pipe, HttpStatus } from '@nestjs/common'; + +@Pipe() +export class ParseIntPipe { + async transform(value, metadata) { + const val = parseInt(value, 10); + if (isNaN(val)) { + throw new HttpException('Validation failed', HttpStatus.BAD_REQUEST); + } + return val; + } +}`; + } + + get bindParam() { + return ` +@Get(':id') +async findOne(@Param('id', new ParseIntPipe()) id) { + return await this.catsService.findOne(id); +}`; + } + + get bindParamJs() { + return ` +@Get(':id') +@Bind(Param('id', new ParseIntPipe())) +async findOne(id) { + return await this.catsService.findOne(id); +}`; + } + + get bindBodyParam() { + return ` +@Post() +async create(@Body(new CustomPipe()) createCatDto: CreateCatDto) { + await this.catsService.create(createCatDto); +}`; + } + + get bindBodyParamJs() { + return ` +@Post() +@Bind(Body(new CustomPipe())) +async create(createCatDto) { + await this.catsService.create(createCatDto); +}`; + } } diff --git a/src/app/homepage/pages/recipes/cqrs/cqrs.component.html b/src/app/homepage/pages/recipes/cqrs/cqrs.component.html new file mode 100644 index 0000000000..68269bfe52 --- /dev/null +++ b/src/app/homepage/pages/recipes/cqrs/cqrs.component.html @@ -0,0 +1,150 @@ +
    +

    Command Query Responsibility Segregation

    +

    + The flow of the simplest CRUD applications could be described using the following steps: +

    +
      +
    1. Controllers layer handle HTTP requests and delegate tasks to the services.
    2. +
    3. Services layer is the place, where the most of the business logic is being done.
    4. +
    5. Services uses Repositories / DAOs to change / persist entities.
    6. +
    7. Entities are our models - just containers for the values, with setters and getters.
    8. +
    +

    + Is it a good approach? Yes, for sure. In most cases, there's no reason to make small and medium-sized applications more complicated. + But sometimes it's not enough, and when our needs becomes more sophisticated we wanna have scalable systems with straightforward data flow. +

    +

    + That's why Nest provides a lightweight CQRS module, which components are well-described below. +

    +

    Commands

    +

    + To make the application easier to understand, each change has to be preceded by Command. + When any command is dispatched - the application has to react on it. + Commands might be dispatched from the services and consumed in appropriate Command Handlers. +

    + + {{ 'heroes-game.service' | extension: heroGameServiceT.isJsActive }} + + +
    {{ heroGameService }}
    +
    {{ heroGameServiceJs }}
    +

    + Here's a sample service, which dispatches KillDragonCommand. Let's see how the command looks like: +

    + + {{ 'kill-dragon.command' | extension: killDragonCommandT.isJsActive }} + + +
    {{ killDragonCommand }}
    +
    {{ killDragonCommandJs }}
    +

    + The CommandBus is a commands stream. It delegates commands to the equivalent handlers. + Each Command has to have corresponding Command Handler: +

    + + {{ 'kill-dragon.handler' | extension: killDragonHandlerT.isJsActive }} + + +
    {{ killDragonHandler }}
    +
    {{ killDragonHandlerJs }}
    +

    + Now, every application state change is a result of the Command occurrence. + The logic's encapsulated in handlers. If we want we can simply add logging here or even more - we can persist our commands in the database (e.g. for the diagnostics purposes). +

    +

    + Why do we need resolve() function? Sometimes we might wanna return a message from handler to the service. Also, we can just call this function at the beginning of the execute() method, so the application would first turn back into the service and return a response to the client and then asynchronously come back here to process the dispatched command. +

    +

    Events

    +

    + Since we've encapsulated commands in the handlers, we prevent interaction between them - the application structure's still not flexible, not reactive. + The solution's to use events. +

    + + {{ 'hero-killed-dragon.event' | extension: killedDragonEventT.isJsActive }} + + +
    {{ killedDragonEvent }}
    +
    {{ killedDragonEventJs }}
    +

    + Events are asynchronous. They're dispatched by models. + Models have to extend the AggregateRoot class. +

    + + {{ 'hero.model' | extension: heroModelT.isJsActive }} + + +
    {{ heroModel }}
    +
    {{ heroModelJs }}
    +

    + The apply() method does not dispatch events yet because there's no relationship between model and the EventPublisher class. + How to tell the model about the publisher? We need to use a publisher mergeObjectContext() method inside our command handler. +

    + + {{ 'kill-dragon.handler' | extension: mergedT.isJsActive }} + + +
    {{ merged }}
    +
    {{ mergedJs }}
    +

    + Now, everything works as we expected. Notice that we need to commit() events since they're not dispatching immediately. + Of course, an object doesn't have to exist already. We can easily merge type context also: +

    +
    {{ mergedType }}
    +

    + That's it. A model has an ability to publish events now. + We have to handle them. +

    +

    + Each event can have a lot of Event Handlers. + They don't have to know about each other. +

    + + {{ 'hero-killed-dragon.handler' | extension: eventHandlerT.isJsActive }} + + +
    {{ eventHandler }}
    +
    {{ eventHandlerJs }}
    +

    + Now we can move the write logic into the event handlers. +

    +

    Sagas

    +

    + This type of Event-Driven Architecture improves application reactiveness and scalability. + Now, when we have events, we can simply react to them in various manners. + The Sagas are the last building block from the architecture point of view. +

    +

    + The sagas are an incredibly powerful feature. + Single saga may listen for 1..* events. It can combine, merge, filter [...] events streams. + RxJS library is the place where the magic comes from. + In simple words, each saga has to return an Observable which contains a command. This command is dispatched asynchronously. +

    + + {{ 'heroes-game.saga' | extension: sagaT.isJsActive }} + + +
    {{ saga }}
    +
    {{ sagaJs }}
    +

    + We declared a rule that when any hero kills the dragon - it should obtain the ancient item. + Then the DropAncientItemCommand will be dispatched and processed by the appropriate handler. +

    +

    Setup

    +

    + The last thing, which we have to take care of is to set up the entire mechanism. +

    + + {{ 'heroes-game.module' | extension: setupT.isJsActive }} + + +
    {{ setup }}
    +
    {{ setupJs }}
    +

    Summary

    +

    + Both CommandBus and EventBus are Observables. + It means that you can easily subscribe to the whole stream and enrich your application with Event Sourcing. +

    +

    + The full source code's available here. +

    +
    \ No newline at end of file diff --git a/src/app/homepage/pages/recipes/cqrs/cqrs.component.scss b/src/app/homepage/pages/recipes/cqrs/cqrs.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/recipes/cqrs/cqrs.component.spec.ts b/src/app/homepage/pages/recipes/cqrs/cqrs.component.spec.ts new file mode 100644 index 0000000000..3d5431cafd --- /dev/null +++ b/src/app/homepage/pages/recipes/cqrs/cqrs.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CqrsComponent } from './cqrs.component'; + +describe('CqrsComponent', () => { + let component: CqrsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CqrsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CqrsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts b/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts new file mode 100644 index 0000000000..f5e177176e --- /dev/null +++ b/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts @@ -0,0 +1,317 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-cqrs', + templateUrl: './cqrs.component.html', + styleUrls: ['./cqrs.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CqrsComponent extends BasePageComponent { + get heroGameService() { + return ` +@Component() +export class HeroesGameService { + constructor(private readonly commandBus: CommandBus) {} + + async killDragon(heroId: string, killDragonDto: KillDragonDto) { + return await this.commandBus.execute( + new KillDragonCommand(heroId, killDragonDto.dragonId) + ); + } +}`; + } + + get heroGameServiceJs() { + return ` +@Component() +@Dependencies(CommandBus) +export class HeroesGameService { + constructor(commandBus) { + this.commandBus = commandBus; + } + + async killDragon(heroId, killDragonDto) { + return await this.commandBus.execute( + new KillDragonCommand(heroId, killDragonDto.dragonId) + ); + } +}`; + } + + get killDragonCommand() { + return ` +export class KillDragonCommand implements ICommand { + constructor( + public readonly heroId: string, + public readonly dragonId: string) {} +}`; + } + + get killDragonCommandJs() { + return ` +export class KillDragonCommand { + constructor(heroId, dragonId) { + this.heroId = heroId; + this.dragonId = dragonId; + } +}`; + } + + get killDragonHandler() { + return ` +@CommandHandler(KillDragonCommand) +export class KillDragonHandler implements ICommandHandler { + constructor(private readonly repository: HeroRepository) {} + + async execute(command: KillDragonCommand, resolve: (value?) => void) { + const { heroId, dragonId } = command; + const hero = this.repository.findOneById(+heroId); + + hero.killEnemy(dragonId); + await this.repository.persist(hero); + resolve(); + } +}`; + } + + get killDragonHandlerJs() { + return ` +@CommandHandler(KillDragonCommand) +@Dependencies(HeroRepository) +export class KillDragonHandler { + constructor(repository) { + this.repository = repository; + } + + async execute(command, resolve) { + const { heroId, dragonId } = command; + const hero = this.repository.findOneById(+heroId); + + hero.killEnemy(dragonId); + await this.repository.persist(hero); + resolve(); + } +}`; + } + + get killedDragonEvent() { + return ` +export class HeroKilledDragonEvent implements IEvent { + constructor( + public readonly heroId: string, + public readonly dragonId: string) {} +}`; + } + + get killedDragonEventJs() { + return ` +export class HeroKilledDragonEvent { + constructor(heroId, dragonId) { + this.heroId = heroId; + this.dragonId = dragonId; + } +}`; + } + + get heroModel() { + return ` +export class Hero extends AggregateRoot { + constructor(private readonly id: string) { + super(); + } + + killEnemy(enemyId: string) { + // logic + this.apply(new HeroKilledDragonEvent(this.id, enemyId)); + } +}`; + } + + get heroModelJs() { + return ` +export class Hero extends AggregateRoot { + constructor(id) { + super(); + this.id = id; + } + + killEnemy(enemyId) { + // logic + this.apply(new HeroKilledDragonEvent(this.id, enemyId)); + } +}`; + } + + get merged() { + return ` +@CommandHandler(KillDragonCommand) +export class KillDragonHandler implements ICommandHandler { + constructor( + private readonly repository: HeroRepository, + private readonly publisher: EventPublisher, + ) {} + + async execute(command: KillDragonCommand, resolve: (value?) => void) { + const { heroId, dragonId } = command; + const hero = this.publisher.mergeObjectContext( + await this.repository.findOneById(+heroId), + ); + hero.killEnemy(dragonId); + hero.commit(); + resolve(); + } +}`; + } + + get mergedJs() { + return ` +@CommandHandler(KillDragonCommand) +@Dependencies(HeroRepository, EventPublisher) +export class KillDragonHandler { + constructor(repository, publisher) { + this.repository = repository; + this.publisher = publisher; + } + + async execute(command, resolve) { + const { heroId, dragonId } = command; + const hero = this.publisher.mergeObjectContext( + await this.repository.findOneById(+heroId), + ); + hero.killEnemy(dragonId); + hero.commit(); + resolve(); + } +}`; + } + + get mergedType() { + return ` +const HeroModel = this.publisher.mergeContext(Hero); +new HeroModel('id');`; + } + + get eventHandler() { + return ` +@EventsHandler(HeroKilledDragonEvent) +export class HeroKilledDragonHandler implements IEventHandler { + constructor(private readonly repository: HeroRepository) {} + + handle(event: HeroKilledDragonEvent) { + // logic + } +}`; + } + + get eventHandlerJs() { + return ` +@EventsHandler(HeroKilledDragonEvent) +@Dependencies(HeroRepository) +export class HeroKilledDragonHandler { + constructor(repository) { + this.repository = repository; + } + + handle(event) { + // logic + } +}`; + } + + get saga() { + return ` +@Component() +export class HeroesGameSagas { + dragonKilled = (events$: EventObservable): Observable => { + return events$.ofType(HeroKilledDragonEvent) + .map((event) => new DropAncientItemCommand(event.heroId, fakeItemID)); + } +}`; + } + + get sagaJs() { + return ` +@Component() +export class HeroesGameSagas { + dragonKilled = (events$) => { + return events$.ofType(HeroKilledDragonEvent) + .map((event) => new DropAncientItemCommand(event.heroId, fakeItemID)); + } +}`; + } + + get setup() { + return ` +export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; +export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; + +@Module({ + modules: [CQRSModule], + controllers: [HeroesGameController], + components: [ + HeroesGameService, + HeroesGameSagas, + ...CommandHandlers, + ...EventHandlers, + HeroRepository, + ] +}) +export class HeroesGameModule implements OnModuleInit { + constructor( + private readonly moduleRef: ModuleRef, + private readonly command$: CommandBus, + private readonly event$: EventBus, + private readonly heroesGameSagas: HeroesGameSagas) {} + + onModuleInit() { + this.command$.setModuleRef(this.moduleRef); + this.event$.setModuleRef(this.moduleRef); + + this.event$.register(EventHandlers); + this.command$.register(CommandHandlers); + this.event$.combineSagas([ + this.heroesGameSagas.dragonKilled, + ]); + } +}`; + } + + get setupJs() { + return ` +export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; +export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; + +@Module({ + modules: [CQRSModule], + controllers: [HeroesGameController], + components: [ + HeroesGameService, + HeroesGameSagas, + ...CommandHandlers, + ...EventHandlers, + HeroRepository, + ] +}) +@Dependencies(ModuleRef, CommandBus, EventBus, HeroesGameSagas) +export class HeroesGameModule { + constructor(moduleRef, command$, event$, heroesGameSagas) { + this.moduleRef = moduleRef; + this.command$ = command$; + this.event$ = event$; + this.heroesGameSagas = heroesGameSagas; + } + + onModuleInit() { + this.command$.setModuleRef(this.moduleRef); + this.event$.setModuleRef(this.moduleRef); + + this.event$.register(EventHandlers); + this.command$.register(CommandHandlers); + this.event$.combineSagas([ + this.heroesGameSagas.dragonKilled, + ]); + } +}`; + } +} diff --git a/src/app/homepage/pages/recipes/mongodb/mongodb.component.html b/src/app/homepage/pages/recipes/mongodb/mongodb.component.html new file mode 100644 index 0000000000..1151e52f17 --- /dev/null +++ b/src/app/homepage/pages/recipes/mongodb/mongodb.component.html @@ -0,0 +1,94 @@ +
    +

    MongoDB (Mongoose)

    +

    + Mongoose is the most popular MongoDB object modeling tool. + To start the adventure with this library we have to install all of the required dependencies: +

    + + + +
    {{ dependencies }}
    +
    {{ dependenciesJs }}
    +

    + The first step we need to do is to establish the connection with our database using connect() function. + The connect() function returns the MongooseThenable by default, but we can simply override it with the native global.Promise to avoid deprecation warnings. In fact, we're creating an async component here. +

    + + {{ 'database.providers' | extension: databaseProvidersT.isJsActive }} + + +
    {{ databaseProviders }}
    +
    + Hint Following best practices, we've declared the custom component in the separated file which has a *.providers.ts suffix. +
    +

    + Then we need to export these providers to make them accessible for the rest part of the application. +

    + + {{ 'database.module' | extension: databaseModuleT.isJsActive }} + + +
    {{ databaseModule }}
    +

    + It's everything. Now we can inject the Connection object using @Inject() decorator. + Each component which would depend on the Connection async component will wait until the Promise would be resolved. +

    +

    Model injection

    +

    + With Mongoose, everything is derived from a Schema. Let's define the CatSchema: +

    + + {{ 'cats/schemas/cat.schema' | extension: catSchemaT.isJsActive }} + + +
    {{ catSchema }}
    +

    + The CatsSchema belongs to the cats directory. + This directory represents the CatsModule. It's your decision where you gonna keep your schema files. From my point of view, the best way's to hold them nearly their domain, in the appropriate module directory. +

    +

    + Now it's time to create a Model component: +

    + + {{ 'cats.providers' | extension: catsProvidersT.isJsActive }} + + +
    {{ catsProviders }}
    +
    {{ catsProvidersJs }}
    +
    + Notice In the real-world applications you should avoid magic strings at all. Both CatModelToken and DbConnectionToken should be kept in the separated constants.ts file. +
    +

    + Now we can inject the CatModelToken to the CatsService using the @Inject() decorator: +

    + + {{ 'cats.service' | extension: catsServiceT.isJsActive }} + + +
    {{ catsService }}
    +
    {{ catsServiceJs }}
    +

    + In the above example I've used the Cat interface. This interface extends the Document from the mongoose package: +

    + cat.interface.ts +
    {{ catInterface }}
    +

    + The database connection's asynchronous, but Nest makes this process's completely invisible for the end-user. + The CatModel component's waiting for the db connection, and the CatsService is delayed until model would be ready to use. + The entire application can start when each component is instantiated. +

    +

    + Here's a final CatsModule: +

    + + {{ 'cats.module' | extension: catsModuleT.isJsActive }} + + +
    {{ catsModule }}
    +
    + Hint Don't forget to import the CatsModule into the root ApplicationModule. +
    +

    + The full source code's available here. +

    +
    \ No newline at end of file diff --git a/src/app/homepage/pages/recipes/mongodb/mongodb.component.scss b/src/app/homepage/pages/recipes/mongodb/mongodb.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/recipes/mongodb/mongodb.component.spec.ts b/src/app/homepage/pages/recipes/mongodb/mongodb.component.spec.ts new file mode 100644 index 0000000000..ef16299dfa --- /dev/null +++ b/src/app/homepage/pages/recipes/mongodb/mongodb.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MongodbComponent } from './mongodb.component'; + +describe('MongodbComponent', () => { + let component: MongodbComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MongodbComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MongodbComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts b/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts new file mode 100644 index 0000000000..e5570e2941 --- /dev/null +++ b/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts @@ -0,0 +1,163 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-mongodb', + templateUrl: './mongodb.component.html', + styleUrls: ['./mongodb.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MongodbComponent extends BasePageComponent { + get dependencies() { + return ` +$ npm install --save mongoose +$ npm install --save-dev @types/mongoose`; + } + + get dependenciesJs() { + return ` +$ npm install --save mongoose`; + } + + get databaseProviders() { + return ` +import * as mongoose from 'mongoose'; + +export const databaseProviders = [ + { + provide: 'DbConnectionToken', + useFactory: async () => { + (mongoose as any).Promise = global.Promise; + return await mongoose.connect('mongodb://localhost/nest', { + useMongoClient: true, + }); + }, + }, +];`; + } + + get databaseModule() { + return ` +import { Module } from '@nestjs/common'; +import { databaseProviders } from './database.providers'; + +@Module({ + components: [...databaseProviders], + exports: [...databaseProviders], +}) +export class DatabaseModule {}`; + } + + get catSchema() { + return ` +import * as mongoose from 'mongoose'; + +export const CatSchema = new mongoose.Schema({ + name: String, + age: Number, + breed: String, +});`; + } + + get catsProviders() { + return ` +import { Connection } from 'mongoose'; +import { CatSchema } from './schemas/cat.schema'; + +export const catsProviders = [ + { + provide: 'CatModelToken', + useFactory: (connection: Connection) => connection.model('Cat', CatSchema), + inject: ['DbConnectionToken'], + }, +];`; + } + + get catsProvidersJs() { + return ` +import { CatSchema } from './schemas/cat.schema'; + +export const catsProviders = [ + { + provide: 'CatModelToken', + useFactory: (connection) => connection.model('Cat', CatSchema), + inject: ['DbConnectionToken'], + }, +];`; + } + + get catsService() { + return ` +import { Model } from 'mongoose'; +import { Component, Inject } from '@nestjs/common'; +import { Cat } from './interfaces/cat.interface'; +import { CreateCatDto } from './dto/create-cat.dto'; + +@Component() +export class CatsService { + constructor( + @Inject('CatModelToken') private readonly catModel: Model) {} + + async create(createCatDto: CreateCatDto): Promise { + const createdCat = new this.catModel(createCatDto); + return await createdCat.save(); + } + + async findAll(): Promise { + return await this.catModel.find().exec(); + } +}`; + } + + get catsServiceJs() { + return ` +import { Component, Dependencies } from '@nestjs/common'; + +@Component() +@Dependencies('CatModelToken') +export class CatsService { + constructor(catModel) { + this.catModel = catModel; + } + + async create(createCatDto) { + const createdCat = new this.catModel(createCatDto); + return await createdCat.save(); + } + + async findAll() { + return await this.catModel.find().exec(); + } +}`; + } + + get catsModule() { + return ` +import { Module } from '@nestjs/common'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; +import { catsProviders } from './cats.providers'; +import { DatabaseModule } from '../database/database.module'; + +@Module({ + modules: [DatabaseModule], + controllers: [CatsController], + components: [ + CatsService, + ...catsProviders, + ], +}) +export class CatsModule {}`; + } + + get catInterface() { + return ` +import { Document } from 'mongoose'; + +export interface Cat extends Document { + readonly name: string; + readonly age: number; + readonly breed: string; +}`; + } +} diff --git a/src/app/homepage/pages/recipes/passport/passport.component.html b/src/app/homepage/pages/recipes/passport/passport.component.html new file mode 100644 index 0000000000..4d5561f646 --- /dev/null +++ b/src/app/homepage/pages/recipes/passport/passport.component.html @@ -0,0 +1,55 @@ +
    +

    Passport integration

    +

    + Passport is the most popular authentication library, probably well-known by almost every node.js developer in the world, and successively used in many production applications. + It's really simple to integrate this tool with Nest framework. + For demonstration purposes, I'll set up the passport-jwt strategy. +

    +

    + To start the adventure with this library we have to install all of the required dependencies: +

    +
    {{ dependencies }}
    +

    + Firstly, we're gonna create the AuthService. This class will contain 2 methods, (1) to create a token using fake user, and (2) to validate the signed user from the decoded JWT (hardcoded true). +

    + + {{ 'auth.service' | extension: authServiceT.isJsActive }} + + +
    {{ authService }}
    +
    {{ authServiceJs }}
    +
    + Hint In a best-case scenario the jwt object and token configuration (secret key and expiration time) should be provided as a custom components and injected through constructor. +
    +

    + Passport uses the concept of strategies to authenticate requests. + In this chapter, we're gonna extend the strategy provided by the passport-jwt package, the JwtStrategy: +

    + + {{ 'jwt.strategy' | extension: jwtStrategyT.isJsActive }} + + +
    {{ jwtStrategy }}
    +
    {{ jwtStrategyJs }}
    +

    + The JwtStrategy uses AuthService to validate the payload (signed user). + When the payload is valid, the request may be handled by the route handler. Otherwise, the user would receive 401 Unauthorized response. +

    +

    + The last step is to create an AuthModule: +

    + + {{ 'auth.module' | extension: authModuleT.isJsActive }} + + +
    {{ authModule }}
    +
    {{ authModuleJs }}
    +

    + The trick's to provide a JwtStrategy as a component, and set up the strategy immediately after instance creation (inside the constructor). + Also, we're binding the functional middleware to the single /auth/authorized route (just for testing purposes). + It's everything. +

    +

    + The full source code's available here. +

    +
    \ No newline at end of file diff --git a/src/app/homepage/pages/recipes/passport/passport.component.scss b/src/app/homepage/pages/recipes/passport/passport.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/recipes/passport/passport.component.spec.ts b/src/app/homepage/pages/recipes/passport/passport.component.spec.ts new file mode 100644 index 0000000000..ea81552b8a --- /dev/null +++ b/src/app/homepage/pages/recipes/passport/passport.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PassportComponent } from './passport.component'; + +describe('PassportComponent', () => { + let component: PassportComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ PassportComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PassportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/recipes/passport/passport.component.ts b/src/app/homepage/pages/recipes/passport/passport.component.ts new file mode 100644 index 0000000000..18218145f9 --- /dev/null +++ b/src/app/homepage/pages/recipes/passport/passport.component.ts @@ -0,0 +1,177 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-passport', + templateUrl: './passport.component.html', + styleUrls: ['./passport.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PassportComponent extends BasePageComponent { + get dependencies() { + return ` +$ npm install --save passport passport-jwt jsonwebtoken`; + } + + get authService() { + return ` +import * as jwt from 'jsonwebtoken'; +import { Component } from '@nestjs/common'; + +@Component() +export class AuthService { + async createToken() { + const expiresIn = 60 * 60, secretOrKey = 'secret'; + const user = { email: 'thisis@example.com' }; + const token = jwt.sign(user, secretOrKey, { expiresIn }); + return { + expires_in: expiresIn, + access_token: token, + }; + } + + async validateUser(signedUser): Promise { + // put some validation logic here + // for example query user by id / email / username + return true; + } +}`; + } + + get authServiceJs() { + return ` +import * as jwt from 'jsonwebtoken'; +import { Component } from '@nestjs/common'; + +@Component() +export class AuthService { + async createToken() { + const expiresIn = 60 * 60, secretOrKey = 'secret'; + const user = { email: 'thisis@example.com' }; + const token = jwt.sign(user, secretOrKey, { expiresIn }); + return { + expires_in: expiresIn, + access_token: token, + }; + } + + async validateUser(signedUser) { + // put some validation logic here + // for example query user by id / email / username + return true; + } +}`; + } + + get jwtStrategy() { + return ` +import * as passport from 'passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Component, Inject } from '@nestjs/common'; +import { AuthService } from '../auth.service'; + +@Component() +export class JwtStrategy extends Strategy { + constructor(private readonly authService: AuthService) { + super( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + passReqToCallback: true, + secretOrKey: 'secret', + }, + async (req, payload, next) => await this.verify(req, payload, next) + ); + passport.use(this); + } + + public async verify(req, payload, done) { + const isValid = await this.authService.validateUser(payload); + if (!isValid) { + return done('Unauthorized', false); + } + done(null, payload); + } +}`; + } + + get jwtStrategyJs() { + return ` +import * as passport from 'passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Component, Dependencies } from '@nestjs/common'; +import { AuthService } from '../auth.service'; + +@Component() +@Dependencies(AuthService) +export class JwtStrategy extends Strategy { + constructor(authService) { + super( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + passReqToCallback: true, + secretOrKey: 'secret', + }, + async (req, payload, next) => await this.verify(req, payload, next) + ); + this.authService = authService; + passport.use(this); + } + + async verify(req, payload, done) { + const isValid = await this.authService.validateUser(payload); + if (!isValid) { + return done('Unauthorized', false); + } + done(null, payload); + } +}`; + } + + get authModule() { + return ` +import * as passport from 'passport'; +import { + Module, + NestModule, + MiddlewaresConsumer, + RequestMethod, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './passport/jwt.strategy'; +import { AuthController } from './auth.controller'; + +@Module({ + components: [AuthService, JwtStrategy], + controllers: [AuthController], +}) +export class AuthModule implements NestModule { + public configure(consumer: MiddlewaresConsumer) { + consumer + .apply(passport.authenticate('jwt', { session: false })) + .forRoutes({ path: '/auth/authorized', method: RequestMethod.ALL }); + } +}`; + } + + get authModuleJs() { + return ` +import * as passport from 'passport'; +import { Module, RequestMethod } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './passport/jwt.strategy'; +import { AuthController } from './auth.controller'; + +@Module({ + components: [AuthService, JwtStrategy], + controllers: [AuthController], +}) +export class AuthModule { + public configure(consumer) { + consumer + .apply(passport.authenticate('jwt', { session: false })) + .forRoutes({ path: '/auth/authorized', method: RequestMethod.ALL }); + } +}`; + } +} + diff --git a/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.html b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.html new file mode 100644 index 0000000000..77d7ccd296 --- /dev/null +++ b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.html @@ -0,0 +1,71 @@ +
    +

    SQL (Sequelize)

    +
    This chapter applies only to TypeScript
    +

    + Sequelize is the most popular Object Relational Mapper (ORM) available in the node.js world. + It's written in plain JavaScript, but there's a sequelize-typescript TypeScript wrapper which provdes set of decorators and some other extras for base sequelize. + To start the adventure with this library we have to install all of the required dependencies: +

    +
    {{ dependencies }}
    +

    + The first step we need to do is to create a Sequelize instance with options object passed into the constructor. + Also, we need to add all of the models (the alternative is to use modelPaths property) and sync() our database tables. +

    + database.providers.ts +
    {{ databaseProviders }}
    +
    + Hint Following best practices, we've declared the custom component in the separated file which has a *.providers.ts suffix. +
    +

    + Then we need to export these providers to make them accessible for the rest part of the application. +

    + database.module.ts +
    {{ databaseModule }}
    +

    + It's everything. Now we can inject the Sequelize object using @Inject() decorator. + Each component which would depend on the Sequelize async component will wait until the Promise would be resolved. +

    +

    Model injection

    +

    + In Sequelize the Model represents a table in the database. Instances of this class represent a database row. + Firstly, we need at least one entity: +

    + cats/cat.entity.ts +
    {{ catEntity }}
    +

    + The Cat entity belongs to the cats directory. + This directory represents the CatsModule. It's your decision where you gonna keep your model files. From my point of view, the best way's to hold them nearly their domain, in the appropriate module directory. +

    +

    + Now it's time to create a Repository component: +

    + cats.providers.ts +
    {{ catsProviders }}
    +
    + Notice In the real-world applications you should avoid magic strings at all. Both PhotoRepositoryToken and DbConnectionToken should be kept in the separated constants.ts file. +
    +

    + In Sequelize we're using static methods to manipulate the data, so we're just creating an alias here. +

    +

    + Now we can inject the CatsRepository to the CatsService using the @Inject() decorator: +

    + cats.service.ts +
    {{ catsService }}
    +

    + The database connection's asynchronous, but Nest makes this process's completely invisible for the end-user. + The CatsRepository component's waiting for the db connection, and the CatsService is delayed until repository would be ready to use. + The entire application can start when each component is instantiated. +

    +

    + Here's a final CatsModule: +

    + cats.module.ts +
    {{ catsModule }}
    +
    + Hint Don't forget to import the CatsModule into the root ApplicationModule. +
    +

    + The full source code's available here. +

    +
    \ No newline at end of file diff --git a/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.scss b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.spec.ts b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.spec.ts new file mode 100644 index 0000000000..129ced4766 --- /dev/null +++ b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SqlSequelizeComponent } from './sql-sequelize.component'; + +describe('SqlSequelizeComponent', () => { + let component: SqlSequelizeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SqlSequelizeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SqlSequelizeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts new file mode 100644 index 0000000000..e4290f7dcf --- /dev/null +++ b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts @@ -0,0 +1,119 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-sql-sequelize', + templateUrl: './sql-sequelize.component.html', + styleUrls: ['./sql-sequelize.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SqlSequelizeComponent extends BasePageComponent { + get dependencies() { + return ` + $ npm install --save sequelize sequelize-typescript mysql2 + $ npm install --save-dev @types/sequelize`; + } + + get databaseProviders() { + return ` +import { Sequelize } from 'sequelize-typescript'; +import { Cat } from '../cats/cat.entity'; + +export const databaseProviders = [ + { + provide: 'SequelizeToken', + useFactory: async () => { + const sequelize = new Sequelize({ + dialect: 'mysql', + host: 'localhost', + port: 3306, + username: 'root', + password: 'password', + database: 'nest', + }); + sequelize.addModels([Cat]); + await sequelize.sync(); + return sequelize; + }, + }, +];`; + } + + get databaseModule() { + return ` + import { Module } from '@nestjs/common'; + import { databaseProviders } from './database.providers'; + + @Module({ + components: [...databaseProviders], + exports: [...databaseProviders], + }) + export class DatabaseModule {}`; + } + + get catEntity() { + return ` +import { Table, Column, Model } from 'sequelize-typescript'; + +@Table +export class Cat extends Model { + @Column + name: string; + + @Column + age: number; + + @Column + breed: string; +}`; + } + + get catsProviders() { + return ` +import { Cat } from './cat.entity'; + +export const catsProviders = [ + { + provide: 'CatsRepository', + useValue: Cat, + }, +];`; + } + + get catsService() { + return ` +import { Component, Inject } from '@nestjs/common'; +import { CreateCatDto } from './dto/create-cat.dto'; +import { Model } from 'sequelize-typescript'; +import { Cat } from './cat.entity'; + +@Component() +export class CatsService { + constructor( + @Inject('CatsRepository') private readonly catsRepository: typeof Model) {} + + async findAll(): Promise { + return await this.catsRepository.findAll(); + } +}`; + } + + get catsModule() { + return ` +import { Module } from '@nestjs/common'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; +import { catsProviders } from './cats.providers'; +import { DatabaseModule } from '../database/database.module'; + +@Module({ + modules: [DatabaseModule], + controllers: [CatsController], + components: [ + CatsService, + ...catsProviders, + ], +}) +export class CatsModule {}`; + } + } diff --git a/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.html b/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.html index 9c27ba9af1..56328fbcf8 100644 --- a/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.html +++ b/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.html @@ -1,13 +1,14 @@

    SQL (TypeORM)

    +
    This chapter applies only to TypeScript

    - The TypeORM library is definitely the most mature Object Relational Mapper (ORM) available in the node.js world. + TypeORM is definitely the most mature Object Relational Mapper (ORM) available in the node.js world. Since it's written in TypeScript, it works pretty well with the Nest framework. - This example describes how to integrate TypeORM with the Nest application, but the process is almost the same -in most of the other cases, such as connection with the MongoDB, Redis, etc. + To start the adventure with this library we have to install all of the required dependencies:

    +
    {{ dependencies }}

    - Let's start from scratch. Firstly, we need to establish the connection with our database using createConnection() function imported from the typeorm package. + The first step we need to do is to establish the connection with our database using createConnection() function imported from the typeorm package. The createConnection() function returns the Promise, so it's necessary to create an async component.

    database.providers.ts @@ -35,7 +36,7 @@

    Repository pattern

    {{ photoEntity }}

    The Photo entity belongs to the photo directory. - This directory represents the PhotoModule. It's your decision where you gonna keep your model's files. From my point of view, the best way's to hold them nearly their domain, in the appropriate module directory. + This directory represents the PhotoModule. It's your decision where you gonna keep your model files. From my point of view, the best way's to hold them nearly their domain, in the appropriate module directory.

    Let's create a Repository component: @@ -43,7 +44,7 @@

    Repository pattern

    photo.providers.ts
    {{ photoProviders }}
    - Notice In the real-world applications you should avoid magic strings at all. Both PhotoRepositoryToken and DbConnectionToken should be kept in the separated constansts.ts file. + Notice In the real-world applications you should avoid magic strings at all. Both PhotoRepositoryToken and DbConnectionToken should be kept in the separated constants.ts file.

    Now we can inject the PhotoRepository to the PhotoService using the @Inject() decorator: @@ -63,4 +64,7 @@

    Repository pattern

    Hint Don't forget to import the PhotoModule into the root ApplicationModule.
    +

    + The full source code's available here. +

    \ No newline at end of file diff --git a/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.ts b/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.ts index 4349149d1a..3fa0ccf45c 100644 --- a/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.ts +++ b/src/app/homepage/pages/recipes/sql-typeorm/sql-typeorm.component.ts @@ -8,6 +8,11 @@ import { BasePageComponent } from '../../page/page.component'; changeDetection: ChangeDetectionStrategy.OnPush }) export class SqlTypeormComponent extends BasePageComponent { + get dependencies() { + return ` +$ npm install --save typeorm mysql`; + } + get databaseProviders() { return ` import { createConnection } from 'typeorm'; @@ -23,7 +28,7 @@ export const databaseProviders = [ password: 'root', database: 'test', entities: [ - __dirname + '../**/*.entity.ts', + __dirname + '/../**/*.entity{.ts,.js}', ], autoSchemaSync: true, }), @@ -92,7 +97,11 @@ import { Photo } from './photo.entity'; @Component() export class PhotoService { constructor( - @Inject('PhotoRepositoryToken') private photoRepository: Repository) {} + @Inject('PhotoRepositoryToken') private readonly photoRepository: Repository) {} + + async findAll(): Promise { + return await this.photoRepository.find(); + } }`; } diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.html b/src/app/homepage/pages/recipes/swagger/swagger.component.html new file mode 100644 index 0000000000..dcf20e0d34 --- /dev/null +++ b/src/app/homepage/pages/recipes/swagger/swagger.component.html @@ -0,0 +1,3 @@ +

    + swagger works! +

    diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.scss b/src/app/homepage/pages/recipes/swagger/swagger.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.spec.ts b/src/app/homepage/pages/recipes/swagger/swagger.component.spec.ts new file mode 100644 index 0000000000..c235bb610d --- /dev/null +++ b/src/app/homepage/pages/recipes/swagger/swagger.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SwaggerComponent } from './swagger.component'; + +describe('SwaggerComponent', () => { + let component: SwaggerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SwaggerComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SwaggerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.ts b/src/app/homepage/pages/recipes/swagger/swagger.component.ts new file mode 100644 index 0000000000..225a091293 --- /dev/null +++ b/src/app/homepage/pages/recipes/swagger/swagger.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-swagger', + templateUrl: './swagger.component.html', + styleUrls: ['./swagger.component.scss'] +}) +export class SwaggerComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/homepage/pages/websockets/adapter/adapter.component.html b/src/app/homepage/pages/websockets/adapter/adapter.component.html index 0c2574021d..10dc51ab7a 100644 --- a/src/app/homepage/pages/websockets/adapter/adapter.component.html +++ b/src/app/homepage/pages/websockets/adapter/adapter.component.html @@ -35,12 +35,19 @@

    Adapter

    For demonstration purposes, we're going to integrate ws library with the Nest application.

    - ws-adapter.ts -
    {{ wsAdapter }}
    + + {{ 'ws-adapter' | extension: wsAdapterT.isJsActive }} + + +
    {{ wsAdapter }}
    +
    {{ wsAdapterJs }}

    Since WsAdapter class is ready to use, we can setup the adapter using useWebSocketAdapter() method:

    - server.ts + + {{ 'server' | extension: setupAdapterT.isJsActive }} + +
    {{ setupAdapter }}

    Now Nest will make use of our WsAdapter instead of the default one. diff --git a/src/app/homepage/pages/websockets/adapter/adapter.component.ts b/src/app/homepage/pages/websockets/adapter/adapter.component.ts index 6f0173ba90..77df21f497 100644 --- a/src/app/homepage/pages/websockets/adapter/adapter.component.ts +++ b/src/app/homepage/pages/websockets/adapter/adapter.component.ts @@ -20,22 +20,60 @@ import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/filter'; export class WsAdapter implements WebSocketAdapter { - public create(port: number) { + create(port: number) { return new WebSocket.Server({ port }); } - public bindClientConnect(server, callback: (...args: any[]) => void) { + bindClientConnect(server, callback: (...args: any[]) => void) { server.on('connection', callback); } - public bindMessageHandlers(client: WebSocket, handlers: MessageMappingProperties[], process: (data) => Observable) { + bindMessageHandlers(client: WebSocket, handlers: MessageMappingProperties[], process: (data) => Observable) { Observable.fromEvent(client, 'message') .switchMap((buffer) => this.bindMessageHandler(buffer, handlers, process)) .filter((result) => !!result) .subscribe((response) => client.send(JSON.stringify(response))); } - public bindMessageHandler(buffer, handlers: MessageMappingProperties[], process: (data) => Observable): Observable { + bindMessageHandler(buffer, handlers: MessageMappingProperties[], process: (data) => Observable): Observable { + const data = JSON.parse(buffer.data); + const messageHandler = handlers.find((handler) => handler.message === data.type); + if (!messageHandler) { + return Observable.empty(); + } + const { callback } = messageHandler; + return process(callback(data)); + } +}`; + } + + get wsAdapterJs() { + return ` +import * as WebSocket from 'ws'; +import { MessageMappingProperties } from '@nestjs/websockets'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/observable/empty'; +import 'rxjs/add/operator/switchMap'; +import 'rxjs/add/operator/filter'; + +export class WsAdapter { + create(port) { + return new WebSocket.Server({ port }); + } + + bindClientConnect(server, callback) { + server.on('connection', callback); + } + + bindMessageHandlers(client, handlers, process) { + Observable.fromEvent(client, 'message') + .switchMap((buffer) => this.bindMessageHandler(buffer, handlers, process)) + .filter((result) => !!result) + .subscribe((response) => client.send(JSON.stringify(response))); + } + + bindMessageHandler(buffer, handlers, process) { const data = JSON.parse(buffer.data); const messageHandler = handlers.find((handler) => handler.message === data.type); if (!messageHandler) { diff --git a/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.html b/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.html index e6165bef79..c9bfd7fa6c 100644 --- a/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.html +++ b/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.html @@ -16,8 +16,12 @@

    Exception Filters

    The Exception Filters are also really similar, and works exactly in the same way as the primary ones.

    - ws-exception.filter.ts -
    {{ wsExceptionFilter }}
    + + {{ 'ws-exception.filter' | extension: wsExceptionFilterT.isJsActive }} + + +
    {{ wsExceptionFilter }}
    +
    {{ wsExceptionFilterJs }}
    Notice It's impossible to setup the websockets exception filters globally.
    diff --git a/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.ts b/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.ts index 5443833bb8..f97fb19014 100644 --- a/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.ts +++ b/src/app/homepage/pages/websockets/exception-filters/exception-filters.component.ts @@ -34,6 +34,22 @@ export class ExceptionFilter implements WsExceptionFilter { message: \`It's a message from the exception filter\`, }); } +}`; + } + + get wsExceptionFilterJs() { + return ` +import { Catch } from '@nestjs/common'; +import { WsException } from '@nestjs/websockets'; + +@Catch(WsException) +export class ExceptionFilter { + catch(exception, client) { + client.emit('exception', { + status: 'error', + message: \`It's a message from the exception filter\`, + }); + } }`; } } \ No newline at end of file diff --git a/src/app/homepage/pages/websockets/gateways/gateways.component.html b/src/app/homepage/pages/websockets/gateways/gateways.component.html index c49cfa832f..e57fd5b92c 100644 --- a/src/app/homepage/pages/websockets/gateways/gateways.component.html +++ b/src/app/homepage/pages/websockets/gateways/gateways.component.html @@ -21,8 +21,12 @@

    Gateways

    The Gateway is listening now, but we're not subscribing to the incoming messages yet. Let's create a handler, which will subscribe to the events messages and respond to the user with the exact same data.

    - events.gateway.ts -
    {{ subscribeEvents }}
    + + {{ 'events.gateway' | extension: subscribeEventsT.isJsActive }} + + +
    {{ subscribeEvents }}
    +
    {{ subscribeEventsJs }}
    Notice The WsResponse interface is imported from @nestjs/common package, while @SubscribeMessage() decorator from @nestjs/websockets.
    @@ -37,8 +41,12 @@

    Asynchronous responses

    Each message handler can be async, so you're able to return the Promise. Moreover, you can return the RxJS Observable, so the values would be emitted until the stream is completed.

    - events.gateway.ts -
    {{ streaming }}
    + + {{ 'events.gateway' | extension: streamingT.isJsActive }} + + +
    {{ streaming }}
    +
    {{ streamingJs }}

    Above message handler will respond 3 times (with each item from the response array).

    diff --git a/src/app/homepage/pages/websockets/gateways/gateways.component.ts b/src/app/homepage/pages/websockets/gateways/gateways.component.ts index 6e90031551..a49f684ac0 100644 --- a/src/app/homepage/pages/websockets/gateways/gateways.component.ts +++ b/src/app/homepage/pages/websockets/gateways/gateways.component.ts @@ -22,6 +22,27 @@ onEvent(client, data): WsResponse { }`; } + get subscribeEventsJs() { + return ` +@SubscribeMessage('events') +onEvent(client, data) { + const event = 'events'; + return { event, data }; +}`; + } + + get streamingJs() { + return ` +@SubscribeMessage('events') +onEvent(client, data) { + const event = 'events'; + const response = [1, 2, 3]; + + return Observable.from(response) + .map((res) => ({ event, data: res })); +}`; + } + get streaming() { return ` @SubscribeMessage('events') diff --git a/src/app/shared/components/tabs/tabs.component.html b/src/app/shared/components/tabs/tabs.component.html new file mode 100644 index 0000000000..4cd91790a3 --- /dev/null +++ b/src/app/shared/components/tabs/tabs.component.html @@ -0,0 +1,8 @@ +
    + + JavaScript + + + TypeScript + +
    \ No newline at end of file diff --git a/src/app/shared/components/tabs/tabs.component.scss b/src/app/shared/components/tabs/tabs.component.scss new file mode 100644 index 0000000000..34446a4243 --- /dev/null +++ b/src/app/shared/components/tabs/tabs.component.scss @@ -0,0 +1,26 @@ +.tabs-wrapper { + position: absolute; + right: 0; + top: 0; + bottom: 0; +} + +.tab { + color: #8e8e8e; + cursor: pointer; + margin: 0; + float: right; + height: 100%; + padding: 15px 20px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + + &:hover:not(.active) { + color: #dadada; + background: #2d2d2d; + } + &.active { + color: #fff; + background: #212121; + } +} \ No newline at end of file diff --git a/src/app/shared/components/tabs/tabs.component.spec.ts b/src/app/shared/components/tabs/tabs.component.spec.ts new file mode 100644 index 0000000000..d3d78bf056 --- /dev/null +++ b/src/app/shared/components/tabs/tabs.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsComponent } from './tabs.component'; + +describe('TabsComponent', () => { + let component: TabsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TabsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/tabs/tabs.component.ts b/src/app/shared/components/tabs/tabs.component.ts new file mode 100644 index 0000000000..5a6670458b --- /dev/null +++ b/src/app/shared/components/tabs/tabs.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-tabs', + templateUrl: './tabs.component.html', + styleUrls: ['./tabs.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TabsComponent { + public isJsActive = false; +} diff --git a/src/app/shared/pipes/extension.pipe.spec.ts b/src/app/shared/pipes/extension.pipe.spec.ts new file mode 100644 index 0000000000..130df5b82c --- /dev/null +++ b/src/app/shared/pipes/extension.pipe.spec.ts @@ -0,0 +1,8 @@ +import { ExtensionPipe } from './extension.pipe'; + +describe('ExtensionPipe', () => { + it('create an instance', () => { + const pipe = new ExtensionPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/shared/pipes/extension.pipe.ts b/src/app/shared/pipes/extension.pipe.ts new file mode 100644 index 0000000000..31ead52755 --- /dev/null +++ b/src/app/shared/pipes/extension.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'extension' +}) +export class ExtensionPipe implements PipeTransform { + transform(value: any, args?: any): any { + return !args ? `${value}.ts` : `${value}.js`; + } +} diff --git a/src/app/store/app-store.module.ts b/src/app/store/app-store.module.ts deleted file mode 100644 index 07fee1f383..0000000000 --- a/src/app/store/app-store.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core'; -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; - -import { rootReducers } from './root-reducers'; -import { rootInitialState } from './initial-state'; -import { AppState } from './common'; -import { rootEffects } from './root-effects'; - -const options = { - initialState: rootInitialState, -}; - -@NgModule({ - imports: [ - StoreModule.forRoot( - rootReducers, - options, - ), - EffectsModule.forRoot(rootEffects), - ], - exports: [ - StoreModule, - EffectsModule, - ] -}) -export class AppStoreModule { } diff --git a/src/app/store/common/index.ts b/src/app/store/common/index.ts deleted file mode 100644 index e9489de768..0000000000 --- a/src/app/store/common/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './interfaces'; \ No newline at end of file diff --git a/src/app/store/common/interfaces/action.interface.ts b/src/app/store/common/interfaces/action.interface.ts deleted file mode 100644 index 0d27708511..0000000000 --- a/src/app/store/common/interfaces/action.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Action { - type: string; - payload?: T; -} \ No newline at end of file diff --git a/src/app/store/common/interfaces/app-state.interface.ts b/src/app/store/common/interfaces/app-state.interface.ts deleted file mode 100644 index f36b47d642..0000000000 --- a/src/app/store/common/interfaces/app-state.interface.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RouterReducerState } from '@ngrx/router-store'; - -import { UserState } from '../../user/interfaces/user-state.interface'; - -export interface AppState { - router: RouterReducerState; - user: UserState; -} diff --git a/src/app/store/common/interfaces/index.ts b/src/app/store/common/interfaces/index.ts deleted file mode 100644 index 1423274415..0000000000 --- a/src/app/store/common/interfaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './action.interface'; -export * from './app-state.interface'; \ No newline at end of file diff --git a/src/app/store/initial-state.ts b/src/app/store/initial-state.ts deleted file mode 100644 index 0b97daf245..0000000000 --- a/src/app/store/initial-state.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AppState } from './common'; - -type Partial = { - [P in keyof T]?: T[P]; -}; - -export const rootInitialState: Partial = {}; \ No newline at end of file diff --git a/src/app/store/root-effects.ts b/src/app/store/root-effects.ts deleted file mode 100644 index 5911da44aa..0000000000 --- a/src/app/store/root-effects.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const rootEffects = [ - -]; \ No newline at end of file diff --git a/src/app/store/root-reducers.ts b/src/app/store/root-reducers.ts deleted file mode 100644 index 3e85d511f5..0000000000 --- a/src/app/store/root-reducers.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { routerReducer } from '@ngrx/router-store'; - -import { userReducer } from './user/reducer'; - -export const rootReducers: any = { - router: routerReducer, - user: userReducer, -}; \ No newline at end of file diff --git a/src/app/store/user/interfaces/user-state.interface.ts b/src/app/store/user/interfaces/user-state.interface.ts deleted file mode 100644 index 1c11d05947..0000000000 --- a/src/app/store/user/interfaces/user-state.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface UserState { - accessToken: string; -} \ No newline at end of file diff --git a/src/app/store/user/reducer.ts b/src/app/store/user/reducer.ts deleted file mode 100644 index 4c4277a4f6..0000000000 --- a/src/app/store/user/reducer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AppState, Action } from '../common'; - -export function userReducer(state: AppState, action: Action) { - switch (action.type) { - } - return state; -} \ No newline at end of file diff --git a/src/app/store/user/selectors.ts b/src/app/store/user/selectors.ts deleted file mode 100644 index 5a315777a4..0000000000 --- a/src/app/store/user/selectors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AppState } from '../common'; - -export const getUser = (state: AppState) => state.user; \ No newline at end of file diff --git a/src/favicon.ico b/src/favicon.ico index c42d227d10707e60cff00100cb21b60c85b4335a..6121a5e7cf873998ac17f20f89cb73392d3377eb 100644 GIT binary patch literal 1150 zcmbu8yKmD_6vkZ%gqWxVEkm)LIF5bO2pAYpx5jo}O+)Ioc{S>Q7yvOab*vbfQH2m< zVq!vwfq{WP05KqNlRncz)3iww$4RPCAyggC4KY+vJz?wE_a1-0`<-(nGt2+6GHXE$#WE9B{XEhvNf-g#$EI; z(K9r;v4orzkt@jV!KAkglVT~K5Ox+%@+5oBJBv4}IbvjsE4Cd|6mtNo0w_#c+BDV=i_Zp;KD<;*eQZpP8t$2vkE=Q!=)reGmgflWHy}J+>chD1SMbGI! zo{(~_LWVEE9JgEy`5QMhpJ~$BVxJarzidxh>y*d#+-N0TYc2GIET2c#fHZ5o4HB~$ zCI^7^_GY__^FdEKpI)Cy_d505$nYzRDYo!9%NfTxuklUd(cNcaHno{ZEj;jckYVDb`3h ZaZdTnmBPb$Zh0Zg=JkFJ!vOqG{03-he(wMP literal 1150 zcmZwFOKclO7zgl40!3-1(sGC@fumVJ<0W`^J>E^7MoHz!B~7CyCAO20S8w#tYZ1o+ z5{L^D2P7mUP6%<}gj7{f5C|b!HFc}n@e3!!kJxL+kC-S)UumZQO+wYC?CQ7w@B3zU zF1v&@;XZPN;7Q4amk4PigmeH#nC#9GgkX)nv1_N__mI^bk=1Imq@`Svn)FNRo!2ET z`I)5UzL8X2c4;fkl3LpL)^6+e+$2872SicBJ&Jletf+VEikkaTQLEP!wX&_K<+$Q0 zzvpI!LvB`l;S3MEK|Czn!q{J*Fn0YBV+qb!cAK#x>N4sQ6xThh^o57%Z5~#B?hH+t zBbpLEswp?Fs_t7~YD(sorYvzyS%fqgwGNd>no|6_OJ07hOD;S;^=3cIJ3*X9kwQsLN0T9ZI~fv;4D9 zjFNsaW9t{wC;P>y&@bMe^oetS`oz?>Pb_kuScN*&Uz{O!14pkFOAynh%P7d#Wzt?6;=!|%qd8}*>&Ummoq@loq4f5)oxbJh$Wu_R!Amo>&m3>G8f-w#{U zhtAn*I*6xHHB!lV}jN`nxNHR#%SS>F`EA&MAM&#XyoEJjV=H;jM4euLNwL}&;CAShOR=R zWr|i0zf1L_leBPfO5jaX?a^cKD{M`*$5toX6TxXyluXdnbMOCOGi!P4O$U rrsSv7rbO$EDY>(~@hTyiSD*!&0MiD11(#v}bs2kphX(fZj|q7KO{csU diff --git a/src/favicon.png b/src/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..da169e5641ceb80a3195ef16b0a3b22085676982 GIT binary patch literal 3379 zcmV-34b1Y1P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0007BNklJA2G%5Vv%5>7K*__MG&-FSY@q5J8e|#1cRX1 z_)jPcTUc2uxY5SNMl0DOMT%IZ_Xo+kW;eW-yziZ3@sbydb>PC>%b7E0E@$-TQ?HQ? z`mD_9Xjy3vnk#nCN}Dl{@m5ARko)~okH1qSViA>x$7S#%sqVqFb_a6)1MP;TN%q4x zGIP7nd>DaVM{Ef{ALJOA+Uy$c0h0x$d)LACgV_vQOM(I7ec_u$4LOjF>~RuT6X9hK zriFOolI{;{JN~U=J|W`0FwayP=|C6}2o^^o9VBtOIz9a2q#J#lqbsirryI}d@ab#& zjd(I0q|booIq~27z8zmXbJA2kg($`Cy6$ei`hqiWr*%19?xkZjt$I7B#Apsz!}QM58DvYAw1` zQ<)R>2&t_beKvxp=Xg;X_)+8?zA^Z|4Y(2FKM=*MfnShI;# zJ`dVPIB)bS?|_HG1VNH};qr~{KYnfL5X6;!kh_-G&wUf(%aDJXxA-9ucDT7rm3m9* zUu%rdp$0gh@gZ;>=!Jfc*6w7)w~{HiIoJu$ce?rT$1?hxdg0Nfj$5ExCIOM?rL_M- zIxan*N%L77@;&p0{r}qt62K7YF~nium-~+0L9b3Qw*r8F2LO1qSM-ZFO(_5X002ov JPDHLkV1mJiQ)U1F literal 0 HcmV?d00001 diff --git a/src/index.html b/src/index.html index ee83c17ddd..00ddd4f178 100644 --- a/src/index.html +++ b/src/index.html @@ -1,29 +1,30 @@ - Documentation | Nest - A node.js framework built on top of TypeScript + Documentation | Nest - A progressive Node.js web framework - + - - - + + + - - + + - + +