NestJS ガード

ガード

ガードは@Injectable()デコレータで注釈が付けられたクラスです。ガードを使う場合は、CanActivateインターフェイスを実装する必要があります。

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

f:id:adrenaline2017:20200803083925p:plain

ガードはクライアントのHTTPリクエストがルートハンドラによって処理されるかどうかを決定する役割を持っています。これまでは、アクセス制限のロジックは主にミドルウェア内部にありました。トークン(ソースコードを構成する最小単位)の検証やプロパティーのリクエストオブジェクトへのアタッチなどは、特定のルートと強く結びついていないので問題はありません。

しかし、ミドルウェアは性質上、next()関数を呼び出した後にどのハンドラが実行されるかはわかりません。一方、ガードはExecutionContextインスタンスへのアクセス権を持っており、評価される内容が正確にわかります。

ヒント ガードは各ミドルウェアの後、パイプの前に実行されます。

認可ガード

一番のガードのユースケースの1つは認証ロジックです。特定のルートは、呼び出し元に十分な権限がある場合にのみ使用できるようにする必要があるためです。作成する予定のAuthGuardは、要求ヘッダーで送信されたトークンを順次抽出して検証します。

auth.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

validateRequest()関数のロジックにかかわらず、重要なのはガードをいかにシンプルに利用するか示すことです。 すべてのガードはcanActivate()関数を提供します。 ガードは、プロミスまたはオブザーバブルを介して、そのブール値の応答を同期的または非同期的に返すことができます。 戻り値は、nestの動作を制御します。

trueを返すと、ユーザー呼び出しが処理されます。 falseを返した場合、Nestは現在処理中のリクエストを無視します。 canActivate()関数は、単一の引数であるExecutionContextインスタンスをとります。 ExecutionContextはArgumentsHostから継承しています(ここで最初に言及します)。 ArgumentsHostは、元のハンドラに渡された引数のラッパーであり、アプリケーションのタイプに基づいて、フードの下に異なる引数配列を含みます。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

ArgumentsHostは、基本配列から正しい引数を選ぶのに役立つ一連の便利なメソッドを提供します。 言い換えれば、ArgumentsHostは引数の配列だけではありません。 たとえば、HTTPアプリケーションコンテキスト内でガードが使用されている場合、ArgumentsHostには[request、response]配列が含まれます。 ただし、現在のコンテキストがWebソケットアプリケーションの場合、この配列は[client、data]と等しくなります。 この設計の決定により、最終的に対応するハンドラに渡されるすべての引数にアクセスできます。

ExecutionContextはもう少しです。 それはArgumentsHostを拡張するだけでなく、現在の実行プロセスの詳細を提供します。

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

getHandler()は現在処理中のハンドラへの参照を返しますが、getClass()はこのハンドラが属するControllerクラスの型を返します。 他の言葉を使用すると、ユーザーがCatsController内で定義され登録されているcreate()メソッドを指している場合、getHandler()はcreate()メソッドとgetClass()への参照を返します。 インスタンス)。

役割ベースの認証

もう少し詳細な例がRolesGuardです。 このガードは、特定の役割を持つユーザーのみにアクセスを許可します。 基本的なガードテンプレートから始めます:

roles.guard.ts JS

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}

バインディングガード
ガードは、コントローラスコープ、メソッドスコープ、グローバルスコープにすることもできます。 ガードを設定するには、@UseGuards()デコレータを使用する必要があります。 このデコレータは無数の引数をとります。つまり、複数のガードを渡してコンマで区切ることができます。

cats.controller.ts JS

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

ヒント @UseGuards()デコレータは、@ nestjs / commonパッケージからインポートされます。 インスタンスの代わりにRolesGuard型を渡し、フレームワークインスタンス化の責任を任せ、依存性注入を可能にしました。 別の利用可能な方法は、直ちに作成されたインスタンスを渡すことです。

cats.controller.ts

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

上記の構成は、このコントローラによって宣言されたすべてのハンドラにガードを添付します。 そのうちの1つだけを制限することに決めたら、メソッドレベルでガードを設定するだけです。 グローバルガードをバインドするには、NestアプリケーションインスタンスのuseGlobalGuards()メソッドを使用します。

const app = await NestFactory.create(ApplicationModule);
app.useGlobalGuards(new RolesGuard());

通知 useGlobalGuards()メソッドは、ゲートウェイとマイクロサービスのガードを設定しません。 グローバルガードは、すべてのコントローラとすべてのルートハンドラに対して、アプリケーション全体で使用されます。 依存性注入に関しては、(上記の例のように)モジュールの外部から登録されたグローバルガードは、いずれのモジュールにも属していないため、依存関係を注入できません。 この問題を解決するには、次の構成を使用して、任意のモジュールから直接ガードを設定できます。

app.module.ts JS

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class ApplicationModule {}

ヒント 代替オプションは、実行コンテキスト機能を使用することです。 また、useClassはカスタムプロバイダの登録を処理する唯一の方法でもありません。 詳細はこちら。

リフレクター

ガードは現在動作していますが、最も重要なガード機能、つまり実行コンテキストを利用していません。

これまでRolesGuardは再利用できません。 どのような役割がハンドラによって処理される必要があるのかをどのように知ることができますか? CatsControllerにはたくさんのものがあります。 管理者だけで利用できるものもあれば誰でも利用できるものもありますが、アクセス権はありません。

そのため、ガードと一緒に、Nestは@ReflectMetadata()デコレータを使用してカスタムメタデータを添付することができます。

cats.controller.ts JS

@Post()
@ReflectMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);

ヒント @ReflectMetadata()デコレータは、@ nestjs / commonパッケージからインポートされます。

上記の構成では、create()メソッドに対してロールメタデータ(ロールはキーで、['admin']は特定の値)を添付しました。 @ReflectMetadata()を直接使用するのは良い方法ではありません。 代わりに、あなた自身のデコレータを常に作成する必要があります:

roles.decorator.ts

import { ReflectMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);

このアプローチははるかにクリーンで読みやすい。 私たちは@Roles()デコレータを持っているので、create()メソッドでそれを使うことができます。

cats.controller.ts

@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

よかった。 もう一度RolesGuardに戻ってみましょう。 これはただちに真を返し、これまで要求を進めることができます。 メタデータを反映させるために、Reflectorヘルパークラスを使用します。このヘルパークラスはフレームワークによってそのまま提供され、@ nestjs / coreパッケージから公開されます。

roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}

ヒント node.jsの世界では、承認されたユーザをリクエストオブジェクトに添付するのが一般的です。 そのため、request.userにユーザーインスタンスが含まれていると仮定しています。

Reflectorでは、メタデータを指定されたキーで簡単に反映させることができます。 上記の例では、ルートハンドラ関数への参照であるため、メタデータを反映するためにgetHandler()を使用しました。 コントローラーの反射部分も追加すれば、このガードをさらに一般的にすることができます。 コントローラメタデータを抽出するには、getHandler()関数の代わりにcontext.getClass()を使用する必要があります。

