水無瀬のプログラミング日記

プログラムの勉強した時のメモ

Aqueduct事始め

はじめに

Dartを触り始めた今日このごろ。
Dartをサーバサイドで使いたかったのでやってみた。
調べた感じaqueductっていうFWが有名っぽかったのでやってみる。

TL;DT.

ソースコード

前提条件

Dartのインストールが終わっている

インストール

$ pub global activate aqueduct

テンプレート作成

$ aqueduct create aqueduct_tutorial
$ cd aqueduct_tutorial

とりあえず起動してみる

$ aqueduct serve
> -- Aqueduct CLI Version: 3.2.1
> -- Aqueduct project version: 3.2.1
> -- Preparing...
> -- Starting application 'aqueduct_tutorial/aqueduct_tutorial'
>     Channel: AqueductTutorialChannel
>     Config: /Users/Tetsuya/Document/Program/aqueduct_tutorial/config.yaml
>     Port: 8888
> [INFO] aqueduct: Server aqueduct/1 started.  
> [INFO] aqueduct: Server aqueduct/2 started.
    
$ curl localhost:8888/example | jq .
> {
>   "key": "value"
> }

Initialization

AqueductのアプリケーションはApplicationChannelから始まる。
アプリケーションに1つサブクラス化し、ルーティングやDBへの接続の初期化を行う。
生成されるやつは下記の通り。(コメントだらけなのでそこは削除。)

import 'aqueduct_tutorial.dart';
    
class AqueductTutorialChannel extends ApplicationChannel {
  // サーバの初期化を行う
  @override
  Future prepare() async {
    logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
  }
    
  // Routingの設定を行う
  @override
  Controller get entryPoint {
    final router = Router();
    
    router
      .route("/example")
      .linkFunction((request) async {
        return Response.ok({"key": "value"});
      });
    
    return router;
  }
}

routeの指定は他にも下記のようにしてワイルドカードなりの指定ができる。

// projects/1, projects/2などにマッチする
router
    .route("/projects/[:id]")
    .link(() => ProjectController());
    
// `/file/`から始まる全パスにマッチする
router
    .route("/file/*")
    .link(() => FileController());
    
// /healthにマッチする
router
    .route("/health")
    .linkFunction((req) async => Response.ok(null));

Controllers

Controllerはリクエストをハンドリングするところ。
Controllerはorverrideされたhandleメソッドでリクエストをハンドリングしてくれる。

class ProjectController extends Controller {
  @override
  FutureOr<RequestOrResponse> handle(Request request) {
    if (request.raw.headers.value("x-secret-key") == "secret!") {
      return request;
    }
    
    return Response.badRequest();
  }
}

このメソッドではRequestResponseを返すことができる。
Responseを返すとその名の通り、Clientにresponseが返る。
Requestを返すと後続?のControllerへと渡っていく。
expressで言うとこのnext()みたいな挙動っぽい。
ControllerからControllerに渡すのは下記みたいにすればできる。

router
    .route("/file/*")
    // ここの`return request`とすれば後続のControllerが呼ばれる
    .link(() => SelfFileController())
    .link(() => NextFileController());

class SelfFileController extends Controller {
  @override
  FutureOr<RequestOrResponse> handle(Request request) {
      print('selfFileController');
      return request;
  }
}

ResourceControllers

ResourceControllerは最もよく使われるControllerとのこと。
Postで/project, Getで/projectなど1つのクラスで複数のリクエストを受けたい時は多々あるはず。
アノテーションでよしなにやってくれる。
これはSpringなりnestjsなりでみるよくあるやつだと思う。

// router
router
    .route("/operation")
    .link(() => RestController());
router
    .route("/operation/:id")
    .link(() => RestController());

// Controller
class RestController extends ResourceController {
  @Operation.get('id')
  Future<Response> getProjectById(@Bind.path("id") int id) async {
    // GET /operation/:id
    return Response.ok(id);
  }
    
  @Operation.post()
  Future<Response> createProject(@Bind.body() Object body) async {
    // POST /operation
    return Response.ok(body);
  }
    
  @Operation.get()
  Future<Response> getAllProjects({@Bind.query("limit") int limit: 10}) async {
    // GET /operation
    return Response.ok({'result': [1,2,3]});
  }
}

叩いてみる

$ curl localhost:8888/operation
> {"result":[1,2,3]}
$ curl localhost:8888/operation/1
> 1
$ curl -XPOST -H "Content-Type: application/json" localhost:8888/operation -d '{"test": "test"}'
> {"test":"test"}

ManagedObjectControllers

ManagedObjectControllerはRestのオペレーションを自動的にDatabaseのクエリにマッピングしてくれるResourceController。
例えばPOSTは行を足してくれるし、GETはSELECTしてくれる。
ResourceControllerと違ってControllerを用意しなくても大丈夫。
ただ、用意してカスタマイズをすることもできる。

router
  .route("/users/[:id]")
  .link(() => ManagedObjectController<Project>(context));

Configuration

Applicationの設定はYAMLファイルに書くことができる。

database:
  host: api.projects.com
  port: 5432
  databaseName: project
port: 8000

読み込む時はYAMLファイルのキーを指定すればOK。

class TodoConfig extends Configuration {
  TodoConfig(String path) : super.fromFile(File(path));
    
