はじめに
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();
}
}
このメソッドではRequest
とResponse
を返すことができる。
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しかやってないけど、悪くはなさそうな感じ。
実際に何かを作ってみてわかることもあると思うので、とりあえず何か作ってみます。
それでは、今回はこの辺で。