const roles = this.reflector.get<string[]>('roles', context.getClass());

ユーザーが十分な権限なしに/ cats POSTエンドポイントを呼び出そうとすると、Nestは自動的に以下の応答を返します。

{
  "statusCode": 403,
  "message": "Forbidden resource"
}

実際、falseを返すガードはHttpExceptionをスローします。 エンドユーザーに別のエラー応答を返す場合は、例外をスローする必要があります。 その後、この例外は例外フィルタによって捕捉されます。

NestJS 例外フィルター

例外フィルター

組み込みの例外階層では、アプリケーション全体でスローされた例外を処理します。 未処理の例外がキャッチされると、適切な応答を受け取ります。 f:id:adrenaline2017:20200803083804p:plain

例外が起きた時にグローバル例外フィルタによって処理され、認識できない時にユーザは以下のJSONレスポンスを受け取ります。

{
  //内部サーバーエラー
  "statusCode": 500,
  "message": "Internal server error"
}

基本の例外

@ nestjs / commonパッケージから公開されている組み込みのHttpExceptionクラスがあります。 ご存知のように、HttpExceptionオブジェクトを投げるとハンドラで捕捉され、関連するJSONレスポンスに変換されます。

CatsControllerには、create()メソッド(POST)があります。 このルートハンドラが何らかの理由で例外をスローするとした場合以下の通りになりなす。

cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

クライアントがこのエンドポイントを呼び出すと、応答は次のようになります。

{
  //クライアントエラー
  "statusCode": 403,
  "message": "Forbidden"
}

HttpExceptionコンストラクタは、最初の引数としてオブジェクトと文字列をとります。 文字列の代わりにオブジェクトを渡すと、レスポンス本体が完全にオーバーライドされます。

cats.controller.ts JS

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, 403);
}

以下の応答がえらえます:

{
  "status": 403,
  "error": "This is a custom message"
}

例外階層

適切な方法は、独自の例外の階層を作成することです。 全てのHTTP例外は、ベースのHttpExceptionクラスから継承する必要があります。 その結果、Nestは例外を認識しエラー応答を完全に処理します。

forbidden.exception.ts

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

ForbiddenExceptionはHttpExceptionを継承するので、create()メソッド内で使用できます。

cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

HTTP例外

NestはコアHttpExceptionを継承する一連の使用可能な例外を提供します。 それらのすべては@ nestjs / commonパッケージから公開されています。

・BadRequestException ・UnauthorizedException ・NotFoundException ・ForbiddenException ・NotAcceptableException ・RequestTimeoutException ・ConflictException ・GoneException ・PayloadTooLargeException ・UnsupportedMediaTypeException ・UnprocessableEntityException ・InternalServerErrorException ・NotImplementedException ・BadGatewayException ・ServiceUnavailableException ・GatewayTimeoutException

例外フィルタ

基本の例外ハンドラは問題ありませんが、いくつかの選択された要因に基づいて別のJSONスキーマを使用するなどいくつかのログを追加する場合、例外層を完全に制御したいことがあります。簡単にするため、例外フィルタと呼ばれる機能が作成されました。

ここでは、HttpExceptionクラスのインスタンスである例外をキャッチし、それに対するカスタム応答ロジックを設定するフィルタを作成します。

http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { HttpException } from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

ヒント 例外フィルタを使う場合は汎用のExceptionFilter インタフェースを実装する必要があります。 これは、catchメソッド(例外:T、host:ArgumentsHost)に有効な署名を付けるよう強制します。 Tは例外のタイプを示します。 @Catch(HttpException)デコレータは、必要なメタデータを例外フィルタにバインドし、この特定のフィルタがHttpExceptionを探していることをNestに通知します。 実際には、@Catch()デコレータは無限の数のパラメータを取るかもしれないので、いくつかのタイプの例外のためにフィルタを設定することができます。カンマで区切ります。

例外プロパティは現在処理されている例外ですが、hostはArgumentsHostオブジェクトです。 ArgumentsHostは、元のハンドラに渡された引数のラッパーであり、アプリケーションのタイプに基づいて、フードの下に異なる引数配列を含みます。

export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

ArgumentsHostは、基本配列から正しい引数を選ぶのに役立つ一連の便利なメソッドを提供します。 言い換えれば、ArgumentsHostは引数の配列だけではありません。 たとえば、HTTPアプリケーションコンテキスト内でフィルタを使用する場合、ArgumentsHostには[request、response]配列が含まれます。 ただし、現在のコンテキストがWebソケットアプリケーションの場合、この配列は[client、data]と等しくなります。 この設計の決定により、最終的に対応するハンドラに渡されるすべての引数にアクセスできます。

バインディングフィルタ

HttpExceptionFilterをcreate()メソッドに結びつけましょう。

cats.controller.ts JS

@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

ヒント @UseFilters()デコレータは、@ nestjs / commonパッケージからインポートされます。 ここで@UseFilters()デコレータを使用しています。 @Catch()と同じですが、無限の数のパラメータが必要です。 HttpExceptionFilterのインスタンスは、すぐにインプレースで作成されています。 別の利用可能な方法は、クラスを(インスタンスではなく)渡すことです。フレームワークインスタンス化の責任を持ち、依存性注入を有効にします。

cats.controller.ts JS

@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

ヒント 可能であれば、インスタンスの代わりにクラスを適用することを推奨します。 Nestはアプリケーション全体で同じクラスのインスタンスを簡単に再利用できるため、メモリ使用量を削減します。 上記の例では、HttpExceptionFilterは単一のcreate()ルートハンドラにのみ適用されますが、利用可能な唯一の方法ではありません。 実際、例外フィルタはメソッドスコープ、コントローラスコープ、グローバルスコープにすることができます。

cats.controller.ts JS

@UseFilters(new HttpExceptionFilter())
export class CatsController {}

この構造は、CatsController内で定義されたすべてのルートハンドラに対してHttpExceptionFilterを設定します。 コントローラスコープの例外フィルタの例です。 最後に使用可能なスコープは、グローバルスコープの例外フィルタです。

main.ts JS

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

警告 useGlobalFilters()メソッドは、ゲートウェイまたはマイクロサービスのフィルタを設定しません。 グローバルフィルタは、すべてのコントローラとすべてのルートハンドラに対して、アプリケーション全体で使用されます。 依存性注入に関しては、モジュールの外部から登録されたグローバルフィルタ(上記の例のように)は、どのモジュールにも属していないため、依存関係を注入できません。 この問題を解決するには、次の構成を使用して、任意のモジュールから直接フィルタを設定できます。

app.module.ts JS

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})
export class ApplicationModule {}

ヒント 代替オプションは、実行コンテキスト機能を使用することです。 また、useClassはカスタムプロバイダの登録を処理する唯一の方法でもありません。 詳細はこちら。