  DatabaseConfiguration database;
  int port;
}

Configを使う時は下記のようにする。
デフォルトではYAMLファイル名はconfig.yamlになっている。

// 読み込むYAMLファイルまでのパスを渡して上げれば良い
TodoConfig config = TodoConfig(options.configurationFilePath);

Running and Concurrency

Aqueductのアプリケーションはaqueduct serveで実行することができる。
Aqueductはマルチスレッドで動かすことができるので、デバッグ用のツール、計測用のツールを付けてアプリケーションを実行するスレッド数を指定して実行できる。

$ aqueduct serve --observe --isolates 5 --port 8888

各スレッドは同じ設定を使いますがそれぞれ別なので、
同じWebサーバーのレプリカを起動するような挙動になるらしい。
このおかげでDBのコネクションプールみたいな動作が暗黙的になるとのこと。

PostgreSQL ORM

Query<T>を使うことでDBのクエリを発行することができる。

下記のような場合だとこれでSELECTを実行できる。

class DbController extends ResourceController {
  DbController(this.context);

  final ManagedContext context;

  @Operation.get()
  Future<Response> getAllProjects() async {
    final query = Query<TutorialProject>(context);

    final results = await query.fetch();

    return Response.ok(results);
  }
}

Defining a Data Model

ORMを使うには、ManagedObject<T>を継承したテーブルと同じ構造を持つクラスを作る必要がある。
Javaとかで言うEntityだと思う。
下記はid,name,dueDateというフィールドを持つテーブルのサンプル。
privateのクラスを作成し、それをベースにManagedObjectを継承したクラスを作成した。

class TutorialProject extends ManagedObject<_TutorialProject> implements _TutorialProject {
  bool get isPastDue => dueDate.difference(DateTime.now()).inSeconds < 0;
}

class _TutorialProject  {
  @primaryKey
  int id;

  @Column(indexed: true)
  String name;

  DateTime dueDate;
}

ManagedObject同士はリレーションを持つことができる。
持てる関係はhas-one,has-many,many-to-manyの3つ。
関係を指定するときには必ずどちらのクラスにも記載が必要になる。
何を言ってるかわからないと思うので、下記のようにすれば良いということ。

class Project extends ManagedObject<_Project> implements _Project {}
class _Project {
  ...

  // has-manyの指定
  ManagedSet<Task> tasks;
}

class Task extends ManagedObject<_Task> implements _Task {}
class _Task {
  ...

  // has-manyで持たれる側
  @Relate(#tasks)
  Project project;
}

Database Migrations

aqueductのCLIは、ManagedObjectを変更した際に自動でdatabaseのmigration用Scriptを生成してくれる。
下記のように実行するとScriptを生成するだけでなく、DBの更新もやってくれる。

$ aqueduct db generate
$ aqueduct db upgrade --connect postgres://user:password@host:5432/database

OAuth2.0

OAuthにもデフォルトで対応してくれているらしい。
ただ残念なことにOAuthの仕組みを良く分かっていないので、なるほど・・・?って感じ。
一応使い方はまとめる。

アプリケーションのサービスとしてAuthServerとそのdelegateを作成する。
delegateトークンの生成方法と保管方法を設定できる。
デフォルトではアクセストークンはランダムな32byteの文字列で、
クライアントID、トークン、アクセスコードはORMを使用してDBに保存される。

import 'package:aqueduct/aqueduct.dart';
import 'package:aqueduct/managed_auth.dart';

class AppApplicationChannel extends ApplicationChannel {
  AuthServer authServer;
  ManagedContext context;

  @override
  Future prepare() async {
    context = ManagedContext(...);

    final delegate = ManagedAuthDelegate<User>(context);
    authServer = AuthServer(delegate);
  }  
}

アクセストークンとユーザー資格情報を交換するための組み込み認証Controllerは、
AuthControllerおよびAuthCodeControllerという名前。

Controller get entryPoint {
  final router = Router();

  // POST /auth/token with username and password (or access code) to get access token
  router
    .route("/auth/token")
    .link(() => AuthController(authServer));

  // GET /auth/code returns login form, POST /auth/code grants access code
  router
    .route("/auth/code")
    .link(() => AuthCodeController(authServer));

  // ProjectController requires request to include access token
  router
    .route("/projects/[:id]")
    .link(() => Authorizer.bearer(authServer))
    .link(() => ProjectController(context));

  return router;
}

aqueductのCLIにはクライアント識別子とアクセス範囲を管理するためのツールがある。
下記のようにすれば良さそう。

    $ aqueduct auth add-client \
      --id com.app.mobile \
      --secret foobar \
      --redirect-uri https://somewhereoutthere.com \
      --allowed-scopes "users projects admin.readonly"

Logging

全リクエストのログを取ることができる。
下記のようにApplicationChannelにloggerを設定すればOK。

class WildfireChannel extends ApplicationChannel {
  @override
  Future prepare() async {
    logger.onRecord.listen((record) {
      print("$record");
    });
  }
}

まとめ

今回はaqueductのTourを進めてみた。
まだTourしかやってないけど、悪くはなさそうな感じ。
実際に何かを作ってみてわかることもあると思うので、とりあえず何か作ってみます。
それでは、今回はこの辺で。

参考サイト