diff --git a/.angular-cli.json b/.angular-cli.json index 9d6153e78b..bc28acc022 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -1,7 +1,7 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { - "name": "scalio-base2-front" + "name": "docs.nestjs.com" }, "apps": [ { diff --git a/README.md b/README.md index 6e397c39c8..e7c7906724 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux [linux-url]: https://travis-ci.org/nestjs/nest -

A progressive Node.js framework for building efficient and scalable web applications. :cat:

+

A progressive Node.js framework for building efficient and scalable server-side applications. :cat:

NPM Version Package License diff --git a/package.json b/package.json index bdc3fe24e5..3d4ce1828e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "license": "MIT", "scripts": { "ng": "ng", - "start": "ng serve --sourcemap=false --port=4100", + "start": "ng serve --sourcemap=false", "build": "ng build", "test": "ng test", "lint": "ng lint", @@ -42,7 +42,7 @@ "zone.js": "^0.8.14" }, "devDependencies": { - "@angular/cli": "^1.6.1", + "@angular/cli": "^1.6.0-rc.2", "@angular/compiler-cli": "^5.1.1", "@angular/language-service": "^5.1.1", "@types/jasmine": "~2.8.2", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index fa0716a916..8e8853391c 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -46,6 +46,17 @@ import { CqrsComponent } from './homepage/pages/recipes/cqrs/cqrs.component'; import { MockgooseComponent } from './homepage/pages/recipes/mockgoose/mockgoose.component'; import { CustomDecoratorsComponent } from './homepage/pages/custom-decorators/custom-decorators.component'; import { SwaggerComponent } from './homepage/pages/recipes/swagger/swagger.component'; +import { ExecutionContextComponent } from './homepage/pages/execution-context/execution-context.component'; +import { QuickStartComponent } from './homepage/pages/graphql/quick-start/quick-start.component'; +import { ResolversMapComponent } from './homepage/pages/graphql/resolvers-map/resolvers-map.component'; +import { MutationsComponent } from './homepage/pages/graphql/mutations/mutations.component'; +import { SubscriptionsComponent } from './homepage/pages/graphql/subscriptions/subscriptions.component'; +import { SchemaStitchingComponent } from './homepage/pages/graphql/schema-stitching/schema-stitching.component'; +import { GuardsInterceptorsComponent } from './homepage/pages/graphql/guards-interceptors/guards-interceptors.component'; +import { IdeComponent } from './homepage/pages/graphql/ide/ide.component'; +import { MvcComponent } from './homepage/pages/techniques/mvc/mvc.component'; +import { SqlComponent } from './homepage/pages/techniques/sql/sql.component'; +import { MongoComponent } from './homepage/pages/techniques/mongo/mongo.component'; const routes: Routes = [ { @@ -140,6 +151,46 @@ const routes: Routes = [ path: 'fundamentals/e2e-testing', component: E2eTestingComponent, data: { title: 'E2E Testing' }, + }, + { + path: 'execution-context', + component: ExecutionContextComponent, + data: { title: 'Execution Context' }, + }, + { + path: 'graphql/quick-start', + component: QuickStartComponent, + data: { title: 'GraphQL - Quick Start' }, + }, + { + path: 'graphql/resolvers-map', + component: ResolversMapComponent, + data: { title: 'GraphQL - Resolvers Map' }, + }, + { + path: 'graphql/mutations', + component: MutationsComponent, + data: { title: 'GraphQL - Mutations' }, + }, + { + path: 'graphql/subscriptions', + component: SubscriptionsComponent, + data: { title: 'GraphQL - Subscriptions' }, + }, + { + path: 'graphql/guards-interceptors', + component: GuardsInterceptorsComponent, + data: { title: 'GraphQL - Guards & Interceptors' }, + }, + { + path: 'graphql/ide', + component: IdeComponent, + data: { title: 'GraphQL - IDE' }, + }, + { + path: 'graphql/schema-stitching', + component: SchemaStitchingComponent, + data: { title: 'GraphQL - Schema Stitching' }, }, { path: 'websockets/gateways', @@ -223,8 +274,12 @@ const routes: Routes = [ }, { path: 'recipes/passport', + redirectTo: 'techniques/authentication', + }, + { + path: 'techniques/authentication', component: PassportComponent, - data: { title: 'Passport integration' }, + data: { title: 'Authentication' }, }, { path: 'recipes/sql-sequelize', @@ -240,6 +295,26 @@ const routes: Routes = [ path: 'recipes/swagger', component: SwaggerComponent, data: { title: 'OpenAPI (Swagger)' }, + }, + { + path: 'techniques/mvc', + component: MvcComponent, + data: { title: 'MVC' }, + }, + { + path: 'techniques/mvc', + component: MvcComponent, + data: { title: 'MVC' }, + }, + { + path: 'techniques/sql', + component: SqlComponent, + data: { title: 'SQL' }, + }, + { + path: 'techniques/mongodb', + component: MongoComponent, + data: { title: 'MongoDB' }, }, { path: 'faq/express-instance', diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 41e331e79e..d021d7249e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -3,6 +3,7 @@ import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { TITLE_SUFFIX, HOMEPAGE_TITLE } from './constants'; +import { SwUpdate } from '@angular/service-worker'; @Component({ selector: 'app-root', @@ -15,7 +16,7 @@ export class AppComponent implements OnInit { private readonly router: Router, private readonly activatedRoute: ActivatedRoute) {} - ngOnInit() { + async ngOnInit() { this.router.events .filter((ev) => ev instanceof NavigationEnd) .subscribe((ev) => { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9cce03bcab..0cc8962904 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -58,9 +58,21 @@ import { MockgooseComponent } from './homepage/pages/recipes/mockgoose/mockgoose import { CustomDecoratorsComponent } from './homepage/pages/custom-decorators/custom-decorators.component'; import {ServiceWorkerModule} from '@angular/service-worker'; import {environment} from '../environments/environment'; +import { ExecutionContextComponent } from './homepage/pages/execution-context/execution-context.component'; +import { QuickStartComponent } from './homepage/pages/graphql/quick-start/quick-start.component'; +import { ResolversMapComponent } from './homepage/pages/graphql/resolvers-map/resolvers-map.component'; +import { MutationsComponent } from './homepage/pages/graphql/mutations/mutations.component'; +import { SubscriptionsComponent } from './homepage/pages/graphql/subscriptions/subscriptions.component'; +import { SchemaStitchingComponent } from './homepage/pages/graphql/schema-stitching/schema-stitching.component'; +import { GuardsInterceptorsComponent } from './homepage/pages/graphql/guards-interceptors/guards-interceptors.component'; +import { IdeComponent } from './homepage/pages/graphql/ide/ide.component'; +import { SqlComponent } from './homepage/pages/techniques/sql/sql.component'; +import { MvcComponent } from './homepage/pages/techniques/mvc/mvc.component'; +import { MongoComponent } from './homepage/pages/techniques/mongo/mongo.component'; const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { - suppressScrollX: true + suppressScrollX: true, + wheelPropagation: true, }; @NgModule({ @@ -68,7 +80,7 @@ const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { BrowserModule, AppRoutingModule, PerfectScrollbarModule, - ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}) + //ServiceWorkerModule.register('/ngsw-worker.js', {enabled: environment.production}) ], declarations: [ AppComponent, @@ -124,6 +136,17 @@ const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = { TabsComponent, ExtensionPipe, CustomDecoratorsComponent, + ExecutionContextComponent, + QuickStartComponent, + ResolversMapComponent, + MutationsComponent, + SubscriptionsComponent, + SchemaStitchingComponent, + GuardsInterceptorsComponent, + IdeComponent, + SqlComponent, + MvcComponent, + MongoComponent, ], bootstrap: [AppComponent], providers: [ diff --git a/src/app/common/directives/match-height.directive.ts b/src/app/common/directives/match-height.directive.ts index d70babf5bb..7a7bb998bd 100644 --- a/src/app/common/directives/match-height.directive.ts +++ b/src/app/common/directives/match-height.directive.ts @@ -1,39 +1,50 @@ import { - Directive, ElementRef, AfterViewChecked, - Input, HostListener, Renderer2, NgZone + Directive, + ElementRef, + AfterViewChecked, + Input, + HostListener, + Renderer2, + NgZone, } from '@angular/core'; @Directive({ - selector: '[matchHeight]' + selector: '[matchHeight]', }) export class MatchHeightDirective implements AfterViewChecked { - constructor( - private readonly zone: NgZone, - private readonly renderer: Renderer2, - private readonly el: ElementRef) {} + constructor( + private readonly zone: NgZone, + private readonly renderer: Renderer2, + private readonly el: ElementRef, + ) {} - ngAfterViewChecked() { - setTimeout(() => this.zone.run(() => this.matchHeight(this.el.nativeElement)), 800); - } - - @HostListener('window:resize') - onResize() { - setTimeout(() => this.matchHeight(this.el.nativeElement), 500); - } + ngAfterViewChecked() { + setTimeout( + () => this.zone.run(() => this.matchHeight(this.el.nativeElement)), + 800, + ); + } - matchHeight(parent: HTMLElement) { - if (!parent) { - return; - } - const children = Array.from(parent.children); - children.forEach((x: HTMLElement) => { - this.renderer.setStyle(x, 'height', 'initial'); - }); - const itemHeights = children.map(x => x.getBoundingClientRect().height); - const maxHeight = itemHeights.reduce((prev, curr) => { - return curr > prev ? curr : prev; - }, 0); + @HostListener('window:resize') + onResize() { + setTimeout(() => this.matchHeight(this.el.nativeElement), 500); + } - children.forEach((x: HTMLElement) => this.renderer.setStyle(x, 'height', `${maxHeight}px`)); + matchHeight(parent: HTMLElement) { + if (!parent) { + return; } -} \ No newline at end of file + const children = Array.from(parent.children); + children.forEach((x: HTMLElement) => { + this.renderer.setStyle(x, 'height', 'initial'); + }); + const itemHeights = children.map(x => x.getBoundingClientRect().height); + const maxHeight = itemHeights.reduce((prev, curr) => { + return curr > prev ? curr : prev; + }, 0); + + children.forEach((x: HTMLElement) => + this.renderer.setStyle(x, 'height', `${maxHeight}px`), + ); + } +} diff --git a/src/app/homepage/homepage.component.html b/src/app/homepage/homepage.component.html index 2ff5f3b7f2..0ca9105779 100644 --- a/src/app/homepage/homepage.component.html +++ b/src/app/homepage/homepage.component.html @@ -17,7 +17,7 @@

- +
diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss index b292e175d9..e95735cacc 100644 --- a/src/app/homepage/homepage.component.scss +++ b/src/app/homepage/homepage.component.scss @@ -78,6 +78,10 @@ background: #f3f3f3; position: relative; + @include media(medium) { + margin: 40px -30px 0; + } + h4, img { display: inline-block; vertical-align: middle; diff --git a/src/app/homepage/menu/menu.component.ts b/src/app/homepage/menu/menu.component.ts index 6fa99158c9..d694f6402b 100644 --- a/src/app/homepage/menu/menu.component.ts +++ b/src/app/homepage/menu/menu.component.ts @@ -42,6 +42,29 @@ export class MenuComponent implements OnInit { { title: 'E2E Testing', path: '/fundamentals/e2e-testing' }, ] }, + { + title: 'Techniques', + isOpened: false, + children: [ + { title: 'MVC', path: '/techniques/mvc' }, + { title: 'SQL', path: '/techniques/sql' }, + { title: 'MongoDB', path: '/techniques/mongodb' }, + { title: 'Authentication', path: '/techniques/authentication' }, + ] + }, + { + title: 'GraphQL', + isOpened: false, + children: [ + { title: 'Quick Start', path: '/graphql/quick-start' }, + { title: 'Resolvers Map', path: '/graphql/resolvers-map' }, + { title: 'Mutations', path: '/graphql/mutations' }, + { title: 'Subscriptions', path: '/graphql/subscriptions' }, + { title: 'Guards & Interceptors', path: '/graphql/guards-interceptors' }, + { title: 'Schema stitching', path: '/graphql/schema-stitching' }, + { title: 'IDE', path: '/graphql/ide' }, + ] + }, { title: 'WebSockets', isOpened: false, @@ -68,12 +91,9 @@ export class MenuComponent implements OnInit { ] }, { - title: 'Advanced', + title: 'Execution Context', isOpened: false, - children: [ - { title: 'Hierarchical Injector', path: '/advanced/hierarchical-injector' }, - { title: 'Mixin Class', path: '/advanced/mixins' }, - ] + path: '/execution-context', }, { title: 'Recipes', @@ -81,14 +101,21 @@ export class MenuComponent implements OnInit { children: [ { title: 'SQL (TypeORM)', path: '/recipes/sql-typeorm' }, { title: 'MongoDB (Mongoose)', path: '/recipes/mongodb' }, - { title: 'MongoDB E2E (Mockgoose)', path: '/recipes/mockgoose' }, { title: 'SQL (Sequelize)', path: '/recipes/sql-sequelize' }, - { title: 'Passport integration', path: '/recipes/passport' }, + { title: 'Authentication (Passport)', path: '/recipes/passport' }, { title: 'CQRS', path: '/recipes/cqrs' }, { title: 'OpenAPI (Swagger)', path: '/recipes/swagger' }, - { title: 'GraphQL', path: '/recipes/graphql', isPending: true } + { title: 'MongoDB E2E (Mockgoose)', path: '/recipes/mockgoose' }, ], }, + { + title: 'Advanced', + isOpened: false, + children: [ + { title: 'Hierarchical Injector', path: '/advanced/hierarchical-injector' }, + { title: 'Mixin Class', path: '/advanced/mixins' }, + ] + }, { title: 'FAQ', isOpened: false, 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 0ae0ec69d8..bdc75d1f80 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 @@ -11,7 +11,7 @@ export class HierarchicalInjectorComponent extends BasePageComponent { get coreModule() { return ` @Module({ - modules: [CommonModule], + imports: [CommonModule], components: [CoreService, ContextService], }) export class CoreModule {}`; 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 0326ebb4ef..cdfd51d592 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 @@ -22,7 +22,7 @@
This chapter applies only to TypeScript

This function takes the isCached() predicate as an argument and assigns it to the mixin class. - The last step is to setup the interceptor: + The last step is to set up the interceptor:

{{ setup }}

diff --git a/src/app/homepage/pages/components/components.component.html b/src/app/homepage/pages/components/components.component.html index be9d8758aa..904d4900ed 100644 --- a/src/app/homepage/pages/components/components.component.html +++ b/src/app/homepage/pages/components/components.component.html @@ -13,7 +13,7 @@

Components

Hint Since Nest enables the possibility to design, organize the dependencies in more OO-way, we strongly recommend - to follow the SOLID principles. + following the SOLID principles.

Let's create a CatsService component: @@ -96,7 +96,7 @@

Last step

app.module.ts
-
server.ts
+
main.ts
diff --git a/src/app/homepage/pages/controllers/controllers.component.html b/src/app/homepage/pages/controllers/controllers.component.html index 8873eb5018..3f128a6ce0 100644 --- a/src/app/homepage/pages/controllers/controllers.component.html +++ b/src/app/homepage/pages/controllers/controllers.component.html @@ -39,7 +39,7 @@

Metadata



Furthermore, the response status code is always 200 by default, except POST requests, when it's 201. - We can easily change this behavior by adding the @HttpCode(...) decorator at a handler-level. + We can easily change this behaviour by adding the @HttpCode(...) decorator at a handler-level. @@ -51,7 +51,7 @@

Metadata

- Notice! It's forbidden to use both two approaches at the same time. Nest detects whether the handler is using @Res() or @Next(), and if it's truth - the standard way is disabled for this single route. + Notice! It's forbidden to use both two approaches at the same time. Nest detects whether the handler is using @Res() or @Next(), and if it's a truth - the standard way is disabled for this single route.

Request object

@@ -126,7 +126,7 @@

More endpoints

Status code manipulation

As mentioned, the response status code is always 200 by default, except POST requests, when it's 201. - We can easily change this behavior by adding the @HttpCode(...) decorator at a handler-level. + We can easily change this behaviour by adding the @HttpCode(...) decorator at a handler-level.

{{ 'cats.controller' | extension: statusCodeT.isJsActive }} @@ -142,14 +142,14 @@

Route parameters

Async / await

We love modern JavaScript, and we know that the data extraction is mostly asynchronous. - That's why Nest supports async functions, and works pretty well with them. + That's why Nest supports async functions and works pretty well with them.

Hint Learn more about async / await here!

- 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: + Every async function has to return the Promise. It means that you can return deferred value and + Nest will resolve it by itself. Let's have a look at the below example:

{{ 'cats.controller' | extension: asyncExampleT.isJsActive }} @@ -234,7 +234,7 @@

Express approach

The second way of manipulating the response is to use express response object. 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: + To show the differences, I'm going to rewrite the CatsController:

@@ -242,7 +242,7 @@

Express approach

{{ 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 + This manner is much less clear from my point of view. I definitely prefer the first approach, but to make the Nest backwards 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 9c52c04de4..3946d496dd 100644 --- a/src/app/homepage/pages/controllers/controllers.component.ts +++ b/src/app/homepage/pages/controllers/controllers.component.ts @@ -167,7 +167,7 @@ export class ApplicationModule {}`; return ` import * as bodyParser from 'body-parser'; import { NestFactory } from '@nestjs/core'; -import { ApplicationModule } from './modules/app.module'; +import { ApplicationModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(ApplicationModule); 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 717da34be6..9be3692383 100644 --- a/src/app/homepage/pages/exception-filters/exception-filters.component.html +++ b/src/app/homepage/pages/exception-filters/exception-filters.component.html @@ -1,152 +1,289 @@
-

Exception Filters

-

- In Nest there's an exceptions layer, which responsibility is to catch the unhandled exceptions and - return the appropriate response to the end-user. -

-
-

- Every exception is handled by the global exception filter and when it's unrecognized (not HttpException or class that inherit HttpException), a user receives the below JSON response: -

-
{{ errorResponse }}
-

HttpException

-

- There's a built-in HttpException class inside the @nestjs/common package. The core exception handler works with this class very well. - When you throw HttpException object, it'll be caught by handler and transformed to the relevant JSON response. -

-

- 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' | extension: createMethodT.isJsActive }} - +

Exception Filters

+

+ In Nest there's an + exceptions layer, which responsibility is to catch the unhandled exceptions and return the appropriate response + to the end-user. +

+
+ +
+

+ Every exception is handled by the global exception filter and when it's + unrecognized (not + HttpException or class that inherit + HttpException), a user receives the below JSON response: +

+
{{ errorResponse }}
+

HttpException

+

+ There's a built-in + HttpException class inside the + @nestjs/common package. The core exception handler works with this class very well. When you throw + HttpException object, it'll be caught by handler and transformed to the relevant JSON response. +

+

+ 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' | extension: createMethodT.isJsActive }} + -
{{ createMethod }}
-
{{ createMethodJs }}
-
- Warning I have used the HttpStatus here. It's a helper enum imported from the @nestjs/common package. -
-

- Now when the client will call this endpoint, the response would looks like below: -

-
{{ forbiddenResponse }}
-

- The HttpException constructor takes string | object as a first argument. If you'd pass object instead of a string, you'll completely override the response body. -

- - {{ 'cats.controller' | extension: exceptionObjT.isJsActive }} - - -
{{ exceptionObj }}
-
{{ exceptionObjJs }}
-

- And that's how the response would look like: -

-
{{ customResponse }}
-

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' | extension: forbiddenExceptionT.isJsActive }} - - -
{{ forbiddenException }}
-

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

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

HTTP exceptions

-

- To reduce the boilerplate code, Nest provides a set of usable exceptions that extends the core HttpException. All of them are available in the @nestjs/common package: -

-
    -
  • BadRequestException
  • -
  • UnauthorizedException
  • -
  • NotFoundException
  • -
  • ForbiddenException
  • -
  • NotAcceptableException
  • -
  • RequestTimeoutException
  • -
  • ConflictException
  • -
  • GoneException
  • -
  • PayloadTooLargeException
  • -
  • UnsupportedMediaTypeException
  • -
  • UnprocessableException
  • -
  • InternalServerErrorException
  • -
  • NotImplementedException
  • -
  • BadGatewayException
  • -
  • ServiceUnavailableException
  • -
  • GatewayTimeoutException
  • -
-

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. - We love generic solutions and making your life easier, that's why the feature called exception filters was created. -

-

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

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

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

-
- Hint Every exception filter should implement the ExceptionFilter interface. It forces to provide the catch() method with the proper signature. -
-

- The @Catch() decorator binds the required metadata to the exception filter. It tells Nest that this filter is looking for HttpException. - The @Catch() takes infinite count of parameters, so you can setup this filter for several types of exceptions, just separate them by a comma. -

-

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

- - {{ 'cats.controller' | extension: forbiddenCreateMethodWithFilterT.isJsActive }} - - -
{{ forbiddenCreateMethodWithFilter }}
-
{{ forbiddenCreateMethodWithFilterJs }}
-
- Warning The @UseFilters() decorator is imported from the @nestjs/common package. -
-

- We've used the @UseFilters() decorator here. Same as @Catch(), it takes infinite count of parameters. -

-

- In the above example, the HttpExceptionFilter is applied only to 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' | 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' | extension: globalScopedFilterT.isJsActive }} - - -
{{ globalScopedFilter }}
-

- The global filters are used across the entire application, for every controller, every route handler. -

-
- Notice The useGlobalFilters() method doesn't setup filters for gateways and microservices. -
+
{{ createMethod }}
+
{{ createMethodJs }}
+
+ Warning I have used the + HttpStatus here. It's a helper enum imported from the + @nestjs/common package. +
+

+ Now when the client will call this endpoint, the response would looks like below: +

+
{{ forbiddenResponse }}
+

+ The + HttpException constructor takes + string | object as a first argument. If you'd pass + object instead of a + string, you'll completely override the response body. +

+ + {{ 'cats.controller' | extension: exceptionObjT.isJsActive }} + + +
{{ exceptionObj }}
+
{{ exceptionObjJs }}
+

+ And that's how the response would look like: +

+
{{ customResponse }}
+

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' | extension: forbiddenExceptionT.isJsActive }} + + +
{{ forbiddenException }}
+

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

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

HTTP exceptions

+

+ To reduce the boilerplate code, Nest provides a set of usable exceptions that extends the core + HttpException. All of them are available in the + @nestjs/common package: +

+
    +
  • + BadRequestException +
  • +
  • + UnauthorizedException +
  • +
  • + NotFoundException +
  • +
  • + ForbiddenException +
  • +
  • + NotAcceptableException +
  • +
  • + RequestTimeoutException +
  • +
  • + ConflictException +
  • +
  • + GoneException +
  • +
  • + PayloadTooLargeException +
  • +
  • + UnsupportedMediaTypeException +
  • +
  • + UnprocessableException +
  • +
  • + InternalServerErrorException +
  • +
  • + NotImplementedException +
  • +
  • + BadGatewayException +
  • +
  • + ServiceUnavailableException +
  • +
  • + GatewayTimeoutException +
  • +
+

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. We love + generic solutions and making your life easier, that's why the feature called + exception filters was created. +

+

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

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

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

+
+ Hint Every exception filter should implement the + ExceptionFilter interface. It forces to provide the + catch() method with the proper signature. +
+

+ The + @Catch(HttpException) decorator binds the required metadata to the exception filter. It tells Nest that this filter + is looking for + HttpException. The + @Catch() takes endless number of parameters, so you can set up this filter for several types of exceptions, just + separate them by a comma. +

+

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

+ + {{ 'cats.controller' | extension: forbiddenCreateMethodWithFilterT.isJsActive }} + + +
{{ forbiddenCreateMethodWithFilter }}
+
{{ forbiddenCreateMethodWithFilterJs }}
+
+ Warning The + @UseFilters() decorator is imported from the + @nestjs/common package. +
+

+ We've used the + @UseFilters() decorator here. Same as + @Catch(), it takes endless number of parameters. +

+

+ In the above example, the + HttpExceptionFilter is applied only to 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' | 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. +

+ + {{ 'main' | extension: globalScopedFilterT.isJsActive }} + + +
{{ globalScopedFilter }}
+

+ The global filters are used across the entire application, for every controller, every route handler. +

+
+ Notice The + useGlobalFilters() method doesn't setup filters for gateways and microservices. +
+

Catch everything

+

+ To handle every occurred exception, you may leave parentheses empty ( + @Catch()): +

+ + {{ 'any-exception.filter' | extension: exceptionFilterT.isJsActive }} + + +
{{ exceptionFilter }}
+
{{ exceptionFilterJs }}
+

Above filter will catch each thrown exception.

+

Global filters

+

+ The global exception filters don't belong to any scope. They live outside modules, thus as a result - they can't inject dependencies. + We need to create an instance immediately. But quite often, global filters depend on other objects, for example, we'd + love to log an exception using + LoggerService, but this service is a part of the + LoggerModule. What's now? +

+

+ The solution is pretty easy. Each Nest application instance is in fact, a created + Nest context. The Nest context is a wrapper around the Nest container, which holds all instantiated classes. + We can grab any existing instance from within any imported module directly using application object. +

+

+ Let's assume that we have a + LoggerExceptionFilter registered in the + LoggerModule. This + LoggerModule is imported into + root module. We can pick the + LoggerExceptionFilter instance using following syntax: +

+
{{ getLoggerExceptionFilter }}
+

+ To grab LoggerExceptionFilter instance we have used 2 methods, well-described in the below table: +

+ + + + + + + + + +
+ get() + + Makes possible to retrieve the instance of the component or controller available inside the processed module. +
+ select() + + Allows you to navigate through the module tree, for example, to pull out a specific instance from the selected module. +
+
+ Hint The root module is selected by default. To select any other module, you need to go through entire modules stack (step by step). +
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 2acfbe04ae..f6f13f8ba8 100644 --- a/src/app/homepage/pages/exception-filters/exception-filters.component.ts +++ b/src/app/homepage/pages/exception-filters/exception-filters.component.ts @@ -105,17 +105,19 @@ async create(createCatDto) { get httpExceptionFilter() { return ` import { ExceptionFilter, Catch } from '@nestjs/common'; -import { HttpException } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, response) { const status = exception.getStatus(); - response.status(status).json({ - statusCode: status, - message: \`It's a message from the exception filter\`, - }); + response + .status(status) + .json({ + statusCode: status, + message: \`It's a message from the exception filter\`, + }); } }`; } @@ -123,17 +125,53 @@ export class HttpExceptionFilter implements ExceptionFilter { get httpExceptionFilterJs() { return ` import { Catch } from '@nestjs/common'; -import { HttpException } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; @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\`, - }); + response + .status(status) + .json({ + statusCode: status, + message: \`It's a message from the exception filter\`, + }); + } +}`; + } + + get exceptionFilter() { + return ` +import { ExceptionFilter, Catch } from '@nestjs/common'; + +@Catch() +export class AnyExceptionFilter implements ExceptionFilter { + catch(exception, response) { + response + .status(500) + .json({ + statusCode: 500, + message: \`It's a message from the exception filter\`, + }); + } +}`; + } + + get exceptionFilterJs() { + return ` +import { ExceptionFilter, Catch } from '@nestjs/common'; + +@Catch() +export class AnyExceptionFilter implements ExceptionFilter { + catch(exception, response) { + response + .status(500) + .json({ + statusCode: 500, + message: \`It's a message from the exception filter\`, + }); } }`; } @@ -174,6 +212,17 @@ async function bootstrap() { await app.listen(3000); } bootstrap(); +`; + } + + get getLoggerExceptionFilter() { + return ` +const app = await NestFactory.create(ApplicationModule); +const loggerFilter = app + .select(LoggerModule) + .get(LoggerExceptionFilter); + +app.useGlobalFilters(loggerFilter); `; } } \ No newline at end of file diff --git a/src/app/homepage/pages/execution-context/execution-context.component.html b/src/app/homepage/pages/execution-context/execution-context.component.html new file mode 100644 index 0000000000..2fe130ccc1 --- /dev/null +++ b/src/app/homepage/pages/execution-context/execution-context.component.html @@ -0,0 +1,46 @@ +
+

Execution Context

+

+ There are several ways of mounting the Nest application. + You can create a web app, microservice or just a Nest execution context. + The Nest context is a wrapper around the Nest container, which holds all instantiated classes. We can grab any existing instance from within any imported module directly using application object. + Thanks to that, you can take advantages of the Nest framework everywhere, including CRON jobs and even build a CLI on top of it. +

+

+ To create a Nest application context, we are using the following syntax: +

+ +
{{ executionContext }}
+

+ Afterwards, Nest allows you to pick any instance registered within Nest application. + Let's imagine that we have a TasksController in the TasksModule. + This class provides a set of usable methods, which we want to call from within CRON job. +

+ +
{{ pickTasksController }}
+

+ And that's it. To grab TasksController instance we have used 2 methods, well-described in the below table: +

+ + + + + + + + + +
+ get() + + Makes possible to retrieve the instance of the component or controller available inside the processed module. +
+ select() + + Allows you to navigate through the module tree, for example, to pull out a specific instance from the selected module. +
+
+ Hint The root module is selected by default. To select any other module, you need to go through entire modules stack (step by step). +
+
+ \ No newline at end of file diff --git a/src/app/homepage/pages/execution-context/execution-context.component.scss b/src/app/homepage/pages/execution-context/execution-context.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/execution-context/execution-context.component.spec.ts b/src/app/homepage/pages/execution-context/execution-context.component.spec.ts new file mode 100644 index 0000000000..a7b49ef4fa --- /dev/null +++ b/src/app/homepage/pages/execution-context/execution-context.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExecutionContextComponent } from './execution-context.component'; + +describe('ExecutionContextComponent', () => { + let component: ExecutionContextComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ExecutionContextComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExecutionContextComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/execution-context/execution-context.component.ts b/src/app/homepage/pages/execution-context/execution-context.component.ts new file mode 100644 index 0000000000..1039909278 --- /dev/null +++ b/src/app/homepage/pages/execution-context/execution-context.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../page/page.component'; + +@Component({ + selector: 'app-execution-context', + templateUrl: './execution-context.component.html', + styleUrls: ['./execution-context.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ExecutionContextComponent extends BasePageComponent { + get executionContext() { + return ` +async function bootstrap() { + const app = await NestFactory.createApplicationContext(ApplicationModule); + // logic.. :) +} +bootstrap();`; + } + + get pickTasksController() { + return ` +const app = await NestFactory.create(ApplicationModule); +const tasksController = app + .select(TasksModule) + .get(TasksController); +`; + } +} diff --git a/src/app/homepage/pages/faq/hybrid-application/hybrid-application.component.html b/src/app/homepage/pages/faq/hybrid-application/hybrid-application.component.html index d610c603cc..d525134826 100644 --- a/src/app/homepage/pages/faq/hybrid-application/hybrid-application.component.html +++ b/src/app/homepage/pages/faq/hybrid-application/hybrid-application.component.html @@ -2,7 +2,7 @@

Hybrid Application

The hybrid application's an application with the connected microservice/s. - It's possible to combine the INestApplication with the infinite count of the INestMicroservice instances. + It's possible to combine the INestApplication with the endless number of the INestMicroservice instances.

{{ hybridApplication }}
\ No newline at end of file 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 98613cdf82..5c47c600e7 100644 --- a/src/app/homepage/pages/first-steps/first-steps.component.html +++ b/src/app/homepage/pages/first-steps/first-steps.component.html @@ -1,7 +1,7 @@

First Steps

- In this set of articles you'll learn the + In this set of articles, you'll learn the core fundamentals of Nest. 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.

@@ -47,21 +47,17 @@

Setup

src
-
modules
-
-
app.controller.ts
-
app.module.ts
-
-
server.ts
+
app.controller.ts
+
app.module.ts
+
main.ts

- Following the convention, newly created modules should be placed inside - modules directory. + Following the convention, newly created modules should have a dedicated directory. @@ -77,23 +73,23 @@

Setup

- +
- server.ts + main.ts The entry file of the application. It uses NestFactory to create the Nest application instance. app.controller.ts Basic controller example with a single route.Basic controller sample with a single route.

The - server.ts includes an async function, which responsibility is to + main.ts includes an async function, which responsibility is to bootstrap our application:

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

To create a Nest application instance, we are using the NestFactory. The - create() method returns an object, which fulfills + create() method returns an object, which fulfils INestApplication interface, and provides a set of usable methods, which are well described in the next chapters.

Running application

@@ -103,7 +99,7 @@

Running application


 $ npm run start

- This command starts the HTTP server on the port defined inside the server.ts file in the src directory. + This command starts the HTTP server on the port defined inside the main.ts file in the src directory. While the application is running, open your browser and navigate to http://localhost:3000/. You should see the Hello world! message.

diff --git a/src/app/homepage/pages/first-steps/first-steps.component.ts b/src/app/homepage/pages/first-steps/first-steps.component.ts index 339845df91..5799ef85ff 100644 --- a/src/app/homepage/pages/first-steps/first-steps.component.ts +++ b/src/app/homepage/pages/first-steps/first-steps.component.ts @@ -11,7 +11,7 @@ export class FirstStepsComponent extends BasePageComponent { get bootstrap(): string { return ` import { NestFactory } from '@nestjs/core'; -import { ApplicationModule } from './modules/app.module'; +import { ApplicationModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(ApplicationModule); diff --git a/src/app/homepage/pages/fundamentals/circular-dependency/circular-dependency.component.ts b/src/app/homepage/pages/fundamentals/circular-dependency/circular-dependency.component.ts index 2a1091cd39..c51557a31f 100644 --- a/src/app/homepage/pages/fundamentals/circular-dependency/circular-dependency.component.ts +++ b/src/app/homepage/pages/fundamentals/circular-dependency/circular-dependency.component.ts @@ -99,7 +99,7 @@ export class CommonService { get forwardRefModule() { return ` @Module({ - modules: [forwardRef(() => CatsModule)], + imports: [forwardRef(() => CatsModule)], }) export class CommonModule {}`; } diff --git a/src/app/homepage/pages/fundamentals/e2e-testing/e2e-testing.component.ts b/src/app/homepage/pages/fundamentals/e2e-testing/e2e-testing.component.ts index 03e483d30d..8ddc7f0c6d 100644 --- a/src/app/homepage/pages/fundamentals/e2e-testing/e2e-testing.component.ts +++ b/src/app/homepage/pages/fundamentals/e2e-testing/e2e-testing.component.ts @@ -13,8 +13,8 @@ export class E2eTestingComponent extends BasePageComponent { import * as express from 'express'; import * as request from 'supertest'; import { Test } from '@nestjs/testing'; -import { CatsModule } from '../../src/modules/cats/cats.module'; -import { CatsService } from '../../src/modules/cats/cats.service'; +import { CatsModule } from '../../src/cats/cats.module'; +import { CatsService } from '../../src/cats/cats.service'; describe('Cats', () => { const server = express(); @@ -22,7 +22,7 @@ describe('Cats', () => { beforeAll(async () => { const module = await Test.createTestingModule({ - modules: [CatsModule], + imports: [CatsModule], }) .overrideComponent(CatsService).useValue(catsService) .compile(); diff --git a/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.html b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.html new file mode 100644 index 0000000000..908804d100 --- /dev/null +++ b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.html @@ -0,0 +1,29 @@ +
+

Guards & Interceptors

+

+ In the GraphQL world, a lot of articles complain how to handle stuff like an authentication, or side-effects of operations. + Should we put it inside the business logic? Shall we use a higher-order function to enhance queries and mutations as well for example with authorization logic? + There's no single answer. +

+

+ The Nest ecosystem is trying to help with this issue using existing features like guards and interceptors. + The idea behind them is to reduce a redundancy and also, create a well-structured applications. +

+

Usage

+

+ You can use both guards and interceptors in the same way as in the simple REST application. + They act equivalently until the request is passed as a rootValue in the graphqlExpress middleware. + Let's have a look at the following code: +

+ +
{{ useGuardsExample }}
+

+ Thanks to that you can move your authentication logic to the guard, or even reuse the same guard class as in the REST application. + The interceptors works in the exact same way: +

+ +
{{ useInterceptorsExample }}
+

+ Write once, use everywhere :) +

+
\ No newline at end of file diff --git a/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.scss b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.spec.ts b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.spec.ts new file mode 100644 index 0000000000..30c3418047 --- /dev/null +++ b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GuardsInterceptorsComponent } from './guards-interceptors.component'; + +describe('GuardsInterceptorsComponent', () => { + let component: GuardsInterceptorsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GuardsInterceptorsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GuardsInterceptorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.ts b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.ts new file mode 100644 index 0000000000..bb6609bb96 --- /dev/null +++ b/src/app/homepage/pages/graphql/guards-interceptors/guards-interceptors.component.ts @@ -0,0 +1,29 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-guards-interceptors', + templateUrl: './guards-interceptors.component.html', + styleUrls: ['./guards-interceptors.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class GuardsInterceptorsComponent extends BasePageComponent { + get useGuardsExample() { + return ` +@Query('author') +@UseGuards(AuthGuard) +async getAuthor(obj, args, context, info) { + const { id } = args; + return await this.authorsService.findOneById(id); +}`; + } + + get useInterceptorsExample() { + return ` +@Mutation() +@UseInterceptors(EventsInterceptor) +async upvotePost(_, { postId }) { + return await this.postsService.upvoteById({ id: postId }); +}`; + } +} diff --git a/src/app/homepage/pages/graphql/ide/ide.component.html b/src/app/homepage/pages/graphql/ide/ide.component.html new file mode 100644 index 0000000000..9c32f1dc5d --- /dev/null +++ b/src/app/homepage/pages/graphql/ide/ide.component.html @@ -0,0 +1,24 @@ +
+

IDE

+

+ One of the most popular GraphQL in-browser IDE is called GraphiQL. + To use a GraphiQL with your application, you need to set up a middleware. + This particular middleware comes with apollo-server-express package that we had to install already. + Its name is a graphiqlExpress(). +

+

+ In order to set up a middleware, we need to open an app.module.ts file once again: +

+ + {{ 'app.module' | extension: appModuleSchemaT.isJsActive }} + + +
{{ createSchema }}
+
{{ createSchemaJs }}
+
+ Hint The graphiqlExpress() offers few other options, read more about them here. +
+

+ Now, when you navigate to the http://localhost:PORT/graphiql you should see a graphical interactive GraphiQL IDE. +

+
diff --git a/src/app/homepage/pages/graphql/ide/ide.component.scss b/src/app/homepage/pages/graphql/ide/ide.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/ide/ide.component.spec.ts b/src/app/homepage/pages/graphql/ide/ide.component.spec.ts new file mode 100644 index 0000000000..38a460d816 --- /dev/null +++ b/src/app/homepage/pages/graphql/ide/ide.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IdeComponent } from './ide.component'; + +describe('IdeComponent', () => { + let component: IdeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ IdeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IdeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/ide/ide.component.ts b/src/app/homepage/pages/graphql/ide/ide.component.ts new file mode 100644 index 0000000000..3afa70b1d0 --- /dev/null +++ b/src/app/homepage/pages/graphql/ide/ide.component.ts @@ -0,0 +1,68 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-ide', + templateUrl: './ide.component.html', + styleUrls: ['./ide.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class IdeComponent extends BasePageComponent { + get createSchema() { + return ` +import { + Module, + MiddlewaresConsumer, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'; +import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql'; + +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule implements NestModule { + constructor(private readonly graphQLFactory: GraphQLFactory) {} + + configure(consumer: MiddlewaresConsumer) { + const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql'); + const schema = this.graphQLFactory.createSchema({ typeDefs }); + + consumer + .apply(graphiqlExpress({ endpointURL: '/graphql' })) + .forRoutes({ path: '/graphiql', method: RequestMethod.GET }) + .apply(graphqlExpress(req => ({ schema, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } + + get createSchemaJs() { + return ` +import { Module, RequestMethod } from '@nestjs/common'; +import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'; +import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql'; + +@Dependencies(GraphQLFactory) +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule { + constructor(graphQLFactory) { + this.graphQLFactory = graphQLFactory; + } + + configure(consumer) { + const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql'); + const schema = this.graphQLFactory.createSchema({ typeDefs }); + + consumer + .apply(graphiqlExpress({ endpointURL: '/graphql' })) + .forRoutes({ path: '/graphiql', method: RequestMethod.GET }) + .apply(graphqlExpress(req => ({ schema, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } +} diff --git a/src/app/homepage/pages/graphql/mutations/mutations.component.html b/src/app/homepage/pages/graphql/mutations/mutations.component.html new file mode 100644 index 0000000000..72269b6d1b --- /dev/null +++ b/src/app/homepage/pages/graphql/mutations/mutations.component.html @@ -0,0 +1,46 @@ +
+

Mutations

+

+ In GraphQL, to modify the server-side data, we're using mutations. Read more about them + here. +

+

+ The official + Apollo documentation shares an + upvotePost() mutation example. This mutation allows to increase a post + votes property value. +

+ +
{{ mutationsExample }}
+

+ In order to create an equivalent mutation in Nest-way, we'll make use of the + @Mutation() decorator. Let's extend our + AuthorResolver used in the previous section (resolvers map). +

+ +
{{ resolversWithNames }}
+

Refactor

+

+ Once again (same as in + resolvers map chapter) we're gonna do a small refactor to take advantages of the Nest architecture, to turn it into a + real-world example. +

+ + + +
{{ realWorldExample }}
+
{{ realWorldExampleJs }}
+

+ That's all. The business logic was moved to the PostsService. +

+

Type definitions

+

+ The last step is to add our mutation to the existing type definitions. +

+ author-types.graphql +
{{ typeDefs }}
+

+ The upvotePost(postId: Int!): Post mutation is here now! +

+
diff --git a/src/app/homepage/pages/graphql/mutations/mutations.component.scss b/src/app/homepage/pages/graphql/mutations/mutations.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/mutations/mutations.component.spec.ts b/src/app/homepage/pages/graphql/mutations/mutations.component.spec.ts new file mode 100644 index 0000000000..6618a5df8e --- /dev/null +++ b/src/app/homepage/pages/graphql/mutations/mutations.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MutationsComponent } from './mutations.component'; + +describe('MutationsComponent', () => { + let component: MutationsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MutationsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MutationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/mutations/mutations.component.ts b/src/app/homepage/pages/graphql/mutations/mutations.component.ts new file mode 100644 index 0000000000..f4beb05b9a --- /dev/null +++ b/src/app/homepage/pages/graphql/mutations/mutations.component.ts @@ -0,0 +1,148 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-mutations', + templateUrl: './mutations.component.html', + styleUrls: ['./mutations.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MutationsComponent extends BasePageComponent { + + get mutationsExample() { + return ` +Mutation: { + upvotePost: (_, { postId }) => { + const post = find(posts, { id: postId }); + if (!post) { + throw new Error(\`Couldn't find post with id \${postId}\`); + } + post.votes += 1; + return post; + }, +}`; + } + + get resolversWithNames() { + return ` +import { Query, Mutation, Resolver, ResolveProperty } from '@nestjs/graphql'; +import { find, filter } from 'lodash'; + +// example data +const authors = [ + { id: 1, firstName: 'Tom', lastName: 'Coleman' }, + { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, + { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, +]; +const posts = [ + { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, + { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, + { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, + { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, +]; + +@Resolver('Author') +export class AuthorResolver { + @Query('author') + getAuthor(obj, args, context, info) { + return find(authors, { id: args.id }); + } + + @Mutation() + upvotePost(_, { postId }) { + const post = find(posts, { id: postId }); + if (!post) { + throw new Error(\`Couldn't find post with id \${postId}\`); + } + post.votes += 1; + return post; + } + + @ResolveProperty('posts') + getPosts(author) { + return filter(posts, { authorId: author.id }); + } +}`; + } + + get realWorldExample() { + return ` +@Resolver('Author') +export class AuthorResolver { + constructor( + private readonly authorsService: AuthorsService, + private readonly postsService: PostsService, + ) {} + + @Query('author') + async getAuthor(obj, args, context, info) { + const { id } = args; + return await this.authorsService.findOneById(id); + } + + @Mutation() + async upvotePost(_, { postId }) { + return await this.postsService.upvoteById({ id: postId }); + } + + @ResolveProperty('posts') + async getPosts(author) { + const { id } = author; + return await this.postsService.findAll({ authorId: id }); + } +}`; + } + + get realWorldExampleJs() { + return ` +@Resolver('Author') +@Dependencies(AuthorsService, PostsService) +export class AuthorResolver { + constructor(authorsService, postsService) { + this.authorsService = authorsService; + this.postsService = postsService; + } + + @Query('author') + async getAuthor(obj, args, context, info) { + const { id } = args; + return await this.authorsService.findOneById(id); + } + + @Mutation() + async upvotePost(_, { postId }) { + return await this.postsService.upvoteById({ id: postId }); + } + + @ResolveProperty('posts') + async getPosts(author) { + const { id } = author; + return await this.postsService.findAll({ authorId: id }); + } +}`; + } + + get typeDefs() { + return ` +type Author { + id: Int! + firstName: String + lastName: String + posts: [Post] +} + +type Post { + id: Int! + title: String + votes: Int +} + +type Query { + author(id: Int!): Author +} + +type Mutation { + upvotePost(postId: Int!): Post +}`; + } +} diff --git a/src/app/homepage/pages/graphql/quick-start/quick-start.component.html b/src/app/homepage/pages/graphql/quick-start/quick-start.component.html new file mode 100644 index 0000000000..d78b691c06 --- /dev/null +++ b/src/app/homepage/pages/graphql/quick-start/quick-start.component.html @@ -0,0 +1,62 @@ +
+

Quick Start

+

+ GraphQL is a new way of thinking about the APIs. Here's a great + comparison between GraphQL and REST. In this set of articles, I'm not gonna explain what the GraphQL is, but rather + show how to work with the dedicated + @nestjs/graphql module. +

+

+ The + GraphQLModule is nothing more than a wrapper around the + Apollo server. We don't reinvent the wheel but provide a ready to use a module instead, that brings a clean way to + play with the GraphQL and Nest together. +

+

Installation

+

+ Firstly, we need to install the required packages: +

+

+$ npm i --save @nestjs/graphql apollo-server-express graphql-tools graphql
+

Apollo Middleware

+

+ Once the packages are installed, we can apply the GraphQL middleware provided by the + apollo-server-express package: +

+ + {{ 'app.module' | extension: appModuleT.isJsActive }} + + +
{{ middleware }}
+
{{ middlewareJs }}
+

+ That's all. We passed an empty object as a GraphQL schema and req (request object) as a rootValue for now. + Additionally, there're a few other available graphqlExpress options and you can read about them here. +

+

Schema

+

+ To create a schema, we are using GraphQLFactory which is a part of the @nestjs/graphql package. + This component provides a createSchema() method that accepts the same object as a makeExecutableSchema() function, well-described here. +

+

+ The schema options object demand at least resolvers and the typeDefs property. + You can pass type definitions manually, or use a utility mergeTypesByPaths() method of the GraphQLFactory. + Let's have a look on the following example: +

+ + {{ 'app.module' | extension: appModuleSchemaT.isJsActive }} + + +
{{ createSchema }}
+
{{ createSchemaJs }}
+
+ Hint Learn more about GraphQL schema here. +
+

+ In this case, the GraphQLFactory will go through each directory, and merge files that have a .graphql extension. + Afterwards, we can create a schema using these particular type definitions. The resolvers will be reflected automatically. +

+

+ Here you can read more about what the resolvers map actually is. +

+
diff --git a/src/app/homepage/pages/graphql/quick-start/quick-start.component.scss b/src/app/homepage/pages/graphql/quick-start/quick-start.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/quick-start/quick-start.component.spec.ts b/src/app/homepage/pages/graphql/quick-start/quick-start.component.spec.ts new file mode 100644 index 0000000000..370c6e8345 --- /dev/null +++ b/src/app/homepage/pages/graphql/quick-start/quick-start.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuickStartComponent } from './quick-start.component'; + +describe('QuickStartComponent', () => { + let component: QuickStartComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ QuickStartComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuickStartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/quick-start/quick-start.component.ts b/src/app/homepage/pages/graphql/quick-start/quick-start.component.ts new file mode 100644 index 0000000000..2a5bfc7b55 --- /dev/null +++ b/src/app/homepage/pages/graphql/quick-start/quick-start.component.ts @@ -0,0 +1,105 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-quick-start', + templateUrl: './quick-start.component.html', + styleUrls: ['./quick-start.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class QuickStartComponent extends BasePageComponent { + get middleware() { + return ` +import { + Module, + MiddlewaresConsumer, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { graphqlExpress } from 'apollo-server-express'; +import { GraphQLModule } from '@nestjs/graphql'; + +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule implements NestModule { + configure(consumer: MiddlewaresConsumer) { + consumer + .apply(graphqlExpress(req => ({ schema: {}, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } + + get middlewareJs() { + return ` +import { Module, RequestMethod } from '@nestjs/common'; +import { graphqlExpress } from 'apollo-server-express'; +import { GraphQLModule } from '@nestjs/graphql'; + +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule { + configure(consumer) { + consumer + .apply(graphqlExpress(req => ({ schema: {}, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } + + get createSchema() { + return ` +import { + Module, + MiddlewaresConsumer, + NestModule, + RequestMethod, +} from '@nestjs/common'; +import { graphqlExpress } from 'apollo-server-express'; +import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql'; + +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule implements NestModule { + constructor(private readonly graphQLFactory: GraphQLFactory) {} + + configure(consumer: MiddlewaresConsumer) { + const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql'); + const schema = this.graphQLFactory.createSchema({ typeDefs }); + + consumer + .apply(graphqlExpress(req => ({ schema, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } + + get createSchemaJs() { + return ` +import { Module, RequestMethod } from '@nestjs/common'; +import { graphqlExpress } from 'apollo-server-express'; +import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql'; + +@Dependencies(GraphQLFactory) +@Module({ + imports: [GraphQLModule], +}) +export class ApplicationModule { + constructor(graphQLFactory) { + this.graphQLFactory = graphQLFactory; + } + + configure(consumer) { + const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql'); + const schema = this.graphQLFactory.createSchema({ typeDefs }); + + consumer + .apply(graphqlExpress(req => ({ schema, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); + } +}`; + } +} diff --git a/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.html b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.html new file mode 100644 index 0000000000..c5858b629b --- /dev/null +++ b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.html @@ -0,0 +1,63 @@ +
+

Resolvers Map

+

+ When using graphql-tools, you have to create a resolvers map manually. + The following example is copied & pasted from the Apollo documentation where you can read more about it: +

+ +
{{ resolversMapExample }}
+

+ With the @nestjs/graphql package, the resolvers map is generated automatically using the metadata. + Let's rewrite the above example with the equivalent Nest-way code. +

+ +
{{ resolvers }}
+

+ The @Resolver() decorator doesn't affect either queries and mutations. + It only tells Nest that each @ResolveProperty() has a parent, which is an Author in this case. +

+
+ Hint If we are using the @Resolver() decorator, we don't have to mark a class as a @Component(), otherwise, it's necessary. +
+

+ Normally, we would use something like a getAuthor() or getPosts() as a method names. + We can do that easily as well: +

+ +
{{ resolversWithNames }}
+
+ Hint The @Resolver() decorator can be used at the method-level as well. +
+

Refactor

+

+ The idea behind the above code is to show the differences between the Apollo and the Nest-way, to allow for a simple transition of your code. + Right now, we're gonna do a small refactor to take advantages of the Nest architecture, to make it a real-world example. +

+ + + +
{{ realWorldExample }}
+
{{ realWorldExampleJs }}
+

+ Now we have to register the AuthorResolver somewhere, for example inside the newly created AuthorsModule. +

+ +
{{ authorsModule }}
+

+ The GraphQLModule will take care of reflecting the metadata and transforming class into the correct resolvers map automatically. + The only thing you have to do is to import this module somewhere, therefore Nest will know that AuthorsModule exists. +

+

Type definitions

+

+ The last missing piece is a type definitions (read more) file. + Let's create it near to the resolver class. +

+ author-types.graphql +
{{ typeDefs }}
+

+ That's all. We created a single author(id: Int!) query. +

+
+ Hint Learn more about GraphQL queries here. +
+
diff --git a/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.scss b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.spec.ts b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.spec.ts new file mode 100644 index 0000000000..ad320386c4 --- /dev/null +++ b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResolversMapComponent } from './resolvers-map.component'; + +describe('ResolversMapComponent', () => { + let component: ResolversMapComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ResolversMapComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ResolversMapComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.ts b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.ts new file mode 100644 index 0000000000..fa78b37c14 --- /dev/null +++ b/src/app/homepage/pages/graphql/resolvers-map/resolvers-map.component.ts @@ -0,0 +1,183 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-resolvers-map', + templateUrl: './resolvers-map.component.html', + styleUrls: ['./resolvers-map.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResolversMapComponent extends BasePageComponent { + get resolversMapExample() { + return ` +import { find, filter } from 'lodash'; + +// example data +const authors = [ + { id: 1, firstName: 'Tom', lastName: 'Coleman' }, + { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, + { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, +]; +const posts = [ + { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, + { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, + { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, + { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, +]; + +const resolverMap = { + Query: { + author(obj, args, context, info) { + return find(authors, { id: args.id }); + }, + }, + Author: { + posts(author) { + return filter(posts, { authorId: author.id }); + }, + }, +};`; + } + + get resolvers() { + return ` +import { Query, Resolver, ResolveProperty } from '@nestjs/graphql'; +import { find, filter } from 'lodash'; + +// example data +const authors = [ + { id: 1, firstName: 'Tom', lastName: 'Coleman' }, + { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, + { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, +]; +const posts = [ + { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, + { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, + { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, + { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, +]; + +@Resolver('Author') +export class AuthorResolver { + @Query() + author(obj, args, context, info) { + return find(authors, { id: args.id }); + } + + @ResolveProperty() + posts(author) { + return filter(posts, { authorId: author.id }); + } +} +`; + } + + get resolversWithNames() { + return ` +import { Query, Resolver, ResolveProperty } from '@nestjs/graphql'; +import { find, filter } from 'lodash'; + +// example data +const authors = [ + { id: 1, firstName: 'Tom', lastName: 'Coleman' }, + { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, + { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, +]; +const posts = [ + { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, + { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, + { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, + { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, +]; + +@Resolver('Author') +export class AuthorResolver { + @Query('author') + getAuthor(obj, args, context, info) { + return find(authors, { id: args.id }); + } + + @ResolveProperty('posts') + getPosts(author) { + return filter(posts, { authorId: author.id }); + } +}`; + } + + get realWorldExample() { + return ` +@Resolver('Author') +export class AuthorResolver { + constructor( + private readonly authorsService: AuthorsService, + private readonly postsService: PostsService, + ) {} + + @Query('author') + async getAuthor(obj, args, context, info) { + const { id } = args; + return await this.authorsService.findOneById(id); + } + + @ResolveProperty('posts') + async getPosts(author) { + const { id } = author; + return await this.postsService.findAll({ authorId: id }); + } +}`; + } + + get realWorldExampleJs() { + return ` +@Resolver('Author') +@Dependencies(AuthorsService, PostsService) +export class AuthorResolver { + constructor(authorsService, postsService) { + this.authorsService = authorsService; + this.postsService = postsService; + } + + @Query('author') + async getAuthor(obj, args, context, info) { + const { id } = args; + return await this.authorsService.findOneById(id); + } + + @ResolveProperty('posts') + async getPosts(author) { + const { id } = author; + return await this.postsService.findAll({ authorId: id }); + } +}`; + } + + + get authorsModule() { + return ` +@Module({ + imports: [PostsModule], + components: [AuthorsService, AuthorResolver], +}) +export class AuthorsModule {}` + } + + get typeDefs() { + return ` +type Author { + id: Int! + firstName: String + lastName: String + posts: [Post] +} + +type Post { + id: Int! + title: String + votes: Int +} + +type Query { + author(id: Int!): Author +}`; + } +} diff --git a/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.html b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.html new file mode 100644 index 0000000000..a8302e37cd --- /dev/null +++ b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.html @@ -0,0 +1,43 @@ +
+

Schema Stitching

+

+ The schema stitching is a feature that allows creating a single GraphQL schema from multiple underlying GraphQL APIs. + You can read more about it here. +

+

Proxying

+

+ To add the ability to proxy fields between schemas, you need to create additional resolvers between them. + Let's have a look on the example from the Apollo documentation: +

+ +
{{ stitchingExample }}
+

+ Here we delegate chirps property of User to another GraphQL API. + To achieve the same result in Nest-way, we use @DelegateProperty() decorator. +

+ +
{{ stitchNestWay }}
+
+ Hint The @Resolver() decorator is used here at the method-level, but you can use it at top (class) level as well. +
+

+ Now let's take a step back to the graphqlExpress middleware. + We need to merge our schemas and add delegates between them. + To create delegates we use createDelegates() method of GraphQLFactory class. +

+ + {{ 'app.module' | extension: appModuleSchemaT.isJsActive }} + + +
{{ createSchema }}
+

+ In order to merge schemas, we have used mergeSchemas() function (read more). + Moreover, there're chirpsSchema and linkTypeDefs variables. + They're copied & pasted directly from the Apollo documentation. +

+ +
{{ chirpsSchema }}
+

+ That's all. +

+
diff --git a/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.scss b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.spec.ts b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.spec.ts new file mode 100644 index 0000000000..5e9718cb29 --- /dev/null +++ b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SchemaStitchingComponent } from './schema-stitching.component'; + +describe('SchemaStitchingComponent', () => { + let component: SchemaStitchingComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SchemaStitchingComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SchemaStitchingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.ts b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.ts new file mode 100644 index 0000000000..4dd02a9c8f --- /dev/null +++ b/src/app/homepage/pages/graphql/schema-stitching/schema-stitching.component.ts @@ -0,0 +1,102 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-schema-stitching', + templateUrl: './schema-stitching.component.html', + styleUrls: ['./schema-stitching.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SchemaStitchingComponent extends BasePageComponent { + get stitchingExample() { + return ` +mergeInfo => ({ + User: { + chirps: { + fragment: \`fragment UserFragment on User { id }\`, + resolve(parent, args, context, info) { + const authorId = parent.id; + return mergeInfo.delegate( + 'query', + 'chirpsByAuthorId', + { + authorId, + }, + context, + info, + ); + }, + }, + } +})`; + } + + get stitchNestWay() { + return ` +@Resolver('User') +@DelegateProperty('chirps') +findChirpsByUserId() { + return (mergeInfo: MergeInfo) => ({ + fragment: \`fragment UserFragment on User { id }\`, + resolve(parent, args, context, info) { + const authorId = parent.id; + return mergeInfo.delegate( + 'query', + 'chirpsByAuthorId', + { + authorId, + }, + context, + info, + ); + }, + }); +}`; + } + + get createSchema() { + return ` +configure(consumer) { + const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql'); + const localSchema = this.graphQLFactory.createSchema({ typeDefs }); + const delegates = this.graphQLFactory.createDelegates(); + const schema = mergeSchemas({ + schemas: [localSchema, chirpSchema, linkTypeDefs], + resolvers: delegates, + }); + + consumer + .apply(graphqlExpress(req => ({ schema, rootValue: req }))) + .forRoutes({ path: '/graphql', method: RequestMethod.ALL }); +}`; + } + + get chirpsSchema() { + return ` +import { makeExecutableSchema } from 'graphql-tools'; + +const chirpSchema = makeExecutableSchema({ + typeDefs: \` + type Chirp { + id: ID! + text: String + authorId: ID! + } + + type Query { + chirpById(id: ID!): Chirp + chirpsByAuthorId(authorId: ID!): [Chirp] + } + \` +}); +const linkTypeDefs = \` + extend type User { + chirps: [Chirp] + } + + extend type Chirp { + author: User + } +\`;`; + } +} diff --git a/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.html b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.html new file mode 100644 index 0000000000..43e4a5941e --- /dev/null +++ b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.html @@ -0,0 +1,38 @@ +
+

Subscriptions

+

+ Subscription is just another GraphQL operation type like Query and Mutation. + It allows creating real-time subscriptions over a bidirectional transport layer, mainly over websockets. + Read more about the subscriptions here. +

+

+ Below is a commentAdded subscription example, copied & pasted directly from the official Apollo documentation: +

+ +
{{ subscriptionOfficialExample }}
+
+ Notice The pubsub is an instance of PubSub class. Read more about it here. +
+

+ In order to create an equivalent subscription in Nest-way, we'll make use of the + @Subscription() decorator. Let's extend our + AuthorResolver used in the resolvers map section. +

+ +
{{ resolversWithNames }}
+

Refactor

+

+ We have used a local PubSub instance here. Instead, we should define PubSub as a component, inject + it through the constructor (using @Inject() decorator), and reuse it across the entire application. + You can read more about Nest custom components here. +

+

Type definitions

+

+ The last step is to update type definitions (read more) file. +

+ author-types.graphql +
{{ typeDefs }}
+

+ That's all. We created a single commentAdded(repoFullName: String!): Comment subscription. +

+
diff --git a/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.scss b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.spec.ts b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.spec.ts new file mode 100644 index 0000000000..205dcf4de2 --- /dev/null +++ b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubscriptionsComponent } from './subscriptions.component'; + +describe('SubscriptionsComponent', () => { + let component: SubscriptionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SubscriptionsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubscriptionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.ts b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.ts new file mode 100644 index 0000000000..da690d83d7 --- /dev/null +++ b/src/app/homepage/pages/graphql/subscriptions/subscriptions.component.ts @@ -0,0 +1,91 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-subscriptions', + templateUrl: './subscriptions.component.html', + styleUrls: ['./subscriptions.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubscriptionsComponent extends BasePageComponent { + get subscriptionOfficialExample() { + return ` +Subscription: { + commentAdded: { + subscribe: () => pubsub.asyncIterator('commentAdded') + } +}`; + } + + get resolversWithNames() { + return ` +import { Query, Resolver, Subscription, ResolveProperty } from '@nestjs/graphql'; +import { find, filter } from 'lodash'; +import { PubSub } from 'graphql-subscriptions'; + +// example data +const authors = [ + { id: 1, firstName: 'Tom', lastName: 'Coleman' }, + { id: 2, firstName: 'Sashko', lastName: 'Stubailo' }, + { id: 3, firstName: 'Mikhail', lastName: 'Novikov' }, +]; +const posts = [ + { id: 1, authorId: 1, title: 'Introduction to GraphQL', votes: 2 }, + { id: 2, authorId: 2, title: 'Welcome to Meteor', votes: 3 }, + { id: 3, authorId: 2, title: 'Advanced GraphQL', votes: 1 }, + { id: 4, authorId: 3, title: 'Launchpad is Cool', votes: 7 }, +]; + +// example pubsub +const pubsub = new PubSub(); + +@Resolver('Author') +export class AuthorResolver { + @Query('author') + getAuthor(obj, args, context, info) { + return find(authors, { id: args.id }); + } + + @Subscription() + commentAdded() { + return { + subscribe: () => pubsub.asyncIterator('commentAdded'), + }; + } + + @ResolveProperty('posts') + getPosts(author) { + return filter(posts, { authorId: author.id }); + } +}`; + } + + get typeDefs() { + return ` +type Author { + id: Int! + firstName: String + lastName: String + posts: [Post] +} + +type Post { + id: Int! + title: String + votes: Int +} + +type Query { + author(id: Int!): Author +} + +type Comment { + id: String + content: String +} + +type Subscription { + commentAdded(repoFullName: String!): Comment +}`; + } +} \ No newline at end of file diff --git a/src/app/homepage/pages/guards/guards.component.html b/src/app/homepage/pages/guards/guards.component.html index 1e7d0243e2..8b17502188 100644 --- a/src/app/homepage/pages/guards/guards.component.html +++ b/src/app/homepage/pages/guards/guards.component.html @@ -48,7 +48,7 @@

RolesGuard

Usage

- 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. + The guards can be controller-scoped, method-scoped and global-scoped. To set up the guard, we're using @UseGuards() decorator. This decorator takes endless number of arguments.

{{ 'cats.controller' | extension: useGuardsT.isJsActive }} @@ -59,7 +59,7 @@

Usage

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 the 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 set up the guard at method level. To bind the global guard, we're using the useGlobalGuards() method of the Nest application instance:

{{ globalGuard }}
@@ -140,6 +140,52 @@

Reflector

Custom error responses

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

+

Global guards

+

+ The global guards don't belong to any scope. They live outside modules, thus as a result - they can't inject dependencies. + We need to create an instance immediately. But quite often, global guards depend on other objects, for example, we'd + love to authenticate request using + AuthService, but this service is a part of the + AuthModule. What's now? +

+

+ The solution is pretty easy. Each Nest application instance is in fact, a created + Nest context. The Nest context is a wrapper around the Nest container, which holds all instantiated classes. + We can grab any existing instance from within any imported module directly using application object. +

+

+ Let's assume that we have a + AuthGuard registered in the + AuthModule. This + AuthModule is imported into + root module. We can pick the + AuthGuard instance using following syntax: +

+
{{ getAuthGuard }}
+

+ To grab AuthGuard instance we have used 2 methods, well-described in the below table: +

+ + + + + + + + + +
+ get() + + Makes possible to retrieve the instance of the component or controller available inside the processed module. +
+ select() + + Allows you to navigate through the module tree, for example, to pull out a specific instance from the selected module. +
+
+ Hint The root module is selected by default. To select any other module, you need to go through entire modules stack (step by step). +
diff --git a/src/app/homepage/pages/guards/guards.component.ts b/src/app/homepage/pages/guards/guards.component.ts index 4771ab8d14..8e4aaece2d 100644 --- a/src/app/homepage/pages/guards/guards.component.ts +++ b/src/app/homepage/pages/guards/guards.component.ts @@ -166,4 +166,15 @@ const roles = this.reflector.get('roles', parent);`; const app = await NestFactory.create(ApplicationModule); app.useGlobalGuards(new RolesGuard());`; } + + get getAuthGuard() { + return ` +const app = await NestFactory.create(ApplicationModule); +const authGuard = app + .select(AuthModule) + .get(AuthGuard); + +app.useGlobalGuards(authGuard); +`; + } } \ 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 8c1d8be4fa..2cd279cd3d 100644 --- a/src/app/homepage/pages/interceptors/interceptors.component.html +++ b/src/app/homepage/pages/interceptors/interceptors.component.html @@ -44,7 +44,7 @@

Before / After

Since stream$ is a RxJS Observable, we have a lot of various operators which we can use to manipulate the stream. In above example, I've used do() operator, which invokes the function upon graceful or exceptional termination of the observable sequence.

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

@@ -115,4 +115,50 @@

Stream overriding

Hint Exceptions thrown within interceptors can be catched by exception filters.
+

Global interceptors

+

+ The global interceptors don't belong to any scope. They live outside modules, thus as a result - they can't inject dependencies. + We need to create an instance immediately. But quite often, global interceptors depend on other objects, for example, we'd + love to dispatch an asynchronous event on each request using + EventsService, but this service is a part of the + EventsModule. What's now? +

+

+ The solution is pretty easy. Each Nest application instance is in fact, a created + Nest context. The Nest context is a wrapper around the Nest container, which holds all instantiated classes. + We can grab any existing instance from within any imported module directly using application object. +

+

+ Let's assume that we have a + EventsInterceptor registered in the + EventsModule. This + EventsModule is imported into + root module. We can pick the + EventsInterceptor instance using following syntax: +

+
{{ getEventsInterceptor }}
+

+ To grab EventsInterceptor instance we have used 2 methods, well-described in the below table: +

+ + + + + + + + + +
+ get() + + Makes possible to retrieve the instance of the component or controller available inside the processed module. +
+ select() + + Allows you to navigate through the module tree, for example, to pull out a specific instance from the selected module. +
+
+ Hint The root module is selected by default. To select any other module, you need to go through entire modules stack (step by step). +
diff --git a/src/app/homepage/pages/interceptors/interceptors.component.ts b/src/app/homepage/pages/interceptors/interceptors.component.ts index 346fa0ac98..f48e288a66 100644 --- a/src/app/homepage/pages/interceptors/interceptors.component.ts +++ b/src/app/homepage/pages/interceptors/interceptors.component.ts @@ -130,7 +130,7 @@ export class CacheInterceptor { get exceptionMapping() { return ` import { Interceptor, NestInterceptor, ExecutionContext, HttpStatus } from '@nestjs/common'; -import { HttpException } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; @@ -148,7 +148,7 @@ export class ExceptionInterceptor implements NestInterceptor { get exceptionMappingJs() { return ` import { Interceptor, HttpStatus } from '@nestjs/common'; -import { HttpException } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; @@ -168,4 +168,15 @@ export class ExceptionInterceptor { const app = await NestFactory.create(ApplicationModule); app.useGlobalInterceptors(new LoggingInterceptor());`; } + + get getEventsInterceptor() { + return ` +const app = await NestFactory.create(ApplicationModule); +const eventsInterceptor = app + .select(EventsModule) + .get(EventsInterceptor); + +app.useGlobalInterceptors(eventsInterceptor); +`; + } } \ 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 db89e4c205..faed104300 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 (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

+

Nest is a framework for building efficient, scalable Node.js server-side 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

diff --git a/src/app/homepage/pages/microservices/basics/basics.component.html b/src/app/homepage/pages/microservices/basics/basics.component.html index 11386051a8..7d6174f622 100644 --- a/src/app/homepage/pages/microservices/basics/basics.component.html +++ b/src/app/homepage/pages/microservices/basics/basics.component.html @@ -13,7 +13,7 @@

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' | extension: bootstrapT.isJsActive }} + {{ 'main' | extension: bootstrapT.isJsActive }}
{{ bootstrap }}
diff --git a/src/app/homepage/pages/microservices/basics/basics.component.ts b/src/app/homepage/pages/microservices/basics/basics.component.ts index fb50be1dcc..834f05d283 100644 --- a/src/app/homepage/pages/microservices/basics/basics.component.ts +++ b/src/app/homepage/pages/microservices/basics/basics.component.ts @@ -11,7 +11,7 @@ export class BasicsComponent extends BasePageComponent { get bootstrap() { return ` import { NestFactory } from '@nestjs/core'; -import { ApplicationModule } from './modules/app.module'; +import { ApplicationModule } from './app.module'; import { Transport } from '@nestjs/microservices'; async function bootstrap() { 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 2ed6652e74..d5328ca529 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 @@ -19,10 +19,10 @@

Server

Moreover, the RabbitMQServer shall extends the abstract Server class. This class supplies the core getHandlers() and send() methods, and helper transformToObservable() method.

- The last step is to setup the RabbitMQServer: + The last step is to set up the RabbitMQServer:

- {{ 'server' | extension: setupServerT.isJsActive }} + {{ 'main' | extension: setupServerT.isJsActive }}
{{ setupServer }}
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 e7e473742d..82ade6c39a 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 @@ -24,6 +24,6 @@

Exception Filters

{{ rpcExceptionFilter }}
{{ rpcExceptionFilterJs }}
- Notice It's impossible to setup the microservices exception filters globally. + Notice It's impossible to set up the microservices exception filters globally.
diff --git a/src/app/homepage/pages/microservices/redis/redis.component.html b/src/app/homepage/pages/microservices/redis/redis.component.html index e6da60eff1..b468fa2bb8 100644 --- a/src/app/homepage/pages/microservices/redis/redis.component.html +++ b/src/app/homepage/pages/microservices/redis/redis.component.html @@ -8,7 +8,7 @@

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' | extension: optionsT.isJsActive }} + {{ 'main' | extension: optionsT.isJsActive }}
{{ options }}
diff --git a/src/app/homepage/pages/middlewares/middlewares.component.html b/src/app/homepage/pages/middlewares/middlewares.component.html index 96a260ffe3..9228e2da6f 100644 --- a/src/app/homepage/pages/middlewares/middlewares.component.html +++ b/src/app/homepage/pages/middlewares/middlewares.component.html @@ -41,7 +41,7 @@

Dependency Injection

Where to put the middlewares?

The middlewares can't be listed in the @Module() decorator. - We have to setup them using configure() method of the module class. + We have to set up them using configure() method of the module class. Modules that include middlewares have to implement the NestModule interface. Let's set up the LoggerMiddleware at the ApplicationModule level.

@@ -55,7 +55,7 @@

Where to put the middlewares?

Hint We could pass here (inside forRoutes()) the single object and just use RequestMethod.ALL.

- In above example we have setup the LoggerMiddleware for /cats route handlers, which we've registered inside the CatsController. + In above example we have set up the LoggerMiddleware for /cats route handlers, which we've registered inside the CatsController. The MiddlewareConsumer is a helper class. It provides several methods to work with the middlewares. All of them can be simply chained. Let's go through those methods.

diff --git a/src/app/homepage/pages/middlewares/middlewares.component.ts b/src/app/homepage/pages/middlewares/middlewares.component.ts index 5ff053f540..d9a91b5f66 100644 --- a/src/app/homepage/pages/middlewares/middlewares.component.ts +++ b/src/app/homepage/pages/middlewares/middlewares.component.ts @@ -44,7 +44,7 @@ import { LoggerMiddleware } from './common/middlewares/logger.middleware'; import { CatsModule } from './cats/cats.module'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule implements NestModule { configure(consumer: MiddlewaresConsumer): void { @@ -63,7 +63,7 @@ import { LoggerMiddleware } from './common/middlewares/logger.middleware'; import { CatsModule } from './cats/cats.module'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule { configure(consumer) { @@ -82,7 +82,7 @@ import { LoggerMiddleware } from './common/middlewares/logger.middleware'; import { CatsModule } from './cats/cats.module'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule implements NestModule { configure(consumer: MiddlewaresConsumer): void { @@ -98,7 +98,7 @@ import { LoggerMiddleware } from './common/middlewares/logger.middleware'; import { CatsModule } from './cats/cats.module'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule { configure(consumer) { @@ -115,7 +115,7 @@ import { CatsModule } from './cats/cats.module'; import { CatsController } from './cats/cats.controller'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule implements NestModule { configure(consumer: MiddlewaresConsumer): void { @@ -134,7 +134,7 @@ import { CatsModule } from './cats/cats.module'; import { CatsController } from './cats/cats.controller'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule { configure(consumer) { @@ -227,7 +227,7 @@ import { CatsModule } from './cats/cats.module'; import { CatsController } from './cats/cats.controller'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule implements NestModule { configure(consumer: MiddlewaresConsumer): void { @@ -244,7 +244,7 @@ import { CatsModule } from './cats/cats.module'; import { CatsController } from './cats/cats.controller'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule { configure(consumer) { diff --git a/src/app/homepage/pages/modules/modules.component.html b/src/app/homepage/pages/modules/modules.component.html index 6199812f80..53c4edf3cc 100644 --- a/src/app/homepage/pages/modules/modules.component.html +++ b/src/app/homepage/pages/modules/modules.component.html @@ -24,7 +24,7 @@

Modules

the set of controllers which have to be created - modules + imports the list of imported modules that export the components which are necessary in this module @@ -79,7 +79,7 @@

CatsModule

app.module.ts
-
server.ts
+
main.ts

Shared Module

@@ -91,7 +91,7 @@

Shared Module

- Every module is a Shared Module in fact. Once created is reused by the each module. + Every module is a Shared Module in fact. Once created is reused by each module. Let's imagine that we're gonna share the CatsService instance between few modules. We need to put the CatsService into exports array as shown below:

@@ -101,22 +101,16 @@

Shared Module

{{ catsModuleShared }}

- Now each module which would import the CatsModule (put CatsModule to the modules array) has an access to the CatsService and will share the same instance with all of the modules which are importing this module too. + Now each module which would import the CatsModule (would put CatsModule to the imports array) has an access to the CatsService and will share the same instance with all of the modules which are importing this module too.

- Notice Never export the controllers! + Notice You should never export the controllers!

Modules re-exporting

The modules can export their components. Moreover, they can re-export modules imported by themselves.

{{ reExportExamle }}
-

Single Scope

-

- They're modules which shouldn't be shared at all. To prevent module from being a singleton, you can use @SingleScope() - decorator, which makes that Nest will always create the new instance of the module when it's imported by another one. -

-
{{ singleScope }}

Dependency Injection

It's natural that module can inject components, which belongs to it (e.g. for the configuration purposes): @@ -128,6 +122,44 @@

Dependency Injection

{{ catsModuleDi }}
{{ catsModuleDiJs }}

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

+

Global modules

+

+ If you have to import the same set of modules everywhere, it can be annoying. + In Angular, the providers are registered in the global scope. + Once defined, they're available everywhere. On the other hand, Nest encapsulates components inside the module scope. + You can't use the module components elsewhere without importing it before. + But sometimes, you may just want to provide a set of things which should be available always - out-of-the-box, for example: helpers, database connection, whatever. + That's why you're able to make the module a global one. +

+
{{ globalScope }}
+

+ The @Global() decorator makes the module a global-scoped. + The global modules should be registered only once, in best case by the root or core module. + Afterwards, the CatsService component will be ubiquitous, although CatsModule won't be imported. +

+
+ Hint Making everything global is not a good solution. Global modules are here to reduce an amount of necessary boilerplate. The imports array is still a best way to make the module API transparent. +
+

Dynamic modules

+

+ The Nest module system comes with a feature called dynamic modules. + It enables you to create customizable modules without any effort. + Let's have a look at the DatabaseModule: +

+
{{ dynamicModules }}
+

+ It defines Connection component by default, but additionally - depending on the passed options and entities - creates a collection of the providers, for example repository components. + In fact, the dynamic module extends the module metadata. + This substantial feature is useful when you need to register components dynamically. + Then you may import the DatabaseModule in the following manner: +

+
{{ importDynamicModules }}
+ diff --git a/src/app/homepage/pages/modules/modules.component.ts b/src/app/homepage/pages/modules/modules.component.ts index 4d4b1672b8..98f227f5ab 100644 --- a/src/app/homepage/pages/modules/modules.component.ts +++ b/src/app/homepage/pages/modules/modules.component.ts @@ -28,7 +28,7 @@ import { Module } from '@nestjs/common'; import { CatsModule } from './cats/cats.module'; @Module({ - modules: [CatsModule], + imports: [CatsModule], }) export class ApplicationModule {} `; @@ -64,6 +64,21 @@ import { CatsService } from './cats.service'; export class CatsModule {}`; } + get globalScope() { + return ` +import { Module, Global } from '@nestjs/common'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; + +@Global() +@Module({ + controllers: [CatsController], + components: [CatsService], + exports: [CatsService] +}) +export class CatsModule {}`; + } + get catsModuleDi() { return ` import { Module } from '@nestjs/common'; @@ -100,9 +115,44 @@ export class CatsModule { get reExportExamle() { return ` @Module({ - modules: [CommonModule], + imports: [CommonModule], exports: [CommonModule], }) export class CoreModule {}`; } + + get dynamicModules() { + return ` +import { Module, DynamicModule } from '@nestjs/common'; +import { createDatabaseProviders } from './database.providers'; +import { Connection } from './connection.component'; + +@Module({ + components: [Connection], +}) +export class DatabaseModule { + static forRoot(entities = [], options?): DynamicModule { + const providers = createDatabaseProviders(options, entities); + return { + module: DatabaseModule, + components: providers, + exports: providers, + }; + } +}`; + } + + get importDynamicModules() { + return ` +import { Module } from '@nestjs/common'; +import { DatabaseModule } from './database/database.module'; +import { User } from './users/entities/user.entity'; + +@Module({ + imports: [ + DatabaseModule.forRoot([User]), + ], +}) +export class ApplicationModule {}`; + } } diff --git a/src/app/homepage/pages/pipes/pipes.component.html b/src/app/homepage/pages/pipes/pipes.component.html index 3ad7179b47..e33c708989 100644 --- a/src/app/homepage/pages/pipes/pipes.component.html +++ b/src/app/homepage/pages/pipes/pipes.component.html @@ -1,151 +1,259 @@
-

Pipes

-

- Pipe is a class with @Pipe() decorator. - The pipe should implements the PipeTransform interface. -

-
-

- A pipe transforms the input data to the desired output. - Also, it could overtake the validation responsibility, since it's possible to throw an exception when the data isn't correct. -

-
- Hint The pipe runs inside the exceptions zone. It means that throwed exceptions are handled by core exceptions handler and exceptions filters applied to the current context. -
-

Built-in pipes

-

- Nest comes with two pipes available out-of-the-box, ValidationPipe and ParseIntPipe. They're exported from the @nestjs/common package. To better understand how do they work, we're gonna built them from scratch here. -

-

What does it look like?

-

- Let's start from the ValidationPipe. Now it only takes and returns the same value. -

- validation.pipe.ts -
{{ validationPipe }}
-
- Notice The ValidationPipe works only with TypeScript. If you are working only with plain JavaScript, I'd recommend using a Joi library. -
-

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

-
    -
  • value
  • -
  • metadata
  • -
-

- The value is a currently processed parameter, while metadata is its metadata. - The metadata object holds few properties: -

-
{{ argumentMetadata }}
-

- These properties describe the parameter. -

- - - - - - - - - - - - - -
typeTells us whether the property is a body @Body(), query @Query(), param @Param(), or custom parameter (read more here).
metatype - The metatype of the property, for example String. It's undefined if you would omit the type declaration in the function signature. -
dataThe string passed to the decorator, for example @Body('string'). It's undefined if you would left the brackets empty.
-
- 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?

-

- Let's focus on the create() method of the CatsController: -

- cats.controler.ts -
{{ createCatsController }}
-

- There's a CreateCatDto body parameter. -

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

- This object always has to be correct, thus 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. -

-

- 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 using decorator-based validation. - Decorator based validation is really powerful with the pipe abilities since we have an access to the metatype of the processed property. - Let's add few decorators to the CreateCatDto. -

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

- Now it's time to finish the ValidationPipe class. -

- validation.pipe.ts -
{{ fullValidationPipe }}
-
- Notice I've used the class-transformer library. It's made by the same author as the class-validator, so they're playing well together. -
-

- Let's go through this code. Firstly, note that transform() function is async. - It's possible because Nest supports both synchronous and asynchronous pipes. - Also, there's a helper function - toValidate(). Its responsibility is to exclude the native JavaScript types from the validation process. - The last important thing is that we have to return the same value. This pipe is a validation specific pipe, so we need to return the exact same property to avoid the overriding. -

-

- The last step is to setup the ValidationPipe. Pipes, same as exception filters can be method-scoped, controller-scoped and global-scoped. - Additionally, a pipe may be param-scoped. We can directly bind the pipe instance to the route param decorator, for example @Body(). - Let's have a look at the below example: -

- cats.controler.ts -
{{ createCatsControllerParamPipe }}
-

- The param-scoped pipes are useful when the validation logic concerns only one, specified parameter. - To setup pipe at a method level, you'll need the UsePipes() decorator. -

- cats.controler.ts -
{{ createCatsControllerMethodPipe }}
-
- 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 it as a global-scoped pipe, for every route handler across the entire application. -

- server.ts -
{{ globalPipe }}
-
- Notice The useGlobalPipes() method doesn't setup pipes for gateways and microservices. -
-

Transformer Pipe

-

- Validation isn't the sole use case. At the beginning of this chapter, I've mentioned that pipe transforms the input data to the desired output. - It's true because the value returned from the transform function completely overrides the previous value of the argument. - 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 }}
+

Pipes

+

+ Pipe is a class with + @Pipe() decorator. The pipe should implements the + PipeTransform interface. +

+
+ +
+

+ A pipe + transforms the input data to the desired output. Also, it could overtake the + validation responsibility, since it's possible to throw an exception when the data isn't correct. +

+
+ Hint The pipe runs inside the exceptions zone. It means that throwed exceptions are handled by core exceptions + handler and + exceptions filters applied to the current context. +
+

Built-in pipes

+

+ Nest comes with two pipes available out-of-the-box, + ValidationPipe and + ParseIntPipe. They're exported from the + @nestjs/common package. To better understand how do they work, we're gonna built them from scratch here. +

+

What does it look like?

+

+ Let's start from the + ValidationPipe. Now it only takes and returns the same value. +

+ validation.pipe.ts +
{{ validationPipe }}
+
+ Notice The + ValidationPipe works only with + TypeScript. If you are using plain JavaScript, I'd recommend considering a + Joi library. +
+

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

+
    +
  • + value +
  • +
  • + metadata +
  • +
+

+ The + value is a currently processed parameter, while + metadata is its metadata. The metadata object holds few properties: +

+
{{ argumentMetadata }}
+

+ These properties describe the parameter. +

+ + + + + + + + + + + + + +
+ type + Tells us whether the property is a body + @Body(), query + @Query(), param + @Param(), or custom parameter (read more + here).
+ metatype + + The metatype of the property, for example + String. It's + undefined if you would omit the type declaration in the function signature. +
+ data + The string passed to the decorator, for example + @Body('string'). It's + undefined if you would left the brackets empty.
+
+ 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?

+

+ Let's focus on the + create() method of the + CatsController: +

+ cats.controler.ts +
{{ createCatsController }}
+

+ There's a + CreateCatDto body parameter. +

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

+ This object always has to be correct, thus 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. +

+

+ 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 using decorator-based validation. Decorator based validation is + really powerful with the + pipe abilities since we have an access to the + metatype of the processed property. Let's add few decorators to the + CreateCatDto. +

+ create-cat.dto.ts +
{{ createCatDtoValidation }}
+

+ Now it's time to finish the + ValidationPipe class. +

+ validation.pipe.ts +
{{ fullValidationPipe }}
+
+ Notice I've used the + class-transformer library. It's made by the same author as the + class-validator, so they're playing well together. +
+

+ Let's go through this code. Firstly, note that + transform() function is + async. It's possible because Nest supports both synchronous and + asynchronous pipes. Also, there's a helper function - + toValidate(). Its responsibility is to exclude the native JavaScript types from the validation process. The last + important thing is that we have to return the same value. This pipe is a validation specific pipe, so we need to return + the exact same property to avoid the + overriding. +

+

+ The last step is to set up the + ValidationPipe. Pipes, same as + exception filters can be method-scoped, controller-scoped and global-scoped. Additionally, a pipe may be param-scoped. + We can directly bind the pipe instance to the route param decorator, for example + @Body(). Let's have a look at the below example: +

+ cats.controler.ts +
{{ createCatsControllerParamPipe }}
+

+ The param-scoped pipes are useful when the validation logic concerns only one, specified parameter. To set up pipe at a method + level, you'll need the + UsePipes() decorator. +

+ cats.controler.ts +
{{ createCatsControllerMethodPipe }}
+
+ 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 set up it as a + global-scoped pipe, for every route handler across the entire application. +

+ main.ts +
{{ globalPipe }}
+
+ Notice The + useGlobalPipes() method doesn't setup pipes for gateways and microservices. +
+

Transformer Pipe

+

+ Validation isn't the sole use case. At the beginning of this chapter, I've mentioned that pipe + transforms the input data to the desired output. It's true because the value returned from the + transform function completely overrides the previous value of the argument. 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 }}
+

Global pipes

+

+ The global pipes don't belong to any scope. They live outside modules, thus as a result - they can't inject dependencies. + We need to create an instance immediately. But sometimes, global pipes depend on other objects. What's now? +

+

+ The solution is pretty easy. Each Nest application instance is in fact, a created + Nest context. The Nest context is a wrapper around the Nest container, which holds all instantiated classes. + We can grab any existing instance from within any imported module directly using application object. +

+

+ Let's assume that we have a + ValidationPipe registered in the + SharedModule. This + SharedModule is imported into + root module. We can pick the + ValidationPipe instance using following syntax: +

+
{{ getValidationPipe }}
+

+ To grab + ValidationPipe instance we have used 2 methods, well-described in the below table: +

+ + + + + + + + + +
+ get() + + Makes possible to retrieve the instance of the component or controller available inside the processed module. +
+ select() + + Allows you to navigate through the module tree, for example, to pull out a specific instance from the selected module. +
+
+ Hint The root module is selected by default. To select any other module, you need to go through entire modules + stack (step by step). +
diff --git a/src/app/homepage/pages/pipes/pipes.component.ts b/src/app/homepage/pages/pipes/pipes.component.ts index 4d9d4f242e..ef4ee128b9 100644 --- a/src/app/homepage/pages/pipes/pipes.component.ts +++ b/src/app/homepage/pages/pipes/pipes.component.ts @@ -183,4 +183,15 @@ async create(createCatDto) { await this.catsService.create(createCatDto); }`; } + + get getValidationPipe() { + return ` +const app = await NestFactory.create(ApplicationModule); +const validationPipe = app + .select(SharedModule) + .get(ValidationPipe); + +app.useGlobalPipes(validationPipe); +`; + } } diff --git a/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts b/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts index f5e177176e..25a2208711 100644 --- a/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts +++ b/src/app/homepage/pages/recipes/cqrs/cqrs.component.ts @@ -247,7 +247,7 @@ export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; @Module({ - modules: [CQRSModule], + imports: [CQRSModule], controllers: [HeroesGameController], components: [ HeroesGameService, @@ -283,7 +283,7 @@ export const CommandHandlers = [KillDragonHandler, DropAncientItemHandler]; export const EventHandlers = [HeroKilledDragonHandler, HeroFoundItemHandler]; @Module({ - modules: [CQRSModule], + imports: [CQRSModule], controllers: [HeroesGameController], components: [ HeroesGameService, diff --git a/src/app/homepage/pages/recipes/mockgoose/mockgoose.component.ts b/src/app/homepage/pages/recipes/mockgoose/mockgoose.component.ts index 9c3794d4fd..a1f623d496 100644 --- a/src/app/homepage/pages/recipes/mockgoose/mockgoose.component.ts +++ b/src/app/homepage/pages/recipes/mockgoose/mockgoose.component.ts @@ -157,7 +157,7 @@ import { catsProviders } from './cats.providers'; import { DatabaseModule } from '../database/database.module'; @Module({ - modules: [DatabaseModule], + imports: [DatabaseModule], controllers: [CatsController], components: [ CatsService, @@ -184,8 +184,8 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as request from 'supertest'; import { Test } from '@nestjs/testing'; -import { CatsModule } from '../../src/modules/cats/cats.module'; -import { CatsService } from '../../src/modules/cats/cats.service'; +import { CatsModule } from '../../src/cats/cats.module'; +import { CatsService } from '../../src/cats/cats.service'; describe('Cats', () => { const server = express(); @@ -193,7 +193,7 @@ describe('Cats', () => { beforeAll(async () => { const module = await Test.createTestingModule({ - modules: [CatsModule], + imports: [CatsModule], }) .compile(); diff --git a/src/app/homepage/pages/recipes/mongodb/mongodb.component.html b/src/app/homepage/pages/recipes/mongodb/mongodb.component.html index e19b514d0c..7cffe11114 100644 --- a/src/app/homepage/pages/recipes/mongodb/mongodb.component.html +++ b/src/app/homepage/pages/recipes/mongodb/mongodb.component.html @@ -1,5 +1,11 @@

MongoDB (Mongoose)

+
+ Warning In this article, you'll learn how to create a DatabaseModule + based on the Mongoose package from scratch using custom components. As a consequence, this solution + contains a lot of overhead that you can omit using ready to use and available out-of-the-box dedicated + @nestjs/mongoose package. To learn more, see here. +

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: @@ -88,7 +94,4 @@

Model injection

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.ts b/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts index e5570e2941..67793cbf5e 100644 --- a/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts +++ b/src/app/homepage/pages/recipes/mongodb/mongodb.component.ts @@ -140,7 +140,7 @@ import { catsProviders } from './cats.providers'; import { DatabaseModule } from '../database/database.module'; @Module({ - modules: [DatabaseModule], + imports: [DatabaseModule], controllers: [CatsController], components: [ CatsService, diff --git a/src/app/homepage/pages/recipes/passport/passport.component.html b/src/app/homepage/pages/recipes/passport/passport.component.html index 87b7ba544c..714c6b14ec 100644 --- a/src/app/homepage/pages/recipes/passport/passport.component.html +++ b/src/app/homepage/pages/recipes/passport/passport.component.html @@ -1,5 +1,5 @@
-

Passport integration

+

Authentication (Passport)

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. 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 index e4290f7dcf..37fd3b70f9 100644 --- a/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts +++ b/src/app/homepage/pages/recipes/sql-sequelize/sql-sequelize.component.ts @@ -84,13 +84,12 @@ export const catsProviders = [ 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) {} + @Inject('CatsRepository') private readonly catsRepository: typeof Cat) {} async findAll(): Promise { return await this.catsRepository.findAll(); @@ -107,7 +106,7 @@ import { catsProviders } from './cats.providers'; import { DatabaseModule } from '../database/database.module'; @Module({ - modules: [DatabaseModule], + imports: [DatabaseModule], controllers: [CatsController], components: [ CatsService, 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 d6df933f8f..3b1a6a0b59 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,6 +1,12 @@

SQL (TypeORM)

This chapter applies only to TypeScript
+
+ Warning In this article, you'll learn how to create a DatabaseModule + based on the TypeORM package from scratch using custom components. As a consequence, this solution + contains a lot of overhead that you can omit using ready to use and available out-of-the-box dedicated + @nestjs/typeorm package. To learn more, see here. +

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. @@ -64,7 +70,4 @@

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 3fa0ccf45c..cfa49afa75 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 @@ -54,23 +54,23 @@ import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Photo { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @Column({ length: 500 }) - name: string; + @Column({ length: 500 }) + name: string; - @Column('text') - description: string; + @Column('text') + description: string; - @Column() - filename: string; + @Column() + filename: string; - @Column('int') - views: number; + @Column('int') + views: number; - @Column() - isPublished: boolean; + @Column() + isPublished: boolean; }`; } @@ -113,7 +113,7 @@ import { photoProviders } from './photo.providers'; import { PhotoService } from './photo.service'; @Module({ - modules: [DatabaseModule], + imports: [DatabaseModule], components: [ ...photoProviders, PhotoService, diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.html b/src/app/homepage/pages/recipes/swagger/swagger.component.html index 6683af1c74..381907d72a 100644 --- a/src/app/homepage/pages/recipes/swagger/swagger.component.html +++ b/src/app/homepage/pages/recipes/swagger/swagger.component.html @@ -13,7 +13,7 @@

Installation

$ npm install --save @nestjs/swagger

Bootstrap

- Once the installation process is done, open your bootstrap file (mostly server.ts) and initialize the Swagger using SwaggerModule class: + Once the installation process is done, open your bootstrap file (mostly ) and initialize the Swagger using SwaggerModule class:

{{ bootstrapFile }}

diff --git a/src/app/homepage/pages/recipes/swagger/swagger.component.ts b/src/app/homepage/pages/recipes/swagger/swagger.component.ts index 2a14b16530..c17a9a1a19 100644 --- a/src/app/homepage/pages/recipes/swagger/swagger.component.ts +++ b/src/app/homepage/pages/recipes/swagger/swagger.component.ts @@ -12,7 +12,7 @@ export class SwaggerComponent extends BasePageComponent { return ` import { NestFactory } from '@nestjs/core'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; -import { ApplicationModule } from './modules/app.module'; +import { ApplicationModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(ApplicationModule); diff --git a/src/app/homepage/pages/techniques/mongo/mongo.component.html b/src/app/homepage/pages/techniques/mongo/mongo.component.html new file mode 100644 index 0000000000..23417e957f --- /dev/null +++ b/src/app/homepage/pages/techniques/mongo/mongo.component.html @@ -0,0 +1,63 @@ +

+

MongoDB

+

+ There are 2 ways of dealing with the MongoDB. You can use a TypeORM that provides a MongoDB support or Mongoose which is the most popular MongoDB object modelling tool. + If you wanna stay with the TypeORM you can follow these steps. + Otherwise, we'll use a dedicated @nestjs/mongoose package. +

+

+ Firstly, we need to install all of the required dependencies: +

+

+$ npm install --save @nestjs/mongoose mongoose
+

+ Once the installation process is completed, we can import the MongooseModule into the root ApplicationModule. +

+ + {{ 'app.module' | extension: importMongooseT.isJsActive }} + + +
{{ importMongoose }}
+

+ The forRoot() method accepts the same configuration object as mongoose.connect() from the Mongoose package. +

+

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. +

+

+ Let's have a look at the CatsModule: +

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

+ This module uses forFeature() method to define which models shall be registered in the current scope. +

+

+ Now we can inject the CatModel to the CatsService using the @InjectModel() decorator: +

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

+ That's all. The full source code's available here. +

+
+ Hint Don't forget to import the CatsModule into the root ApplicationModule. +
+
\ No newline at end of file diff --git a/src/app/homepage/pages/techniques/mongo/mongo.component.scss b/src/app/homepage/pages/techniques/mongo/mongo.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/techniques/mongo/mongo.component.spec.ts b/src/app/homepage/pages/techniques/mongo/mongo.component.spec.ts new file mode 100644 index 0000000000..7f5bbbc6ef --- /dev/null +++ b/src/app/homepage/pages/techniques/mongo/mongo.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MongoComponent } from './mongo.component'; + +describe('MongoComponent', () => { + let component: MongoComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MongoComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MongoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/techniques/mongo/mongo.component.ts b/src/app/homepage/pages/techniques/mongo/mongo.component.ts new file mode 100644 index 0000000000..271756d2eb --- /dev/null +++ b/src/app/homepage/pages/techniques/mongo/mongo.component.ts @@ -0,0 +1,97 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-mongo', + templateUrl: './mongo.component.html', + styleUrls: ['./mongo.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MongoComponent extends BasePageComponent { + get importMongoose() { + return ` +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; + +@Module({ + imports: [MongooseModule.forRoot('mongodb://localhost/nest')], +}) +export class ApplicationModule {}`; + } + + get catSchema() { + return ` +import * as mongoose from 'mongoose'; + +export const CatSchema = new mongoose.Schema({ + name: String, + age: Number, + breed: String, +});`; + } + + get catsModule() { + return ` +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { CatsController } from './cats.controller'; +import { CatsService } from './cats.service'; +import { CatSchema } from './schemas/cat.schema'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }])], + controllers: [CatsController], + components: [CatsService], +}) +export class CatsModule {}` + } + + get catsService() { + return ` +import { Model } from 'mongoose'; +import { Component } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Cat } from './interfaces/cat.interface'; +import { CreateCatDto } from './dto/create-cat.dto'; +import { CatSchema } from './schemas/cat.schema'; + +@Component() +export class CatsService { + constructor(@InjectModel(CatSchema) 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 { Model } from 'mongoose'; +import { Component, Dependencies } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { CatSchema } from './schemas/cat.schema'; + +@Component() +@Dependencies(InjectModel(CatSchema)) +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(); + } +}`; + } +} diff --git a/src/app/homepage/pages/techniques/mvc/mvc.component.html b/src/app/homepage/pages/techniques/mvc/mvc.component.html new file mode 100644 index 0000000000..e85c088090 --- /dev/null +++ b/src/app/homepage/pages/techniques/mvc/mvc.component.html @@ -0,0 +1,59 @@ +
+

MVC

+

+ Nest uses express library under the hood, therefore every tutorial about MVC (Model-View-Controller) pattern in express concerns Nest as well. + Firstly, let's clone a Nest starter 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
+

+ In order to create a simple MVC app, we have to install a template engine: +

+

+$ npm install --save jade
+

+ I have chosen jade because it's the most popular engine at the moment, but personally, I prefer a Mustache. + Once the installation process is completed, we need to configure the express instance using following code: +

+ + {{ 'main' | extension: mainT.isJsActive }} + + +
{{ main }}
+

+ We told express that the public directory will be used for storing static assets, + views will contain templates, and a jade template engine should be used to render an HTML output. +

+

+ Now, let's create a views directory and an index.jade template inside this folder. + Inside template, we are gonna print a message passed from the controller: +

+ index.jade +
{{ index }}
+

+ Afterwards, open the app.controller file and replace the root() method with the following code: +

+ + {{ 'app.controller' | extension: rootT.isJsActive }} + + +
{{ root }}
+
{{ rootJs }}
+
+ Hint In fact, when Nest detects @Res() decorator, it injects express response object. Learn more about its abilities here. +
+

+ That's all. While the application is running, open your browser and navigate to http://localhost:3000/. You should see the Hello world! message. +

+
\ No newline at end of file diff --git a/src/app/homepage/pages/techniques/mvc/mvc.component.scss b/src/app/homepage/pages/techniques/mvc/mvc.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/techniques/mvc/mvc.component.spec.ts b/src/app/homepage/pages/techniques/mvc/mvc.component.spec.ts new file mode 100644 index 0000000000..84191a57f6 --- /dev/null +++ b/src/app/homepage/pages/techniques/mvc/mvc.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MvcComponent } from './mvc.component'; + +describe('MvcComponent', () => { + let component: MvcComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MvcComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MvcComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/techniques/mvc/mvc.component.ts b/src/app/homepage/pages/techniques/mvc/mvc.component.ts new file mode 100644 index 0000000000..0d0e83ff7c --- /dev/null +++ b/src/app/homepage/pages/techniques/mvc/mvc.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-mvc', + templateUrl: './mvc.component.html', + styleUrls: ['./mvc.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MvcComponent extends BasePageComponent { + get main() { + return ` +import * as express from 'express'; +import * as path from 'path'; +import { NestFactory } from '@nestjs/core'; +import { ApplicationModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(ApplicationModule); + + app.use(express.static(path.join(__dirname, 'public'))); + app.set('views', __dirname + '/views'); + app.set('view engine', 'jade'); + + await app.listen(3000); +} +bootstrap(); +`; + } + + get index() { + return ` +html +head +body + p= message`; + } + + get root() { + return ` +@Get() +root(@Res() res) { + res.render('index', { message: 'Hello world!' }); +}`; + } + + get rootJs() { + return ` +@Get() +@Bind(Res()) +root(res) { + res.render('index', { message: 'Hello world!' }); +}`; + } +} diff --git a/src/app/homepage/pages/techniques/sql/sql.component.html b/src/app/homepage/pages/techniques/sql/sql.component.html new file mode 100644 index 0000000000..b64b956c8e --- /dev/null +++ b/src/app/homepage/pages/techniques/sql/sql.component.html @@ -0,0 +1,89 @@ +
+

SQL

+

+ To reduce a boilerplate necessary to start the adventure with the databases, Nest comes with the ready to use @nestjs/typeorm package. + We have selected TypeORM because it 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. +

+

+ Firstly, we need to install all of the required dependencies: +

+

+$ npm install --save @nestjs/typeorm typeorm mysql
+
+ Notice In this chapter we'll use a MySQL database, but TypeORM provides a support for a lot of different ones such as PostgreSQL, SQLite, and even MongoDB (NoSQL). +
+

+ Once the installation process is completed, we can import the TypeOrmModule into the root ApplicationModule. +

+ + {{ 'app.module' | extension: importTypeOrmT.isJsActive }} + + +
{{ importTypeOrm }}
+

+ The forRoot() method accepts the same configuration object as createConnection() from the TypeORM package. + Futhermore, instead of passing anything to the forRoot(), we can create an ormconfig.json file in the project root directory. +

+ ormconfig.json +
{{ ormconfig }}
+

+ Now we can simply leave the parenthesis empty: +

+ + {{ 'app.module' | extension: importTypeOrmEmptyT.isJsActive }} + + +
{{ importTypeOrmEmpty }}
+

+ Afterwards, the Connection and EntityManager will be available to inject across entire project (without importing any module elsewhere), for example in this way: +

+ + {{ 'app.module' | extension: importTypeOrmEmptyT.isJsActive }} + + +
{{ importConnectionInstance }}
+
{{ importConnectionInstanceJs }}
+

Repository pattern

+

+ The TypeORM supports the repository design pattern, so each entity has its own Repository. These repositories can be obtained from the database connection. +

+

+ Firstly, we need at least one entity. We're gonna reuse the Photo entity from the offical documentation. +

+ + {{ 'photo/photo.entity' | extension: photoEntityT.isJsActive }} + + +
{{ photoEntity }}
+

+ The Photo entity belongs to the photo 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 have a look at the PhotoModule: +

+ + {{ 'photo/photo.module' | extension: photoModuleT.isJsActive }} + + +
{{ photoModule }}
+

+ This module uses forFeature() method to define which repositories shall be registered in the current scope. +

+

+ Now we can inject the PhotoRepository to the PhotoService using the @InjectRepository() decorator: +

+ + {{ 'photo/photo.service' | extension: photoServiceT.isJsActive }} + + +
{{ photoService }}
+
{{ photoServiceJs }}
+

+ That's all. The full source code's available here. +

+
+ Hint Don't forget to import the PhotoModule into the root ApplicationModule. +
+
\ No newline at end of file diff --git a/src/app/homepage/pages/techniques/sql/sql.component.scss b/src/app/homepage/pages/techniques/sql/sql.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/homepage/pages/techniques/sql/sql.component.spec.ts b/src/app/homepage/pages/techniques/sql/sql.component.spec.ts new file mode 100644 index 0000000000..2a41b33f55 --- /dev/null +++ b/src/app/homepage/pages/techniques/sql/sql.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SqlComponent } from './sql.component'; + +describe('SqlComponent', () => { + let component: SqlComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SqlComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SqlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/homepage/pages/techniques/sql/sql.component.ts b/src/app/homepage/pages/techniques/sql/sql.component.ts new file mode 100644 index 0000000000..3599005a7c --- /dev/null +++ b/src/app/homepage/pages/techniques/sql/sql.component.ts @@ -0,0 +1,165 @@ +import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { BasePageComponent } from '../../page/page.component'; + +@Component({ + selector: 'app-sql', + templateUrl: './sql.component.html', + styleUrls: ['./sql.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SqlComponent extends BasePageComponent { + get importTypeOrm() { + return ` +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'mysql', + host: 'localhost', + port: 3306, + username: 'root', + password: 'root', + database: 'test', + entities: [__dirname + '/../**/*.entity{.ts,.js}'], + synchronize: true, + }), + ], +}) +export class ApplicationModule {}`; + } + + get ormconfig() { + return ` +{ + "type": "mysql", + "host": "localhost", + "port": 3306, + "username": "root", + "password": "root", + "database": "test", + "entities": ["src/**/**.entity{.ts,.js}"], + "autoSchemaSync": true +}`; + } + + get importTypeOrmEmpty() { + return ` +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [TypeOrmModule.forRoot()], +}) +export class ApplicationModule {}`; + } + + get photoEntity() { + return ` +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class Photo { + @PrimaryGeneratedColumn() + id: number; + + @Column({ length: 500 }) + name: string; + + @Column('text') + description: string; + + @Column() + filename: string; + + @Column('int') + views: number; + + @Column() + isPublished: boolean; +}`; + } + + get importConnectionInstance() { + return ` +import { Connection } from 'typeorm'; + +@Module({ + imports: [TypeOrmModule.forRoot(), PhotoModule], +}) +export class ApplicationModule { + constructor(private readonly connection: Connection) {} +}`; + } + + get importConnectionInstanceJs() { + return ` +import { Connection } from 'typeorm'; + +@Dependencies(Connection) +@Module({ + imports: [TypeOrmModule.forRoot(), PhotoModule], +}) +export class ApplicationModule { + constructor(connection) { + this.connection = connection; + } +}`; + } + + get photoModule() { + return ` +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PhotoService } from './photo.service'; +import { PhotoController } from './photo.controller'; +import { Photo } from './photo.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Photo])], + components: [PhotoService], + controllers: [PhotoController], +}) +export class PhotoModule {}`; + } + + get photoService() { + return ` +import { Component, Inject } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Photo } from './photo.entity'; + +@Component() +export class PhotoService { + constructor( + @InjectRepository(Photo) + private readonly photoRepository: Repository, + ) {} + + async findAll(): Promise { + return await this.photoRepository.find(); + } +}`; + } + + get photoServiceJs() { + return ` +import { Component, Dependencies } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Photo } from './photo.entity'; + +@Component() +@Dependencies(InjectRepository(Photo)) +export class PhotoService { + constructor(photoRepository) { + this.photoRepository = photoRepository; + } + + async findAll() { + return await this.photoRepository.find(); + } +}`; + } +} diff --git a/src/app/homepage/pages/websockets/adapter/adapter.component.html b/src/app/homepage/pages/websockets/adapter/adapter.component.html index 10dc51ab7a..f2b13fc8b4 100644 --- a/src/app/homepage/pages/websockets/adapter/adapter.component.html +++ b/src/app/homepage/pages/websockets/adapter/adapter.component.html @@ -45,7 +45,7 @@

Adapter

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

- {{ 'server' | extension: setupAdapterT.isJsActive }} + {{ 'main' | extension: setupAdapterT.isJsActive }}
{{ setupAdapter }}
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 c9bfd7fa6c..5bcdaad304 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 @@ -23,6 +23,6 @@

Exception Filters

{{ wsExceptionFilter }}
{{ wsExceptionFilterJs }}
- Notice It's impossible to setup the websockets exception filters globally. + Notice It's impossible to set up the websockets exception filters globally.
diff --git a/src/app/shared/components/tabs/tabs.component.scss b/src/app/shared/components/tabs/tabs.component.scss index 34446a4243..3b12767b7d 100644 --- a/src/app/shared/components/tabs/tabs.component.scss +++ b/src/app/shared/components/tabs/tabs.component.scss @@ -1,8 +1,21 @@ +@import './../../../../scss/utils.scss'; + .tabs-wrapper { position: absolute; right: 0; top: 0; bottom: 0; + + @include media(medium) { + position: static; + margin: 15px -20px -15px; + + .tab { + float: none; + display: inline-block; + margin: 0 !important; + } + } } .tab { diff --git a/src/index.html b/src/index.html index 00ddd4f178..ce5a52e29b 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,7 @@ - + @@ -14,11 +14,11 @@ - + - + @@ -40,6 +40,15 @@ + diff --git a/src/styles.scss b/src/styles.scss index 32017a2542..d620529041 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,5 @@ @import "~@angular/material/prebuilt-themes/indigo-pink.css"; +@import '~perfect-scrollbar/css/perfect-scrollbar.css'; @import "./scss/hljs.scss"; @import "./scss/variables.scss";