すべてをキャッチ

発生したすべての例外を処理するには(例外タイプに関係なく)、かっこは空白のままにしておいてください。 @キャッチ()。

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}

上記の例では、フィルタは、スローされた各例外を特定のクラスのセットに限定せずにキャッチします。

継承

通常、アプリケーションの要件を満たすために作成された、完全にカスタマイズされた例外フィルタを作成します。 ただし、すでに実装されているコア例外フィルタを再利用したい場合や、特定の要因に基づいて動作を上書きする場合は、ユースケースが存在する可能性があります。

例外処理をベースフィルタに委譲するには、BaseExceptionFilterを拡張し、inherited catch()メソッドを呼び出す必要があります。 また、HttpServerの参照を注入してsuper()呼び出しに渡す必要があります。

import { Catch, ArgumentsHost, HttpServer } from '@nestjs/common';
import { BaseExceptionFilter, HTTP_SERVER_REF } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  constructor(@Inject(HTTP_SERVER_REF) applicationRef: HttpServer) {
    super(applicationRef);
  }

  catch(exception: any, host: ArgumentsHost) {
    super.catch(exception, host);
  }
}

警告 基本クラスを拡張するフィルタは、フレームワーク自体によってインスタンス化されなければなりません(newキーワードを使用して手動でインスタンスを作成しないでください)。 明らかに、カスタマイズされたビジネスロジック(例えば、さまざまな条件を追加する)を使用して、上記の実装を強化する必要があります。

NestJS パイプ

パイプ

パイプは、入力データを自分が望む出力に変換します。 また、データが正しくないときに例外をスローする可能性があるので、検証することもできます。 f:id:adrenaline2017:20200803083851p:plain

パイプは@Injectable()デコレータで注釈が付けられたクラスです。 パイプを使用する場合はPipeTransformインタフェースをimplementsする必要があります。

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

ヒント パイプは例外がスローされると、現在のコンテキストに適用されるコア例外ハンドラと例外フィルタによって処理されます。

ビルトイン パイプ

Nestには、すぐに使用できる2つのパイプ「ValidationPipe」と「ParseIntPipe」があります。 それらは@ nestjs / commonパッケージからエクスポート出来ます。

どの様に動作するか

ValidationPipeから始めましょう。 値を取ってすぐに同じ値を返す恒等関数のように動作します。

validation.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

ヒント PipeTransform <T、R>は汎用インタフェースで、Tは入力値の型を表し、Rはtransform()メソッドの戻り型です。

すべてのパイプはtransform()メソッドを提供する必要があります。 このメソッドは引数(値とメタデータ)をとります:

値は現在処理されているパラメータですが、metadataはメタデータを意味します。 メタデータオブジェクトにはいくつかのプロパティがあります。

export interface ArgumentMetadata {
  readonly type: 'body' | 'query' | 'param' | 'custom';
  readonly metatype?: new (...args) => any;
  readonly data?: string;
}

これらのプロパティは入力パラメータを記述します。

type プロパティがBody @Body()、@ Query()、param @Param()、またはカスタムパラメータ(詳細はこちら)を参照してください
metatype プロパティのメタタイプ(Stringなど)。 関数のシグネチャで型宣言を省略するか、バニラJavaScriptを使用するかは未定義です
data @Body( 'string')のようにデコレータに渡される文字列。 大括弧を空白のままにすると、定義されていません

警告 TypeScriptインタフェースは、変換中に消えます。 したがって、クラスの代わりにインタフェースを使用する場合、メタタイプの値はObjectと等しくなります。

ポイントとは何ですか?

しばらくの間、CatsControllerのcreate()メソッドに注目しましょう。 cats.controller.ts

@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

CreateCatDtoボディパラメータは以下の通りです

create-cat.dto.ts

export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

このオブジェクトは常に正しいものでなければならないため、これらの3つのメンバーを検証する必要があります。 ルートハンドラメソッドの中で実行することもできますが、SRP(Single Responsibility Rule)を解除します。 2番目のアイデアは、バリデータークラスを作成してそこにタスクを委任することですが、各メソッドの開始時に毎回このバリデーターを使用する必要があります。

これは、パイプを使用することを考慮する必要がある最初のユースケースです。

オブジェクトスキーマ検証

頻繁に遭遇する場合のアプローチの1つは、スキーマベースの検証を使用することです。 Joiライブラリは、読みやすいAPIを使って簡単にスキーマ(構造)を作成できるツールです。 オブジェクトスキーマを使用するパイプを作成するには、スキーマをコンストラクタ引数として取る単純なクラスを作成する必要があります。

import * as Joi from 'joi';
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class JoiValidationPipe implements PipeTransform {
  constructor(private readonly schema) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const { error } = Joi.validate(value, this.schema);
    if (error) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }
}

バインディングパイプ

パイプの結び付けは非常に簡単です。@UsePipes()デコレータを使用し、有効なJoiスキーマを持つパイプインスタンスを作成する必要があります。

@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

クラスバリデーター

このセクションはTypeScriptにのみ適用されます。 Nestはクラスバリデーターとうまく機能します。 この素晴らしいライブラリは、デコレータベースの検証を使用することを可能にします。 デコレータベースの検証は、処理されたプロパティのメタタイプにアクセスできるため、パイプ機能を使用すると非常に強力です。 ただし、開始する前に、必要なパッケージをインストールする必要があります。

$ npm i --save class-validator class-transformer

ライブラリがインストールされたら、CreateCatDtoクラスにいくつかのデコレータを追加できます。

create-cat.dto.ts

import { IsString, IsInt } from 'class-validator';

export class CreateCatDto {
  @IsString()
  readonly name: string;

  @IsInt()
  readonly age: number;

  @IsString()
  readonly breed: string;
}

これが終了したら、ValidationPipeクラスを作成できます。

validation.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }
    const object = plainToClass(metatype, value);
    const errors = await validate(object);
    if (errors.length > 0) {
      throw new BadRequestException('Validation failed');
    }
    return value;
  }

  private toValidate(metatype): boolean {
    const types = [String, Boolean, Number, Array, Object];
    return !types.find((type) => metatype === type);
  }
}

通知 クラス変換ライブラリを使用しました。これは、クラスバリデーターライブラリと同じ作者によって作成されたもので、その結果、それらは非常にうまくいっています。

このコードを見てみましょう。まず、transform()関数は非同期であることに注意してください。 Nestは同期パイプと非同期パイプの両方をサポートしているため、可能です。また、ヘルパー関数toValidate()もあります。パフォーマンス上の理由により、ネイティブJavaScriptタイプを検証プロセスから除外する責任があります。最後に言及する部分は、同じ価値を返さなければならないということです。このパイプはバリデーションに特化したパイプなので、オーバーライドを避けるために同じプロパティを返す必要があります(先に述べたように、パイプ変換は必要な出力に変換されます)。

最後のステップは、ValidationPipeを設定することです。例外フィルタと同様に、パイプはメソッドスコープ、コントローラスコープ、グローバルスコープにすることができます。さらに、パイプをパラメータスコープにすることもできます。 @Body()デコレータのように、パイプインスタンスをルートパラメータデコレータに直接結びつけることができます。以下の例を見てみましょう:

cats.controller.ts

@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

paramスコープのパイプは、検証ロジックが指定された1つのパラメータにのみ関係する場合に便利です。 メソッドレベルでパイプを設定するには、UsePipes()デコレータが必要です。

cats.controller.ts

@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

ヒント @UsePipes()デコレータは、@ nestjs / commonパッケージからインポートされます。 ValidationPipeのインスタンスは即座にインプレースで作成されています。 別の利用可能な方法は、クラスを(インスタンスではなく)渡すことです。フレームワークインスタンス化の責任を持ち、依存性注入を有効にします。

cats.controller.ts

@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

ValidationPipeは可能な限り汎用的に作成されているため、アプリケーション全体のすべてのルートハンドラに対して、グローバルスコープのパイプとして設定します。

main.ts

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

通知 useGlobalPipes()メソッドは、ゲートウェイとマイクロサービスのパイプを設定しません。

グローバルパイプは、すべてのコントローラとすべてのルートハンドラに対して、アプリケーション全体で使用されます。 依存関係注入の観点からは、(上記の例のように)モジュールの外部から登録されたグローバルパイプは、いずれのモジュールにも属さないため依存関係を注入できません。 この問題を解決するには、以下の構造を使用して、どのモジュールからでもパイプを直接設定できます。

app.module.ts

import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: CustomGlobalPipe,
    },
  ],
})
export class ApplicationModule {}

ヒント 代替オプションは、実行コンテキスト機能を使用することです。 また、useClassはカスタムプロバイダの登録を処理する唯一の方法でもありません。

トランスフォーマ パイプ

パイプは検証だけがユースケースではありません。 この章の冒頭で、パイプは入力データを目的の出力に変換することもできると述べました。 これは、変換関数から返された値が引数の前の値を完全に上書きするためです。 場合によっては、クライアントから渡されたデータに変更を加える必要があります。 また、一部の部品が欠落する可能性があるため、デフォルト値を適用する必要があります。 トランスフォーマ パイプは、クライアントの要リクエストとリクエストハンドラとの間のギャップを埋める。

parse-int.pipe.ts

import { PipeTransform, Injectable, ArgumentMetadata, HttpStatus, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

ここでは、文字列を整数値に解析するParseIntPipeがあります。 選択されたパラメータにパイプを単に結び付けることができます:

@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

上記の構成により、要求が対応するハンドラに触れる前にParseIntPipeが実行されます。

もう1つの有用なケースは、データベースからidで既存のユーザーエンティティを選択することです。

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

組み込みValidationPipe 幸運なことに、ValidationPipeとParseIntPipeはビルトインパイプなので、パイプを独自に構築する必要はありません(ValidationPipeには、クラスバリデーターとクラストランスフォーマーの両方のパッケージがインストールされている必要があります)。

組み込みのValidationPipeには、この章で説明したものよりも多くのオプションが用意されています。このオプションは、簡単にするために基本的に保持されており、学習曲線を減らしています。 コントローラ関数内のcreateCatDtoを見ると、実際のCreateCatDtoインスタンスではないことがわかります。 これは、このパイプが予想されるタイプに変換することなく、ペイロードのみを検証するためです。 ただし、パイプにペイロードを変更させたい場合は、適切なオプションを渡して設定することができます

cats.controller.ts

@Post()
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

このパイプはclass-validatorとclass-transformerライブラリに基づいているので、それ以上のものを得ることができます。 コンストラクタのオプションオプションを見てください。

export interface ValidationPipeOptions extends ValidatorOptions {
  transform?: boolean;
}

transform属性とすべてのクラスバリデーターオプション(ValidatorOptionsインターフェースから継承)があります。

Option Type Description
skipMissingProperties ブール値 trueに設定すると、バリデータは検証対象オブジェクトにないすべてのプロパティの検証をスキップします
whitelist ブール値 trueに設定すると、バリデーターは、デコレーターを使用しないプロパティの検証済みオブジェクトを取り除きます
forbidNonWhitelisted ブール値 trueに設定すると、ホワイトリストに登録されていないプロパティを取り除く代わりに、バリデータは例外をスローします
forbidUnknownValues ブール値 trueに設定すると、未知のオブジェクトの検証が直ちに失敗します
disableErrorMessages ブール値 trueに設定すると、検証エラーはクライアントに転送されません
groups 文字列 オブジェクトの検証中に使用されるグループ
dismissDefaultMessages ブール値 trueに設定すると、検証ではデフォルトのメッセージは使用されません。エラーメッセージは、明示的に設定されていなければ、常に定義されません
validationError.target ブール値 ValidationErrorでターゲットを公開するかどうかを示します
validationError.value ブール値 検証された値をValidationErrorで公開するかどうかを示します

NestJS ミドルウェア

ミドルウェア

ミドルウェアは、ルートハンドラの前に呼び出されます。 ミドルウェア関数は、リクエストオブジェクト (req)、レスポンスオブジェクト (res)、およびアプリケーションのリクエストレスポンスサイクルにおける次のミドルウェア関数に対するアクセス権限を持つ関数です。次のミドルウェア関数は一般的に、next という変数で表されます。 f:id:adrenaline2017:20200803083544p:plain

NestのミドルウェアはExpressミドルウェアと同じです。

ミドルウェア機能は以下の通りです。

・任意のコードを実行する ・リクエストとレスポンスオブジェクトの変更 ・リクエスト/レスポンスサイクルを終了させる ・スタック内の次のミドルウェア機能を呼び出す(next関数)

現在のミドルウェアの機能がリクエスト/レスポンスサイクルを終了しない場合、next()を呼び出して制御を次のミドルウェアの機能に渡す必要があります。 そうしないとリクエストはハングアップのままになります。

Nestミドルウェアは関数かクラス(「@Injectable()デコレータ」を持つ)のいずれかです。 クラスにはNestMiddlewareインターフェイスをimplementsする必要がありますが、関数には特別な要件はありません。 LoggerMiddlewareの例を見てみましょう。

logger.middleware.ts

import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  resolve(...args: any[]): MiddlewareFunction {
    return (req, res, next) => {
      console.log('Request...');
      next();
    };
  }
}

resolve()メソッドは、通常のライブラリのミドルウェア(req、res、next)=> を返す必要があります。

ミドルウェアの適用

前記でコントローラやプロバイダを@Module()デコレータに設定した様に、ミドルウェアを設定することは出来ません。そこで configure()メソッドを使用してそれらを設定する必要があります。 ミドルウェアを含むモジュールは、NestModuleインタフェースを実装する必要があります。 ApplicationModuleクラスにLoggerMiddlewareを設定しましょう。

app.module.ts JS

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

上記の例では、以前にCatsController内で定義した/ catsルートハンドラのLoggerMiddlewareを設定しました。

app.module.ts

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

ルート forRoutes({ path: 'ab*cd', method: RequestMethod.ALL })ワイルドカード

パターンベースのルートもサポートされています。 たとえば、アスタリスクワイルドカードとして使用され、任意の文字の組み合わせに一致します。

forRoutes({ path: 'ab*cd', method: RequestMethod.ALL })

上の経路パスは、abcd、ab_cd、abecdなどと一致します。 文字?、+、*、()は正規表現の部分集合です。 ハイフン( - )とドット(。)は、文字列ベースのパスによって文字通り解釈されます。

ミドルウェアカスタマー

MiddlewareConsumerはヘルパークラス(static メソッドと static 変数だけが含まれているクラス)です。 ミドルウェアを管理するいくつかの組み込みメソッドを提供します。 それらのすべては単純に連鎖することができます。 forRoutes()は、単一の文字列、複数の文字列、RouteInfoオブジェクト、コントローラクラス、さらに複数のコントローラクラスをとることができます。 ほとんどの場合、コントローラを渡してコンマで区切るだけです。 以下は、単一のコントローラを使用した例です。

app.module.ts JS

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}

ヒント

apply()メソッドは、単一のミドルウェアまたはミドルウェアの配列をとることができます。 クラスが使用されている間は、特定のルートを除外したいことがよくあります。 その場合はexclude()メソッドを使用します。

app.module.ts JS

import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST },
      )
      .forRoutes(CatsController);
  }
}

したがって、LoggerMiddlewareは、exclude()関数に渡されたこれらの2つを除いて、CatsController内で定義されたすべてのルートにバインドされます。 exclude()メソッドはミドルウェアでは機能しませんのでご注意ください。 さらに、この機能では、より一般的なルート(ワイルドカードなど)からのパスは除外されません。 そのような場合は、経路制限ロジックを直接ミドルウェアに置き、要求のURLを比較するなどしてください。

設定可能なミドルウェア

時々、ミドルウェアの動作はカスタム値に依存します。 ユーザーロールの配列、オプションオブジェクトなどがあります。 with()メソッドを使用してresolve()に追加の引数を適用できます。 以下の例を参照してください。

app.module.ts JS

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .with('ApplicationModule')
      .forRoutes(CatsController);
  }
}

ApplicationModuleをwith()メソッドに渡しました。 その後、LoggerMiddlewareのresolve()メソッドを調整する必要があります。

logger.middleware.ts

import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  resolve(name: string): MiddlewareFunction {
    return (req, res, next) => {
      console.log(`[${name}] Request...`); 
      next();
    };
 }
}

この場合、nameプロパティの値は 'ApplicationModule'になります。

非同期ミドルウェア

resolve()メソッド内で非同期関数を返すことを妨げる事はありません。 また、resolve()メソッドを非同期にすることもできます。 この共通のパターンは、据え置きのミドルウェアと呼ばれます。

logger.middleware.ts

import { Injectable, NestMiddleware, MiddlewareFunction } from '@nestjs/common';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  async resolve(name: string): Promise<MiddlewareFunction> {
    await someAsyncJob();

    return async (req, res, next) => {
      await someAsyncJob();
      console.log(`[${name}] Request...`); 
      next();
    };
 }
}

機能ミドルウェア

LoggerMiddlewareはかなり短いです。 メンバーはなく、追加のメソッドも依存もありません。 なぜ単純な関数を使うことができないのですか? それは良い質問です、実際には - そうすることができます。 このタイプのミドルウェアは、機能ミドルウェアと呼ばれます。 ロガーを関数に変換しましょう。

logger.middleware.ts JS

export function logger(req, res, next) {
  console.log(`Request...`);
  next();
};

ApplicationModule内で使用します。

app.module.ts JS

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { logger } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(logger)
      .forRoutes(CatsController);
  }
}

ヒント ミドルウェアが依存関係を必要としないたびに、機能的なミドルウェアを使用することを検討しましょう。

複数のミドルウェア

前述のように、順次実行される複数のミドルウェアをバインドするために、apply()メソッド内でコンマで区切ることができます。

export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(cors(), helmet(), logger)
      .forRoutes(CatsController);
  }
}

グローバルミドルウェア

登録された各ルートにミドルウェアを一度に結びつけるために、INestApplicationインスタンスによって提供されるuse()メソッドを利用できます:

const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);

DApps開発環境の構築と実装

概要

f:id:adrenaline2017:20190706100955j:plain

Solidity, Truffle, Ganache, Metamaskを組み合わせてEhereumで実行出来きるDappsを開発します。

開発環境の構築

以下の項目が開発をする際に必要となる環境になります。

Solidity

Ethereumでスマートコントラクトを開発する為の言語です。構文がJavaScriptに似てるので、非常に親みやすい言語だと思います。 solidityはsolcと呼ばれるSolidityコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。

Truffle

SolidityでDappsを開発する際のフレームワークです。solidityで書いたコードのコンパイル、デプロイ、テストを簡単に行うことが出来ます。

Ganache

プライベートブロックチェーン環境を構築することができるGUIアプリケーションです。本番環境ではgethが必要になりますが、開発環境ではプライベートチェーン使うのでGanacheが向いています。

Visual Studio Code

言わずと知れたコードエディタです。Solidityのプラグインを利用することができます。ターミナルからgit, truffle, Ganacheが利用でき、コントラクトのコンパイル、デプロイ、テストが簡単に出来るのでRemixよりも開発がスムーズに行えます。

Metamask

Ethereum のウォレットで、Chrome拡張機能として提供されています。dappsの秘密鍵管理やトランザクション確認の機能をmetamaskで利用します。

インストール

開発環境の導入方法になります。

Truffle

最新版をインストール

$ npm install -g truffle

バージョンの確認

$ truffle version

Truffle v5.0.13 (core: 5.0.13)
Solidity v0.5.0 (solc-js)
Node v10.14.1
Web3.js v1.0.0-beta.37

ganache

ganacheの公式サイトからファルをダウンロードしてインストールします。

Metamask

Chrome拡張機能なのでMetaMaskから「+CHROMEに追加」をクリックしてプラグインをインストールします。

実装

今回はTruffleの公式サイトに紹介されているチュートリアル「pet shop」を実装していきたいと思います。pet shopではペットの飼い主を決定するためのペットショップのシステムを構築します。

開発の流れ

サーバーサイド

  1. サンプルコードをダウンロード
  2. コントラクトの作成
  3. コンパイル
  4. マイグレションの作成
  5. デプロイ
  6. テストの作成
  7. テスト

サーバーサイド

  1. app.jsを編集
  2. metamaskを起動しプライベートネットに接続

それではまずサーバーサイドの実装を行います。

サーバーサイドの実装

サンプルコードをダウンロード

pet-shopフォルダを作成

$ mkdir pet-shop && cd pet-shop

pet-shopのサンプルをダウンロード

$ truffle unbox pet-shop

コントラクトの作成

「pet-shop」ディレクトリにある「contracts」内に「Adoption.sol」というファイルを作成し、そこにコントラクトを作成します。 $ truffle create contract Adoption コントラクトの内容は以下の通りです。

pragma solidity >= 0.5.0 < 0.7.0;
contract Adoption {
    address[16] public adopters;

    function adopt(uint petId) public returns (uint) {
      require(petId >= 0 && petId <= 15, 'petId must be between 0 and 15');
      adopters[petId] = msg.sender;

      return petId;
    }

    function getAdopters() public view returns (address[16] memory) {

      return adopters;
    }
}

Adoption.solについて説明します。

pragma solidity ^0.5.0;

Solidityはsolcと呼ばれるSolidiコンパイラによってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換されます。そのコンパイルする際に使用するコンパイラのバージョンを指定しています。

contract Adoption {

コントラクトの名前はAdoptionとします。

address[16] public adopters;

address型の配列でアドレスを16個まで保持できるadoptersを定義しています。アクセス修飾子はpublicなのでコントラクト内外部及び継承先で参照することが出来ます。

function adopt(uint petId) public returns (uint) {
    require(petId >= 0 && petId <= 15);
    adopters[petId] = msg.sender;
    
    return petId;
    }

adopt関数を定義しています。この関数はpetIdを引数として取り、requireでpetIdが0〜15であるか確認します。この条件に当てはまる場合、msg.senderをadopters配列に追加します。msg.sederは関数(adopt関数)を呼び出したユーザー若しくはスマートコントラクトのアドレスを参照するものです。今回はpetIdをmsg.seder(自分のアドレス)に紐ずけています。そしてpetIdを返しています。

function getAdopters() public view returns (address[16] memory) {

        return adopters;
    }

getAdopters関数ではAdoption関数で追加したpetIdと、それに紐づいたsddressを返します。返すデータはmemoryで宣言された通り一時的に保存されます。

コンパイル

作成したコントラクトをコンパイラを使ってEVM(Ethereum Virtual Machine)が扱える専用のバイトコードに変換します。

$ truffle compile

マイグレションの作成

変換したバイトコードをEthrereumネットワークにデプロイするために、マイグレーションファイルを作成します。

$ truffle create migration deploy_contracts

「migrations」ディレクトリに「deploy_contracts.js」ファイルを作成します。

var Adoption = artifacts.require("Adoption");
 module.exports = function(deploy) {
 deploy.deploy(Adoption);
}

マイグレーションを行う際にartifacts.require()を介してコントラクトをTruffleに対し指示します。引数はコントラクト名(Adoption)と同じにする必要があります。module.exportsでモジュール化を行いエクスポートします。deploy.deploy(Adoption)では指定した特定のコントラクトをデプロイすることを意味しています。

デプロイ

デプロイする際には開発用ネットワークであるプライベートネットワークをGanacheを使用して実行します。インストールしたGanacheを起動してください。

Truffleを使ってデプロイを行ないます。

$ truffle migrate

テストの作成

「test」ディレクトリに「TestAdoption.sol」ファイルを作成します。

pragma solidity ^0.5.0;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());
    uint expectedPetId = 8;
    address expectedAdopter = address(this);
    
    function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }
    
    function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }
    
    function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
    }
}

詳細について説明していきます。

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

Assert.solはアサーション関数で、このアサーション関数を使用しテストの合否をチェクします。DeployedAddresses.solはデプロイされたコントラクトのアドレスを取得します。

通常nodeのモジュールは”node modules”にありますが、この2つのファイルは探してもありません。”Assert.sol”はココにあり、”DeployedAddresses.sol”はテスト時に動的に作成されます。

contract TestAdoption {
    Adoption adoption = Adoption(DeployedAddresses.Adoption());

Adoption型のadoption変数にコントラクトのアドレスを格納します。

 uint expectedPetId = 8;
    address expectedAdopter = address(this);

テスト用のpetIdをexpectedPetIdとし8を代入します。またテスト用のアドレスをexpectedAdopterとし、このコントラクトを呼び出したアドレスを代入します。

function testUserCanAdoptPet() public {
        uint returnedId = adoption.adopt(expectedPetId);
        Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
    }

ここではAdoptionコントラクトのadoption関数をテストしています。adoption関数に引数としてexpectedPetId(内容は8)を渡した場合に返ってくる値が同じかどうかチェックしています。つまり指定したpetIdを入れた時に指定したpetIdが返ってくるかテストしています。

function testGetAdopterAddressByPetId() public {
        address adopter = adoption.adopters(expectedPetId);
        Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
    }

ここではAdoptionコントラクトのadopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり8番目にペットの飼い主のアドレスが登録されているかテストしています。

function testGetAdopterAddressByPetIdInArray() public {
        address[16] memory adopters = adoption.getAdopters();
        Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the   expected pet should be this contract");
    }

ここではAdoptionコントラクトのgetAdopters関数をテストしています。 adopters関数に引数としてexpectedPetId(内容は8)を渡した場合に対応するアドレスを返すかどうかチェックしています。つまり配列の8番目にこのコントラクを呼び出したアドレスが入っているかどうかチェックしています。

Truffleのテストはsolidityの他にJavaScriptで書くことも出来ます。

let Adoption = artifacts.require("Adoption");

contract("Adoption", async (accounts) => {
  describe("adopt method", async () => {
    let contract;
    const expectedPetId = 8;
    const expectAdopter = accounts[0];

    beforeEach(async () => {
      contract = await Adoption.new();
    });

    it("adoption of the expected pet should match what is returned", async () => {
      const result = await contract.adopt.call(expectedPetId);
      assert.equal(result, expectedPetId);
    });

    it("owner of the expected pet should be this contract", async () => {
      await contract.adopt(expectedPetId);
      const result = await contract.adopters.call(expectedPetId);
      assert.equal(result, expectAdopter);
    });

    it("owner of the expected pet should be this contract", async () => {
      await contract.adopt(expectedPetId);
      const result = await contract.getAdopters.call();
      assert.equal(result[expectedPetId], expectAdopter);
    });
  });
});

テスト

作成したtestファイルを使ってテストを実行します。

$ truffle test

以上でサーバーサイドの実装は終わりになります。 次にサーバサイドの開発になります。

サーバサイドの実装

JavaScriptAPIであるweb3を使うことで、イーサリアムのノードと通信することができます。フロントの実装では、このAPIを用いてブラウザからコントラクトにアクセスすることが可能になります。

それではapp.js(src/js/app.js)を編集していきます。

App = {
   web3Provider: null,
   contracts: {},
   init: function() {
   $.getJSON('../pets.json', function(data) {
   var petsRow = $('#petsRow');
   var petTemplate = $('#petTemplate');
   for (i = 0; i < data.length; i ++) {
    petTemplate.find('.panel-title').text(data[i].name);
    petTemplate.find('img').attr('src', data[i].picture);
    petTemplate.find('.pet-breed').text(data[i].breed);
    petTemplate.find('.pet-age').text(data[i].age);
    petTemplate.find('.pet-location').text(data[i].location);
    petTemplate.find('.btn-adopt').attr('data-id', data[i].id);
    petsRow.append(petTemplate.html());
   }
  });
   return App.initWeb3();
  },
  initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },
  initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },
  bindEvents: function() {
   $(document).on('click', '.btn-adopt', App.handleAdopt);
  },
  markAdopted: function(adopters, account) {
   var adoptionInstance;
  App.contracts.Adoption.deployed().then(function(instance) {
   adoptionInstance = instance;
   return adoptionInstance.getAdopters.call();
  }).then(function(adopters) {
   for (i = 0; i < adopters.length; i++) {
    if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
     $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
    }
   }
  }).catch(function(err) {
   console.log(err.message);
  });
   },
  handleAdopt: function(event) {
   event.preventDefault();
   var petId = parseInt($(event.target).data('id'));
   var adoptionInstance;
  web3.eth.getAccounts(function(error, accounts) {
   if (error) {
    console.log(error);
   }
   var account = accounts[0];
   App.contracts.Adoption.deployed().then(function(instance) {
    adoptionInstance = instance;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }
  };
  $(function() {
   $(window).load(function() {
    App.init();
   });
  });

詳細について説明していきます。

init: function() {
   $.getJSON('../pets.json', function(data) {
   var petsRow = $('#petsRow');
   var petTemplate = $('#petTemplate');
   for (i = 0; i < data.length; i ++) {
    petTemplate.find('.panel-title').text(data[i].name);
    petTemplate.find('img').attr('src', data[i].picture);
    petTemplate.find('.pet-breed').text(data[i].breed);
    petTemplate.find('.pet-age').text(data[i].age);
    petTemplate.find('.pet-location').text(data[i].location);
    petTemplate.find('.btn-adopt').attr('data-id', data[i].id);
    petsRow.append(petTemplate.html());
   }
  });
   return App.initWeb3();
  },

pets.jsonにある15匹の犬のデータを1匹ずつ呼び出し、index.htmlのpetsRowとpetTemplateに渡し、画面上に表示しています。

initWeb3: function() {
  if (typeof web3 !== 'undefined') {
   App.web3Provider = web3.currentProvider;
  } else {
   App.web3Provider = new Web3.providers.HttpProvider('http://localhost:7545');
  }
   web3 = new Web3(App.web3Provider);
   return App.initContract();
  },

ここではweb3のインスタンス化を行なっています。既存にweb3のインスタンスがあるか確認します。起動している場合はプロバイダを取得しインスタンス化してオブジェクトを作成します。既存にインスタンスがない場合は新規でプロバイダのインスタンスを作成します。

initContract: function() {
   $.getJSON('Adoption.json', function(data) {
   var AdoptionArtifact = data;
   App.contracts.Adoption = TruffleContract(AdoptionArtifact);
   App.contracts.Adoption.setProvider(App.web3Provider);
   return App.markAdopted();
  });
   return App.bindEvents();
  },

ここではコントラクトのインスタンス化を行なっています。Adoptionコントラクトをインスタンス化するために、truffleのtruffle-contractというライブラリのTruffleContract()関数を使用します。この関数の引数にArtifactを入れ、コントラクトをインスタンス化します。そして、web3のインスタンス化で作ったApp.web3Providerをそのコントラクトにセットします。

markAdopted: function(adopters, account) {
   var adoptionInstance;
  App.contracts.Adoption.deployed().then(function(instance) {
   adoptionInstance = instance;
   return adoptionInstance.getAdopters.call();
  }).then(function(adopters) {
   for (i = 0; i < adopters.length; i++) {
    if (adopters[i] !== '0x0000000000000000000000000000000000000000') {
     $('.panel-pet').eq(i).find('button').text('Success').attr('disabled', true);
    }
   }
  }).catch(function(err) {
   console.log(err.message);
  });
   },

ここではボタンが押された際にUIを変更する処理を行なっています。 adoptionInstanceにインスタンスを取得しAdoptionコントラクトのgetAdoption関数を呼び出してブロックチェーンに書かれている情報を返します。1匹ずつ状態を確認し、アドレスが紐づけられていた場合には画面のボタン表示を「Adopte」から「Success」に切り替え、ボタンを押してもトランザクションが発生しない様にします。

handleAdopt: function(event) {
   event.preventDefault();
   var petId = parseInt($(event.target).data('id'));
   var adoptionInstance;
  web3.eth.getAccounts(function(error, accounts) {
   if (error) {
    console.log(error);
   }
   var account = accounts[0];
   App.contracts.Adoption.deployed().then(function(instance) {
    adoptionInstance = instance;
    return adoptionInstance.adopt(petId, {from: account});
   }).then(function(result) {
    return App.markAdopted();
   }).catch(function(err) {
    console.log(err.message);
   });
  });
   }

ここではボタンが押された際にペットを採用する処理を行なっています。押したボタンのpetIdを確認し成功した場合にはadoptionInstanceを呼び出して画面上のUIの変更を行います。失敗した場合はエラーを表示します。 ボタンを押すことでトランザクションを作成しブロックチェーンに情報を書き込みます。

metamaskを起動しプライベートネットに接続

「Ganache」の画面に表示されているINDEX0のPRIVATE KEYをコピーします。インストールしたMetamaskを起動して「アカウントのインポート」から追加を行います。Metamaskのネットワークに新たに「http://127.0.0.1:7545」を追加し選択します。

以上でサーバーサイドとフロントサイドの実装は終わりです。 ターミナルで下記コマンドを実行するとブラウザが立ち上がりdappsを表示することが出来ます。

$ npm run dev

Dapps開発の基本

概要

Dappsの本格的な開発に入る前に、まず開発の流れを把握する意味でブラウザに「Hello World!」と表示する、簡単なDappsを開発してみたいと思います。

開発環境

  • Solidity
  • Truffle
  • Ganache
  • Metamask

※開発環境の構築についてはこちらの記事を参考にして下さい

開発の流れ

  1. プロジェクトの構築
  2. コントラクトの作成
  3. コントラクトのコンパイル
  4. コントラクトのデプロイ
  5. コントラクトのテスト
  6. フロントエンドの構築

プロジェクトの構築

hello-worldフォルダを作成し、truffle initで初期化します。

$ mkdir hello-world
$ cd hello-world
$ truffle init

hello-worldディレクトリは以下の様な構造になります。

├── contracts
├── migrations
├── test
└── truffle-config.js

コントラクトの作成

コントラクトを作成します。

$ truffle create contract HelloWorld

コントラクトの内容はget()メソッドで「HelloWorld!」を返す簡単なコントラクトです。

pragma solidity ^0.5.0;

contract HelloWorld {
  // string型の変数wordを定義
  string word;

  // 起動時にword変数に「Hello World!」を入れる
  constructor() public {
    word = 'Hello World!';
  }

  // get()関数はword変数を返す
  function get() public view returns (string memory) {
    return word;
  }
}

コントラクトのコンパイル

テストに通ったので、作成したHelloWorld.solをコンパイルします。 コンパイルするとbuildディレクトリ下にHelloWorld.jsonが作成されます。

$ truffle compile

コントラクトのデプロイ

コントラクトをネットワークにデプロイする為に、マイグレーションファイルを作成します。

$ truffle create migration HelloWorld

すると1××××_deploy_contract.jsと言うファイルがmigrationsディレクトリ下に作成されます。 マイグレーションファイルの内容は以下の通りにします。

// artifacts.require()でどのコントラクトとやり取りしたいかをTruffleに伝える
let HelloWorld = artifacts.require("HelloWorld")
// deployer.deploy()でデプロイするコイントラクトを指定
module.exports = function(deployer) {
  deployer.deploy(HelloWorld);
};

作成したマイグレーションファイルを使ってブロックチェーンにデプロイする為にローカルでプライベートチェーンを生成するGanacheを起動します。

f:id:adrenaline2017:20190707084302p:plain

Ganacheの設定に合わせる為にtruffle-config.jsを編集します。

module.exports = {
    networks: {
         development: {
         host: "127.0.0.1",     // ホスト
         port: 7545,            // ポート番号
         network_id: "*", 
        },
},

デプロイは以下の通りです。

$ truffle migrate

コントラクトのテスト

下記のコマンドでtestフォルダ下にhello_world.jsが作成されます。

$ truffle create test HelloWorld

テストの内容は以下の通りです。

// artifacts.require()で指定のコントラクトを呼び出す
const HelloWorld = artifacts.require("HelloWorld");

contract("HelloWorld", function() {
  // 変数を定義
  describe('word method', function(){
    let obj;
    const str = 'Hello World!';
    // テスト毎にHelloWorldコントラクトを作成
    before(async function() {
      obj = await HelloWorld.new();
    });
    // HelloWorldコントラクトのgetメソッドをテスト
    it("should get word", async function () {
      const result = await obj.get();
      assert.equal(result, str);
    });
  });
});

以下のコマンドでテストを走らせます。

$ truffle test test/hello_world.js

テストの実行結果が以下の通りになると成功です。

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



  Contract: HelloWorld
    word method
      ✓ should get word (68ms)


  1 passing (293ms)

フロントエンドの構築

htmlとjsファイルの作成します。ファイルを作成するディレクトリの構成は以下の通りになります。

├── build
├── contracts
├── hello_world.js
├── index.html
├── migrations
├── test
└── truffle-config.js

index.htmlを以下の通りにします。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>Ðapps - Hello World</title>
    <script type="text/javascript" src="hello_world.js"></script>
  </head>
  <body>
    <div id="contract_result">loading...</div>
  </body>
</html>

hello_world.jsファイルを作成し、フロントのロジックを実装します。hello_world.jsにあるabiはweb3jsとコントラクトの相互関係をとり持つ役割があります。abiとaddressはHelloWorld.jsonにある内容を使うのでコピペして下さい。

var abi = [
  {
    "constant": true,
    "inputs": [],
    "name": "get",
    "outputs": [
      {
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "pure",
    "type": "function",
    "signature": "0x6d4ce63c"
  }
];
var address = "0xd6856EEB9f6B3934E5925414e2c9D911a567e4c3";
   
  window.onload = function() {
    var contract = web3.eth.contract(abi).at(address);
    contract.get((error, result) => {
      document.getElementById("contract_result").textContent = result;
    });
  };

Metamaskを起動します。Metamaskを使うことで、ブラウザでブロックチェーンとやりとりすることができるようになります。Ganacheと接続する為に、Metamaskの設定はGanacheの設定に合わせる必要があります。 f:id:adrenaline2017:20190707230902p:plain

しかし、Metamaskはweb3オブジェクトを生成しないので、このままでは画面に表示することが出来ません。そこで今回は簡易サバーとしてlive-serverを使用します。live-serverはブラウザの自動更新をしてくれる、簡易ローカルサーバーを起動できるツールです。

$ npm i -g live-server
$live-server

ブラウザが起動して画面に「Hello World!」と表示されれば成功です。

これでdappsを開発する流れが一通り掴めたかと思います。 次回は本格的なdappsの開発のチュートリアルに挑戦してみたいと思います。

sendTransactionとcall

概要

solidityの関数を呼び出しにはsendTransactioncallの2パターンがあります。今回はsendTransactioncallの違いと使い分けについて取り上げたいと思います。

sendTransactionとcallの違い

sendTransaction

web3.eth.sendTransaction({
    from: '0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe',
    to: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe',
    value: '1000000000000000'
})
.then(function(receipt){
    ...
});

sendTransactionブロックチェーンネットワークにトランザクションをブロードキャストする関数の呼び出し方です。つまりデータはマイナーによってマイニングされ、そのデータが有効な場合はブロックチェーンに記録され公開されます。

この呼び出しはブロックチェーンの状態を変更するので、Etherを消費します。マイナーがトランザクションをブロックに含めない可能性があるので、非同期で行われます。 そのためトランザクションの即時の戻り値は、常にトランザクションのハッシュになります。 関数へのトランザクションの戻り値を取得するにはeventを使用します。

関数にpureviewが付与されていない場合は、ブロックチェーンの状態を変更する操作なのでsendTransactionで呼び出します。

call

web3.eth.call({
    to: "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", 
    data: "0xc6888fa10000000000000000000000000000000000000000000000000000000000000003"
})
.then(console.log);

callブロックチェーンネットワークにトランザクションをブロードキャストしない関数の呼び出し方です。つまりデータはマイナーによってマイニングされないので、ブロックチェーンには記録されません。

この呼び出しは読み取り専用の操作でなので、Etherを消費しません。 関数は実行され結果が返ってきますが、実行後はすべての状態が破棄されます。callは同期であり、コントラクト関数の戻り値はすぐに返されます。

関数にpureviewが付与されている場合は、呼び出し専用の操作なのでcallで呼び出します。

sendTransactionとcallの使い分け

はじめにcallを使い次にsendTransactionで呼び出すことをお薦めします。

sendTransactionはEtherを使用するのでcallを使って下調べをする必要があります。sendTransactionを使用した場合に例外が発生する可能性が あるので、事前にcallを使ってsendTransactionに 問題があるかデバッグすることが出来ます。